summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java4
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java119
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java22
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java24
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java255
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java116
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java35
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java947
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java596
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java119
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java98
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java4
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java27
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java60
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java104
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java273
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java57
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java42
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java66
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java23
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java35
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java88
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java18
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java376
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java372
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java48
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java121
-rw-r--r--libs/WindowManager/Jetpack/window-extensions-release.aarbin19210 -> 30713 bytes
-rw-r--r--libs/WindowManager/Shell/res/color/decor_button_dark_color.xml21
-rw-r--r--libs/WindowManager/Shell/res/color/decor_button_light_color.xml21
-rw-r--r--libs/WindowManager/Shell/res/color/decor_caption_title_color.xml23
-rw-r--r--libs/WindowManager/Shell/res/color/taskbar_background.xml3
-rw-r--r--libs/WindowManager/Shell/res/drawable/decor_caption_title.xml22
-rw-r--r--libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml32
-rw-r--r--libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml36
-rw-r--r--libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml24
-rw-r--r--libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml (renamed from libs/WindowManager/Shell/res/drawable/letterbox_education_ic_letterboxed_app.xml)15
-rw-r--r--libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml16
-rw-r--r--libs/WindowManager/Shell/res/drawable/letterbox_education_ic_screen_rotation.xml26
-rw-r--r--libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml18
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml3
-rw-r--r--libs/WindowManager/Shell/res/layout/caption_window_decoration.xml55
-rw-r--r--libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml2
-rw-r--r--libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml33
-rw-r--r--libs/WindowManager/Shell/res/values-af/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-am/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ar/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-as/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-az/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-be/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-bg/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-bn/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-bs/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-bs/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ca/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-cs/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-da/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-de/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-el/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-es/strings.xml8
-rw-r--r--libs/WindowManager/Shell/res/values-es/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-et/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-eu/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-eu/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-fa/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-fi/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-fr/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-fr/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-gl/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-gu/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-hi/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-hi/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-hr/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-hu/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-hy/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-in/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-is/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-it/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-iw/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ja/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ka/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-kk/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-km/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-kn/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ko/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ky/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-lo/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-lt/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-lv/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-mk/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ml/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-mn/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-mr/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ms/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-my/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-nb/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-nb/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ne/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-nl/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-or/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-pa/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-pl/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-pt/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ro/strings.xml34
-rw-r--r--libs/WindowManager/Shell/res/values-ro/strings_tv.xml16
-rw-r--r--libs/WindowManager/Shell/res/values-ru/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-si/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-sk/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-sl/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-sq/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-sr/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-sv/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-sw/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ta/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-te/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-television/config.xml9
-rw-r--r--libs/WindowManager/Shell/res/values-th/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-tl/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-tr/strings_tv.xml16
-rw-r--r--libs/WindowManager/Shell/res/values-uk/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-ur/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-uz/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-vi/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values-zu/strings_tv.xml14
-rw-r--r--libs/WindowManager/Shell/res/values/attrs.xml4
-rw-r--r--libs/WindowManager/Shell/res/values/config.xml4
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml40
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java112
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java42
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java39
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java230
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java164
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java159
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java210
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java347
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java233
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java134
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java357
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java213
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java93
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java387
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java56
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java135
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java341
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java323
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java16
-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.java54
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java369
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java104
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java175
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java59
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java75
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java431
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java57
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java57
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java81
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java57
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java307
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java41
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java455
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java429
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java)17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java275
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt89
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md87
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md69
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md83
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md83
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java67
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java259
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java41
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java455
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java)15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java73
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java687
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java385
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java59
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java250
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java174
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java245
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java352
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java59
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java55
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java418
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java1314
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerWindowManager.java117
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivity.java110
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivityController.java150
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java326
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreen.java85
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java762
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java376
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java348
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/MinimizedDockShadow.java99
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java386
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java118
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java59
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java53
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java77
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java241
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java55
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java46
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java142
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java60
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java333
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java123
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java65
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java70
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java203
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java504
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java96
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java204
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java62
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java925
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java147
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl103
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java104
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java181
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java82
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java144
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java99
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java595
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java292
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java324
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java1333
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java298
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java224
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ConfigurationChangeListener.java51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java36
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java128
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java278
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java89
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java77
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java)25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelper.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperController.java82
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java398
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java412
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java163
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java220
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java273
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java234
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java111
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java)191
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java)279
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java117
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java)23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl)27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java148
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/util/StagedSplitBounds.java)20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java260
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java217
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java46
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java295
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java99
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorLinearLayout.java72
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java90
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java391
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt128
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt115
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt104
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt128
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt121
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt178
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/OWNERS2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt109
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt117
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt76
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt44
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt178
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt116
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt104
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt126
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt110
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt115
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt124
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt129
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt196
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt153
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt169
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt155
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenRotateTransition.kt47
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt160
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt149
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OWNERS2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt122
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt228
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt114
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt113
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt136
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt133
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt116
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt111
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt26
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt13
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt5
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt12
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt7
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt5
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt148
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt18
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt123
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt139
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt129
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt59
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml11
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml30
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml33
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java27
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java61
-rw-r--r--libs/WindowManager/Shell/tests/unittest/Android.bp8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java55
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java93
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java184
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java45
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java96
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java107
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java188
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java123
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java104
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java77
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java42
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java36
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java119
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java142
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java181
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java64
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java65
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java27
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java35
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java261
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt99
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java37
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java65
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java248
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java302
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java166
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java43
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java22
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java86
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java25
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipDummySurfaceControlTx.java66
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java99
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java96
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java109
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java172
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java23
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt169
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java160
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java)17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java210
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java15
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java82
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java30
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java80
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java411
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperControllerTest.java58
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java316
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java367
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java417
-rw-r--r--libs/dream/lowlight/Android.bp47
-rw-r--r--libs/dream/lowlight/AndroidManifest.xml (renamed from libs/WindowManager/Shell/res/color/unfold_transition_background.xml)9
-rw-r--r--libs/dream/lowlight/res/values/config.xml20
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java117
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java61
-rw-r--r--libs/dream/lowlight/tests/Android.bp45
-rw-r--r--libs/dream/lowlight/tests/AndroidManifest.xml33
-rw-r--r--libs/dream/lowlight/tests/AndroidTest.xml31
-rw-r--r--libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java98
-rw-r--r--libs/hwui/FrameInfoVisualizer.cpp2
-rw-r--r--libs/hwui/JankTracker.cpp5
-rw-r--r--libs/hwui/jni/android_graphics_Canvas.cpp28
-rw-r--r--libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp14
-rw-r--r--libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h3
-rw-r--r--libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp13
-rw-r--r--libs/hwui/pipeline/skia/SkiaVulkanPipeline.h1
-rw-r--r--libs/hwui/renderthread/CanvasContext.cpp18
-rw-r--r--libs/hwui/tests/unit/JankTrackerTests.cpp65
-rw-r--r--libs/hwui/tests/unit/SkiaPipelineTests.cpp4
491 files changed, 26293 insertions, 19790 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
index 921552b6cfbb..68ff806c6765 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
@@ -199,7 +199,7 @@ public final class CommonFoldingFeature {
throw new IllegalArgumentException(
"Display feature rectangle cannot have zero width and height simultaneously.");
}
- this.mRect = rect;
+ this.mRect = new Rect(rect);
}
/** Returns the type of the feature. */
@@ -217,7 +217,7 @@ public final class CommonFoldingFeature {
/** Returns the bounds of the feature. */
@NonNull
public Rect getRect() {
- return mRect;
+ return new Rect(mRect);
}
@Override
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index fdcb7be597d5..cc2bb63ca8e1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -22,7 +22,6 @@ import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
import static androidx.window.common.CommonFoldingFeature.parseListFromString;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
@@ -30,22 +29,25 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;
+import androidx.window.util.AcceptOnceConsumer;
import androidx.window.util.BaseDataProducer;
-import androidx.window.util.DataProducer;
import com.android.internal.R;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Consumer;
/**
- * An implementation of {@link androidx.window.util.DataProducer} that returns the device's posture
- * by mapping the state returned from {@link DeviceStateManager} to values provided in the resources
- * config at {@link R.array#config_device_state_postures}.
+ * An implementation of {@link androidx.window.util.BaseDataProducer} that returns
+ * the device's posture by mapping the state returned from {@link DeviceStateManager} to
+ * values provided in the resources' config at {@link R.array#config_device_state_postures}.
*/
-public final class DeviceStateManagerFoldingFeatureProducer extends
- BaseDataProducer<List<CommonFoldingFeature>> {
+public final class DeviceStateManagerFoldingFeatureProducer
+ extends BaseDataProducer<List<CommonFoldingFeature>> {
private static final String TAG =
DeviceStateManagerFoldingFeatureProducer.class.getSimpleName();
private static final boolean DEBUG = false;
@@ -54,15 +56,11 @@ public final class DeviceStateManagerFoldingFeatureProducer extends
private int mCurrentDeviceState = INVALID_DEVICE_STATE;
- private final DeviceStateCallback mDeviceStateCallback = (state) -> {
- mCurrentDeviceState = state;
- notifyDataChanged();
- };
@NonNull
- private final DataProducer<String> mRawFoldSupplier;
+ private final BaseDataProducer<String> mRawFoldSupplier;
public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context,
- @NonNull DataProducer<String> rawFoldSupplier) {
+ @NonNull BaseDataProducer<String> rawFoldSupplier) {
mRawFoldSupplier = rawFoldSupplier;
String[] deviceStatePosturePairs = context.getResources()
.getStringArray(R.array.config_device_state_postures);
@@ -70,7 +68,8 @@ public final class DeviceStateManagerFoldingFeatureProducer extends
String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
if (deviceStatePostureMapping.length != 2) {
if (DEBUG) {
- Log.e(TAG, "Malformed device state posture pair: " + deviceStatePosturePair);
+ Log.e(TAG, "Malformed device state posture pair: "
+ + deviceStatePosturePair);
}
continue;
}
@@ -82,7 +81,8 @@ public final class DeviceStateManagerFoldingFeatureProducer extends
posture = Integer.parseInt(deviceStatePostureMapping[1]);
} catch (NumberFormatException e) {
if (DEBUG) {
- Log.e(TAG, "Failed to parse device state or posture: " + deviceStatePosturePair,
+ Log.e(TAG, "Failed to parse device state or posture: "
+ + deviceStatePosturePair,
e);
}
continue;
@@ -92,32 +92,95 @@ public final class DeviceStateManagerFoldingFeatureProducer extends
}
if (mDeviceStateToPostureMap.size() > 0) {
- context.getSystemService(DeviceStateManager.class)
- .registerCallback(context.getMainExecutor(), mDeviceStateCallback);
+ DeviceStateCallback deviceStateCallback = (state) -> {
+ mCurrentDeviceState = state;
+ mRawFoldSupplier.getData(this::notifyFoldingFeatureChange);
+ };
+ Objects.requireNonNull(context.getSystemService(DeviceStateManager.class))
+ .registerCallback(context.getMainExecutor(), deviceStateCallback);
}
}
- @Override
- @Nullable
- public Optional<List<CommonFoldingFeature>> getData() {
- final int globalHingeState = globalHingeState();
- Optional<String> displayFeaturesString = mRawFoldSupplier.getData();
- if (displayFeaturesString.isEmpty() || TextUtils.isEmpty(displayFeaturesString.get())) {
- return Optional.empty();
+ /**
+ * Add a callback to mCallbacks if there is no device state. This callback will be run
+ * once a device state is set. Otherwise,run the callback immediately.
+ */
+ private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback,
+ String displayFeaturesString) {
+ if (isCurrentStateValid()) {
+ callback.accept(calculateFoldingFeature(displayFeaturesString));
+ } else {
+ // This callback will be added to mCallbacks and removed once it runs once.
+ AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
+ new AcceptOnceConsumer<>(this, callback);
+ addDataChangedCallback(singleRunCallback);
}
- return Optional.of(parseListFromString(displayFeaturesString.get(), globalHingeState));
+ }
+
+ /**
+ * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the
+ * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was
+ * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}.
+ * Returns a boolean value of whether the device state is valid.
+ */
+ private boolean isCurrentStateValid() {
+ // If the device state is not found in the map, indexOfKey returns a negative number.
+ return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState) >= 0;
}
@Override
- protected void onListenersChanged(Set<Runnable> callbacks) {
+ protected void onListenersChanged(
+ @NonNull Set<Consumer<List<CommonFoldingFeature>>> callbacks) {
super.onListenersChanged(callbacks);
if (callbacks.isEmpty()) {
- mRawFoldSupplier.removeDataChangedCallback(this::notifyDataChanged);
+ mCurrentDeviceState = INVALID_DEVICE_STATE;
+ mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
+ } else {
+ mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
+ }
+ }
+
+ @NonNull
+ @Override
+ public Optional<List<CommonFoldingFeature>> getCurrentData() {
+ Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
+ if (!isCurrentStateValid()) {
+ return Optional.empty();
+ } else {
+ return displayFeaturesString.map(this::calculateFoldingFeature);
+ }
+ }
+
+ /**
+ * Adds the data to the storeFeaturesConsumer when the data is ready.
+ * @param storeFeaturesConsumer a consumer to collect the data when it is first available.
+ */
+ public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) {
+ mRawFoldSupplier.getData((String displayFeaturesString) -> {
+ if (TextUtils.isEmpty(displayFeaturesString)) {
+ storeFeaturesConsumer.accept(new ArrayList<>());
+ } else {
+ runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString);
+ }
+ });
+ }
+
+ private void notifyFoldingFeatureChange(String displayFeaturesString) {
+ if (!isCurrentStateValid()) {
+ return;
+ }
+ if (TextUtils.isEmpty(displayFeaturesString)) {
+ notifyDataChanged(new ArrayList<>());
} else {
- mRawFoldSupplier.addDataChangedCallback(this::notifyDataChanged);
+ notifyDataChanged(calculateFoldingFeature(displayFeaturesString));
}
}
+ private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) {
+ final int globalHingeState = globalHingeState();
+ return parseListFromString(displayFeaturesString, globalHingeState);
+ }
+
private int globalHingeState() {
return mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
index 69ad1badce60..7906342d445d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
@@ -32,6 +32,7 @@ import com.android.internal.R;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Consumer;
/**
* Implementation of {@link androidx.window.util.DataProducer} that produces a
@@ -40,7 +41,7 @@ import java.util.Set;
* settings where the {@link String} property is saved with the key
* {@link RawFoldingFeatureProducer#DISPLAY_FEATURES}. If this value is null or empty then the
* value in {@link android.content.res.Resources} is used. If both are empty then
- * {@link RawFoldingFeatureProducer#getData()} returns an empty object.
+ * {@link RawFoldingFeatureProducer#getData} returns an empty object.
* {@link RawFoldingFeatureProducer} listens to changes in the setting so that it can override
* the system {@link CommonFoldingFeature} data.
*/
@@ -63,12 +64,13 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> {
@Override
@NonNull
- public Optional<String> getData() {
+ public void getData(Consumer<String> dataConsumer) {
String displayFeaturesString = getFeatureString();
if (displayFeaturesString == null) {
- return Optional.empty();
+ dataConsumer.accept("");
+ } else {
+ dataConsumer.accept(displayFeaturesString);
}
- return Optional.of(displayFeaturesString);
}
/**
@@ -84,7 +86,7 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> {
}
@Override
- protected void onListenersChanged(Set<Runnable> callbacks) {
+ protected void onListenersChanged(Set<Consumer<String>> callbacks) {
if (callbacks.isEmpty()) {
unregisterObserversIfNeeded();
} else {
@@ -92,6 +94,12 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> {
}
}
+ @NonNull
+ @Override
+ public Optional<String> getCurrentData() {
+ return Optional.of(getFeatureString());
+ }
+
/**
* Registers settings observers, if needed. When settings observers are registered for this
* producer callbacks for changes in data will be triggered.
@@ -125,8 +133,8 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> {
@Override
public void onChange(boolean selfChange, Uri uri) {
if (mDisplayFeaturesUri.equals(uri)) {
- notifyDataChanged();
+ notifyDataChanged(getFeatureString());
}
}
}
-}
+} \ No newline at end of file
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index bdf703c9bd38..fb0a9db6a20b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -20,11 +20,14 @@ import android.app.ActivityThread;
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.area.WindowAreaComponentImpl;
import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
import androidx.window.extensions.embedding.SplitController;
import androidx.window.extensions.layout.WindowLayoutComponent;
import androidx.window.extensions.layout.WindowLayoutComponentImpl;
+
/**
* The reference implementation of {@link WindowExtensions} that implements the initial API version.
*/
@@ -33,7 +36,9 @@ public class WindowExtensionsImpl implements WindowExtensions {
private final Object mLock = new Object();
private volatile WindowLayoutComponent mWindowLayoutComponent;
private volatile SplitController mSplitController;
+ private volatile WindowAreaComponent mWindowAreaComponent;
+ // TODO(b/241126279) Introduce constants to better version functionality
@Override
public int getVendorApiLevel() {
return 1;
@@ -75,4 +80,23 @@ public class WindowExtensionsImpl implements WindowExtensions {
}
return mSplitController;
}
+
+ /**
+ * Returns a reference implementation of {@link WindowAreaComponent} if available,
+ * {@code null} otherwise. The implementation must match the API level reported in
+ * {@link WindowExtensions#getWindowAreaComponent()}.
+ * @return {@link WindowAreaComponent} OEM implementation.
+ */
+ public WindowAreaComponent getWindowAreaComponent() {
+ if (mWindowAreaComponent == null) {
+ synchronized (mLock) {
+ if (mWindowAreaComponent == null) {
+ Context context = ActivityThread.currentApplication();
+ mWindowAreaComponent =
+ new WindowAreaComponentImpl(context);
+ }
+ }
+ }
+ return mWindowAreaComponent;
+ }
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
new file mode 100644
index 000000000000..3adae7006369
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -0,0 +1,255 @@
+/*
+ * 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 androidx.window.extensions.area;
+
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Reference implementation of androidx.window.extensions.area OEM interface for use with
+ * WindowManager Jetpack.
+ *
+ * This component currently supports Rear Display mode with the ability to add and remove
+ * status listeners for this mode.
+ *
+ * The public methods in this class are thread-safe.
+ **/
+public class WindowAreaComponentImpl implements WindowAreaComponent,
+ DeviceStateManager.DeviceStateCallback {
+
+ private final Object mLock = new Object();
+
+ private final DeviceStateManager mDeviceStateManager;
+ private final Executor mExecutor;
+
+ @GuardedBy("mLock")
+ private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>();
+ private final int mRearDisplayState;
+ @WindowAreaSessionState
+ private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
+
+ @GuardedBy("mLock")
+ private int mCurrentDeviceState = INVALID_DEVICE_STATE;
+ @GuardedBy("mLock")
+ private int mCurrentDeviceBaseState = INVALID_DEVICE_STATE;
+ @GuardedBy("mLock")
+ private DeviceStateRequest mDeviceStateRequest;
+
+ public WindowAreaComponentImpl(@NonNull Context context) {
+ mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
+ mExecutor = context.getMainExecutor();
+
+ // TODO(b/236022708) Move rear display state to device state config file
+ mRearDisplayState = context.getResources().getInteger(
+ R.integer.config_deviceStateRearDisplay);
+
+ mDeviceStateManager.registerCallback(mExecutor, this);
+ }
+
+ /**
+ * Adds a listener interested in receiving updates on the RearDisplayStatus
+ * of the device. Because this is being called from the OEM provided
+ * extensions, we will post the result of the listener on the executor
+ * provided by the developer at the initial call site.
+ *
+ * Depending on the initial state of the device, we will return either
+ * {@link WindowAreaComponent#STATUS_AVAILABLE} or
+ * {@link WindowAreaComponent#STATUS_UNAVAILABLE} if the feature is supported or not in that
+ * state respectively. When the rear display feature is triggered, we update the status to be
+ * {@link WindowAreaComponent#STATUS_UNAVAILABLE}. TODO(b/240727590) Prefix with AREA_
+ *
+ * TODO(b/239833099) Add a STATUS_ACTIVE option to let apps know if a feature is currently
+ * enabled.
+ *
+ * @param consumer {@link Consumer} interested in receiving updates to the status of
+ * rear display mode.
+ */
+ public void addRearDisplayStatusListener(
+ @NonNull Consumer<@WindowAreaStatus Integer> consumer) {
+ synchronized (mLock) {
+ mRearDisplayStatusListeners.add(consumer);
+
+ // If current device state is still invalid, we haven't gotten our initial value yet
+ if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
+ return;
+ }
+ consumer.accept(getCurrentStatus());
+ }
+ }
+
+ /**
+ * Removes a listener no longer interested in receiving updates.
+ * @param consumer no longer interested in receiving updates to RearDisplayStatus
+ */
+ public void removeRearDisplayStatusListener(
+ @NonNull Consumer<@WindowAreaStatus Integer> consumer) {
+ synchronized (mLock) {
+ mRearDisplayStatusListeners.remove(consumer);
+ }
+ }
+
+ /**
+ * Creates and starts a rear display session and provides updates to the
+ * callback provided. Because this is being called from the OEM provided
+ * extensions, we will post the result of the listener on the executor
+ * provided by the developer at the initial call site.
+ *
+ * When we enable rear display mode, we submit a request to {@link DeviceStateManager}
+ * to override the device state to the state that corresponds to RearDisplay
+ * mode. When the {@link DeviceStateRequest} is activated, we let the
+ * consumer know that the session is active by sending
+ * {@link WindowAreaComponent#SESSION_STATE_ACTIVE}.
+ *
+ * @param activity to provide updates to the client on
+ * the status of the Session
+ * @param rearDisplaySessionCallback to provide updates to the client on
+ * the status of the Session
+ */
+ public void startRearDisplaySession(@NonNull Activity activity,
+ @NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) {
+ synchronized (mLock) {
+ if (mDeviceStateRequest != null) {
+ // Rear display session is already active
+ throw new IllegalStateException(
+ "Unable to start new rear display session as one is already active");
+ }
+ mDeviceStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
+ mDeviceStateManager.requestState(
+ mDeviceStateRequest,
+ mExecutor,
+ new DeviceStateRequestCallbackAdapter(rearDisplaySessionCallback)
+ );
+ }
+ }
+
+ /**
+ * Ends the current rear display session and provides updates to the
+ * callback provided. Because this is being called from the OEM provided
+ * extensions, we will post the result of the listener on the executor
+ * provided by the developer.
+ */
+ public void endRearDisplaySession() {
+ synchronized (mLock) {
+ if (mDeviceStateRequest != null || isRearDisplayActive()) {
+ mDeviceStateRequest = null;
+ mDeviceStateManager.cancelStateRequest();
+ } else {
+ throw new IllegalStateException(
+ "Unable to cancel a rear display session as there is no active session");
+ }
+ }
+ }
+
+ @Override
+ public void onBaseStateChanged(int state) {
+ synchronized (mLock) {
+ mCurrentDeviceBaseState = state;
+ if (state == mCurrentDeviceState) {
+ updateStatusConsumers(getCurrentStatus());
+ }
+ }
+ }
+
+ @Override
+ public void onStateChanged(int state) {
+ synchronized (mLock) {
+ mCurrentDeviceState = state;
+ updateStatusConsumers(getCurrentStatus());
+ }
+ }
+
+ @GuardedBy("mLock")
+ private int getCurrentStatus() {
+ if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE
+ || isRearDisplayActive()) {
+ return WindowAreaComponent.STATUS_UNAVAILABLE;
+ }
+ return WindowAreaComponent.STATUS_AVAILABLE;
+ }
+
+ /**
+ * Helper method to determine if a rear display session is currently active by checking
+ * if the current device configuration matches that of rear display. This would be true
+ * if there is a device override currently active (base state != current state) and the current
+ * state is that which corresponds to {@code mRearDisplayState}
+ * @return {@code true} if the device is in rear display mode and {@code false} if not
+ */
+ @GuardedBy("mLock")
+ private boolean isRearDisplayActive() {
+ return (mCurrentDeviceState != mCurrentDeviceBaseState) && (mCurrentDeviceState
+ == mRearDisplayState);
+ }
+
+ @GuardedBy("mLock")
+ private void updateStatusConsumers(@WindowAreaStatus int windowAreaStatus) {
+ synchronized (mLock) {
+ for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) {
+ mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus);
+ }
+ }
+ }
+
+ /**
+ * Callback for the {@link DeviceStateRequest} to be notified of when the request has been
+ * activated or cancelled. This callback provides information to the client library
+ * on the status of the RearDisplay session through {@code mRearDisplaySessionCallback}
+ */
+ private class DeviceStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
+
+ private final Consumer<Integer> mRearDisplaySessionCallback;
+
+ DeviceStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
+ mRearDisplaySessionCallback = callback;
+ }
+
+ @Override
+ public void onRequestActivated(@NonNull DeviceStateRequest request) {
+ synchronized (mLock) {
+ if (request.equals(mDeviceStateRequest)) {
+ mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE;
+ mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
+ updateStatusConsumers(getCurrentStatus());
+ }
+ }
+ }
+
+ @Override
+ public void onRequestCanceled(DeviceStateRequest request) {
+ synchronized (mLock) {
+ if (request.equals(mDeviceStateRequest)) {
+ mDeviceStateRequest = null;
+ }
+ mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
+ mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
+ updateStatusConsumers(getCurrentStatus());
+ }
+ }
+ }
+}
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 3ff531573f1f..74303e2fab7c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import android.app.Activity;
import android.app.WindowConfiguration.WindowingMode;
import android.content.Intent;
-import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
@@ -29,8 +28,10 @@ import android.util.ArrayMap;
import android.window.TaskFragmentCreationParams;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizer;
+import android.window.TaskFragmentTransaction;
import android.window.WindowContainerTransaction;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -51,34 +52,26 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
@VisibleForTesting
final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>();
- /**
- * Mapping from the client assigned unique token to the TaskFragment parent
- * {@link Configuration}.
- */
- final Map<IBinder, Configuration> mFragmentParentConfigs = new ArrayMap<>();
-
+ @NonNull
private final TaskFragmentCallback mCallback;
+
@VisibleForTesting
+ @Nullable
TaskFragmentAnimationController mAnimationController;
/**
* Callback that notifies the controller about changes to task fragments.
*/
interface TaskFragmentCallback {
- void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo);
- void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo);
- void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo);
- void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken,
- @NonNull Configuration parentConfig);
- void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
- @NonNull IBinder activityToken);
+ void onTransactionReady(@NonNull TaskFragmentTransaction transaction);
}
/**
* @param executor callbacks from WM Core are posted on this executor. It should be tied to the
* UI thread that all other calls into methods of this class are also on.
*/
- JetpackTaskFragmentOrganizer(@NonNull Executor executor, TaskFragmentCallback callback) {
+ JetpackTaskFragmentOrganizer(@NonNull Executor executor,
+ @NonNull TaskFragmentCallback callback) {
super(executor);
mCallback = callback;
}
@@ -101,6 +94,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
}
/** No longer overrides the animation if the transition is on the given Task. */
+ @GuardedBy("mLock")
void stopOverrideSplitAnimation(int taskId) {
if (mAnimationController != null) {
mAnimationController.unregisterRemoteAnimations(taskId);
@@ -153,41 +147,31 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
* @param wct WindowContainerTransaction in which the task fragment should be resized.
* @param fragmentToken token of an existing TaskFragment.
*/
- void expandTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+ void expandTaskFragment(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken) {
resizeTaskFragment(wct, fragmentToken, new Rect());
setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */);
updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
}
/**
- * Expands an existing TaskFragment to fill parent.
- * @param fragmentToken token of an existing TaskFragment.
- */
- void expandTaskFragment(IBinder fragmentToken) {
- WindowContainerTransaction wct = new WindowContainerTransaction();
- expandTaskFragment(wct, fragmentToken);
- applyTransaction(wct);
- }
-
- /**
* Expands an Activity to fill parent by moving it to a new TaskFragment.
* @param fragmentToken token to create new TaskFragment with.
* @param activity activity to move to the fill-parent TaskFragment.
*/
- void expandActivity(IBinder fragmentToken, Activity activity) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
+ void expandActivity(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+ @NonNull Activity activity) {
createTaskFragmentAndReparentActivity(
wct, fragmentToken, activity.getActivityToken(), new Rect(),
WINDOWING_MODE_UNDEFINED, activity);
- applyTransaction(wct);
}
/**
* @param ownerToken The token of the activity that creates this task fragment. It does not
* have to be a child of this task fragment, but must belong to the same task.
*/
- void createTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
- IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
+ void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+ @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
final TaskFragmentCreationParams fragmentOptions =
createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode);
wct.createTaskFragment(fragmentOptions);
@@ -197,9 +181,9 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
* @param ownerToken The token of the activity that creates this task fragment. It does not
* have to be a child of this task fragment, but must belong to the same task.
*/
- private void createTaskFragmentAndReparentActivity(
- WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
- @NonNull Rect bounds, @WindowingMode int windowingMode, Activity activity) {
+ private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
+ @WindowingMode int windowingMode, @NonNull Activity activity) {
createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
}
@@ -208,9 +192,9 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
* @param ownerToken The token of the activity that creates this task fragment. It does not
* have to be a child of this task fragment, but must belong to the same task.
*/
- private void createTaskFragmentAndStartActivity(
- WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
- @NonNull Rect bounds, @WindowingMode int windowingMode, Intent activityIntent,
+ private void createTaskFragmentAndStartActivity(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
+ @WindowingMode int windowingMode, @NonNull Intent activityIntent,
@Nullable Bundle activityOptions) {
createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions);
@@ -231,8 +215,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
wct.setAdjacentTaskFragments(primary, secondary, adjacentParams);
}
- TaskFragmentCreationParams createFragmentOptions(IBinder fragmentToken, IBinder ownerToken,
- Rect bounds, @WindowingMode int windowingMode) {
+ TaskFragmentCreationParams createFragmentOptions(@NonNull IBinder fragmentToken,
+ @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
if (mFragmentInfos.containsKey(fragmentToken)) {
throw new IllegalArgumentException(
"There is an existing TaskFragment with fragmentToken=" + fragmentToken);
@@ -247,7 +231,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
.build();
}
- void resizeTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
+ void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
@Nullable Rect bounds) {
if (!mFragmentInfos.containsKey(fragmentToken)) {
throw new IllegalArgumentException(
@@ -259,8 +243,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds);
}
- void updateWindowingMode(WindowContainerTransaction wct, IBinder fragmentToken,
- @WindowingMode int windowingMode) {
+ void updateWindowingMode(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken, @WindowingMode int windowingMode) {
if (!mFragmentInfos.containsKey(fragmentToken)) {
throw new IllegalArgumentException(
"Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
@@ -268,7 +252,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
wct.setWindowingMode(mFragmentInfos.get(fragmentToken).getToken(), windowingMode);
}
- void deleteTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+ void deleteTaskFragment(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken) {
if (!mFragmentInfos.containsKey(fragmentToken)) {
throw new IllegalArgumentException(
"Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
@@ -276,51 +261,16 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken());
}
- @Override
- public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
- final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
- mFragmentInfos.put(fragmentToken, taskFragmentInfo);
-
- if (mCallback != null) {
- mCallback.onTaskFragmentAppeared(taskFragmentInfo);
- }
- }
-
- @Override
- public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
- final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
- mFragmentInfos.put(fragmentToken, taskFragmentInfo);
-
- if (mCallback != null) {
- mCallback.onTaskFragmentInfoChanged(taskFragmentInfo);
- }
+ void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
+ mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
}
- @Override
- public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+ void removeTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
mFragmentInfos.remove(taskFragmentInfo.getFragmentToken());
- mFragmentParentConfigs.remove(taskFragmentInfo.getFragmentToken());
-
- if (mCallback != null) {
- mCallback.onTaskFragmentVanished(taskFragmentInfo);
- }
}
@Override
- public void onTaskFragmentParentInfoChanged(
- @NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) {
- mFragmentParentConfigs.put(fragmentToken, parentConfig);
-
- if (mCallback != null) {
- mCallback.onTaskFragmentParentInfoChanged(fragmentToken, parentConfig);
- }
- }
-
- @Override
- public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
- @NonNull IBinder activityToken) {
- if (mCallback != null) {
- mCallback.onActivityReparentToTask(taskId, activityIntent, activityToken);
- }
+ public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
+ mCallback.onTransactionReady(transaction);
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index f09a91018bf0..00be5a6e3416 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -16,26 +16,36 @@
package androidx.window.extensions.embedding;
-import android.annotation.NonNull;
import android.app.Activity;
+import android.content.res.Configuration;
import android.util.Pair;
import android.util.Size;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
/**
* Client-side descriptor of a split that holds two containers.
*/
class SplitContainer {
+ @NonNull
private final TaskFragmentContainer mPrimaryContainer;
+ @NonNull
private final TaskFragmentContainer mSecondaryContainer;
+ @NonNull
private final SplitRule mSplitRule;
+ @NonNull
+ private SplitAttributes mSplitAttributes;
SplitContainer(@NonNull TaskFragmentContainer primaryContainer,
@NonNull Activity primaryActivity,
@NonNull TaskFragmentContainer secondaryContainer,
- @NonNull SplitRule splitRule) {
+ @NonNull SplitRule splitRule,
+ @NonNull SplitAttributes splitAttributes) {
mPrimaryContainer = primaryContainer;
mSecondaryContainer = secondaryContainer;
mSplitRule = splitRule;
+ mSplitAttributes = splitAttributes;
if (shouldFinishPrimaryWithSecondary(splitRule)) {
if (mPrimaryContainer.getRunningActivityCount() == 1
@@ -68,6 +78,26 @@ class SplitContainer {
return mSplitRule;
}
+ @NonNull
+ SplitAttributes getSplitAttributes() {
+ return mSplitAttributes;
+ }
+
+ /**
+ * Updates the {@link SplitAttributes} to this container.
+ * It is usually used when there's a folding state change or
+ * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction, int,
+ * Configuration)}.
+ */
+ void setSplitAttributes(@NonNull SplitAttributes splitAttributes) {
+ mSplitAttributes = splitAttributes;
+ }
+
+ @NonNull
+ TaskContainer getTaskContainer() {
+ return getPrimaryContainer().getTaskContainer();
+ }
+
/** Returns the minimum dimension pair of primary container and secondary container. */
@NonNull
Pair<Size, Size> getMinDimensionsPair() {
@@ -137,6 +167,7 @@ class SplitContainer {
+ " primaryContainer=" + mPrimaryContainer
+ " secondaryContainer=" + mSecondaryContainer
+ " splitRule=" + mSplitRule
+ + " splitAttributes" + mSplitAttributes
+ "}";
}
}
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 c9a0d7d99cc6..203ece091e46 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -16,8 +16,22 @@
package androidx.window.extensions.embedding;
+import static android.app.ActivityManager.START_SUCCESS;
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.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.getTransitionType;
+import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
@@ -27,12 +41,13 @@ import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAs
import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair;
import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds;
-import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide;
+import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit;
import android.app.Activity;
import android.app.ActivityClient;
import android.app.ActivityOptions;
import android.app.ActivityThread;
+import android.app.Application;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
@@ -43,23 +58,31 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.os.SystemProperties;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.util.Size;
import android.util.SparseArray;
+import android.view.WindowMetrics;
import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentParentInfo;
+import android.window.TaskFragmentTransaction;
import android.window.WindowContainerTransaction;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
+import androidx.window.extensions.WindowExtensionsProvider;
+import androidx.window.extensions.layout.WindowLayoutComponentImpl;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -70,6 +93,8 @@ import java.util.function.Consumer;
public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
ActivityEmbeddingComponent {
static final String TAG = "SplitController";
+ static final boolean ENABLE_SHELL_TRANSITIONS =
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
@VisibleForTesting
@GuardedBy("mLock")
@@ -79,6 +104,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
private final List<EmbeddingRule> mSplitRules = new ArrayList<>();
/**
+ * A developer-defined {@link SplitAttributes} calculator to compute the current
+ * {@link SplitAttributes} with the current device and window states.
+ * It is registered via {@link #setSplitAttributesCalculator(SplitAttributesCalculator)}
+ * and unregistered via {@link #clearSplitAttributesCalculator()}.
+ * This is called when:
+ * <ul>
+ * <li>{@link SplitPresenter#updateSplitContainer(SplitContainer, TaskFragmentContainer,
+ * WindowContainerTransaction)}</li>
+ * <li>There's a started Activity which matches {@link SplitPairRule} </li>
+ * <li>Checking whether the place holder should be launched if there's a Activity matches
+ * {@link SplitPlaceholderRule} </li>
+ * </ul>
+ */
+ @GuardedBy("mLock")
+ @Nullable
+ private SplitAttributesCalculator mSplitAttributesCalculator;
+ /**
* Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info
* below it.
* When the app is host of multiple Tasks, there can be multiple splits controlled by the same
@@ -88,24 +130,65 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>();
- // Callback to Jetpack to notify about changes to split states.
- @NonNull
+ /** Callback to Jetpack to notify about changes to split states. */
+ @Nullable
private Consumer<List<SplitInfo>> mEmbeddingCallback;
private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
private final Handler mHandler;
- private final Object mLock = new Object();
+ final Object mLock = new Object();
+ private final ActivityStartMonitor mActivityStartMonitor;
+ @NonNull
+ final WindowLayoutComponentImpl mWindowLayoutComponent;
public SplitController() {
+ this((WindowLayoutComponentImpl) Objects.requireNonNull(WindowExtensionsProvider
+ .getWindowExtensions().getWindowLayoutComponent()));
+ }
+
+ @VisibleForTesting
+ SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent) {
final MainThreadExecutor executor = new MainThreadExecutor();
mHandler = executor.mHandler;
mPresenter = new SplitPresenter(executor, this);
- ActivityThread activityThread = ActivityThread.currentActivityThread();
+ final ActivityThread activityThread = ActivityThread.currentActivityThread();
+ final Application application = activityThread.getApplication();
// Register a callback to be notified about activities being created.
- activityThread.getApplication().registerActivityLifecycleCallbacks(
- new LifecycleCallbacks());
+ application.registerActivityLifecycleCallbacks(new LifecycleCallbacks());
// Intercept activity starts to route activities to new containers if necessary.
Instrumentation instrumentation = activityThread.getInstrumentation();
- instrumentation.addMonitor(new ActivityStartMonitor());
+
+ mActivityStartMonitor = new ActivityStartMonitor();
+ instrumentation.addMonitor(mActivityStartMonitor);
+ mWindowLayoutComponent = windowLayoutComponent;
+ mWindowLayoutComponent.addFoldingStateChangedCallback(new FoldingFeatureListener());
+ }
+
+ private class FoldingFeatureListener implements Consumer<List<CommonFoldingFeature>> {
+ @Override
+ public void accept(List<CommonFoldingFeature> foldingFeatures) {
+ synchronized (mLock) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ for (int i = 0; i < mTaskContainers.size(); i++) {
+ final TaskContainer taskContainer = mTaskContainers.valueAt(i);
+ if (!taskContainer.isVisible()) {
+ continue;
+ }
+ if (taskContainer.getDisplayId() != DEFAULT_DISPLAY) {
+ continue;
+ }
+ // TODO(b/238948678): Support reporting display features in all windowing modes.
+ if (taskContainer.isInMultiWindow()) {
+ continue;
+ }
+ if (taskContainer.isEmpty()) {
+ continue;
+ }
+ updateContainersInTask(wct, taskContainer);
+ updateAnimationOverride(taskContainer);
+ }
+ mPresenter.applyTransaction(wct);
+ }
+ }
}
/** Updates the embedding rules applied to future activity launches. */
@@ -120,6 +203,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
}
+ @Override
+ public void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator) {
+ synchronized (mLock) {
+ mSplitAttributesCalculator = calculator;
+ }
+ }
+
+ @Override
+ public void clearSplitAttributesCalculator() {
+ synchronized (mLock) {
+ mSplitAttributesCalculator = null;
+ }
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ SplitAttributesCalculator getSplitAttributesCalculator() {
+ return mSplitAttributesCalculator;
+ }
+
@NonNull
List<EmbeddingRule> getSplitRules() {
return mSplitRules;
@@ -136,161 +239,340 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
}
+ /**
+ * Called when the transaction is ready so that the organizer can update the TaskFragments based
+ * on the changes in transaction.
+ */
@Override
- public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
+ public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
synchronized (mLock) {
- TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
- if (container == null) {
- return;
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
+ for (TaskFragmentTransaction.Change change : changes) {
+ final int taskId = change.getTaskId();
+ final TaskFragmentInfo info = change.getTaskFragmentInfo();
+ switch (change.getType()) {
+ case TYPE_TASK_FRAGMENT_APPEARED:
+ mPresenter.updateTaskFragmentInfo(info);
+ onTaskFragmentAppeared(wct, info);
+ break;
+ case TYPE_TASK_FRAGMENT_INFO_CHANGED:
+ mPresenter.updateTaskFragmentInfo(info);
+ onTaskFragmentInfoChanged(wct, info);
+ break;
+ case TYPE_TASK_FRAGMENT_VANISHED:
+ mPresenter.removeTaskFragmentInfo(info);
+ onTaskFragmentVanished(wct, info);
+ break;
+ case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED:
+ onTaskFragmentParentInfoChanged(wct, taskId,
+ change.getTaskFragmentParentInfo());
+ break;
+ case TYPE_TASK_FRAGMENT_ERROR:
+ final Bundle errorBundle = change.getErrorBundle();
+ final IBinder errorToken = change.getErrorCallbackToken();
+ final TaskFragmentInfo errorTaskFragmentInfo = errorBundle.getParcelable(
+ KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, TaskFragmentInfo.class);
+ final int opType = errorBundle.getInt(KEY_ERROR_CALLBACK_OP_TYPE);
+ final Throwable exception = errorBundle.getSerializable(
+ KEY_ERROR_CALLBACK_THROWABLE, Throwable.class);
+ if (errorTaskFragmentInfo != null) {
+ mPresenter.updateTaskFragmentInfo(errorTaskFragmentInfo);
+ }
+ onTaskFragmentError(wct, errorToken, errorTaskFragmentInfo, opType,
+ exception);
+ break;
+ case TYPE_ACTIVITY_REPARENTED_TO_TASK:
+ onActivityReparentedToTask(
+ wct,
+ taskId,
+ change.getActivityIntent(),
+ change.getActivityToken());
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown TaskFragmentEvent=" + change.getType());
+ }
}
- container.setInfo(taskFragmentInfo);
- if (container.isFinished()) {
- mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
- }
+ // Notify the server, and the server should apply and merge the
+ // WindowContainerTransaction to the active sync to finish the TaskFragmentTransaction.
+ mPresenter.onTransactionHandled(transaction.getTransactionToken(), wct,
+ getTransitionType(wct), false /* shouldApplyIndependently */);
updateCallbackIfNecessary();
}
}
- @Override
- public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
- synchronized (mLock) {
- TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
- if (container == null) {
- return;
- }
+ /**
+ * Called when a TaskFragment is created and organized by this organizer.
+ *
+ * @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
+ * @param taskFragmentInfo Info of the TaskFragment that is created.
+ */
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(container.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentInfo taskFragmentInfo) {
+ final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
+ if (container == null) {
+ return;
+ }
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- final boolean wasInPip = isInPictureInPicture(container);
- container.setInfo(taskFragmentInfo);
- final boolean isInPip = isInPictureInPicture(container);
- // Check if there are no running activities - consider the container empty if there are
- // no non-finishing activities left.
- if (!taskFragmentInfo.hasRunningActivity()) {
- if (taskFragmentInfo.isTaskFragmentClearedForPip()) {
- // Do not finish the dependents if the last activity is reparented to PiP.
- // Instead, the original split should be cleanup, and the dependent may be
- // expanded to fullscreen.
- cleanupForEnterPip(wct, container);
- mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
- } else if (taskFragmentInfo.isTaskClearedForReuse()) {
- // Do not finish the dependents if this TaskFragment was cleared due to
- // launching activity in the Task.
- mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
- } else if (!container.isWaitingActivityAppear()) {
- // Do not finish the container before the expected activity appear until
- // timeout.
- mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct);
- }
- } else if (wasInPip && isInPip) {
- // No update until exit PIP.
- return;
- } else if (isInPip) {
- // Enter PIP.
- // All overrides will be cleanup.
- container.setLastRequestedBounds(null /* bounds */);
- container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED);
- cleanupForEnterPip(wct, container);
- } else if (wasInPip) {
- // Exit PIP.
- // Updates the presentation of the container. Expand or launch placeholder if
- // needed.
- updateContainer(wct, container);
- }
- mPresenter.applyTransaction(wct);
- updateCallbackIfNecessary();
+ container.setInfo(wct, taskFragmentInfo);
+ if (container.isFinished()) {
+ mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
+ } else {
+ // Update with the latest Task configuration.
+ updateContainer(wct, container);
}
}
- @Override
- public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
- synchronized (mLock) {
- final TaskFragmentContainer container = getContainer(
- taskFragmentInfo.getFragmentToken());
- if (container != null) {
- // Cleanup if the TaskFragment vanished is not requested by the organizer.
- removeContainer(container);
- // Make sure the top container is updated.
- final TaskFragmentContainer newTopContainer = getTopActiveContainer(
- container.getTaskId());
- if (newTopContainer != null) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- updateContainer(wct, newTopContainer);
- mPresenter.applyTransaction(wct);
- }
- updateCallbackIfNecessary();
+ /**
+ * Called when the status of an organized TaskFragment is changed.
+ *
+ * @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
+ * @param taskFragmentInfo Info of the TaskFragment that is changed.
+ */
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(container.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentInfo taskFragmentInfo) {
+ final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
+ if (container == null) {
+ return;
+ }
+
+ final boolean wasInPip = isInPictureInPicture(container);
+ container.setInfo(wct, taskFragmentInfo);
+ final boolean isInPip = isInPictureInPicture(container);
+ // Check if there are no running activities - consider the container empty if there are
+ // no non-finishing activities left.
+ if (!taskFragmentInfo.hasRunningActivity()) {
+ if (taskFragmentInfo.isTaskFragmentClearedForPip()) {
+ // Do not finish the dependents if the last activity is reparented to PiP.
+ // Instead, the original split should be cleanup, and the dependent may be
+ // expanded to fullscreen.
+ cleanupForEnterPip(wct, container);
+ mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
+ } else if (taskFragmentInfo.isTaskClearedForReuse()) {
+ // Do not finish the dependents if this TaskFragment was cleared due to
+ // launching activity in the Task.
+ mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
+ } else if (!container.isWaitingActivityAppear()) {
+ // Do not finish the container before the expected activity appear until
+ // timeout.
+ mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
}
- cleanupTaskFragment(taskFragmentInfo.getFragmentToken());
+ } else if (wasInPip && isInPip) {
+ // No update until exit PIP.
+ return;
+ } else if (isInPip) {
+ // Enter PIP.
+ // All overrides will be cleanup.
+ container.setLastRequestedBounds(null /* bounds */);
+ container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED);
+ cleanupForEnterPip(wct, container);
+ } else if (wasInPip) {
+ // Exit PIP.
+ // Updates the presentation of the container. Expand or launch placeholder if
+ // needed.
+ updateContainer(wct, container);
}
}
- @Override
- public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken,
- @NonNull Configuration parentConfig) {
- synchronized (mLock) {
- final TaskFragmentContainer container = getContainer(fragmentToken);
- if (container != null) {
- onTaskConfigurationChanged(container.getTaskId(), parentConfig);
- if (isInPictureInPicture(parentConfig)) {
- // No need to update presentation in PIP until the Task exit PIP.
- return;
- }
- mPresenter.updateContainer(container);
- updateCallbackIfNecessary();
+ /**
+ * Called when an organized TaskFragment is removed.
+ *
+ * @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
+ * @param taskFragmentInfo Info of the TaskFragment that is removed.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentInfo taskFragmentInfo) {
+ final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
+ if (container != null) {
+ // Cleanup if the TaskFragment vanished is not requested by the organizer.
+ removeContainer(container);
+ // Make sure the top container is updated.
+ final TaskFragmentContainer newTopContainer = getTopActiveContainer(
+ container.getTaskId());
+ if (newTopContainer != null) {
+ updateContainer(wct, newTopContainer);
}
}
+ cleanupTaskFragment(taskFragmentInfo.getFragmentToken());
}
- @Override
- public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
- @NonNull IBinder activityToken) {
- synchronized (mLock) {
- // If the activity belongs to the current app process, we treat it as a new activity
- // launch.
- final Activity activity = getActivity(activityToken);
- if (activity != null) {
- // We don't allow split as primary for new launch because we currently only support
- // launching to top. We allow split as primary for activity reparent because the
- // activity may be split as primary before it is reparented out. In that case, we
- // want to show it as primary again when it is reparented back.
- if (!resolveActivityToContainer(activity, true /* isOnReparent */)) {
- // When there is no embedding rule matched, try to place it in the top container
- // like a normal launch.
- placeActivityInTopContainer(activity);
- }
- updateCallbackIfNecessary();
- return;
- }
+ /**
+ * Called when the parent leaf Task of organized TaskFragments is changed.
+ * When the leaf Task is changed, the organizer may want to update the TaskFragments in one
+ * transaction.
+ *
+ * For case like screen size change, it will trigger {@link #onTaskFragmentParentInfoChanged}
+ * with new Task bounds, but may not trigger {@link #onTaskFragmentInfoChanged} because there
+ * can be an override bounds.
+ *
+ * @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
+ * @param taskId Id of the parent Task that is changed.
+ * @param parentInfo {@link TaskFragmentParentInfo} of the parent Task.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
+ int taskId, @NonNull TaskFragmentParentInfo parentInfo) {
+ final TaskContainer taskContainer = getTaskContainer(taskId);
+ if (taskContainer == null || taskContainer.isEmpty()) {
+ Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId);
+ return;
+ }
+ taskContainer.updateTaskFragmentParentInfo(parentInfo);
+ if (!taskContainer.isVisible()) {
+ // Don't update containers if the task is not visible. We only update containers when
+ // parentInfo#isVisibleRequested is true.
+ return;
+ }
+ onTaskContainerInfoChanged(taskContainer, parentInfo.getConfiguration());
+ if (isInPictureInPicture(parentInfo.getConfiguration())) {
+ // No need to update presentation in PIP until the Task exit PIP.
+ return;
+ }
+ updateContainersInTask(wct, taskContainer);
+ }
- final TaskContainer taskContainer = getTaskContainer(taskId);
- if (taskContainer == null || taskContainer.isInPictureInPicture()) {
- // We don't embed activity when it is in PIP.
- return;
+ private void updateContainersInTask(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskContainer taskContainer) {
+ // Update all TaskFragments in the Task. Make a copy of the list since some may be
+ // removed on updating.
+ final List<TaskFragmentContainer> containers =
+ new ArrayList<>(taskContainer.mContainers);
+ for (int i = containers.size() - 1; i >= 0; i--) {
+ final TaskFragmentContainer container = containers.get(i);
+ // Wait until onTaskFragmentAppeared to update new container.
+ if (!container.isFinished() && !container.isWaitingActivityAppear()) {
+ updateContainer(wct, container);
}
+ }
+ }
- // If the activity belongs to a different app process, we treat it as starting new
- // intent, since both actions might result in a new activity that should appear in an
- // organized TaskFragment.
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId,
- activityIntent, null /* launchingActivity */);
- if (targetContainer == null) {
+ /**
+ * Called when an Activity is reparented to the Task with organized TaskFragment. For example,
+ * when an Activity enters and then exits Picture-in-picture, it will be reparented back to its
+ * original Task. In this case, we need to notify the organizer so that it can check if the
+ * Activity matches any split rule.
+ *
+ * @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
+ * @param taskId The Task that the activity is reparented to.
+ * @param activityIntent The intent that the activity is original launched with.
+ * @param activityToken If the activity belongs to the same process as the organizer, this
+ * will be the actual activity token; if the activity belongs to a
+ * different process, the server will generate a temporary token that
+ * the organizer can use to reparent the activity through
+ * {@link WindowContainerTransaction} if needed.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
+ int taskId, @NonNull Intent activityIntent,
+ @NonNull IBinder activityToken) {
+ // If the activity belongs to the current app process, we treat it as a new activity
+ // launch.
+ final Activity activity = getActivity(activityToken);
+ if (activity != null) {
+ // We don't allow split as primary for new launch because we currently only support
+ // launching to top. We allow split as primary for activity reparent because the
+ // activity may be split as primary before it is reparented out. In that case, we
+ // want to show it as primary again when it is reparented back.
+ if (!resolveActivityToContainer(wct, activity, true /* isOnReparent */)) {
// When there is no embedding rule matched, try to place it in the top container
// like a normal launch.
- targetContainer = taskContainer.getTopTaskFragmentContainer();
+ placeActivityInTopContainer(wct, activity);
}
- if (targetContainer == null) {
- return;
+ return;
+ }
+
+ final TaskContainer taskContainer = getTaskContainer(taskId);
+ if (taskContainer == null || taskContainer.isInPictureInPicture()) {
+ // We don't embed activity when it is in PIP.
+ return;
+ }
+
+ // If the activity belongs to a different app process, we treat it as starting new
+ // intent, since both actions might result in a new activity that should appear in an
+ // organized TaskFragment.
+ TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId,
+ activityIntent, null /* launchingActivity */);
+ if (targetContainer == null) {
+ // When there is no embedding rule matched, try to place it in the top container
+ // like a normal launch.
+ targetContainer = taskContainer.getTopTaskFragmentContainer();
+ }
+ if (targetContainer == null) {
+ return;
+ }
+ wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
+ activityToken);
+ // Because the activity does not belong to the organizer process, we wait until
+ // onTaskFragmentAppeared to trigger updateCallbackIfNecessary().
+ }
+
+ /**
+ * Called when the {@link WindowContainerTransaction} created with
+ * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side.
+ *
+ * @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
+ * @param errorCallbackToken token set in
+ * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)}
+ * @param taskFragmentInfo The {@link TaskFragmentInfo}. This could be {@code null} if no
+ * TaskFragment created.
+ * @param opType The {@link WindowContainerTransaction.HierarchyOp} of the failed
+ * transaction operation.
+ * @param exception exception from the server side.
+ */
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(container.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
+ @Nullable IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo,
+ int opType, @NonNull Throwable exception) {
+ Log.e(TAG, "onTaskFragmentError=" + exception.getMessage());
+ switch (opType) {
+ case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
+ case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: {
+ final TaskFragmentContainer container;
+ if (taskFragmentInfo != null) {
+ container = getContainer(taskFragmentInfo.getFragmentToken());
+ } else {
+ container = null;
+ }
+ if (container == null) {
+ break;
+ }
+
+ // Update the latest taskFragmentInfo and perform necessary clean-up
+ container.setInfo(wct, taskFragmentInfo);
+ container.clearPendingAppearedActivities();
+ if (container.isEmpty()) {
+ mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
+ }
+ break;
}
- wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
- activityToken);
- mPresenter.applyTransaction(wct);
- // Because the activity does not belong to the organizer process, we wait until
- // onTaskFragmentAppeared to trigger updateCallbackIfNecessary().
+ default:
+ Log.e(TAG, "onTaskFragmentError: taskFragmentInfo = " + taskFragmentInfo
+ + ", opType = " + opType);
}
}
- /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */
+ /** Called on receiving {@link #onTaskFragmentVanished} for cleanup. */
+ @GuardedBy("mLock")
private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) {
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
final TaskContainer taskContainer = mTaskContainers.valueAt(i);
@@ -306,14 +588,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
}
- private void onTaskConfigurationChanged(int taskId, @NonNull Configuration config) {
- final TaskContainer taskContainer = mTaskContainers.get(taskId);
- if (taskContainer == null) {
- return;
- }
+ @GuardedBy("mLock")
+ private void onTaskContainerInfoChanged(@NonNull TaskContainer taskContainer,
+ @NonNull Configuration config) {
final boolean wasInPip = taskContainer.isInPictureInPicture();
final boolean isInPIp = isInPictureInPicture(config);
- taskContainer.setWindowingMode(config.windowConfiguration.getWindowingMode());
// We need to check the animation override when enter/exit PIP or has bounds changed.
boolean shouldUpdateAnimationOverride = wasInPip != isInPIp;
@@ -331,32 +610,49 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Updates if we should override transition animation. We only want to override if the Task
* bounds is large enough for at least one split rule.
*/
+ @GuardedBy("mLock")
private void updateAnimationOverride(@NonNull TaskContainer taskContainer) {
- if (!taskContainer.isTaskBoundsInitialized()
- || !taskContainer.isWindowingModeInitialized()) {
+ if (ENABLE_SHELL_TRANSITIONS) {
+ // TODO(b/207070762): cleanup with legacy app transition
+ // Animation will be handled by WM Shell with Shell transition enabled.
+ return;
+ }
+ if (!taskContainer.isTaskBoundsInitialized()) {
// We don't know about the Task bounds/windowingMode yet.
return;
}
- // We only want to override if it supports split.
- if (supportSplit(taskContainer)) {
+ // We only want to override if the TaskContainer may show split.
+ if (mayShowSplit(taskContainer)) {
mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId());
} else {
mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId());
}
}
- private boolean supportSplit(@NonNull TaskContainer taskContainer) {
+ /** Returns whether the given {@link TaskContainer} may show in split. */
+ // Suppress GuardedBy warning because lint asks to mark this method as
+ // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @GuardedBy("mLock")
+ private boolean mayShowSplit(@NonNull TaskContainer taskContainer) {
// No split inside PIP.
if (taskContainer.isInPictureInPicture()) {
return false;
}
+ // Always assume the TaskContainer if SplitAttributesCalculator is set
+ if (mSplitAttributesCalculator != null) {
+ return true;
+ }
// Check if the parent container bounds can support any split rule.
for (EmbeddingRule rule : mSplitRules) {
if (!(rule instanceof SplitRule)) {
continue;
}
- if (shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) {
+ final SplitRule splitRule = (SplitRule) rule;
+ final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(
+ taskContainer.getTaskProperties(), splitRule, null /* minDimensionsPair */);
+ if (shouldShowSplit(splitAttributes)) {
return true;
}
}
@@ -364,10 +660,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
@VisibleForTesting
- void onActivityCreated(@NonNull Activity launchedActivity) {
+ @GuardedBy("mLock")
+ void onActivityCreated(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity launchedActivity) {
// TODO(b/229680885): we don't support launching into primary yet because we want to always
// launch the new activity on top.
- resolveActivityToContainer(launchedActivity, false /* isOnReparent */);
+ resolveActivityToContainer(wct, launchedActivity, false /* isOnReparent */);
updateCallbackIfNecessary();
}
@@ -381,7 +679,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* in a state that the caller shouldn't handle.
*/
@VisibleForTesting
- boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) {
+ @GuardedBy("mLock")
+ boolean resolveActivityToContainer(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity activity, boolean isOnReparent) {
if (isInPictureInPicture(activity) || activity.isFinishing()) {
// We don't embed activity when it is in PIP, or finishing. Return true since we don't
// want any extra handling.
@@ -389,7 +689,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
if (!isOnReparent && getContainerWithActivity(activity) == null
- && getInitialTaskFragmentToken(activity) != null) {
+ && getTaskFragmentTokenFromActivityClientRecord(activity) != null) {
// We can't find the new launched activity in any recorded container, but it is
// currently placed in an embedded TaskFragment. This can happen in two cases:
// 1. the activity is embedded in another app.
@@ -413,12 +713,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// 1. Whether the new launched activity should always expand.
if (shouldExpand(activity, null /* intent */)) {
- expandActivity(activity);
+ expandActivity(wct, activity);
return true;
}
// 2. Whether the new launched activity should launch a placeholder.
- if (launchPlaceholderIfNecessary(activity, !isOnReparent)) {
+ if (launchPlaceholderIfNecessary(wct, activity, !isOnReparent)) {
return true;
}
@@ -433,11 +733,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Can't find any activity below.
return false;
}
- if (putActivitiesIntoSplitIfNecessary(activityBelow, activity)) {
+ if (putActivitiesIntoSplitIfNecessary(wct, activityBelow, activity)) {
// Have split rule of [ activityBelow | launchedActivity ].
return true;
}
- if (isOnReparent && putActivitiesIntoSplitIfNecessary(activity, activityBelow)) {
+ if (isOnReparent && putActivitiesIntoSplitIfNecessary(wct, activity, activityBelow)) {
// Have split rule of [ launchedActivity | activityBelow].
return true;
}
@@ -460,19 +760,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Can't find the top activity on the other split TaskFragment.
return false;
}
- if (putActivitiesIntoSplitIfNecessary(otherTopActivity, activity)) {
+ if (putActivitiesIntoSplitIfNecessary(wct, otherTopActivity, activity)) {
// Have split rule of [ otherTopActivity | launchedActivity ].
return true;
}
// Have split rule of [ launchedActivity | otherTopActivity].
- return isOnReparent && putActivitiesIntoSplitIfNecessary(activity, otherTopActivity);
+ return isOnReparent && putActivitiesIntoSplitIfNecessary(wct, activity, otherTopActivity);
}
/**
* Places the given activity to the top most TaskFragment in the task if there is any.
*/
@VisibleForTesting
- void placeActivityInTopContainer(@NonNull Activity activity) {
+ void placeActivityInTopContainer(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity activity) {
if (getContainerWithActivity(activity) != null) {
// The activity has already been put in a TaskFragment. This is likely to be done by
// the server when the activity is started.
@@ -488,21 +789,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return;
}
targetContainer.addPendingAppearedActivity(activity);
- final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
activity.getActivityToken());
- mPresenter.applyTransaction(wct);
}
/**
* Starts an activity to side of the launchingActivity with the provided split config.
*/
- private void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(container.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @GuardedBy("mLock")
+ private void startActivityToSide(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity launchingActivity, @NonNull Intent intent,
@Nullable Bundle options, @NonNull SplitRule sideRule,
- @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) {
+ @NonNull SplitAttributes splitAttributes, @Nullable Consumer<Exception> failureCallback,
+ boolean isPlaceholder) {
try {
- mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule,
- isPlaceholder);
+ mPresenter.startActivityToSide(wct, launchingActivity, intent, options, sideRule,
+ splitAttributes, isPlaceholder);
} catch (Exception e) {
if (failureCallback != null) {
failureCallback.accept(e);
@@ -514,19 +819,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Expands the given activity by either expanding the TaskFragment it is currently in or putting
* it into a new expanded TaskFragment.
*/
- private void expandActivity(@NonNull Activity activity) {
+ @GuardedBy("mLock")
+ private void expandActivity(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity activity) {
final TaskFragmentContainer container = getContainerWithActivity(activity);
if (shouldContainerBeExpanded(container)) {
// Make sure that the existing container is expanded.
- mPresenter.expandTaskFragment(container.getTaskFragmentToken());
+ mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
} else {
// Put activity into a new expanded container.
final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity));
- mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity);
+ mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity);
}
}
/** Whether the given new launched activity is in a split with a rule matched. */
+ // Suppress GuardedBy warning because lint asks to mark this method as
+ // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @GuardedBy("mLock")
private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) {
final TaskFragmentContainer container = getContainerWithActivity(launchedActivity);
final SplitContainer splitContainer = getActiveSplitForContainer(container);
@@ -607,8 +918,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Checks if there is a rule to split the two activities. If there is one, puts them into split
* and returns {@code true}. Otherwise, returns {@code false}.
*/
- private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity,
- @NonNull Activity secondaryActivity) {
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @GuardedBy("mLock")
+ private boolean putActivitiesIntoSplitIfNecessary(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) {
final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity);
if (splitRule == null) {
return false;
@@ -616,8 +931,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final TaskFragmentContainer primaryContainer = getContainerWithActivity(
primaryActivity);
final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer);
+ final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity);
if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer()
- && canReuseContainer(splitRule, splitContainer.getSplitRule())) {
+ && canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics)) {
// Can launch in the existing secondary container if the rules share the same
// presentation.
final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
@@ -626,23 +942,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return true;
}
secondaryContainer.addPendingAppearedActivity(secondaryActivity);
- final WindowContainerTransaction wct = new WindowContainerTransaction();
if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
secondaryActivity, null /* secondaryIntent */)
!= RESULT_EXPAND_FAILED_NO_TF_INFO) {
wct.reparentActivityToTaskFragment(
secondaryContainer.getTaskFragmentToken(),
secondaryActivity.getActivityToken());
- mPresenter.applyTransaction(wct);
return true;
}
}
// Create new split pair.
- mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule);
+ mPresenter.createNewSplitContainer(wct, primaryActivity, secondaryActivity, splitRule);
return true;
}
- private void onActivityConfigurationChanged(@NonNull Activity activity) {
+ @GuardedBy("mLock")
+ private void onActivityConfigurationChanged(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity activity) {
if (activity.isFinishing()) {
// Do nothing if the activity is currently finishing.
return;
@@ -661,15 +977,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
// Check if activity requires a placeholder
- launchPlaceholderIfNecessary(activity, false /* isOnCreated */);
+ launchPlaceholderIfNecessary(wct, activity, false /* isOnCreated */);
}
@VisibleForTesting
+ @GuardedBy("mLock")
void onActivityDestroyed(@NonNull Activity activity) {
// Remove any pending appeared activity, as the server won't send finished activity to the
// organizer.
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
- mTaskContainers.valueAt(i).cleanupPendingAppearedActivity(activity);
+ mTaskContainers.valueAt(i).onActivityDestroyed(activity);
}
// We didn't trigger the callback if there were any pending appeared activities, so check
// again after the pending is removed.
@@ -680,8 +997,53 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Called when we have been waiting too long for the TaskFragment to become non-empty after
* creation.
*/
+ @GuardedBy("mLock")
void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) {
- mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ onTaskFragmentAppearEmptyTimeout(wct, container);
+ // Can be applied independently as a timeout callback.
+ mPresenter.applyTransaction(wct, getTransitionType(wct),
+ true /* shouldApplyIndependently */);
+ }
+
+ /**
+ * Called when we have been waiting too long for the TaskFragment to become non-empty after
+ * creation.
+ */
+ @GuardedBy("mLock")
+ void onTaskFragmentAppearEmptyTimeout(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer container) {
+ mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
+ }
+
+ @Nullable
+ @GuardedBy("mLock")
+ private TaskFragmentContainer resolveStartActivityIntentFromNonActivityContext(
+ @NonNull WindowContainerTransaction wct, @NonNull Intent intent) {
+ final int taskCount = mTaskContainers.size();
+ if (taskCount == 0) {
+ // We don't have other Activity to check split with.
+ return null;
+ }
+ if (taskCount > 1) {
+ Log.w(TAG, "App is calling startActivity from a non-Activity context when it has"
+ + " more than one Task. If the new launch Activity is in a different process,"
+ + " and it is expected to be embedded, please start it from an Activity"
+ + " instead.");
+ return null;
+ }
+
+ // Check whether the Intent should be embedded in the known Task.
+ final TaskContainer taskContainer = mTaskContainers.valueAt(0);
+ if (taskContainer.isInPictureInPicture()
+ || taskContainer.getTopNonFinishingActivity() == null) {
+ // We don't embed activity when it is in PIP, or if we can't find any other owner
+ // activity in the Task.
+ return null;
+ }
+
+ return resolveStartActivityIntent(wct, taskContainer.getTaskId(), intent,
+ null /* launchingActivity */);
}
/**
@@ -701,6 +1063,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
*/
@VisibleForTesting
@Nullable
+ @GuardedBy("mLock")
TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) {
/*
@@ -763,6 +1126,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
/**
* Returns an empty expanded {@link TaskFragmentContainer} that we can launch an activity into.
*/
+ @GuardedBy("mLock")
@Nullable
private TaskFragmentContainer createEmptyExpandedContainer(
@NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
@@ -793,6 +1157,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Returns a container for the new activity intent to launch into as splitting with the primary
* activity.
*/
+ @GuardedBy("mLock")
@Nullable
private TaskFragmentContainer getSecondaryContainerForSplitIfAny(
@NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
@@ -803,8 +1168,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity);
final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer);
+ final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity);
if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer()
- && (canReuseContainer(splitRule, splitContainer.getSplitRule())
+ && (canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics)
// TODO(b/231845476) we should always respect clearTop.
|| !respectClearTop)
&& mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
@@ -843,12 +1209,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return newContainer(pendingAppearedActivity, pendingAppearedActivity, taskId);
}
+ @GuardedBy("mLock")
TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity,
@NonNull Activity activityInTask, int taskId) {
return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */,
activityInTask, taskId);
}
+ @GuardedBy("mLock")
TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent,
@NonNull Activity activityInTask, int taskId) {
return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
@@ -865,13 +1233,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* if needed.
* @param taskId parent Task of the new TaskFragment.
*/
+ @GuardedBy("mLock")
TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity,
@Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) {
if (activityInTask == null) {
throw new IllegalArgumentException("activityInTask must not be null,");
}
if (!mTaskContainers.contains(taskId)) {
- mTaskContainers.put(taskId, new TaskContainer(taskId));
+ mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask));
}
final TaskContainer taskContainer = mTaskContainers.get(taskId);
final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity,
@@ -883,10 +1252,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
Log.w(TAG, "Can't find bounds from activity=" + activityInTask);
}
}
- if (!taskContainer.isWindowingModeInitialized()) {
- taskContainer.setWindowingMode(activityInTask.getResources().getConfiguration()
- .windowConfiguration.getWindowingMode());
- }
updateAnimationOverride(taskContainer);
return container;
}
@@ -895,12 +1260,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Creates and registers a new split with the provided containers and configuration. Finishes
* existing secondary containers if found for the given primary container.
*/
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @GuardedBy("mLock")
void registerSplit(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity,
@NonNull TaskFragmentContainer secondaryContainer,
- @NonNull SplitRule splitRule) {
+ @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes) {
final SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity,
- secondaryContainer, splitRule);
+ secondaryContainer, splitRule, splitAttributes);
// Remove container later to prevent pinning escaping toast showing in lock task mode.
if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) {
removeExistingSecondaryContainers(wct, primaryContainer);
@@ -909,6 +1278,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
/** Cleanups all the dependencies when the TaskFragment is entering PIP. */
+ @GuardedBy("mLock")
private void cleanupForEnterPip(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container) {
final TaskContainer taskContainer = container.getTaskContainer();
@@ -1022,9 +1392,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Updates the presentation of the container. If the container is part of the split or should
* have a placeholder, it will also update the other part of the split.
*/
+ @GuardedBy("mLock")
void updateContainer(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container) {
- if (launchPlaceholderIfNecessary(container)) {
+ if (launchPlaceholderIfNecessary(wct, container)) {
// Placeholder was launched, the positions will be updated when the activity is added
// to the secondary container.
return;
@@ -1049,7 +1420,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Skip position update - one or both containers are finished.
return;
}
- if (dismissPlaceholderIfNecessary(splitContainer)) {
+ final TaskContainer taskContainer = splitContainer.getTaskContainer();
+ final SplitRule splitRule = splitContainer.getSplitRule();
+ final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair();
+ final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(
+ taskContainer.getTaskProperties(), splitRule, minDimensionsPair);
+ splitContainer.setSplitAttributes(splitAttributes);
+ if (dismissPlaceholderIfNecessary(wct, splitContainer)) {
// Placeholder was finished, the positions will be updated when its container is emptied
return;
}
@@ -1111,16 +1488,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
/**
* Checks if the container requires a placeholder and launches it if necessary.
*/
- private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) {
+ @GuardedBy("mLock")
+ private boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer container) {
final Activity topActivity = container.getTopNonFinishingActivity();
if (topActivity == null) {
return false;
}
- return launchPlaceholderIfNecessary(topActivity, false /* isOnCreated */);
+ return launchPlaceholderIfNecessary(wct, topActivity, false /* isOnCreated */);
}
- boolean launchPlaceholderIfNecessary(@NonNull Activity activity, boolean isOnCreated) {
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @GuardedBy("mLock")
+ boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity activity, boolean isOnCreated) {
if (activity.isFinishing()) {
return false;
}
@@ -1144,18 +1528,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return false;
}
+ final TaskContainer.TaskProperties taskProperties = mPresenter.getTaskProperties(activity);
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity,
placeholderRule.getPlaceholderIntent());
- if (!shouldShowSideBySide(
- mPresenter.getParentContainerBounds(activity), placeholderRule,
- minDimensionsPair)) {
+ final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(taskProperties,
+ placeholderRule, minDimensionsPair);
+ if (!SplitPresenter.shouldShowSplit(splitAttributes)) {
return false;
}
// TODO(b/190433398): Handle failed request
final Bundle options = getPlaceholderOptions(activity, isOnCreated);
- startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), options,
- placeholderRule, null /* failureCallback */, true /* isPlaceholder */);
+ startActivityToSide(wct, activity, placeholderRule.getPlaceholderIntent(), options,
+ placeholderRule, splitAttributes, null /* failureCallback */,
+ true /* isPlaceholder */);
return true;
}
@@ -1180,8 +1566,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return options.toBundle();
}
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
@VisibleForTesting
- boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) {
+ @GuardedBy("mLock")
+ boolean dismissPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct,
+ @NonNull SplitContainer splitContainer) {
if (!splitContainer.isPlaceholderContainer()) {
return false;
}
@@ -1190,12 +1581,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// The placeholder should remain after it was first shown.
return false;
}
-
- if (shouldShowSideBySide(splitContainer)) {
+ final SplitAttributes splitAttributes = splitContainer.getSplitAttributes();
+ if (SplitPresenter.shouldShowSplit(splitAttributes)) {
return false;
}
-
- mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(),
+ mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(),
false /* shouldFinishDependent */);
return true;
}
@@ -1204,6 +1594,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Returns the rule to launch a placeholder for the activity with the provided component name
* if it is configured in the split config.
*/
+ @GuardedBy("mLock")
private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) {
for (EmbeddingRule rule : mSplitRules) {
if (!(rule instanceof SplitPlaceholderRule)) {
@@ -1220,6 +1611,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
/**
* Notifies listeners about changes to split states if necessary.
*/
+ @GuardedBy("mLock")
private void updateCallbackIfNecessary() {
if (mEmbeddingCallback == null) {
return;
@@ -1241,6 +1633,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* null, that indicates that the active split states are in an intermediate state and should
* not be reported.
*/
+ @GuardedBy("mLock")
@Nullable
private List<SplitInfo> getActiveSplitStates() {
List<SplitInfo> splitStates = new ArrayList<>();
@@ -1260,12 +1653,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final ActivityStack secondaryContainer = container.getSecondaryContainer()
.toActivityStack();
final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer,
- // Splits that are not showing side-by-side are reported as having 0 split
- // ratio, since by definition in the API the primary container occupies no
- // width of the split when covered by the secondary.
- shouldShowSideBySide(container)
- ? container.getSplitRule().getSplitRatio()
- : 0.0f);
+ container.getSplitAttributes());
splitStates.add(splitState);
}
}
@@ -1303,6 +1691,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Returns a split rule for the provided pair of primary activity and secondary activity intent
* if available.
*/
+ @GuardedBy("mLock")
@Nullable
private SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
@NonNull Intent secondaryActivityIntent) {
@@ -1321,6 +1710,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
/**
* Returns a split rule for the provided pair of primary and secondary activities if available.
*/
+ @GuardedBy("mLock")
@Nullable
private SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
@NonNull Activity secondaryActivity) {
@@ -1373,22 +1763,29 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return ActivityThread.currentActivityThread().getActivity(activityToken);
}
+ @VisibleForTesting
+ ActivityStartMonitor getActivityStartMonitor() {
+ return mActivityStartMonitor;
+ }
+
/**
- * Gets the token of the initial TaskFragment that embedded this activity. Do not rely on it
- * after creation because the activity could be reparented.
+ * Gets the token of the TaskFragment that embedded this activity. It is available as soon as
+ * the activity is created and attached, so it can be used during {@link #onActivityCreated}
+ * before the server notifies the organizer to avoid racing condition.
*/
@VisibleForTesting
@Nullable
- IBinder getInitialTaskFragmentToken(@NonNull Activity activity) {
+ IBinder getTaskFragmentTokenFromActivityClientRecord(@NonNull Activity activity) {
final ActivityThread.ActivityClientRecord record = ActivityThread.currentActivityThread()
.getActivityClient(activity.getActivityToken());
- return record != null ? record.mInitialTaskFragmentToken : null;
+ return record != null ? record.mTaskFragmentToken : null;
}
/**
* Returns {@code true} if an Activity with the provided component name should always be
* expanded to occupy full task bounds. Such activity must not be put in a split.
*/
+ @GuardedBy("mLock")
private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) {
for (EmbeddingRule rule : mSplitRules) {
if (!(rule instanceof ActivityRule)) {
@@ -1414,6 +1811,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* 'sticky' and the placeholder was finished when fully overlapping the primary container.
* @return {@code true} if the associated container should be retained (and not be finished).
*/
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @GuardedBy("mLock")
boolean shouldRetainAssociatedContainer(@NonNull TaskFragmentContainer finishingContainer,
@NonNull TaskFragmentContainer associatedContainer) {
SplitContainer splitContainer = getActiveSplitForContainers(associatedContainer,
@@ -1432,7 +1833,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
// Decide whether the associated container should be retained based on the current
// presentation mode.
- if (shouldShowSideBySide(splitContainer)) {
+ if (shouldShowSplit(splitContainer)) {
return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior);
} else {
return !shouldFinishAssociatedContainerWhenStacked(finishBehavior);
@@ -1456,10 +1857,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter {
@Override
- public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
+ public void onActivityPreCreated(@NonNull Activity activity,
+ @Nullable Bundle savedInstanceState) {
synchronized (mLock) {
final IBinder activityToken = activity.getActivityToken();
- final IBinder initialTaskFragmentToken = getInitialTaskFragmentToken(activity);
+ final IBinder initialTaskFragmentToken =
+ getTaskFragmentTokenFromActivityClientRecord(activity);
// If the activity is not embedded, then it will not have an initial task fragment
// token so no further action is needed.
if (initialTaskFragmentToken == null) {
@@ -1485,25 +1888,35 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
@Override
- public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) {
+ public void onActivityPostCreated(@NonNull Activity activity,
+ @Nullable Bundle savedInstanceState) {
// Calling after Activity#onCreate is complete to allow the app launch something
// first. In case of a configured placeholder activity we want to make sure
// that we don't launch it if an activity itself already requested something to be
// launched to side.
synchronized (mLock) {
- SplitController.this.onActivityCreated(activity);
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ SplitController.this.onActivityCreated(wct, activity);
+ // The WCT should be applied and merged to the activity launch transition.
+ mPresenter.applyTransaction(wct, getTransitionType(wct),
+ false /* shouldApplyIndependently */);
}
}
@Override
- public void onActivityConfigurationChanged(Activity activity) {
+ public void onActivityConfigurationChanged(@NonNull Activity activity) {
synchronized (mLock) {
- SplitController.this.onActivityConfigurationChanged(activity);
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ SplitController.this.onActivityConfigurationChanged(wct, activity);
+ // The WCT should be applied and merged to the Task change transition so that the
+ // placeholder is launched in the same transition.
+ mPresenter.applyTransaction(wct, getTransitionType(wct),
+ false /* shouldApplyIndependently */);
}
}
@Override
- public void onActivityPostDestroyed(Activity activity) {
+ public void onActivityPostDestroyed(@NonNull Activity activity) {
synchronized (mLock) {
SplitController.this.onActivityDestroyed(activity);
}
@@ -1515,7 +1928,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
private final Handler mHandler = new Handler(Looper.getMainLooper());
@Override
- public void execute(Runnable r) {
+ public void execute(@NonNull Runnable r) {
mHandler.post(r);
}
}
@@ -1524,39 +1937,78 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* A monitor that intercepts all activity start requests originating in the client process and
* can amend them to target a specific task fragment to form a split.
*/
- private class ActivityStartMonitor extends Instrumentation.ActivityMonitor {
+ @VisibleForTesting
+ class ActivityStartMonitor extends Instrumentation.ActivityMonitor {
+ @VisibleForTesting
+ Intent mCurrentIntent;
@Override
public Instrumentation.ActivityResult onStartActivity(@NonNull Context who,
@NonNull Intent intent, @NonNull Bundle options) {
- // TODO(b/190433398): Check if the activity is configured to always be expanded.
-
- // Check if activity should be put in a split with the activity that launched it.
- if (!(who instanceof Activity)) {
- return super.onStartActivity(who, intent, options);
- }
- final Activity launchingActivity = (Activity) who;
- if (isInPictureInPicture(launchingActivity)) {
- // We don't embed activity when it is in PIP.
- return super.onStartActivity(who, intent, options);
+ // TODO(b/232042367): Consolidate the activity create handling so that we can handle
+ // cross-process the same as normal.
+
+ final Activity launchingActivity;
+ if (who instanceof Activity) {
+ // We will check if the new activity should be split with the activity that launched
+ // it.
+ launchingActivity = (Activity) who;
+ if (isInPictureInPicture(launchingActivity)) {
+ // We don't embed activity when it is in PIP.
+ return super.onStartActivity(who, intent, options);
+ }
+ } else {
+ // When the context to start activity is not an Activity context, we will check if
+ // the new activity should be embedded in the known Task belonging to the organizer
+ // process. @see #resolveStartActivityIntentFromNonActivityContext
+ // It is a current security limitation that we can't access the activity info of
+ // other process even if it is in the same Task.
+ launchingActivity = null;
}
synchronized (mLock) {
- final int taskId = getTaskId(launchingActivity);
final WindowContainerTransaction wct = new WindowContainerTransaction();
- final TaskFragmentContainer launchedInTaskFragment = resolveStartActivityIntent(wct,
- taskId, intent, launchingActivity);
+ final TaskFragmentContainer launchedInTaskFragment;
+ if (launchingActivity != null) {
+ final int taskId = getTaskId(launchingActivity);
+ launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent,
+ launchingActivity);
+ } else {
+ launchedInTaskFragment = resolveStartActivityIntentFromNonActivityContext(wct,
+ intent);
+ }
if (launchedInTaskFragment != null) {
- mPresenter.applyTransaction(wct);
+ // Make sure the WCT is applied immediately instead of being queued so that the
+ // TaskFragment will be ready before activity attachment.
+ mPresenter.applyTransaction(wct, getTransitionType(wct),
+ false /* shouldApplyIndependently */);
// Amend the request to let the WM know that the activity should be placed in
// the dedicated container.
options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
launchedInTaskFragment.getTaskFragmentToken());
+ mCurrentIntent = intent;
}
}
return super.onStartActivity(who, intent, options);
}
+
+ @Override
+ public void onStartActivityResult(int result, @NonNull Bundle bOptions) {
+ super.onStartActivityResult(result, bOptions);
+ if (mCurrentIntent != null && result != START_SUCCESS) {
+ // Clear the pending appeared intent if the activity was not started successfully.
+ final IBinder token = bOptions.getBinder(
+ ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN);
+ if (token != null) {
+ final TaskFragmentContainer container = getContainer(token);
+ if (container != null) {
+ container.clearPendingAppearedIntentIfNeeded(mCurrentIntent);
+ }
+ }
+ }
+ mCurrentIntent = null;
+ }
}
/**
@@ -1574,29 +2026,40 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if
* there is any.
*/
- private static boolean canReuseContainer(SplitRule rule1, SplitRule rule2) {
+ private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2,
+ @NonNull WindowMetrics parentWindowMetrics) {
if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) {
return false;
}
- return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2);
+ return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2,
+ parentWindowMetrics);
}
/** Whether the two rules have the same presentation. */
- private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) {
+ @VisibleForTesting
+ static boolean haveSamePresentation(@NonNull SplitPairRule rule1,
+ @NonNull SplitPairRule rule2, @NonNull WindowMetrics parentWindowMetrics) {
+ if (rule1.getTag() != null || rule2.getTag() != null) {
+ // Tag must be unique if it is set. We don't want to reuse the container if the rules
+ // have different tags because they can have different SplitAttributes later through
+ // SplitAttributesCalculator.
+ return Objects.equals(rule1.getTag(), rule2.getTag());
+ }
+ // If both rules don't have tag, compare all SplitRules' properties that may affect their
+ // SplitAttributes.
// TODO(b/231655482): add util method to do the comparison in SplitPairRule.
- return rule1.getSplitRatio() == rule2.getSplitRatio()
- && rule1.getLayoutDirection() == rule2.getLayoutDirection()
- && rule1.getFinishPrimaryWithSecondary()
- == rule2.getFinishPrimaryWithSecondary()
- && rule1.getFinishSecondaryWithPrimary()
- == rule2.getFinishSecondaryWithPrimary();
+ return rule1.getDefaultSplitAttributes().equals(rule2.getDefaultSplitAttributes())
+ && rule1.checkParentMetrics(parentWindowMetrics)
+ == rule2.checkParentMetrics(parentWindowMetrics)
+ && rule1.getFinishPrimaryWithSecondary() == rule2.getFinishPrimaryWithSecondary()
+ && rule1.getFinishSecondaryWithPrimary() == rule2.getFinishSecondaryWithPrimary();
}
/**
* Whether it is ok for other rule to reuse the {@link TaskFragmentContainer} of the given
* rule.
*/
- private static boolean isContainerReusableRule(SplitRule rule) {
+ private static boolean isContainerReusableRule(@NonNull SplitRule rule) {
// We don't expect to reuse the placeholder rule.
if (!(rule instanceof SplitPairRule)) {
return false;
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 a89847a30d20..79603233ae14 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -22,11 +22,11 @@ import android.app.Activity;
import android.app.ActivityThread;
import android.app.WindowConfiguration;
import android.app.WindowConfiguration.WindowingMode;
-import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
@@ -38,12 +38,25 @@ import android.view.WindowInsets;
import android.view.WindowMetrics;
import android.window.WindowContainerTransaction;
+import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType;
+import androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams;
+import androidx.window.extensions.embedding.TaskContainer.TaskProperties;
+import androidx.window.extensions.layout.DisplayFeature;
+import androidx.window.extensions.layout.FoldingFeature;
+import androidx.window.extensions.layout.WindowLayoutInfo;
import com.android.internal.annotations.VisibleForTesting;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executor;
/**
@@ -65,11 +78,25 @@ 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;
+
+ @IntDef(value = {
+ CONTAINER_POSITION_LEFT,
+ CONTAINER_POSITION_TOP,
+ CONTAINER_POSITION_RIGHT,
+ CONTAINER_POSITION_BOTTOM,
+ })
+ private @interface ContainerPosition {}
+
/**
* Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
* Activity, Activity, Intent)}.
* No need to expand the splitContainer because screen is big enough to
- * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied.
+ * {@link #shouldShowSplit(SplitAttributes)} and minimum dimensions is
+ * satisfied.
*/
static final int RESULT_NOT_EXPANDED = 0;
/**
@@ -77,7 +104,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
* Activity, Activity, Intent)}.
* The splitContainer should be expanded. It is usually because minimum dimensions is not
* satisfied.
- * @see #shouldShowSideBySide(Rect, SplitRule, Pair)
+ * @see #shouldShowSplit(SplitAttributes)
*/
static final int RESULT_EXPANDED = 1;
/**
@@ -100,39 +127,26 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
})
private @interface ResultCode {}
+ @VisibleForTesting
+ static final SplitAttributes EXPAND_CONTAINERS_ATTRIBUTES =
+ new SplitAttributes.Builder()
+ .setSplitType(new ExpandContainersSplitType())
+ .build();
+
private final SplitController mController;
- SplitPresenter(@NonNull Executor executor, SplitController controller) {
+ SplitPresenter(@NonNull Executor executor, @NonNull SplitController controller) {
super(executor, controller);
mController = controller;
registerOrganizer();
}
/**
- * Updates the presentation of the provided container.
- */
- void updateContainer(@NonNull TaskFragmentContainer container) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- mController.updateContainer(wct, container);
- applyTransaction(wct);
- }
-
- /**
* Deletes the specified container and all other associated and dependent containers in the same
* transaction.
*/
- void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- cleanupContainer(container, shouldFinishDependent, wct);
- applyTransaction(wct);
- }
-
- /**
- * Deletes the specified container and all other associated and dependent containers in the same
- * transaction.
- */
- void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent,
- @NonNull WindowContainerTransaction wct) {
+ void cleanupContainer(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
container.finish(shouldFinishDependent, this, wct, mController);
final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer(
@@ -147,14 +161,17 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
* @return The newly created secondary container.
*/
@NonNull
+ @GuardedBy("mController.mLock")
TaskFragmentContainer createNewSplitWithEmptySideContainer(
@NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
@NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) {
- final Rect parentBounds = getParentContainerBounds(primaryActivity);
+ final TaskProperties taskProperties = getTaskProperties(primaryActivity);
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
primaryActivity, secondaryIntent);
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
- primaryActivity, minDimensionsPair);
+ final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule,
+ minDimensionsPair);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes);
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
primaryActivity, primaryRectBounds, null);
@@ -162,8 +179,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
final int taskId = primaryContainer.getTaskId();
final TaskFragmentContainer secondaryContainer = mController.newContainer(
secondaryIntent, primaryActivity, taskId);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds,
- rule, primaryActivity, minDimensionsPair);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
+ splitAttributes);
final int windowingMode = mController.getTaskContainer(taskId)
.getWindowingModeForSplitTaskFragment(secondaryRectBounds);
createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
@@ -172,9 +189,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
// Set adjacent to each other so that the containers below will be invisible.
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
- minDimensionsPair);
+ splitAttributes);
- mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
+ mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule,
+ splitAttributes);
return secondaryContainer;
}
@@ -190,25 +208,29 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
* created and the activity will be re-parented to it.
* @param rule The split rule to be applied to the container.
*/
- void createNewSplitContainer(@NonNull Activity primaryActivity,
- @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
-
- final Rect parentBounds = getParentContainerBounds(primaryActivity);
+ @GuardedBy("mController.mLock")
+ void createNewSplitContainer(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity,
+ @NonNull SplitPairRule rule) {
+ final TaskProperties taskProperties = getTaskProperties(primaryActivity);
final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity,
secondaryActivity);
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
- primaryActivity, minDimensionsPair);
+ final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule,
+ minDimensionsPair);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes);
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
primaryActivity, primaryRectBounds, null);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
- primaryActivity, minDimensionsPair);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
+ splitAttributes);
final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity(
secondaryActivity);
TaskFragmentContainer containerToAvoid = primaryContainer;
- if (rule.shouldClearTop() && curSecondaryContainer != null) {
- // Do not reuse the current TaskFragment if the rule is to clear top.
+ if (curSecondaryContainer != null
+ && (rule.shouldClearTop() || primaryContainer.isAbove(curSecondaryContainer))) {
+ // Do not reuse the current TaskFragment if the rule is to clear top, or if it is below
+ // the primary TaskFragment.
containerToAvoid = curSecondaryContainer;
}
final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
@@ -216,11 +238,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
// Set adjacent to each other so that the containers below will be invisible.
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
- minDimensionsPair);
-
- mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
+ splitAttributes);
- applyTransaction(wct);
+ mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule,
+ splitAttributes);
}
/**
@@ -262,15 +283,16 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
* @param rule The split rule to be applied to the container.
* @param isPlaceholder Whether the launch is a placeholder.
*/
- void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
- @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) {
- final Rect parentBounds = getParentContainerBounds(launchingActivity);
- final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
- launchingActivity, activityIntent);
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
- launchingActivity, minDimensionsPair);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
- launchingActivity, minDimensionsPair);
+ @GuardedBy("mController.mLock")
+ void startActivityToSide(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity launchingActivity, @NonNull Intent activityIntent,
+ @Nullable Bundle activityOptions, @NonNull SplitRule rule,
+ @NonNull SplitAttributes splitAttributes, boolean isPlaceholder) {
+ final TaskProperties taskProperties = getTaskProperties(launchingActivity);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
+ splitAttributes);
TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
launchingActivity);
@@ -284,9 +306,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
launchingActivity, taskId);
final int windowingMode = mController.getTaskContainer(taskId)
.getWindowingModeForSplitTaskFragment(primaryRectBounds);
- final WindowContainerTransaction wct = new WindowContainerTransaction();
mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
- rule);
+ rule, splitAttributes);
startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds,
launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds,
activityIntent, activityOptions, rule, windowingMode);
@@ -294,7 +315,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
// When placeholder is launched in split, we should keep the focus on the primary.
wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
}
- applyTransaction(wct);
}
/**
@@ -303,22 +323,24 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
* @param updatedContainer The task fragment that was updated and caused this split update.
* @param wct WindowContainerTransaction that this update should be performed with.
*/
+ @GuardedBy("mController.mLock")
void updateSplitContainer(@NonNull SplitContainer splitContainer,
@NonNull TaskFragmentContainer updatedContainer,
@NonNull WindowContainerTransaction wct) {
- // Getting the parent bounds using the updated container - it will have the recent value.
- final Rect parentBounds = getParentContainerBounds(updatedContainer);
+ // Getting the parent configuration using the updated container - it will have the recent
+ // value.
final SplitRule rule = splitContainer.getSplitRule();
final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer();
final Activity activity = primaryContainer.getTopNonFinishingActivity();
if (activity == null) {
return;
}
- final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair();
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
- activity, minDimensionsPair);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
- activity, minDimensionsPair);
+ final TaskProperties taskProperties = getTaskProperties(updatedContainer);
+ final SplitAttributes splitAttributes = splitContainer.getSplitAttributes();
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
+ splitAttributes);
final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
// Whether the placeholder is becoming side-by-side with the primary from fullscreen.
final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer()
@@ -330,7 +352,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds);
resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds);
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
- minDimensionsPair);
+ splitAttributes);
if (isPlaceholderBecomingSplit) {
// When placeholder is shown in split, we should keep the focus on the primary.
wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
@@ -342,14 +364,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
}
+ @GuardedBy("mController.mLock")
private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer primaryContainer,
@NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule,
- @NonNull Pair<Size, Size> minDimensionsPair) {
- final Rect parentBounds = getParentContainerBounds(primaryContainer);
+ @NonNull SplitAttributes splitAttributes) {
// Clear adjacent TaskFragments if the container is shown in fullscreen, or the
// secondaryContainer could not be finished.
- if (!shouldShowSideBySide(parentBounds, splitRule, minDimensionsPair)) {
+ if (!shouldShowSplit(splitAttributes)) {
setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
null /* secondary */, null /* splitRule */);
} else {
@@ -435,8 +457,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
* Expands the split container if the current split bounds are smaller than the Activity or
* Intent that is added to the container.
*
- * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)}
- * and if {@link android.window.TaskFragmentInfo} has reported to the client side.
+ * @return the {@link ResultCode} based on
+ * {@link #shouldShowSplit(SplitAttributes)} and if
+ * {@link android.window.TaskFragmentInfo} has reported to the client side.
*/
@ResultCode
int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct,
@@ -446,7 +469,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be"
+ " non-null.");
}
- final Rect taskBounds = getParentContainerBounds(primaryActivity);
final Pair<Size, Size> minDimensionsPair;
if (secondaryActivity != null) {
minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity);
@@ -455,7 +477,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
secondaryIntent);
}
// Expand the splitContainer if minimum dimensions are not satisfied.
- if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) {
+ final TaskContainer taskContainer = splitContainer.getTaskContainer();
+ final SplitAttributes splitAttributes = sanitizeSplitAttributes(
+ taskContainer.getTaskProperties(), splitContainer.getSplitAttributes(),
+ minDimensionsPair);
+ splitContainer.setSplitAttributes(splitAttributes);
+ if (!shouldShowSplit(splitAttributes)) {
// If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment
// bounds. Return failure to create a new SplitContainer which fills task bounds.
if (splitContainer.getPrimaryContainer().getInfo() == null
@@ -469,47 +496,74 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
return RESULT_NOT_EXPANDED;
}
- static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) {
- return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */);
+ static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
+ return shouldShowSplit(splitContainer.getSplitAttributes());
}
- static boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) {
- final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer());
-
- return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule(),
- splitContainer.getMinDimensionsPair());
+ static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) {
+ return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType);
}
- static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule,
- @Nullable Pair<Size, Size> minDimensionsPair) {
- // TODO(b/190433398): Supply correct insets.
- final WindowMetrics parentMetrics = new WindowMetrics(parentBounds,
- new WindowInsets(new Rect()));
- // Don't show side by side if bounds is not qualified.
- if (!rule.checkParentMetrics(parentMetrics)) {
- return false;
+ @GuardedBy("mController.mLock")
+ @NonNull
+ SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties,
+ @NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) {
+ final Configuration taskConfiguration = taskProperties.getConfiguration();
+ final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration);
+ final SplitAttributesCalculator calculator = mController.getSplitAttributesCalculator();
+ final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes();
+ final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics);
+ if (calculator == null) {
+ if (!isDefaultMinSizeSatisfied) {
+ return EXPAND_CONTAINERS_ATTRIBUTES;
+ }
+ return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes,
+ minDimensionsPair);
}
- final float splitRatio = rule.getSplitRatio();
- // We only care the size of the bounds regardless of its position.
- final Rect primaryBounds = getPrimaryBounds(parentBounds, splitRatio, true /* isLtr */);
- final Rect secondaryBounds = getSecondaryBounds(parentBounds, splitRatio, true /* isLtr */);
+ final WindowLayoutInfo windowLayoutInfo = mController.mWindowLayoutComponent
+ .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(),
+ taskConfiguration.windowConfiguration);
+ final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams(
+ taskWindowMetrics, taskConfiguration, defaultSplitAttributes,
+ isDefaultMinSizeSatisfied, windowLayoutInfo, rule.getTag());
+ final SplitAttributes splitAttributes = calculator.computeSplitAttributesForParams(params);
+ return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair);
+ }
+ /**
+ * Returns {@link #EXPAND_CONTAINERS_ATTRIBUTES} if the passed {@link SplitAttributes} doesn't
+ * meet the minimum dimensions set in {@link ActivityInfo.WindowLayout}. Otherwise, returns
+ * the passed {@link SplitAttributes}.
+ */
+ @NonNull
+ private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties,
+ @NonNull SplitAttributes splitAttributes,
+ @Nullable Pair<Size, Size> minDimensionsPair) {
if (minDimensionsPair == null) {
- return true;
+ return splitAttributes;
}
- return !boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first)
- && !boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second);
+ final FoldingFeature foldingFeature = getFoldingFeature(taskProperties);
+ final Configuration taskConfiguration = taskProperties.getConfiguration();
+ final Rect primaryBounds = getPrimaryBounds(taskConfiguration, splitAttributes,
+ foldingFeature);
+ final Rect secondaryBounds = getSecondaryBounds(taskConfiguration, splitAttributes,
+ foldingFeature);
+ if (boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first)
+ || boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second)) {
+ return EXPAND_CONTAINERS_ATTRIBUTES;
+ }
+ return splitAttributes;
}
@NonNull
- static Pair<Size, Size> getActivitiesMinDimensionsPair(Activity primaryActivity,
- Activity secondaryActivity) {
+ static Pair<Size, Size> getActivitiesMinDimensionsPair(@NonNull Activity primaryActivity,
+ @NonNull Activity secondaryActivity) {
return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity));
}
@NonNull
- static Pair<Size, Size> getActivityIntentMinDimensionsPair(Activity primaryActivity,
- Intent secondaryIntent) {
+ static Pair<Size, Size> getActivityIntentMinDimensionsPair(@NonNull Activity primaryActivity,
+ @NonNull Intent secondaryIntent) {
return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent));
}
@@ -560,20 +614,25 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@VisibleForTesting
@NonNull
- static Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds,
- @NonNull SplitRule rule, @NonNull Activity primaryActivity,
- @Nullable Pair<Size, Size> minDimensionsPair) {
- if (!shouldShowSideBySide(parentBounds, rule, minDimensionsPair)) {
+ Rect getBoundsForPosition(@Position int position, @NonNull TaskProperties taskProperties,
+ @NonNull SplitAttributes splitAttributes) {
+ final Configuration taskConfiguration = taskProperties.getConfiguration();
+ final FoldingFeature foldingFeature = getFoldingFeature(taskProperties);
+ final SplitType splitType = computeSplitType(splitAttributes, taskConfiguration,
+ foldingFeature);
+ final SplitAttributes computedSplitAttributes = new SplitAttributes.Builder()
+ .setSplitType(splitType)
+ .setLayoutDirection(splitAttributes.getLayoutDirection())
+ .build();
+ if (!shouldShowSplit(computedSplitAttributes)) {
return new Rect();
}
- final boolean isLtr = isLtr(primaryActivity, rule);
- final float splitRatio = rule.getSplitRatio();
-
switch (position) {
case POSITION_START:
- return getPrimaryBounds(parentBounds, splitRatio, isLtr);
+ return getPrimaryBounds(taskConfiguration, computedSplitAttributes, foldingFeature);
case POSITION_END:
- return getSecondaryBounds(parentBounds, splitRatio, isLtr);
+ return getSecondaryBounds(taskConfiguration, computedSplitAttributes,
+ foldingFeature);
case POSITION_FILL:
default:
return new Rect();
@@ -581,74 +640,303 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
}
@NonNull
- private static Rect getPrimaryBounds(@NonNull Rect parentBounds, float splitRatio,
- boolean isLtr) {
- return isLtr ? getLeftContainerBounds(parentBounds, splitRatio)
- : getRightContainerBounds(parentBounds, 1 - splitRatio);
+ private Rect getPrimaryBounds(@NonNull Configuration taskConfiguration,
+ @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ if (!shouldShowSplit(splitAttributes)) {
+ return new Rect();
+ }
+ switch (splitAttributes.getLayoutDirection()) {
+ case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: {
+ return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature);
+ }
+ case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: {
+ return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature);
+ }
+ case SplitAttributes.LayoutDirection.LOCALE: {
+ final boolean isLtr = taskConfiguration.getLayoutDirection()
+ == View.LAYOUT_DIRECTION_LTR;
+ return isLtr
+ ? getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature)
+ : getRightContainerBounds(taskConfiguration, splitAttributes,
+ foldingFeature);
+ }
+ case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: {
+ return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature);
+ }
+ case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: {
+ return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown layout direction:"
+ + splitAttributes.getLayoutDirection());
+ }
+ }
+
+ @NonNull
+ private Rect getSecondaryBounds(@NonNull Configuration taskConfiguration,
+ @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ if (!shouldShowSplit(splitAttributes)) {
+ return new Rect();
+ }
+ switch (splitAttributes.getLayoutDirection()) {
+ case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: {
+ return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature);
+ }
+ case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: {
+ return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature);
+ }
+ case SplitAttributes.LayoutDirection.LOCALE: {
+ final boolean isLtr = taskConfiguration.getLayoutDirection()
+ == View.LAYOUT_DIRECTION_LTR;
+ return isLtr
+ ? getRightContainerBounds(taskConfiguration, splitAttributes,
+ foldingFeature)
+ : getLeftContainerBounds(taskConfiguration, splitAttributes,
+ foldingFeature);
+ }
+ case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: {
+ return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature);
+ }
+ case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: {
+ return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown layout direction:"
+ + splitAttributes.getLayoutDirection());
+ }
+ }
+
+ @NonNull
+ private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration,
+ @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
+ CONTAINER_POSITION_LEFT, foldingFeature);
+ final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds();
+ return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom);
}
@NonNull
- private static Rect getSecondaryBounds(@NonNull Rect parentBounds, float splitRatio,
- boolean isLtr) {
- return isLtr ? getRightContainerBounds(parentBounds, splitRatio)
- : getLeftContainerBounds(parentBounds, 1 - splitRatio);
+ private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration,
+ @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
+ CONTAINER_POSITION_RIGHT, foldingFeature);
+ final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
+ return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom);
}
- private static Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
- return new Rect(
- parentBounds.left,
- parentBounds.top,
- (int) (parentBounds.left + parentBounds.width() * splitRatio),
- parentBounds.bottom);
+ @NonNull
+ private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration,
+ @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
+ CONTAINER_POSITION_TOP, foldingFeature);
+ final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
+ return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom);
}
- private static Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
- return new Rect(
- (int) (parentBounds.left + parentBounds.width() * splitRatio),
- parentBounds.top,
- parentBounds.right,
- parentBounds.bottom);
+ @NonNull
+ private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration,
+ @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
+ CONTAINER_POSITION_BOTTOM, foldingFeature);
+ final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
+ return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom);
}
/**
- * Checks if a split with the provided rule should be displays in left-to-right layout
- * direction, either always or with the current configuration.
+ * Computes the boundary position between the primary and the secondary containers for the given
+ * {@link ContainerPosition} with {@link SplitAttributes}, current window and device states.
+ * <ol>
+ * <li>For {@link #CONTAINER_POSITION_TOP}, it computes the boundary with the bottom
+ * container, which is {@link Rect#bottom} of the top container bounds.</li>
+ * <li>For {@link #CONTAINER_POSITION_BOTTOM}, it computes the boundary with the top
+ * container, which is {@link Rect#top} of the bottom container bounds.</li>
+ * <li>For {@link #CONTAINER_POSITION_LEFT}, it computes the boundary with the right
+ * container, which is {@link Rect#right} of the left container bounds.</li>
+ * <li>For {@link #CONTAINER_POSITION_RIGHT}, it computes the boundary with the bottom
+ * container, which is {@link Rect#left} of the right container bounds.</li>
+ * </ol>
+ *
+ * @see #getTopContainerBounds(Configuration, SplitAttributes, FoldingFeature)
+ * @see #getBottomContainerBounds(Configuration, SplitAttributes, FoldingFeature)
+ * @see #getLeftContainerBounds(Configuration, SplitAttributes, FoldingFeature)
+ * @see #getRightContainerBounds(Configuration, SplitAttributes, FoldingFeature)
*/
- private static boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) {
- switch (rule.getLayoutDirection()) {
- case LayoutDirection.LOCALE:
- return context.getResources().getConfiguration().getLayoutDirection()
- == View.LAYOUT_DIRECTION_LTR;
- case LayoutDirection.RTL:
- return false;
- case LayoutDirection.LTR:
+ private int computeBoundaryBetweenContainers(@NonNull Configuration taskConfiguration,
+ @NonNull SplitAttributes splitAttributes, @ContainerPosition int position,
+ @Nullable FoldingFeature foldingFeature) {
+ final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
+ final int startPoint = shouldSplitHorizontally(splitAttributes)
+ ? parentBounds.top
+ : parentBounds.left;
+ final int dimen = shouldSplitHorizontally(splitAttributes)
+ ? parentBounds.height()
+ : parentBounds.width();
+ final SplitType splitType = splitAttributes.getSplitType();
+ if (splitType instanceof RatioSplitType) {
+ final RatioSplitType splitRatio = (RatioSplitType) splitType;
+ return (int) (startPoint + dimen * splitRatio.getRatio());
+ }
+ // At this point, SplitType must be a HingeSplitType and foldingFeature must be
+ // non-null. RatioSplitType and ExpandContainerSplitType have been handled earlier.
+ Objects.requireNonNull(foldingFeature);
+ if (!(splitType instanceof HingeSplitType)) {
+ throw new IllegalArgumentException("Unknown splitType:" + splitType);
+ }
+ final Rect hingeArea = foldingFeature.getBounds();
+ switch (position) {
+ case CONTAINER_POSITION_LEFT:
+ return hingeArea.left;
+ case CONTAINER_POSITION_TOP:
+ return hingeArea.top;
+ case CONTAINER_POSITION_RIGHT:
+ return hingeArea.right;
+ case CONTAINER_POSITION_BOTTOM:
+ return hingeArea.bottom;
default:
+ throw new IllegalArgumentException("Unknown position:" + position);
+ }
+ }
+
+ @Nullable
+ private FoldingFeature getFoldingFeature(@NonNull TaskProperties taskProperties) {
+ final int displayId = taskProperties.getDisplayId();
+ final WindowConfiguration windowConfiguration = taskProperties.getConfiguration()
+ .windowConfiguration;
+ final WindowLayoutInfo info = mController.mWindowLayoutComponent
+ .getCurrentWindowLayoutInfo(displayId, windowConfiguration);
+ final List<DisplayFeature> displayFeatures = info.getDisplayFeatures();
+ if (displayFeatures.isEmpty()) {
+ return null;
+ }
+ final List<FoldingFeature> foldingFeatures = new ArrayList<>();
+ for (DisplayFeature displayFeature : displayFeatures) {
+ if (displayFeature instanceof FoldingFeature) {
+ foldingFeatures.add((FoldingFeature) displayFeature);
+ }
+ }
+ // TODO(b/240219484): Support device with multiple hinges.
+ if (foldingFeatures.size() != 1) {
+ return null;
+ }
+ return foldingFeatures.get(0);
+ }
+
+ /**
+ * Indicates that this {@link SplitAttributes} splits the task horizontally. Returns
+ * {@code false} if this {@link SplitAttributes} splits the task vertically.
+ */
+ private static boolean shouldSplitHorizontally(SplitAttributes splitAttributes) {
+ switch (splitAttributes.getLayoutDirection()) {
+ case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
+ case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Computes the {@link SplitType} with the {@link SplitAttributes} and the current device and
+ * window state.
+ * If passed {@link SplitAttributes#getSplitType} is a {@link RatioSplitType}. It reversed
+ * the ratio if the computed {@link SplitAttributes#getLayoutDirection} is
+ * {@link SplitAttributes.LayoutDirection.LEFT_TO_RIGHT} or
+ * {@link SplitAttributes.LayoutDirection.BOTTOM_TO_TOP} to make the bounds calculation easier.
+ * If passed {@link SplitAttributes#getSplitType} is a {@link HingeSplitType}, it checks
+ * the current device and window states to determine whether the split container should split
+ * by hinge or use {@link HingeSplitType#getFallbackSplitType}.
+ */
+ 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;
+ } else if (splitType instanceof RatioSplitType) {
+ final RatioSplitType splitRatio = (RatioSplitType) splitType;
+ // 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;
+ }
+ }
+ } else if (splitType instanceof HingeSplitType) {
+ final HingeSplitType hinge = (HingeSplitType) splitType;
+ @WindowingMode
+ final int windowingMode = taskConfiguration.windowConfiguration.getWindowingMode();
+ return shouldSplitByHinge(splitAttributes, foldingFeature, windowingMode)
+ ? hinge : hinge.getFallbackSplitType();
+ }
+ throw new IllegalArgumentException("Unknown SplitType:" + splitType);
+ }
+
+ private static boolean shouldSplitByHinge(@NonNull SplitAttributes splitAttributes,
+ @Nullable FoldingFeature foldingFeature, @WindowingMode int taskWindowingMode) {
+ // Only HingeSplitType may split the task bounds by hinge.
+ if (!(splitAttributes.getSplitType() instanceof HingeSplitType)) {
+ return false;
+ }
+ // Device is not foldable, so there's no hinge to match.
+ if (foldingFeature == null) {
+ return false;
+ }
+ // The task is in multi-window mode. Match hinge doesn't make sense because current task
+ // bounds may not fit display bounds.
+ if (WindowConfiguration.inMultiWindowMode(taskWindowingMode)) {
+ return false;
}
+ // Return true if how the split attributes split the task bounds matches the orientation of
+ // folding area orientation.
+ return shouldSplitHorizontally(splitAttributes) == isFoldingAreaHorizontal(foldingFeature);
+ }
+
+ private static boolean isFoldingAreaHorizontal(@NonNull FoldingFeature foldingFeature) {
+ final Rect bounds = foldingFeature.getBounds();
+ return bounds.width() > bounds.height();
}
@NonNull
- static Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
- return container.getTaskContainer().getTaskBounds();
+ static TaskProperties getTaskProperties(@NonNull TaskFragmentContainer container) {
+ return container.getTaskContainer().getTaskProperties();
}
@NonNull
- Rect getParentContainerBounds(@NonNull Activity activity) {
- final TaskFragmentContainer container = mController.getContainerWithActivity(activity);
- if (container != null) {
- return getParentContainerBounds(container);
+ TaskProperties getTaskProperties(@NonNull Activity activity) {
+ final TaskContainer taskContainer = mController.getTaskContainer(
+ mController.getTaskId(activity));
+ if (taskContainer != null) {
+ return taskContainer.getTaskProperties();
}
- // Obtain bounds from Activity instead because the Activity hasn't been embedded yet.
- return getNonEmbeddedActivityBounds(activity);
+ // Use a copy of configuration because activity's configuration may be updated later,
+ // or we may get unexpected TaskContainer's configuration if Activity's configuration is
+ // updated. An example is Activity is going to be in split.
+ return new TaskProperties(activity.getDisplayId(),
+ new Configuration(activity.getResources().getConfiguration()));
}
- /**
- * Obtains the bounds from a non-embedded Activity.
- * <p>
- * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most
- * cases unless we want to obtain task bounds before
- * {@link TaskContainer#isTaskBoundsInitialized()}.
- */
+ @NonNull
+ WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) {
+ return getTaskWindowMetrics(getTaskProperties(activity).getConfiguration());
+ }
+
+ @NonNull
+ private static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) {
+ final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds();
+ // TODO(b/190433398): Supply correct insets.
+ return new WindowMetrics(taskBounds, WindowInsets.CONSUMED);
+ }
+
+ /** Obtains the bounds from a non-embedded Activity. */
@NonNull
static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) {
final WindowConfiguration windowConfiguration =
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 0ea5603b1f3d..91573ffef568 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -21,15 +21,19 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.WindowConfiguration;
import android.app.WindowConfiguration.WindowingMode;
+import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.IBinder;
import android.util.ArraySet;
import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -41,13 +45,10 @@ class TaskContainer {
/** The unique task id. */
private final int mTaskId;
+ // TODO(b/240219484): consolidate to mConfiguration
/** Available window bounds of this Task. */
private final Rect mTaskBounds = new Rect();
- /** Windowing mode of this Task. */
- @WindowingMode
- private int mWindowingMode = WINDOWING_MODE_UNDEFINED;
-
/** Active TaskFragments in this Task. */
@NonNull
final List<TaskFragmentContainer> mContainers = new ArrayList<>();
@@ -56,24 +57,56 @@ class TaskContainer {
@NonNull
final List<SplitContainer> mSplitContainers = new ArrayList<>();
+ @NonNull
+ private final Configuration mConfiguration;
+
+ private int mDisplayId;
+
+ private boolean mIsVisible;
+
/**
* TaskFragments that the organizer has requested to be closed. They should be removed when
- * the organizer receives {@link SplitController#onTaskFragmentVanished(TaskFragmentInfo)} event
- * for them.
+ * the organizer receives
+ * {@link SplitController#onTaskFragmentVanished(WindowContainerTransaction, TaskFragmentInfo)}
+ * event for them.
*/
final Set<IBinder> mFinishedContainer = new ArraySet<>();
- TaskContainer(int taskId) {
+ /**
+ * The {@link TaskContainer} constructor
+ *
+ * @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) {
throw new IllegalArgumentException("Invalid Task id");
}
mTaskId = taskId;
+ // Make a copy in case the activity's config is updated, and updates the TaskContainer's
+ // config unexpectedly.
+ mConfiguration = new Configuration(activityInTask.getResources().getConfiguration());
+ mDisplayId = activityInTask.getDisplayId();
+ // Note that it is always called when there's a new Activity is started, which implies
+ // the host task is visible.
+ mIsVisible = true;
}
int getTaskId() {
return mTaskId;
}
+ int getDisplayId() {
+ return mDisplayId;
+ }
+
+ boolean isVisible() {
+ return mIsVisible;
+ }
+
@NonNull
Rect getTaskBounds() {
return mTaskBounds;
@@ -93,13 +126,21 @@ class TaskContainer {
return !mTaskBounds.isEmpty();
}
- void setWindowingMode(int windowingMode) {
- mWindowingMode = windowingMode;
+ @NonNull
+ Configuration getConfiguration() {
+ // Make a copy in case the config is updated unexpectedly.
+ return new Configuration(mConfiguration);
}
- /** Whether the Task windowing mode has been initialized. */
- boolean isWindowingModeInitialized() {
- return mWindowingMode != WINDOWING_MODE_UNDEFINED;
+ @NonNull
+ TaskProperties getTaskProperties() {
+ return new TaskProperties(mDisplayId, mConfiguration);
+ }
+
+ void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) {
+ mConfiguration.setTo(info.getConfiguration());
+ mDisplayId = info.getDisplayId();
+ mIsVisible = info.isVisibleRequested();
}
/**
@@ -122,13 +163,20 @@ class TaskContainer {
// DecorCaptionView won't work correctly. As a result, have the TaskFragment to be in the
// Task windowing mode if the Task is in multi window.
// TODO we won't need this anymore after we migrate Freeform caption to WM Shell.
- return WindowConfiguration.inMultiWindowMode(mWindowingMode)
- ? mWindowingMode
- : WINDOWING_MODE_MULTI_WINDOW;
+ return isInMultiWindow() ? getWindowingMode() : WINDOWING_MODE_MULTI_WINDOW;
}
boolean isInPictureInPicture() {
- return mWindowingMode == WINDOWING_MODE_PINNED;
+ return getWindowingMode() == WINDOWING_MODE_PINNED;
+ }
+
+ boolean isInMultiWindow() {
+ return WindowConfiguration.inMultiWindowMode(getWindowingMode());
+ }
+
+ @WindowingMode
+ private int getWindowingMode() {
+ return getConfiguration().windowConfiguration.getWindowingMode();
}
/** Whether there is any {@link TaskFragmentContainer} below this Task. */
@@ -136,6 +184,13 @@ class TaskContainer {
return mContainers.isEmpty() && mFinishedContainer.isEmpty();
}
+ /** Called when the activity is destroyed. */
+ void onActivityDestroyed(@NonNull Activity activity) {
+ for (TaskFragmentContainer container : mContainers) {
+ container.onActivityDestroyed(activity);
+ }
+ }
+
/** Removes the pending appeared activity from all TaskFragments in this Task. */
void cleanupPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) {
for (TaskFragmentContainer container : mContainers) {
@@ -161,4 +216,32 @@ class TaskContainer {
}
return null;
}
+
+ int indexOf(@NonNull TaskFragmentContainer child) {
+ return mContainers.indexOf(child);
+ }
+
+ /**
+ * A wrapper class which contains the display ID and {@link Configuration} of a
+ * {@link TaskContainer}
+ */
+ static final class TaskProperties {
+ private final int mDisplayId;
+ @NonNull
+ private final Configuration mConfiguration;
+
+ TaskProperties(int displayId, @NonNull Configuration configuration) {
+ mDisplayId = displayId;
+ mConfiguration = configuration;
+ }
+
+ int getDisplayId() {
+ return mDisplayId;
+ }
+
+ @NonNull
+ Configuration getConfiguration() {
+ return mConfiguration;
+ }
+ }
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
index cdee9e386b33..af5d8c561874 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
@@ -16,7 +16,6 @@
package androidx.window.extensions.embedding;
-import static android.graphics.Matrix.MSCALE_X;
import static android.graphics.Matrix.MTRANS_X;
import static android.graphics.Matrix.MTRANS_Y;
@@ -41,30 +40,44 @@ class TaskFragmentAnimationAdapter {
*/
private static final int LAYER_NO_OVERRIDE = -1;
+ @NonNull
final Animation mAnimation;
+ @NonNull
final RemoteAnimationTarget mTarget;
+ @NonNull
final SurfaceControl mLeash;
+ /** Area in absolute coordinate that the animation surface shouldn't go beyond. */
+ @NonNull
+ private final Rect mWholeAnimationBounds = new Rect();
+ @NonNull
final Transformation mTransformation = new Transformation();
+ @NonNull
final float[] mMatrix = new float[9];
+ @NonNull
final float[] mVecs = new float[4];
+ @NonNull
final Rect mRect = new Rect();
private boolean mIsFirstFrame = true;
private int mOverrideLayer = LAYER_NO_OVERRIDE;
TaskFragmentAnimationAdapter(@NonNull Animation animation,
@NonNull RemoteAnimationTarget target) {
- this(animation, target, target.leash);
+ this(animation, target, target.leash, target.screenSpaceBounds);
}
/**
* @param leash the surface to animate.
+ * @param wholeAnimationBounds area in absolute coordinate that the animation surface shouldn't
+ * go beyond.
*/
TaskFragmentAnimationAdapter(@NonNull Animation animation,
- @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash) {
+ @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash,
+ @NonNull Rect wholeAnimationBounds) {
mAnimation = animation;
mTarget = target;
mLeash = leash;
+ mWholeAnimationBounds.set(wholeAnimationBounds);
}
/**
@@ -94,23 +107,32 @@ class TaskFragmentAnimationAdapter {
/** To be overridden by subclasses to adjust the animation surface change. */
void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ // Update the surface position and alpha.
mTransformation.getMatrix().postTranslate(
mTarget.localBounds.left, mTarget.localBounds.top);
t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
t.setAlpha(mLeash, mTransformation.getAlpha());
- // Get current animation position.
+
+ // Get current surface bounds in absolute coordinate.
+ // positionX/Y are in local coordinate, so minus the local offset to get the slide amount.
final int positionX = Math.round(mMatrix[MTRANS_X]);
final int positionY = Math.round(mMatrix[MTRANS_Y]);
- // The exiting surface starts at position: mTarget.localBounds and moves with
- // positionX varying. Offset our crop region by the amount we have slided so crop
- // regions stays exactly on the original container in split.
- final int cropOffsetX = mTarget.localBounds.left - positionX;
- final int cropOffsetY = mTarget.localBounds.top - positionY;
- final Rect cropRect = new Rect();
- cropRect.set(mTarget.localBounds);
- // Because window crop uses absolute position.
- cropRect.offsetTo(0, 0);
- cropRect.offset(cropOffsetX, cropOffsetY);
+ final Rect cropRect = new Rect(mTarget.screenSpaceBounds);
+ final Rect localBounds = mTarget.localBounds;
+ cropRect.offset(positionX - localBounds.left, positionY - localBounds.top);
+
+ // Store the current offset of the surface top left from (0,0) in absolute coordinate.
+ final int offsetX = cropRect.left;
+ final int offsetY = cropRect.top;
+
+ // Intersect to make sure the animation happens within the whole animation bounds.
+ if (!cropRect.intersect(mWholeAnimationBounds)) {
+ // Hide the surface when it is outside of the animation area.
+ t.setAlpha(mLeash, 0);
+ }
+
+ // cropRect is in absolute coordinate, so we need to translate it to surface top left.
+ cropRect.offset(-offsetX, -offsetY);
t.setCrop(mLeash, cropRect);
}
@@ -124,52 +146,6 @@ class TaskFragmentAnimationAdapter {
}
/**
- * Should be used when the {@link RemoteAnimationTarget} is in split with others, and want to
- * animate together as one. This adapter will offset the animation leash to make the animate of
- * two windows look like a single window.
- */
- static class SplitAdapter extends TaskFragmentAnimationAdapter {
- private final boolean mIsLeftHalf;
- private final int mWholeAnimationWidth;
-
- /**
- * @param isLeftHalf whether this is the left half of the animation.
- * @param wholeAnimationWidth the whole animation windows width.
- */
- SplitAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target,
- boolean isLeftHalf, int wholeAnimationWidth) {
- super(animation, target);
- mIsLeftHalf = isLeftHalf;
- mWholeAnimationWidth = wholeAnimationWidth;
- if (wholeAnimationWidth == 0) {
- throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth");
- }
- }
-
- @Override
- void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
- float posX = mTarget.localBounds.left;
- final float posY = mTarget.localBounds.top;
- // This window is half of the whole animation window. Offset left/right to make it
- // look as one with the other half.
- mTransformation.getMatrix().getValues(mMatrix);
- final int targetWidth = mTarget.localBounds.width();
- final float scaleX = mMatrix[MSCALE_X];
- final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2;
- final float curOffset = targetWidth * (1 - scaleX) / 2;
- final float offsetDiff = totalOffset - curOffset;
- if (mIsLeftHalf) {
- posX += offsetDiff;
- } else {
- posX -= offsetDiff;
- }
- mTransformation.getMatrix().postTranslate(posX, posY);
- t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
- t.setAlpha(mLeash, mTransformation.getAlpha());
- }
- }
-
- /**
* Should be used for the animation of the snapshot of a {@link RemoteAnimationTarget} that has
* size change.
*/
@@ -177,7 +153,7 @@ class TaskFragmentAnimationAdapter {
SnapshotAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) {
// Start leash is the snapshot of the starting surface.
- super(animation, target, target.startLeash);
+ super(animation, target, target.startLeash, target.screenSpaceBounds);
}
@Override
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
index f721341a3647..ee2e139bb0b2 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
@@ -30,6 +30,8 @@ import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationDefinition;
import android.window.TaskFragmentOrganizer;
+import androidx.annotation.NonNull;
+
import com.android.internal.annotations.VisibleForTesting;
/** Controls the TaskFragment remote animations. */
@@ -45,7 +47,7 @@ class TaskFragmentAnimationController {
/** Task Ids that we have registered for remote animation. */
private final ArraySet<Integer> mRegisterTasks = new ArraySet<>();
- TaskFragmentAnimationController(TaskFragmentOrganizer organizer) {
+ TaskFragmentAnimationController(@NonNull TaskFragmentOrganizer organizer) {
mOrganizer = organizer;
mDefinition = new RemoteAnimationDefinition();
final RemoteAnimationAdapter animationAdapter =
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index c4f37091a491..8c416e881059 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -112,6 +112,7 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
}
/** Creates the animator given the transition type and windows. */
+ @NonNull
private Animator createAnimator(@WindowManager.TransitionOldType int transit,
@NonNull RemoteAnimationTarget[] targets,
@NonNull IRemoteAnimationFinishedCallback finishedCallback) {
@@ -161,6 +162,7 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
}
/** List of {@link TaskFragmentAnimationAdapter} to handle animations on all window targets. */
+ @NonNull
private List<TaskFragmentAnimationAdapter> createAnimationAdapters(
@WindowManager.TransitionOldType int transit,
@NonNull RemoteAnimationTarget[] targets) {
@@ -180,12 +182,14 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
}
}
+ @NonNull
private List<TaskFragmentAnimationAdapter> createOpenAnimationAdapters(
@NonNull RemoteAnimationTarget[] targets) {
return createOpenCloseAnimationAdapters(targets, true /* isOpening */,
mAnimationSpec::loadOpenAnimation);
}
+ @NonNull
private List<TaskFragmentAnimationAdapter> createCloseAnimationAdapters(
@NonNull RemoteAnimationTarget[] targets) {
return createOpenCloseAnimationAdapters(targets, false /* isOpening */,
@@ -196,6 +200,7 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
* Creates {@link TaskFragmentAnimationAdapter} for OPEN and CLOSE types of transition.
* @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type.
*/
+ @NonNull
private List<TaskFragmentAnimationAdapter> createOpenCloseAnimationAdapters(
@NonNull RemoteAnimationTarget[] targets, boolean isOpening,
@NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider) {
@@ -208,10 +213,10 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
for (RemoteAnimationTarget target : targets) {
if (target.mode != MODE_CLOSING) {
openingTargets.add(target);
- openingWholeScreenBounds.union(target.localBounds);
+ openingWholeScreenBounds.union(target.screenSpaceBounds);
} else {
closingTargets.add(target);
- closingWholeScreenBounds.union(target.localBounds);
+ closingWholeScreenBounds.union(target.screenSpaceBounds);
}
}
@@ -238,27 +243,17 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
return adapters;
}
+ @NonNull
private TaskFragmentAnimationAdapter createOpenCloseAnimationAdapter(
@NonNull RemoteAnimationTarget target,
@NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider,
@NonNull Rect wholeAnimationBounds) {
final Animation animation = animationProvider.apply(target, wholeAnimationBounds);
- final Rect targetBounds = target.localBounds;
- if (targetBounds.left == wholeAnimationBounds.left
- && targetBounds.right != wholeAnimationBounds.right) {
- // This is the left split of the whole animation window.
- return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target,
- true /* isLeftHalf */, wholeAnimationBounds.width());
- } else if (targetBounds.left != wholeAnimationBounds.left
- && targetBounds.right == wholeAnimationBounds.right) {
- // This is the right split of the whole animation window.
- return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target,
- false /* isLeftHalf */, wholeAnimationBounds.width());
- }
- // Open/close window that fills the whole animation.
- return new TaskFragmentAnimationAdapter(animation, target);
+ return new TaskFragmentAnimationAdapter(animation, target, target.leash,
+ wholeAnimationBounds);
}
+ @NonNull
private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters(
@NonNull RemoteAnimationTarget[] targets) {
final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
index 586ac1f212a1..ef5ea563de12 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
@@ -26,6 +26,7 @@ import android.graphics.Rect;
import android.os.Handler;
import android.provider.Settings;
import android.view.RemoteAnimationTarget;
+import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
@@ -68,16 +69,14 @@ class TaskFragmentAnimationSpec {
// The transition animation should be adjusted based on the developer option.
final ContentResolver resolver = mContext.getContentResolver();
- mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
- Settings.Global.TRANSITION_ANIMATION_SCALE,
- mContext.getResources().getFloat(
- R.dimen.config_appTransitionAnimationDurationScaleDefault));
+ mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
resolver.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false,
new SettingsObserver(handler));
}
/** For target that doesn't need to be animated. */
+ @NonNull
static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) {
// Noop but just keep the target showing/hiding.
final float alpha = target.mode == MODE_CLOSING ? 0f : 1f;
@@ -85,6 +84,7 @@ class TaskFragmentAnimationSpec {
}
/** Animation for target that is opening in a change transition. */
+ @NonNull
Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) {
final Rect bounds = target.localBounds;
// The target will be animated in from left or right depends on its position.
@@ -101,6 +101,7 @@ class TaskFragmentAnimationSpec {
}
/** Animation for target that is closing in a change transition. */
+ @NonNull
Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) {
final Rect bounds = target.localBounds;
// The target will be animated out to left or right depends on its position.
@@ -121,6 +122,7 @@ class TaskFragmentAnimationSpec {
* @return the return array always has two elements. The first one is for the start leash, and
* the second one is for the end leash.
*/
+ @NonNull
Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) {
// Both start bounds and end bounds are in screen coordinates. We will post translate
// to the local coordinates in TaskFragmentAnimationAdapter#onAnimationUpdate
@@ -177,30 +179,60 @@ class TaskFragmentAnimationSpec {
return new Animation[]{startSet, endSet};
}
+ @NonNull
Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target,
@NonNull Rect wholeAnimationBounds) {
final boolean isEnter = target.mode != MODE_CLOSING;
- final Animation animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
- ? com.android.internal.R.anim.task_fragment_open_enter
- : com.android.internal.R.anim.task_fragment_open_exit);
- animation.initialize(target.localBounds.width(), target.localBounds.height(),
+ final Animation animation;
+ // Background color on TaskDisplayArea has already been set earlier in
+ // WindowContainer#getAnimationAdapter.
+ if (target.showBackdrop) {
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? com.android.internal.R.anim.task_fragment_clear_top_open_enter
+ : com.android.internal.R.anim.task_fragment_clear_top_open_exit);
+ } else {
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? com.android.internal.R.anim.task_fragment_open_enter
+ : com.android.internal.R.anim.task_fragment_open_exit);
+ }
+ // Use the whole animation bounds instead of the change bounds, so that when multiple change
+ // targets are opening at the same time, the animation applied to each will be the same.
+ // Otherwise, we may see gap between the activities that are launching together.
+ animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
wholeAnimationBounds.width(), wholeAnimationBounds.height());
animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
return animation;
}
+ @NonNull
Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target,
@NonNull Rect wholeAnimationBounds) {
final boolean isEnter = target.mode != MODE_CLOSING;
- final Animation animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
- ? com.android.internal.R.anim.task_fragment_close_enter
- : com.android.internal.R.anim.task_fragment_close_exit);
- animation.initialize(target.localBounds.width(), target.localBounds.height(),
+ final Animation animation;
+ if (target.showBackdrop) {
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? com.android.internal.R.anim.task_fragment_clear_top_close_enter
+ : com.android.internal.R.anim.task_fragment_clear_top_close_exit);
+ } else {
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? com.android.internal.R.anim.task_fragment_close_enter
+ : com.android.internal.R.anim.task_fragment_close_exit);
+ }
+ // Use the whole animation bounds instead of the change bounds, so that when multiple change
+ // targets are closing at the same time, the animation applied to each will be the same.
+ // Otherwise, we may see gap between the activities that are finishing together.
+ animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
wholeAnimationBounds.width(), wholeAnimationBounds.height());
animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
return animation;
}
+ private float getTransitionAnimationScaleSetting() {
+ return WindowManager.fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+ Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
+ R.dimen.config_appTransitionAnimationDurationScaleDefault)));
+ }
+
private class SettingsObserver extends ContentObserver {
SettingsObserver(@NonNull Handler handler) {
super(handler);
@@ -208,9 +240,7 @@ class TaskFragmentAnimationSpec {
@Override
public void onChange(boolean selfChange) {
- mTransitionAnimationScaleSetting = Settings.Global.getFloat(
- mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE,
- mTransitionAnimationScaleSetting);
+ mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
}
}
}
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 abf32a26efa2..18712aed1be6 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -18,9 +18,8 @@ package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.Activity;
+import android.app.ActivityThread;
import android.app.WindowConfiguration.WindowingMode;
import android.content.Intent;
import android.graphics.Rect;
@@ -30,6 +29,10 @@ import android.util.Size;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
@@ -175,6 +178,7 @@ class TaskFragmentContainer {
&& mInfo.getActivities().size() == collectNonFinishingActivities().size();
}
+ @NonNull
ActivityStack toActivityStack() {
return new ActivityStack(collectNonFinishingActivities(), isEmpty());
}
@@ -187,17 +191,72 @@ class TaskFragmentContainer {
// Remove the pending activity from other TaskFragments.
mTaskContainer.cleanupPendingAppearedActivity(pendingAppearedActivity);
mPendingAppearedActivities.add(pendingAppearedActivity);
+ updateActivityClientRecordTaskFragmentToken(pendingAppearedActivity);
+ }
+
+ /**
+ * Updates the {@link ActivityThread.ActivityClientRecord#mTaskFragmentToken} for the
+ * activity. This makes sure the token is up-to-date if the activity is relaunched later.
+ */
+ private void updateActivityClientRecordTaskFragmentToken(@NonNull Activity activity) {
+ final ActivityThread.ActivityClientRecord record = ActivityThread
+ .currentActivityThread().getActivityClient(activity.getActivityToken());
+ if (record != null) {
+ record.mTaskFragmentToken = mToken;
+ }
}
void removePendingAppearedActivity(@NonNull Activity pendingAppearedActivity) {
mPendingAppearedActivities.remove(pendingAppearedActivity);
}
+ void clearPendingAppearedActivities() {
+ final List<Activity> cleanupActivities = new ArrayList<>(mPendingAppearedActivities);
+ // Clear mPendingAppearedActivities so that #getContainerWithActivity won't return the
+ // current TaskFragment.
+ mPendingAppearedActivities.clear();
+ mPendingAppearedIntent = null;
+
+ // For removed pending activities, we need to update the them to their previous containers.
+ for (Activity activity : cleanupActivities) {
+ final TaskFragmentContainer curContainer = mController.getContainerWithActivity(
+ activity);
+ if (curContainer != null) {
+ curContainer.updateActivityClientRecordTaskFragmentToken(activity);
+ }
+ }
+ }
+
+ /** Called when the activity is destroyed. */
+ void onActivityDestroyed(@NonNull Activity activity) {
+ removePendingAppearedActivity(activity);
+ if (mInfo != null) {
+ // Remove the activity now because there can be a delay before the server callback.
+ mInfo.getActivities().remove(activity.getActivityToken());
+ }
+ }
+
@Nullable
Intent getPendingAppearedIntent() {
return mPendingAppearedIntent;
}
+ void setPendingAppearedIntent(@Nullable Intent intent) {
+ mPendingAppearedIntent = intent;
+ }
+
+ /**
+ * Clears the pending appeared Intent if it is the same as given Intent. Otherwise, the
+ * pending appeared Intent is cleared when TaskFragmentInfo is set and is not empty (has
+ * running activities).
+ */
+ void clearPendingAppearedIntentIfNeeded(@NonNull Intent intent) {
+ if (mPendingAppearedIntent == null || mPendingAppearedIntent != intent) {
+ return;
+ }
+ mPendingAppearedIntent = null;
+ }
+
boolean hasActivity(@NonNull IBinder token) {
if (mInfo != null && mInfo.getActivities().contains(token)) {
return true;
@@ -228,15 +287,26 @@ class TaskFragmentContainer {
return mInfo;
}
- void setInfo(@NonNull TaskFragmentInfo info) {
+ @GuardedBy("mController.mLock")
+ void setInfo(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentInfo info) {
if (!mIsFinished && mInfo == null && info.isEmpty()) {
- // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if it is
- // still empty after timeout.
- mAppearEmptyTimeout = () -> {
+ // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if no
+ // pending appeared intent/activities. Otherwise, wait and removing the TaskFragment if
+ // it is still empty after timeout.
+ if (mPendingAppearedIntent != null || !mPendingAppearedActivities.isEmpty()) {
+ mAppearEmptyTimeout = () -> {
+ synchronized (mController.mLock) {
+ mAppearEmptyTimeout = null;
+ // Call without the pass-in wct when timeout. We need to applyWct directly
+ // in this case.
+ mController.onTaskFragmentAppearEmptyTimeout(this);
+ }
+ };
+ mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
+ } else {
mAppearEmptyTimeout = null;
- mController.onTaskFragmentAppearEmptyTimeout(this);
- };
- mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
+ mController.onTaskFragmentAppearEmptyTimeout(wct, this);
+ }
} else if (mAppearEmptyTimeout != null && !info.isEmpty()) {
mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
mAppearEmptyTimeout = null;
@@ -362,7 +432,7 @@ class TaskFragmentContainer {
// In case we have requested to reparent the activity to another container (as
// pendingAppeared), we don't want to finish it with this container.
&& mController.getContainerWithActivity(activity) == this) {
- activity.finish();
+ wct.finishActivity(activity.getActivityToken());
}
}
@@ -387,7 +457,7 @@ class TaskFragmentContainer {
|| controller.shouldRetainAssociatedActivity(this, activity)) {
continue;
}
- activity.finish();
+ wct.finishActivity(activity.getActivityToken());
}
mActivitiesToFinishOnExit.clear();
}
@@ -470,6 +540,18 @@ class TaskFragmentContainer {
return new Size(maxMinWidth, maxMinHeight);
}
+ /** Whether the current TaskFragment is above the {@code other} TaskFragment. */
+ boolean isAbove(@NonNull TaskFragmentContainer other) {
+ if (mTaskContainer != other.mTaskContainer) {
+ throw new IllegalArgumentException(
+ "Trying to compare two TaskFragments in different Task.");
+ }
+ if (this == other) {
+ throw new IllegalArgumentException("Trying to compare a TaskFragment with itself.");
+ }
+ return mTaskContainer.indexOf(this) > mTaskContainer.indexOf(other);
+ }
+
@Override
public String toString() {
return toString(true /* includeContainersToFinishOnExit */);
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 c1d1c8e8d4e0..c76f568e117f 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -20,23 +20,26 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
+import static androidx.window.util.ExtensionHelper.isZero;
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.ActivityManager;
-import android.app.ActivityManager.AppTask;
+import android.app.ActivityClient;
import android.app.Application;
import android.app.WindowConfiguration;
+import android.content.ComponentCallbacks;
import android.content.Context;
+import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
import android.util.ArrayMap;
-import android.util.Log;
+import android.window.WindowContext;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiContext;
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
@@ -44,9 +47,9 @@ import androidx.window.common.RawFoldingFeatureProducer;
import androidx.window.util.DataProducer;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
@@ -61,12 +64,17 @@ import java.util.function.Consumer;
public class WindowLayoutComponentImpl implements WindowLayoutComponent {
private static final String TAG = "SampleExtension";
- private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
+ private final Map<Context, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
new ArrayMap<>();
private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer;
- public WindowLayoutComponentImpl(Context context) {
+ private final List<CommonFoldingFeature> mLastReportedFoldingFeatures = new ArrayList<>();
+
+ private final Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners =
+ new ArrayMap<>();
+
+ public WindowLayoutComponentImpl(@NonNull Context context) {
((Application) context.getApplicationContext())
.registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged());
RawFoldingFeatureProducer foldingFeatureProducer = new RawFoldingFeatureProducer(context);
@@ -75,46 +83,84 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
}
+ /** Registers to listen to {@link CommonFoldingFeature} changes */
+ public void addFoldingStateChangedCallback(Consumer<List<CommonFoldingFeature>> consumer) {
+ mFoldingFeatureProducer.addDataChangedCallback(consumer);
+ }
+
/**
* Adds a listener interested in receiving updates to {@link WindowLayoutInfo}
*
* @param activity hosting a {@link android.view.Window}
* @param consumer interested in receiving updates to {@link WindowLayoutInfo}
*/
+ @Override
public void addWindowLayoutInfoListener(@NonNull Activity activity,
@NonNull Consumer<WindowLayoutInfo> consumer) {
- mWindowLayoutChangeListeners.put(activity, consumer);
- onDisplayFeaturesChanged();
+ addWindowLayoutInfoListener((Context) activity, consumer);
}
/**
- * Removes a listener no longer interested in receiving updates.
- *
- * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo}
+ * Similar to {@link #addWindowLayoutInfoListener(Activity, Consumer)}, but takes a UI Context
+ * as a parameter.
*/
- public void removeWindowLayoutInfoListener(
+ // TODO(b/204073440): Add @Override to hook the API in WM extensions library.
+ public void addWindowLayoutInfoListener(@NonNull @UiContext Context context,
@NonNull Consumer<WindowLayoutInfo> consumer) {
- mWindowLayoutChangeListeners.values().remove(consumer);
- onDisplayFeaturesChanged();
+ if (mWindowLayoutChangeListeners.containsKey(context)
+ || mWindowLayoutChangeListeners.containsValue(consumer)) {
+ // Early return if the listener or consumer has been registered.
+ return;
+ }
+ if (!context.isUiContext()) {
+ throw new IllegalArgumentException("Context must be a UI Context, which should be"
+ + " an Activity or a WindowContext");
+ }
+ mFoldingFeatureProducer.getData((features) -> {
+ // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer.
+ WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features);
+ consumer.accept(newWindowLayout);
+ });
+ mWindowLayoutChangeListeners.put(context, consumer);
+
+ if (context instanceof WindowContext) {
+ final IBinder windowContextToken = context.getWindowContextToken();
+ final WindowContextConfigListener listener =
+ new WindowContextConfigListener(windowContextToken);
+ context.registerComponentCallbacks(listener);
+ mWindowContextConfigListeners.put(windowContextToken, listener);
+ }
}
- void updateWindowLayout(@NonNull Activity activity,
- @NonNull WindowLayoutInfo newLayout) {
- Consumer<WindowLayoutInfo> consumer = mWindowLayoutChangeListeners.get(activity);
- if (consumer != null) {
- consumer.accept(newLayout);
+ /**
+ * Removes a listener no longer interested in receiving updates.
+ *
+ * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo}
+ */
+ @Override
+ public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) {
+ for (Context context : mWindowLayoutChangeListeners.keySet()) {
+ if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) {
+ continue;
+ }
+ if (context instanceof WindowContext) {
+ final IBinder token = context.getWindowContextToken();
+ context.unregisterComponentCallbacks(mWindowContextConfigListeners.get(token));
+ mWindowContextConfigListeners.remove(token);
+ }
+ break;
}
+ mWindowLayoutChangeListeners.values().remove(consumer);
}
@NonNull
- Set<Activity> getActivitiesListeningForLayoutChanges() {
+ Set<Context> getContextsListeningForLayoutChanges() {
return mWindowLayoutChangeListeners.keySet();
}
- @NonNull
private boolean isListeningForLayoutChanges(IBinder token) {
- for (Activity activity: getActivitiesListeningForLayoutChanges()) {
- if (token.equals(activity.getWindow().getAttributes().token)) {
+ for (Context context: getContextsListeningForLayoutChanges()) {
+ if (token.equals(Context.getToken(context))) {
return true;
}
}
@@ -128,12 +174,12 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
/**
* A convenience method to translate from the common feature state to the extensions feature
* state. More specifically, translates from {@link CommonFoldingFeature.State} to
- * {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED}. If it is not
+ * {@link FoldingFeature#STATE_FLAT} or {@link FoldingFeature#STATE_HALF_OPENED}. If it is not
* possible to translate, then we will return a {@code null} value.
*
* @param state if it matches a value in {@link CommonFoldingFeature.State}, {@code null}
- * otherwise. @return a {@link FoldingFeature.STATE_FLAT} or
- * {@link FoldingFeature.STATE_HALF_OPENED} if the given state matches a value in
+ * otherwise. @return a {@link FoldingFeature#STATE_FLAT} or
+ * {@link FoldingFeature#STATE_HALF_OPENED} if the given state matches a value in
* {@link CommonFoldingFeature.State} and {@code null} otherwise.
*/
@Nullable
@@ -147,17 +193,48 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
}
}
- private void onDisplayFeaturesChanged() {
- for (Activity activity : getActivitiesListeningForLayoutChanges()) {
- WindowLayoutInfo newLayout = getWindowLayoutInfo(activity);
- updateWindowLayout(activity, newLayout);
+ private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) {
+ mLastReportedFoldingFeatures.clear();
+ mLastReportedFoldingFeatures.addAll(storedFeatures);
+ for (Context context : getContextsListeningForLayoutChanges()) {
+ // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer.
+ Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(context);
+ WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, storedFeatures);
+ layoutConsumer.accept(newWindowLayout);
}
}
+ /**
+ * Translates the {@link DisplayFeature} into a {@link WindowLayoutInfo} when a
+ * valid state is found.
+ * @param context a proxy for the {@link android.view.Window} that contains the
+ * {@link DisplayFeature}.
+ */
+ private WindowLayoutInfo getWindowLayoutInfo(@NonNull @UiContext Context context,
+ List<CommonFoldingFeature> storedFeatures) {
+ List<DisplayFeature> displayFeatureList = getDisplayFeatures(context, storedFeatures);
+ return new WindowLayoutInfo(displayFeatureList);
+ }
+
+ /**
+ * Gets the current {@link WindowLayoutInfo} computed with passed {@link WindowConfiguration}.
+ *
+ * @return current {@link WindowLayoutInfo} on the default display. Returns
+ * empty {@link WindowLayoutInfo} on secondary displays.
+ */
@NonNull
- private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
- List<DisplayFeature> displayFeatures = getDisplayFeatures(activity);
- return new WindowLayoutInfo(displayFeatures);
+ public WindowLayoutInfo getCurrentWindowLayoutInfo(int displayId,
+ @NonNull WindowConfiguration windowConfiguration) {
+ return getWindowLayoutInfo(displayId, windowConfiguration, mLastReportedFoldingFeatures);
+ }
+
+ /** @see #getWindowLayoutInfo(Context, List) */
+ private WindowLayoutInfo getWindowLayoutInfo(int displayId,
+ @NonNull WindowConfiguration windowConfiguration,
+ List<CommonFoldingFeature> storedFeatures) {
+ List<DisplayFeature> displayFeatureList = getDisplayFeatures(displayId, windowConfiguration,
+ storedFeatures);
+ return new WindowLayoutInfo(displayFeatureList);
}
/**
@@ -173,97 +250,111 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
* bounds are not valid, constructing a {@link FoldingFeature} will throw an
* {@link IllegalArgumentException} since this can cause negative UI effects down stream.
*
- * @param activity a proxy for the {@link android.view.Window} that contains the
+ * @param context a proxy for the {@link android.view.Window} that contains the
* {@link DisplayFeature}.
- * @return a {@link List} of valid {@link DisplayFeature} that
* are within the {@link android.view.Window} of the {@link Activity}
*/
- private List<DisplayFeature> getDisplayFeatures(@NonNull Activity activity) {
+ private List<DisplayFeature> getDisplayFeatures(
+ @NonNull @UiContext Context context, List<CommonFoldingFeature> storedFeatures) {
+ if (!shouldReportDisplayFeatures(context)) {
+ return Collections.emptyList();
+ }
+ return getDisplayFeatures(context.getDisplayId(),
+ context.getResources().getConfiguration().windowConfiguration,
+ storedFeatures);
+ }
+
+ /** @see #getDisplayFeatures(Context, List) */
+ private List<DisplayFeature> getDisplayFeatures(int displayId,
+ @NonNull WindowConfiguration windowConfiguration,
+ List<CommonFoldingFeature> storedFeatures) {
List<DisplayFeature> features = new ArrayList<>();
- int displayId = activity.getDisplay().getDisplayId();
if (displayId != DEFAULT_DISPLAY) {
- Log.w(TAG, "This sample doesn't support display features on secondary displays");
return features;
}
- if (isTaskInMultiWindowMode(activity)) {
- // 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 features;
- }
+ for (CommonFoldingFeature baseFeature : storedFeatures) {
+ Integer state = convertToExtensionState(baseFeature.getState());
+ if (state == null) {
+ continue;
+ }
+ Rect featureRect = baseFeature.getRect();
+ rotateRectToDisplayRotation(displayId, featureRect);
+ transformToWindowSpaceRect(windowConfiguration, featureRect);
- Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData();
- if (storedFeatures.isPresent()) {
- for (CommonFoldingFeature baseFeature : storedFeatures.get()) {
- Integer state = convertToExtensionState(baseFeature.getState());
- if (state == null) {
- continue;
- }
- Rect featureRect = baseFeature.getRect();
- rotateRectToDisplayRotation(displayId, featureRect);
- transformToWindowSpaceRect(activity, featureRect);
-
- if (!isRectZero(featureRect)) {
- // TODO(b/228641877) Remove guarding if when fixed.
- features.add(new FoldingFeature(featureRect, baseFeature.getType(), state));
- }
+ if (!isZero(featureRect)) {
+ // TODO(b/228641877): Remove guarding when fixed.
+ features.add(new FoldingFeature(featureRect, baseFeature.getType(), state));
}
}
return features;
}
/**
- * Checks whether the task associated with the activity is in multi-window. If task info is not
- * available it defaults to {@code true}.
+ * Checks whether display features should be reported for the activity.
+ * TODO(b/238948678): Support reporting display features in all windowing modes.
*/
- private boolean isTaskInMultiWindowMode(@NonNull Activity activity) {
- final ActivityManager am = activity.getSystemService(ActivityManager.class);
- if (am == null) {
- return true;
+ private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) {
+ int displayId = context.getDisplay().getDisplayId();
+ if (displayId != DEFAULT_DISPLAY) {
+ // Display features are not supported on secondary displays.
+ return false;
}
-
- final List<AppTask> appTasks = am.getAppTasks();
- final int taskId = activity.getTaskId();
- AppTask task = null;
- for (AppTask t : appTasks) {
- if (t.getTaskInfo().taskId == taskId) {
- task = t;
- break;
- }
+ final int windowingMode;
+ if (context instanceof Activity) {
+ windowingMode = ActivityClient.getInstance().getTaskWindowingMode(
+ context.getActivityToken());
+ } else {
+ // TODO(b/242674941): use task windowing mode for window context that associates with
+ // activity.
+ windowingMode = context.getResources().getConfiguration().windowConfiguration
+ .getWindowingMode();
}
- if (task == null) {
- // The task might be removed on the server already.
- return true;
+ if (windowingMode == -1) {
+ // If we cannot determine the task windowing mode for any reason, it is likely that we
+ // won't be able to determine its position correctly as well. DisplayFeatures' bounds
+ // in this case can't be computed correctly, so we should skip.
+ return false;
}
- return WindowConfiguration.inMultiWindowMode(task.getTaskInfo().getWindowingMode());
+ // 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 !WindowConfiguration.inMultiWindowMode(windowingMode);
}
- /**
- * Returns {@link true} if a {@link Rect} has zero width and zero height,
- * {@code false} otherwise.
- */
- private boolean isRectZero(Rect rect) {
- return rect.width() == 0 && rect.height() == 0;
+ private void onDisplayFeaturesChangedIfListening(@NonNull IBinder token) {
+ if (isListeningForLayoutChanges(token)) {
+ mFoldingFeatureProducer.getData(
+ WindowLayoutComponentImpl.this::onDisplayFeaturesChanged);
+ }
}
private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
super.onActivityCreated(activity, savedInstanceState);
- onDisplayFeaturesChangedIfListening(activity);
+ onDisplayFeaturesChangedIfListening(activity.getActivityToken());
}
@Override
public void onActivityConfigurationChanged(Activity activity) {
super.onActivityConfigurationChanged(activity);
- onDisplayFeaturesChangedIfListening(activity);
+ onDisplayFeaturesChangedIfListening(activity.getActivityToken());
}
+ }
- private void onDisplayFeaturesChangedIfListening(Activity activity) {
- IBinder token = activity.getWindow().getAttributes().token;
- if (token == null || isListeningForLayoutChanges(token)) {
- onDisplayFeaturesChanged();
- }
+ private final class WindowContextConfigListener implements ComponentCallbacks {
+ final IBinder mToken;
+
+ WindowContextConfigListener(IBinder token) {
+ mToken = token;
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ onDisplayFeaturesChangedIfListening(mToken);
}
+
+ @Override
+ public void onLowMemory() {}
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
index 970f0a2af632..5bfb0ebdcaa8 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
@@ -28,41 +28,42 @@ import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
-import android.util.Log;
import androidx.annotation.NonNull;
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
import androidx.window.common.RawFoldingFeatureProducer;
-import androidx.window.util.DataProducer;
+import androidx.window.util.BaseDataProducer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.Optional;
/**
* Reference implementation of androidx.window.sidecar OEM interface for use with
* WindowManager Jetpack.
*/
class SampleSidecarImpl extends StubSidecar {
- private static final String TAG = "SampleSidecar";
-
- private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer;
-
+ private List<CommonFoldingFeature> mStoredFeatures = new ArrayList<>();
SampleSidecarImpl(Context context) {
((Application) context.getApplicationContext())
.registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged());
- DataProducer<String> settingsFeatureProducer = new RawFoldingFeatureProducer(context);
- mFoldingFeatureProducer = new DeviceStateManagerFoldingFeatureProducer(context,
- settingsFeatureProducer);
+ BaseDataProducer<String> settingsFeatureProducer = new RawFoldingFeatureProducer(context);
+ BaseDataProducer<List<CommonFoldingFeature>> foldingFeatureProducer =
+ new DeviceStateManagerFoldingFeatureProducer(context,
+ settingsFeatureProducer);
- mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
+ foldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
}
- private void onDisplayFeaturesChanged() {
+ private void setStoredFeatures(List<CommonFoldingFeature> storedFeatures) {
+ mStoredFeatures = storedFeatures;
+ }
+
+ private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) {
+ setStoredFeatures(storedFeatures);
updateDeviceState(getDeviceState());
for (IBinder windowToken : getWindowsListeningForLayoutChanges()) {
SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken);
@@ -79,16 +80,16 @@ class SampleSidecarImpl extends StubSidecar {
}
private int deviceStateFromFeature() {
- List<CommonFoldingFeature> storedFeatures = mFoldingFeatureProducer.getData()
- .orElse(Collections.emptyList());
- for (int i = 0; i < storedFeatures.size(); i++) {
- CommonFoldingFeature feature = storedFeatures.get(i);
+ 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;
@@ -109,7 +110,6 @@ class SampleSidecarImpl extends StubSidecar {
private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) {
int displayId = activity.getDisplay().getDisplayId();
if (displayId != DEFAULT_DISPLAY) {
- Log.w(TAG, "This sample doesn't support display features on secondary displays");
return Collections.emptyList();
}
@@ -119,18 +119,15 @@ class SampleSidecarImpl extends StubSidecar {
return Collections.emptyList();
}
- Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData();
List<SidecarDisplayFeature> features = new ArrayList<>();
- if (storedFeatures.isPresent()) {
- for (CommonFoldingFeature baseFeature : storedFeatures.get()) {
- SidecarDisplayFeature feature = new SidecarDisplayFeature();
- Rect featureRect = baseFeature.getRect();
- rotateRectToDisplayRotation(displayId, featureRect);
- transformToWindowSpaceRect(activity, featureRect);
- feature.setRect(featureRect);
- feature.setType(baseFeature.getType());
- features.add(feature);
- }
+ for (CommonFoldingFeature baseFeature : mStoredFeatures) {
+ SidecarDisplayFeature feature = new SidecarDisplayFeature();
+ Rect featureRect = baseFeature.getRect();
+ rotateRectToDisplayRotation(displayId, featureRect);
+ transformToWindowSpaceRect(activity, featureRect);
+ feature.setRect(featureRect);
+ feature.setType(baseFeature.getType());
+ features.add(feature);
}
return Collections.unmodifiableList(features);
}
@@ -138,7 +135,7 @@ class SampleSidecarImpl extends StubSidecar {
@Override
protected void onListenersChanged() {
if (hasListeners()) {
- onDisplayFeaturesChanged();
+ onDisplayFeaturesChanged(mStoredFeatures);
}
}
@@ -158,7 +155,7 @@ class SampleSidecarImpl extends StubSidecar {
private void onDisplayFeaturesChangedForActivity(@NonNull Activity activity) {
IBinder token = activity.getWindow().getAttributes().token;
if (token == null || mWindowLayoutChangeListenerTokens.contains(token)) {
- onDisplayFeaturesChanged();
+ onDisplayFeaturesChanged(mStoredFeatures);
}
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
new file mode 100644
index 000000000000..7624b693ac43
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
@@ -0,0 +1,42 @@
+/*
+ * 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 androidx.window.util;
+
+import android.annotation.NonNull;
+
+import java.util.function.Consumer;
+
+/**
+ * A base class that works with {@link BaseDataProducer} to add/remove a consumer that should
+ * only be used once when {@link BaseDataProducer#notifyDataChanged} is called.
+ * @param <T> The type of data this producer returns through {@link DataProducer#getData}.
+ */
+public class AcceptOnceConsumer<T> implements Consumer<T> {
+ private final Consumer<T> mCallback;
+ private final DataProducer<T> mProducer;
+
+ public AcceptOnceConsumer(@NonNull DataProducer<T> producer, @NonNull Consumer<T> callback) {
+ mProducer = producer;
+ mCallback = callback;
+ }
+
+ @Override
+ public void accept(@NonNull T t) {
+ mCallback.accept(t);
+ mProducer.removeDataChangedCallback(this);
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
index 930db3b701b7..cbaa27712015 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
@@ -16,41 +16,75 @@
package androidx.window.util;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import java.util.LinkedHashSet;
+import java.util.Optional;
import java.util.Set;
+import java.util.function.Consumer;
/**
* Base class that provides the implementation for the callback mechanism of the
- * {@link DataProducer} API.
+ * {@link DataProducer} API. This class is thread safe for adding, removing, and notifying
+ * consumers.
*
- * @param <T> The type of data this producer returns through {@link #getData()}.
+ * @param <T> The type of data this producer returns through {@link DataProducer#getData}.
*/
public abstract class BaseDataProducer<T> implements DataProducer<T> {
- private final Set<Runnable> mCallbacks = new LinkedHashSet<>();
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final Set<Consumer<T>> mCallbacks = new LinkedHashSet<>();
+
+ /**
+ * Adds a callback to the set of callbacks listening for data. Data is delivered through
+ * {@link BaseDataProducer#notifyDataChanged(Object)}. This method is thread safe. Callers
+ * should ensure that callbacks are thread safe.
+ * @param callback that will receive data from the producer.
+ */
@Override
- public final void addDataChangedCallback(@NonNull Runnable callback) {
- mCallbacks.add(callback);
- onListenersChanged(mCallbacks);
+ public final void addDataChangedCallback(@NonNull Consumer<T> callback) {
+ synchronized (mLock) {
+ mCallbacks.add(callback);
+ Optional<T> currentData = getCurrentData();
+ currentData.ifPresent(callback);
+ onListenersChanged(mCallbacks);
+ }
}
+ /**
+ * Removes a callback to the set of callbacks listening for data. This method is thread safe
+ * for adding.
+ * @param callback that was registered in
+ * {@link BaseDataProducer#addDataChangedCallback(Consumer)}.
+ */
@Override
- public final void removeDataChangedCallback(@NonNull Runnable callback) {
- mCallbacks.remove(callback);
- onListenersChanged(mCallbacks);
+ public final void removeDataChangedCallback(@NonNull Consumer<T> callback) {
+ synchronized (mLock) {
+ mCallbacks.remove(callback);
+ onListenersChanged(mCallbacks);
+ }
}
- protected void onListenersChanged(Set<Runnable> callbacks) {}
+ protected void onListenersChanged(Set<Consumer<T>> callbacks) {}
+
+ /**
+ * @return the current data if available and {@code Optional.empty()} otherwise.
+ */
+ @NonNull
+ public abstract Optional<T> getCurrentData();
/**
- * Called to notify all registered callbacks that the data provided by {@link #getData()} has
- * changed.
+ * Called to notify all registered consumers that the data provided
+ * by {@link DataProducer#getData} has changed. Calls to this are thread save but callbacks need
+ * to ensure thread safety.
*/
- protected void notifyDataChanged() {
- for (Runnable callback : mCallbacks) {
- callback.run();
+ protected void notifyDataChanged(T value) {
+ synchronized (mLock) {
+ for (Consumer<T> callback : mCallbacks) {
+ callback.accept(value);
+ }
}
}
-}
+} \ No newline at end of file
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java
index d4d1a23b756b..ec301dc34aaa 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java
@@ -18,26 +18,27 @@ package androidx.window.util;
import android.annotation.NonNull;
-import java.util.Optional;
+import java.util.function.Consumer;
/**
- * Produces data through {@link #getData()} and provides a mechanism for receiving a callback when
- * the data managed by the produces has changed.
+ * Produces data through {@link DataProducer#getData} and provides a mechanism for receiving
+ * a callback when the data managed by the produces has changed.
*
- * @param <T> The type of data this producer returns through {@link #getData()}.
+ * @param <T> The type of data this producer returns through {@link DataProducer#getData}.
*/
public interface DataProducer<T> {
/**
- * Returns the data currently stored in the provider, or {@link Optional#empty()} if the
- * provider has no data.
+ * Emits the first available data at that point in time.
+ * @param dataConsumer a {@link Consumer} that will receive one value.
*/
- Optional<T> getData();
+ void getData(@NonNull Consumer<T> dataConsumer);
/**
- * Adds a callback to be notified when the data returned from {@link #getData()} has changed.
+ * Adds a callback to be notified when the data returned
+ * from {@link DataProducer#getData} has changed.
*/
- void addDataChangedCallback(@NonNull Runnable callback);
+ void addDataChangedCallback(@NonNull Consumer<T> callback);
- /** Removes a callback previously added with {@link #addDataChangedCallback(Runnable)}. */
- void removeDataChangedCallback(@NonNull Runnable callback);
+ /** Removes a callback previously added with {@link #addDataChangedCallback(Consumer)}. */
+ void removeDataChangedCallback(@NonNull Consumer<T> callback);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
index 2a593f15a9de..9e2611f392a3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
@@ -21,14 +21,16 @@ import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
-import android.app.Activity;
+import android.app.WindowConfiguration;
+import android.content.Context;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.view.DisplayInfo;
import android.view.Surface;
+import android.view.WindowManager;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import androidx.annotation.UiContext;
/**
* Util class for both Sidecar and Extensions.
@@ -86,26 +88,31 @@ public final class ExtensionHelper {
}
/** Transforms rectangle from absolute coordinate space to the window coordinate space. */
- public static void transformToWindowSpaceRect(Activity activity, Rect inOutRect) {
- Rect windowRect = getWindowBounds(activity);
- if (windowRect == null) {
- inOutRect.setEmpty();
- return;
- }
- if (!Rect.intersects(inOutRect, windowRect)) {
+ public static void transformToWindowSpaceRect(@NonNull @UiContext Context context,
+ Rect inOutRect) {
+ transformToWindowSpaceRect(getWindowBounds(context), inOutRect);
+ }
+
+ /** @see ExtensionHelper#transformToWindowSpaceRect(Context, Rect) */
+ public static void transformToWindowSpaceRect(@NonNull WindowConfiguration windowConfiguration,
+ Rect inOutRect) {
+ transformToWindowSpaceRect(windowConfiguration.getBounds(), inOutRect);
+ }
+
+ private static void transformToWindowSpaceRect(@NonNull Rect bounds, @NonNull Rect inOutRect) {
+ if (!inOutRect.intersect(bounds)) {
inOutRect.setEmpty();
return;
}
- inOutRect.intersect(windowRect);
- inOutRect.offset(-windowRect.left, -windowRect.top);
+ inOutRect.offset(-bounds.left, -bounds.top);
}
/**
* Gets the current window bounds in absolute coordinates.
*/
- @Nullable
- private static Rect getWindowBounds(@NonNull Activity activity) {
- return activity.getWindowManager().getCurrentWindowMetrics().getBounds();
+ @NonNull
+ private static Rect getWindowBounds(@NonNull @UiContext Context context) {
+ return context.getSystemService(WindowManager.class).getCurrentWindowMetrics().getBounds();
}
/**
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index effc1a3ef3ea..40f7a273980a 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -16,9 +16,12 @@
package androidx.window.extensions.embedding;
+import static android.view.Display.DEFAULT_DISPLAY;
+
import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS;
import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import android.annotation.NonNull;
@@ -26,32 +29,68 @@ import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Pair;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerToken;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType;
+import androidx.window.extensions.layout.DisplayFeature;
+import androidx.window.extensions.layout.FoldingFeature;
+import androidx.window.extensions.layout.WindowLayoutInfo;
+
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
public class EmbeddingTestUtils {
static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
static final int TASK_ID = 10;
- static final float SPLIT_RATIO = 0.5f;
+ static final SplitType SPLIT_TYPE = SplitType.RatioSplitType.splitEqually();
+ static final SplitAttributes SPLIT_ATTRIBUTES = new SplitAttributes.Builder().build();
+ static final String TEST_TAG = "test";
/** Default finish behavior in Jetpack. */
static final int DEFAULT_FINISH_PRIMARY_WITH_SECONDARY = FINISH_NEVER;
static final int DEFAULT_FINISH_SECONDARY_WITH_PRIMARY = FINISH_ALWAYS;
+ private static final float SPLIT_RATIO = 0.5f;
private EmbeddingTestUtils() {}
/** Gets the bounds of a TaskFragment that is in split. */
static Rect getSplitBounds(boolean isPrimary) {
- final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO);
+ return getSplitBounds(isPrimary, false /* shouldSplitHorizontally */);
+ }
+
+ /** Gets the bounds of a TaskFragment that is in split. */
+ static Rect getSplitBounds(boolean isPrimary, boolean shouldSplitHorizontally) {
+ final int dimension = (int) (
+ (shouldSplitHorizontally ? TASK_BOUNDS.height() : TASK_BOUNDS.width())
+ * SPLIT_RATIO);
+ if (shouldSplitHorizontally) {
+ return isPrimary
+ ? new Rect(
+ TASK_BOUNDS.left,
+ TASK_BOUNDS.top,
+ TASK_BOUNDS.right,
+ TASK_BOUNDS.top + dimension)
+ : new Rect(
+ TASK_BOUNDS.left,
+ TASK_BOUNDS.top + dimension,
+ TASK_BOUNDS.right,
+ TASK_BOUNDS.bottom);
+ }
return isPrimary
- ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width,
- TASK_BOUNDS.bottom)
+ ? new Rect(
+ TASK_BOUNDS.left,
+ TASK_BOUNDS.top,
+ TASK_BOUNDS.left + dimension,
+ TASK_BOUNDS.bottom)
: new Rect(
- TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right,
+ TASK_BOUNDS.left + dimension,
+ TASK_BOUNDS.top,
+ TASK_BOUNDS.right,
TASK_BOUNDS.bottom);
}
@@ -69,10 +108,15 @@ public class EmbeddingTestUtils {
activityPair -> false,
targetPair::equals,
w -> true)
- .setSplitRatio(SPLIT_RATIO)
+ .setDefaultSplitAttributes(
+ new SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE)
+ .build()
+ )
.setShouldClearTop(clearTop)
.setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
.setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY)
+ .setTag(TEST_TAG)
.build();
}
@@ -101,10 +145,15 @@ public class EmbeddingTestUtils {
targetPair::equals,
activityIntentPair -> false,
w -> true)
- .setSplitRatio(SPLIT_RATIO)
+ .setDefaultSplitAttributes(
+ new SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE)
+ .build()
+ )
.setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
.setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
.setShouldClearTop(clearTop)
+ .setTag(TEST_TAG)
.build();
}
@@ -130,4 +179,29 @@ public class EmbeddingTestUtils {
primaryBounds.width() + 1, primaryBounds.height() + 1);
return aInfo;
}
+
+ static TaskContainer createTestTaskContainer() {
+ Resources resources = mock(Resources.class);
+ doReturn(new Configuration()).when(resources).getConfiguration();
+ Activity activity = mock(Activity.class);
+ doReturn(resources).when(activity).getResources();
+ doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId();
+
+ return new TaskContainer(TASK_ID, activity);
+ }
+
+ static WindowLayoutInfo createWindowLayoutInfo() {
+ final FoldingFeature foldingFeature = new FoldingFeature(
+ new Rect(
+ TASK_BOUNDS.left,
+ TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 - 5,
+ TASK_BOUNDS.right,
+ TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 + 5
+ ),
+ FoldingFeature.TYPE_HINGE,
+ FoldingFeature.STATE_HALF_OPENED);
+ final List<DisplayFeature> displayFeatures = new ArrayList<>();
+ displayFeatures.add(foldingFeature);
+ return new WindowLayoutInfo(displayFeatures);
+ }
}
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 4d2595275f20..957a24873998 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
@@ -19,14 +19,15 @@ package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer;
-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.verify;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -36,6 +37,7 @@ import android.graphics.Point;
import android.os.Handler;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentTransaction;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -56,6 +58,8 @@ import java.util.ArrayList;
* Build/Install/Run:
* atest WMJetpackUnitTests:JetpackTaskFragmentOrganizerTest
*/
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -114,12 +118,12 @@ public class JetpackTaskFragmentOrganizerTest {
@Test
public void testExpandTaskFragment() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
new Intent(), taskContainer, mSplitController);
final TaskFragmentInfo info = createMockInfo(container);
mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info);
- container.setInfo(info);
+ container.setInfo(mTransaction, info);
mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken());
@@ -127,6 +131,14 @@ public class JetpackTaskFragmentOrganizerTest {
WINDOWING_MODE_UNDEFINED);
}
+ @Test
+ public void testOnTransactionReady() {
+ final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+ mOrganizer.onTransactionReady(transaction);
+
+ verify(mCallback).onTransactionReady(transaction);
+ }
+
private TaskFragmentInfo createMockInfo(TaskFragmentContainer container) {
return new TaskFragmentInfo(container.getTaskFragmentToken(),
mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */,
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 ad496a906a33..25d034756265 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
@@ -16,18 +16,32 @@
package androidx.window.extensions.embedding;
+import static android.app.ActivityManager.START_CANCELED;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
-import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED;
+import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
+
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY;
+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.createActivityInfoWithMinDimensions;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -65,12 +79,19 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
+import android.view.WindowInsets;
+import android.view.WindowMetrics;
import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentOrganizer;
+import android.window.TaskFragmentParentInfo;
+import android.window.TaskFragmentTransaction;
import android.window.WindowContainerTransaction;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import androidx.window.extensions.layout.WindowLayoutComponentImpl;
+import androidx.window.extensions.layout.WindowLayoutInfo;
import org.junit.Before;
import org.junit.Test;
@@ -88,6 +109,8 @@ import java.util.List;
* Build/Install/Run:
* atest WMJetpackUnitTests:SplitControllerTest
*/
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -104,6 +127,8 @@ public class SplitControllerTest {
private WindowContainerTransaction mTransaction;
@Mock
private Handler mHandler;
+ @Mock
+ private WindowLayoutComponentImpl mWindowLayoutComponent;
private SplitController mSplitController;
private SplitPresenter mSplitPresenter;
@@ -111,11 +136,13 @@ public class SplitControllerTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mSplitController = new SplitController();
+ doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent)
+ .getCurrentWindowLayoutInfo(anyInt(), any());
+ mSplitController = new SplitController(mWindowLayoutComponent);
mSplitPresenter = mSplitController.mPresenter;
spyOn(mSplitController);
spyOn(mSplitPresenter);
- doNothing().when(mSplitPresenter).applyTransaction(any());
+ doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean());
final Configuration activityConfig = new Configuration();
activityConfig.windowConfiguration.setBounds(TASK_BOUNDS);
activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
@@ -126,7 +153,7 @@ public class SplitControllerTest {
@Test
public void testGetTopActiveContainer() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
// tf1 has no running activity so is not active.
final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */,
new Intent(), taskContainer, mSplitController);
@@ -157,14 +184,14 @@ public class SplitControllerTest {
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
doReturn(new ArrayList<>()).when(info).getActivities();
doReturn(true).when(info).isEmpty();
- tf1.setInfo(info);
+ tf1.setInfo(mTransaction, info);
assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after"
+ " creation.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
doReturn(false).when(info).isEmpty();
- tf1.setInfo(info);
+ tf1.setInfo(mTransaction, info);
assertWithMessage("Must return null because tf1 becomes empty.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isNull();
@@ -176,19 +203,21 @@ public class SplitControllerTest {
doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken();
// The TaskFragment has been removed in the server, we only need to cleanup the reference.
- mSplitController.onTaskFragmentVanished(mInfo);
+ mSplitController.onTaskFragmentVanished(mTransaction, mInfo);
verify(mSplitPresenter, never()).deleteTaskFragment(any(), any());
verify(mSplitController).removeContainer(tf);
- verify(mActivity, never()).finish();
+ verify(mTransaction, never()).finishActivity(any());
}
@Test
public void testOnTaskFragmentAppearEmptyTimeout() {
final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
- mSplitController.onTaskFragmentAppearEmptyTimeout(tf);
+ doCallRealMethod().when(mSplitController).onTaskFragmentAppearEmptyTimeout(any(), any());
+ mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf);
- verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */);
+ verify(mSplitPresenter).cleanupContainer(mTransaction, tf,
+ false /* shouldFinishDependent */);
}
@Test
@@ -228,8 +257,8 @@ public class SplitControllerTest {
spyOn(tf);
doReturn(mActivity).when(tf).getTopNonFinishingActivity();
doReturn(true).when(tf).isEmpty();
- doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mActivity,
- false /* isOnCreated */);
+ doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mTransaction,
+ mActivity, false /* isOnCreated */);
doNothing().when(mSplitPresenter).updateSplitContainer(any(), any(), any());
mSplitController.updateContainer(mTransaction, tf);
@@ -249,12 +278,14 @@ public class SplitControllerTest {
mSplitController.updateContainer(mTransaction, tf);
- verify(mSplitController, never()).dismissPlaceholderIfNecessary(any());
+ verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any());
// Verify if tf is not in the top splitContainer,
final SplitContainer splitContainer = mock(SplitContainer.class);
doReturn(tf).when(splitContainer).getPrimaryContainer();
doReturn(tf).when(splitContainer).getSecondaryContainer();
+ doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer();
+ doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule();
final List<SplitContainer> splitContainers =
mSplitController.getTaskContainer(TASK_ID).mSplitContainers;
splitContainers.add(splitContainer);
@@ -263,7 +294,7 @@ public class SplitControllerTest {
mSplitController.updateContainer(mTransaction, tf);
- verify(mSplitController, never()).dismissPlaceholderIfNecessary(any());
+ verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any());
// Verify if one or both containers in the top SplitContainer are finished,
// dismissPlaceholder() won't be called.
@@ -272,12 +303,12 @@ public class SplitControllerTest {
mSplitController.updateContainer(mTransaction, tf);
- verify(mSplitController, never()).dismissPlaceholderIfNecessary(any());
+ verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any());
// Verify if placeholder should be dismissed, updateSplitContainer() won't be called.
doReturn(false).when(tf).isFinished();
doReturn(true).when(mSplitController)
- .dismissPlaceholderIfNecessary(splitContainer);
+ .dismissPlaceholderIfNecessary(mTransaction, splitContainer);
mSplitController.updateContainer(mTransaction, tf);
@@ -285,7 +316,7 @@ public class SplitControllerTest {
// Verify if the top active split is updated if both of its containers are not finished.
doReturn(false).when(mSplitController)
- .dismissPlaceholderIfNecessary(splitContainer);
+ .dismissPlaceholderIfNecessary(mTransaction, splitContainer);
mSplitController.updateContainer(mTransaction, tf);
@@ -293,35 +324,57 @@ public class SplitControllerTest {
}
@Test
+ public void testOnStartActivityResultError() {
+ final Intent intent = new Intent();
+ final TaskContainer taskContainer = createTestTaskContainer();
+ final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+ intent, taskContainer, mSplitController);
+ final SplitController.ActivityStartMonitor monitor =
+ mSplitController.getActivityStartMonitor();
+
+ container.setPendingAppearedIntent(intent);
+ final Bundle bundle = new Bundle();
+ bundle.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+ container.getTaskFragmentToken());
+ monitor.mCurrentIntent = intent;
+ doReturn(container).when(mSplitController).getContainer(any());
+
+ monitor.onStartActivityResult(START_CANCELED, bundle);
+ assertNull(container.getPendingAppearedIntent());
+ }
+
+ @Test
public void testOnActivityCreated() {
- mSplitController.onActivityCreated(mActivity);
+ mSplitController.onActivityCreated(mTransaction, mActivity);
// Disallow to split as primary because we want the new launch to be always on top.
- verify(mSplitController).resolveActivityToContainer(mActivity, false /* isOnReparent */);
+ verify(mSplitController).resolveActivityToContainer(mTransaction, mActivity,
+ false /* isOnReparent */);
}
@Test
- public void testOnActivityReparentToTask_sameProcess() {
- mSplitController.onActivityReparentToTask(TASK_ID, new Intent(),
+ public void testOnActivityReparentedToTask_sameProcess() {
+ mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, new Intent(),
mActivity.getActivityToken());
// Treated as on activity created, but allow to split as primary.
- verify(mSplitController).resolveActivityToContainer(mActivity, true /* isOnReparent */);
+ verify(mSplitController).resolveActivityToContainer(mTransaction,
+ mActivity, true /* isOnReparent */);
// Try to place the activity to the top TaskFragment when there is no matched rule.
- verify(mSplitController).placeActivityInTopContainer(mActivity);
+ verify(mSplitController).placeActivityInTopContainer(mTransaction, mActivity);
}
@Test
- public void testOnActivityReparentToTask_diffProcess() {
+ public void testOnActivityReparentedToTask_diffProcess() {
// Create an empty TaskFragment to initialize for the Task.
mSplitController.newContainer(new Intent(), mActivity, TASK_ID);
final IBinder activityToken = new Binder();
final Intent intent = new Intent();
- mSplitController.onActivityReparentToTask(TASK_ID, intent, activityToken);
+ mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken);
// Treated as starting new intent
- verify(mSplitController, never()).resolveActivityToContainer(any(), anyBoolean());
+ verify(mSplitController, never()).resolveActivityToContainer(any(), any(), anyBoolean());
verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent),
isNull());
}
@@ -483,26 +536,29 @@ public class SplitControllerTest {
@Test
public void testPlaceActivityInTopContainer() {
- mSplitController.placeActivityInTopContainer(mActivity);
+ mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
- verify(mSplitPresenter, never()).applyTransaction(any());
+ verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any());
- mSplitController.newContainer(new Intent(), mActivity, TASK_ID);
- mSplitController.placeActivityInTopContainer(mActivity);
+ // Place in the top container if there is no other rule matched.
+ final TaskFragmentContainer topContainer = mSplitController
+ .newContainer(new Intent(), mActivity, TASK_ID);
+ mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
- verify(mSplitPresenter).applyTransaction(any());
+ verify(mTransaction).reparentActivityToTaskFragment(topContainer.getTaskFragmentToken(),
+ mActivity.getActivityToken());
// Not reparent if activity is in a TaskFragment.
- clearInvocations(mSplitPresenter);
+ clearInvocations(mTransaction);
mSplitController.newContainer(mActivity, TASK_ID);
- mSplitController.placeActivityInTopContainer(mActivity);
+ mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
- verify(mSplitPresenter, never()).applyTransaction(any());
+ verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any());
}
@Test
public void testResolveActivityToContainer_noRuleMatched() {
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertFalse(result);
@@ -514,7 +570,7 @@ public class SplitControllerTest {
setupExpandRule(mActivity);
// When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
mActivity);
@@ -522,7 +578,8 @@ public class SplitControllerTest {
assertTrue(result);
assertNotNull(container);
verify(mSplitController).newContainer(mActivity, TASK_ID);
- verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity);
+ verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(),
+ mActivity);
}
@Test
@@ -531,11 +588,11 @@ public class SplitControllerTest {
// When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitPresenter).expandTaskFragment(container.getTaskFragmentToken());
+ verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken());
}
@Test
@@ -545,14 +602,15 @@ public class SplitControllerTest {
// When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
final Activity activity = createMockActivity();
addSplitTaskFragments(activity, mActivity);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
mActivity);
assertTrue(result);
assertNotNull(container);
- verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity);
+ verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(),
+ mActivity);
}
@Test
@@ -562,13 +620,13 @@ public class SplitControllerTest {
(SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
// Launch placeholder if the activity is not in any TaskFragment.
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+ verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT,
mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */),
- placeholderRule, true /* isPlaceholder */);
+ placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */);
}
@Test
@@ -579,12 +637,12 @@ public class SplitControllerTest {
final Activity activity = createMockActivity();
mSplitController.newContainer(mActivity, TASK_ID);
mSplitController.newContainer(activity, TASK_ID);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertFalse(result);
- verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(),
- anyBoolean());
+ verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(),
+ any(), anyBoolean());
}
@Test
@@ -595,13 +653,13 @@ public class SplitControllerTest {
// Launch placeholder if the activity is in the topmost expanded TaskFragment.
mSplitController.newContainer(mActivity, TASK_ID);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+ verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT,
mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */),
- placeholderRule, true /* isPlaceholder */);
+ placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */);
}
@Test
@@ -611,12 +669,12 @@ public class SplitControllerTest {
// Don't launch placeholder if the activity is in primary split.
final Activity secondaryActivity = createMockActivity();
addSplitTaskFragments(mActivity, secondaryActivity);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertFalse(result);
- verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(),
- anyBoolean());
+ verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(),
+ any(), anyBoolean());
}
@Test
@@ -628,13 +686,13 @@ public class SplitControllerTest {
// Launch placeholder if the activity is in secondary split.
final Activity primaryActivity = createMockActivity();
addSplitTaskFragments(primaryActivity, mActivity);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+ verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT,
mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */),
- placeholderRule, true /* isPlaceholder */);
+ placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */);
}
@Test
@@ -653,14 +711,15 @@ public class SplitControllerTest {
primaryContainer,
mActivity,
secondaryContainer,
- splitRule);
+ splitRule,
+ SPLIT_ATTRIBUTES);
clearInvocations(mSplitController);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt());
- verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any());
+ verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
}
@Test
@@ -680,11 +739,12 @@ public class SplitControllerTest {
primaryContainer,
mActivity,
secondaryContainer,
- splitRule);
+ splitRule,
+ SPLIT_ATTRIBUTES);
final Activity launchedActivity = createMockActivity();
primaryContainer.addPendingAppearedActivity(launchedActivity);
- assertFalse(mSplitController.resolveActivityToContainer(launchedActivity,
+ assertFalse(mSplitController.resolveActivityToContainer(mTransaction, launchedActivity,
false /* isOnReparent */));
}
@@ -696,12 +756,12 @@ public class SplitControllerTest {
// Activity is already in secondary split, no need to create new split.
addSplitTaskFragments(primaryActivity, mActivity);
clearInvocations(mSplitController);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt());
- verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any());
+ verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
}
@Test
@@ -714,7 +774,7 @@ public class SplitControllerTest {
addSplitTaskFragments(primaryActivity, secondaryActivity);
mSplitController.getContainerWithActivity(secondaryActivity)
.addPendingAppearedActivity(mActivity);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertFalse(result);
@@ -738,8 +798,9 @@ public class SplitControllerTest {
primaryContainer,
mActivity,
secondaryContainer,
- placeholderRule);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ placeholderRule,
+ SPLIT_ATTRIBUTES);
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
@@ -753,7 +814,7 @@ public class SplitControllerTest {
final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
TASK_ID);
container.addPendingAppearedActivity(mActivity);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
@@ -769,14 +830,15 @@ public class SplitControllerTest {
final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
TASK_ID);
container.addPendingAppearedActivity(mActivity);
- boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertFalse(result);
assertEquals(container, mSplitController.getContainerWithActivity(mActivity));
// Allow to split as primary.
- result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */);
+ result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
+ true /* isOnReparent */);
assertTrue(result);
assertSplitPair(mActivity, activityBelow);
@@ -794,7 +856,7 @@ public class SplitControllerTest {
final TaskFragmentContainer secondaryContainer = mSplitController.getContainerWithActivity(
activityBelow);
secondaryContainer.addPendingAppearedActivity(mActivity);
- final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
mActivity);
@@ -815,14 +877,15 @@ public class SplitControllerTest {
final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
primaryActivity);
primaryContainer.addPendingAppearedActivity(mActivity);
- boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertFalse(result);
assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity));
- result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */);
+ result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
+ true /* isOnReparent */);
assertTrue(result);
assertSplitPair(mActivity, primaryActivity);
@@ -840,7 +903,7 @@ public class SplitControllerTest {
container.addPendingAppearedActivity(mActivity);
// Allow to split as primary.
- boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
true /* isOnReparent */);
assertTrue(result);
@@ -858,7 +921,7 @@ public class SplitControllerTest {
TASK_ID);
container.addPendingAppearedActivity(mActivity);
- boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
@@ -876,22 +939,23 @@ public class SplitControllerTest {
doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity));
clearInvocations(mSplitPresenter);
- boolean result = mSplitController.resolveActivityToContainer(mActivity,
+ boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */);
assertEquals(mSplitController.getContainerWithActivity(secondaryActivity),
mSplitController.getContainerWithActivity(mActivity));
- verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any());
+ verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any(), any());
}
@Test
public void testResolveActivityToContainer_inUnknownTaskFragment() {
- doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity);
+ doReturn(new Binder()).when(mSplitController)
+ .getTaskFragmentTokenFromActivityClientRecord(mActivity);
// No need to handle when the new launched activity is in an unknown TaskFragment.
- assertTrue(mSplitController.resolveActivityToContainer(mActivity,
+ assertTrue(mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */));
}
@@ -940,13 +1004,153 @@ public class SplitControllerTest {
assertTrue(primaryContainer.isFinished());
assertTrue(secondaryContainer0.isFinished());
assertTrue(secondaryContainer1.isFinished());
- verify(mActivity).finish();
- verify(secondaryActivity0).finish();
- verify(secondaryActivity1).finish();
+ verify(mTransaction).finishActivity(mActivity.getActivityToken());
+ verify(mTransaction).finishActivity(secondaryActivity0.getActivityToken());
+ verify(mTransaction).finishActivity(secondaryActivity1.getActivityToken());
assertTrue(taskContainer.mContainers.isEmpty());
assertTrue(taskContainer.mSplitContainers.isEmpty());
}
+ @Test
+ public void testOnTransactionReady_taskFragmentAppeared() {
+ final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+ final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+ transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_APPEARED)
+ .setTaskId(TASK_ID)
+ .setTaskFragmentToken(new Binder())
+ .setTaskFragmentInfo(info));
+ mSplitController.onTransactionReady(transaction);
+
+ verify(mSplitController).onTaskFragmentAppeared(any(), eq(info));
+ verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(),
+ anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testOnTransactionReady_taskFragmentInfoChanged() {
+ final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+ final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+ transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_INFO_CHANGED)
+ .setTaskId(TASK_ID)
+ .setTaskFragmentToken(new Binder())
+ .setTaskFragmentInfo(info));
+ mSplitController.onTransactionReady(transaction);
+
+ verify(mSplitController).onTaskFragmentInfoChanged(any(), eq(info));
+ verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(),
+ anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testOnTransactionReady_taskFragmentVanished() {
+ final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+ final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+ transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_VANISHED)
+ .setTaskId(TASK_ID)
+ .setTaskFragmentToken(new Binder())
+ .setTaskFragmentInfo(info));
+ mSplitController.onTransactionReady(transaction);
+
+ verify(mSplitController).onTaskFragmentVanished(any(), eq(info));
+ verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(),
+ anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testOnTransactionReady_taskFragmentParentInfoChanged() {
+ final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+ final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(Configuration.EMPTY,
+ DEFAULT_DISPLAY, true);
+ transaction.addChange(new TaskFragmentTransaction.Change(
+ TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED)
+ .setTaskId(TASK_ID)
+ .setTaskFragmentParentInfo(parentInfo));
+ mSplitController.onTransactionReady(transaction);
+
+ verify(mSplitController).onTaskFragmentParentInfoChanged(any(), eq(TASK_ID),
+ eq(parentInfo));
+ verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(),
+ anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testOnTransactionReady_taskFragmentParentError() {
+ final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+ final IBinder errorToken = new Binder();
+ final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+ final int opType = HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
+ final Exception exception = new SecurityException("test");
+ final Bundle errorBundle = TaskFragmentOrganizer.putErrorInfoInBundle(exception, info,
+ opType);
+ transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_ERROR)
+ .setErrorCallbackToken(errorToken)
+ .setErrorBundle(errorBundle));
+ mSplitController.onTransactionReady(transaction);
+
+ verify(mSplitController).onTaskFragmentError(any(), eq(errorToken), eq(info), eq(opType),
+ eq(exception));
+ verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(),
+ anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testOnTransactionReady_activityReparentedToTask() {
+ final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+ final Intent intent = mock(Intent.class);
+ final IBinder activityToken = new Binder();
+ transaction.addChange(new TaskFragmentTransaction.Change(TYPE_ACTIVITY_REPARENTED_TO_TASK)
+ .setTaskId(TASK_ID)
+ .setActivityIntent(intent)
+ .setActivityToken(activityToken));
+ mSplitController.onTransactionReady(transaction);
+
+ verify(mSplitController).onActivityReparentedToTask(any(), eq(TASK_ID), eq(intent),
+ eq(activityToken));
+ verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(),
+ anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testHasSamePresentation() {
+ SplitPairRule splitRule1 = new SplitPairRule.Builder(
+ activityPair -> true,
+ activityIntentPair -> true,
+ windowMetrics -> true)
+ .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY)
+ .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
+ .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
+ .build();
+ SplitPairRule splitRule2 = new SplitPairRule.Builder(
+ activityPair -> true,
+ activityIntentPair -> true,
+ windowMetrics -> true)
+ .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY)
+ .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
+ .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
+ .build();
+
+ assertTrue("Rules must have same presentation if tags are null and has same properties.",
+ SplitController.haveSamePresentation(splitRule1, splitRule2,
+ new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
+
+ splitRule2 = new SplitPairRule.Builder(
+ activityPair -> true,
+ activityIntentPair -> true,
+ windowMetrics -> true)
+ .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY)
+ .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
+ .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
+ .setTag(TEST_TAG)
+ .build();
+
+ assertFalse("Rules must have different presentations if tags are not equal regardless"
+ + "of other properties",
+ SplitController.haveSamePresentation(splitRule1, splitRule2,
+ new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
+
+
+ }
+
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
final Activity activity = mock(Activity.class);
@@ -956,6 +1160,7 @@ public class SplitControllerTest {
doReturn(activity).when(mSplitController).getActivity(activityToken);
doReturn(TASK_ID).when(activity).getTaskId();
doReturn(new ActivityInfo()).when(activity).getActivityInfo();
+ doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId();
return activity;
}
@@ -970,7 +1175,7 @@ public class SplitControllerTest {
private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container,
@NonNull Activity activity) {
final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity);
- container.setInfo(info);
+ container.setInfo(mTransaction, info);
mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info);
}
@@ -994,7 +1199,7 @@ public class SplitControllerTest {
private void setupPlaceholderRule(@NonNull Activity primaryActivity) {
final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT,
primaryActivity::equals, i -> false, w -> true)
- .setSplitRatio(SPLIT_RATIO)
+ .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
.build();
mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule));
}
@@ -1047,7 +1252,8 @@ public class SplitControllerTest {
primaryContainer,
primaryContainer.getTopNonFinishingActivity(),
secondaryContainer,
- rule);
+ rule,
+ SPLIT_ATTRIBUTES);
// We need to set those in case we are not respecting clear top.
// TODO(b/231845476) we should always respect clearTop.
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 d79319666c01..6dae0a1086b3 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
@@ -16,29 +16,35 @@
package androidx.window.extensions.embedding;
+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 androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY;
+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.createActivityInfoWithMinDimensions;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createWindowLayoutInfo;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
+import static androidx.window.extensions.embedding.SplitPresenter.EXPAND_CONTAINERS_ATTRIBUTES;
import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END;
import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL;
import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START;
import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPANDED;
import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
import static androidx.window.extensions.embedding.SplitPresenter.RESULT_NOT_EXPANDED;
-import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition;
import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
-import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -65,6 +71,8 @@ import android.window.WindowContainerTransaction;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import androidx.window.extensions.layout.WindowLayoutComponentImpl;
+import androidx.window.extensions.layout.WindowLayoutInfo;
import org.junit.Before;
import org.junit.Test;
@@ -72,12 +80,16 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+
/**
* Test class for {@link SplitPresenter}.
*
* Build/Install/Run:
* atest WMJetpackUnitTests:SplitPresenterTest
*/
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -91,13 +103,17 @@ public class SplitPresenterTest {
private TaskFragmentInfo mTaskFragmentInfo;
@Mock
private WindowContainerTransaction mTransaction;
+ @Mock
+ private WindowLayoutComponentImpl mWindowLayoutComponent;
private SplitController mController;
private SplitPresenter mPresenter;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mController = new SplitController();
+ doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent)
+ .getCurrentWindowLayoutInfo(anyInt(), any());
+ mController = new SplitController(mWindowLayoutComponent);
mPresenter = mController.mPresenter;
spyOn(mController);
spyOn(mPresenter);
@@ -159,59 +175,263 @@ public class SplitPresenterTest {
@Test
public void testShouldShowSideBySide() {
- Activity secondaryActivity = createMockActivity();
- final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity);
+ assertTrue(SplitPresenter.shouldShowSplit(SPLIT_ATTRIBUTES));
- assertTrue(shouldShowSideBySide(TASK_BOUNDS, splitRule));
+ final SplitAttributes expandContainers = new SplitAttributes.Builder()
+ .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType())
+ .build();
- // Set minDimensions of primary container to larger than primary bounds.
- final Rect primaryBounds = getSplitBounds(true /* isPrimary */);
- Pair<Size, Size> minDimensionsPair = new Pair<>(
- new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null);
+ assertFalse(SplitPresenter.shouldShowSplit(expandContainers));
+ }
- assertFalse(shouldShowSideBySide(TASK_BOUNDS, splitRule, minDimensionsPair));
+ @Test
+ public void testGetBoundsForPosition_expandContainers() {
+ final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final SplitAttributes splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType())
+ .build();
+
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
}
@Test
- public void testGetBoundsForPosition() {
- Activity secondaryActivity = createMockActivity();
- final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity);
- final Rect primaryBounds = getSplitBounds(true /* isPrimary */);
- final Rect secondaryBounds = getSplitBounds(false /* isPrimary */);
+ public void testGetBoundsForPosition_splitVertically() {
+ final Rect primaryBounds = getSplitBounds(true /* isPrimary */,
+ false /* splitHorizontally */);
+ final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
+ false /* splitHorizontally */);
+ final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ SplitAttributes splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
+ .build();
assertEquals("Primary bounds must be reported.",
primaryBounds,
- getBoundsForPosition(POSITION_START, TASK_BOUNDS, splitRule,
- mActivity, null /* miniDimensionsPair */));
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("Secondary bounds must be reported.",
+ secondaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+
+ splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
+ .setLayoutDirection(SplitAttributes.LayoutDirection.RIGHT_TO_LEFT)
+ .build();
+
+ assertEquals("Secondary bounds must be reported.",
+ secondaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("Primary bounds must be reported.",
+ primaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+
+ splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .build();
+ // Layout direction should follow screen layout for SplitAttributes.LayoutDirection.LOCALE.
+ taskProperties.getConfiguration().screenLayout |= Configuration.SCREENLAYOUT_LAYOUTDIR_RTL;
+
+ assertEquals("Secondary bounds must be reported.",
+ secondaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("Primary bounds must be reported.",
+ primaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ }
+
+ @Test
+ public void testGetBoundsForPosition_splitHorizontally() {
+ final Rect primaryBounds = getSplitBounds(true /* isPrimary */,
+ true /* splitHorizontally */);
+ final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
+ true /* splitHorizontally */);
+ final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ SplitAttributes splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
+ .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
+ .build();
+
+ assertEquals("Primary bounds must be reported.",
+ primaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("Secondary bounds must be reported.",
+ secondaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+
+ splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
+ .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP)
+ .build();
assertEquals("Secondary bounds must be reported.",
secondaryBounds,
- getBoundsForPosition(POSITION_END, TASK_BOUNDS, splitRule,
- mActivity, null /* miniDimensionsPair */));
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("Primary bounds must be reported.",
+ primaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- getBoundsForPosition(POSITION_FILL, TASK_BOUNDS, splitRule,
- mActivity, null /* miniDimensionsPair */));
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ }
- Pair<Size, Size> minDimensionsPair = new Pair<>(
- new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null);
+ @Test
+ public void testGetBoundsForPosition_useHingeFallback() {
+ final Rect primaryBounds = getSplitBounds(true /* isPrimary */,
+ false /* splitHorizontally */);
+ final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
+ false /* splitHorizontally */);
+ final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final SplitAttributes splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(new SplitAttributes.SplitType.HingeSplitType(
+ SplitAttributes.SplitType.RatioSplitType.splitEqually()
+ )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
+ .build();
+
+ // There's no hinge on the device. Use fallback SplitType.
+ doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent)
+ .getCurrentWindowLayoutInfo(anyInt(), any());
+
+ assertEquals("PrimaryBounds must be reported.",
+ primaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
- assertEquals("Fullscreen bounds must be reported because of min dimensions.",
+ assertEquals("SecondaryBounds must be reported.",
+ secondaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
new Rect(),
- getBoundsForPosition(POSITION_START, TASK_BOUNDS,
- splitRule, mActivity, minDimensionsPair));
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+
+ // Hinge is reported, but the host task is in multi-window mode. Still use fallback
+ // splitType.
+ doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent)
+ .getCurrentWindowLayoutInfo(anyInt(), any());
+ taskProperties.getConfiguration().windowConfiguration
+ .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+
+ assertEquals("PrimaryBounds must be reported.",
+ primaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("SecondaryBounds must be reported.",
+ secondaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+
+ // Hinge is reported, and the host task is in fullscreen, but layout direction doesn't match
+ // folding area orientation. Still use fallback splitType.
+ doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent)
+ .getCurrentWindowLayoutInfo(anyInt(), any());
+ taskProperties.getConfiguration().windowConfiguration
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ assertEquals("PrimaryBounds must be reported.",
+ primaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("SecondaryBounds must be reported.",
+ secondaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ }
+
+ @Test
+ public void testGetBoundsForPosition_fallbackToExpandContainers() {
+ final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final SplitAttributes splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(new SplitAttributes.SplitType.HingeSplitType(
+ new SplitAttributes.SplitType.ExpandContainersSplitType()
+ )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
+ .build();
+
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ }
+
+ @Test
+ public void testGetBoundsForPosition_useHingeSplitType() {
+ final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final SplitAttributes splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(new SplitAttributes.SplitType.HingeSplitType(
+ new SplitAttributes.SplitType.ExpandContainersSplitType()
+ )).setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
+ .build();
+ final WindowLayoutInfo windowLayoutInfo = createWindowLayoutInfo();
+ doReturn(windowLayoutInfo).when(mWindowLayoutComponent)
+ .getCurrentWindowLayoutInfo(anyInt(), any());
+ final Rect hingeBounds = windowLayoutInfo.getDisplayFeatures().get(0).getBounds();
+ final Rect primaryBounds = new Rect(
+ TASK_BOUNDS.left,
+ TASK_BOUNDS.top,
+ TASK_BOUNDS.right,
+ hingeBounds.top
+ );
+ final Rect secondaryBounds = new Rect(
+ TASK_BOUNDS.left,
+ hingeBounds.bottom,
+ TASK_BOUNDS.right,
+ TASK_BOUNDS.bottom
+ );
+
+ assertEquals("PrimaryBounds must be reported.",
+ primaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+
+ assertEquals("SecondaryBounds must be reported.",
+ secondaryBounds,
+ mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
}
@Test
public void testExpandSplitContainerIfNeeded() {
- SplitContainer splitContainer = mock(SplitContainer.class);
Activity secondaryActivity = createMockActivity();
SplitRule splitRule = createSplitRule(mActivity, secondaryActivity);
TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID);
TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID);
- doReturn(splitRule).when(splitContainer).getSplitRule();
- doReturn(primaryTf).when(splitContainer).getPrimaryContainer();
- doReturn(secondaryTf).when(splitContainer).getSecondaryContainer();
+ SplitContainer splitContainer = new SplitContainer(primaryTf, secondaryActivity,
+ secondaryTf, splitRule, SPLIT_ATTRIBUTES);
assertThrows(IllegalArgumentException.class, () ->
mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity,
@@ -221,31 +441,95 @@ public class SplitPresenterTest {
splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
verify(mPresenter, never()).expandTaskFragment(any(), any());
+ splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo();
assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded(
mTransaction, splitContainer, mActivity, secondaryActivity,
null /* secondaryIntent */));
- primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity));
- secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
+ splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+ primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity));
+ secondaryTf.setInfo(mTransaction,
+ createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
- verify(mPresenter).expandTaskFragment(eq(mTransaction),
- eq(primaryTf.getTaskFragmentToken()));
- verify(mPresenter).expandTaskFragment(eq(mTransaction),
- eq(secondaryTf.getTaskFragmentToken()));
+ verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
+ verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
+ splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
clearInvocations(mPresenter);
assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
splitContainer, mActivity, null /* secondaryActivity */,
new Intent(ApplicationProvider.getApplicationContext(),
MinimumDimensionActivity.class)));
- verify(mPresenter).expandTaskFragment(eq(mTransaction),
- eq(primaryTf.getTaskFragmentToken()));
- verify(mPresenter).expandTaskFragment(eq(mTransaction),
- eq(secondaryTf.getTaskFragmentToken()));
+ verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
+ verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
+ }
+
+ @Test
+ public void testCreateNewSplitContainer_secondaryAbovePrimary() {
+ final Activity secondaryActivity = createMockActivity();
+ final TaskFragmentContainer bottomTf = mController.newContainer(secondaryActivity, TASK_ID);
+ final TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID);
+ final SplitPairRule rule = new SplitPairRule.Builder(pair ->
+ pair.first == mActivity && pair.second == secondaryActivity, pair -> false,
+ metrics -> true)
+ .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
+ .setShouldClearTop(false)
+ .build();
+
+ mPresenter.createNewSplitContainer(mTransaction, mActivity, secondaryActivity, rule);
+
+ assertEquals(primaryTf, mController.getContainerWithActivity(mActivity));
+ final TaskFragmentContainer secondaryTf = mController.getContainerWithActivity(
+ secondaryActivity);
+ assertNotEquals(bottomTf, secondaryTf);
+ assertTrue(secondaryTf.isAbove(primaryTf));
+ }
+
+ @Test
+ public void testComputeSplitAttributes() {
+ final SplitPairRule splitPairRule = new SplitPairRule.Builder(
+ activityPair -> true,
+ activityIntentPair -> true,
+ windowMetrics -> windowMetrics.getBounds().equals(TASK_BOUNDS))
+ .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY)
+ .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
+ .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
+ .build();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+
+ assertEquals(SPLIT_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
+ splitPairRule, null /* minDimensionsPair */));
+
+ final Pair<Size, Size> minDimensionsPair = new Pair<>(
+ new Size(TASK_BOUNDS.width(), TASK_BOUNDS.height()), null);
+
+ assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
+ splitPairRule, minDimensionsPair));
+
+ taskProperties.getConfiguration().windowConfiguration.setBounds(new Rect(
+ TASK_BOUNDS.left + 1, TASK_BOUNDS.top + 1, TASK_BOUNDS.right + 1,
+ TASK_BOUNDS.bottom + 1));
+
+ assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
+ splitPairRule, null /* minDimensionsPair */));
+
+ final SplitAttributes splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(
+ new SplitAttributes.SplitType.HingeSplitType(
+ SplitAttributes.SplitType.RatioSplitType.splitEqually()
+ )
+ ).build();
+
+ mController.setSplitAttributesCalculator(params -> {
+ return splitAttributes;
+ });
+
+ assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties,
+ splitPairRule, null /* minDimensionsPair */));
}
private Activity createMockActivity() {
@@ -259,4 +543,10 @@ public class SplitPresenterTest {
doReturn(mock(IBinder.class)).when(activity).getActivityToken();
return activity;
}
+
+ private static TaskContainer.TaskProperties getTaskProperty() {
+ final Configuration configuration = new Configuration();
+ configuration.windowConfiguration.setBounds(TASK_BOUNDS);
+ return new TaskContainer.TaskProperties(DEFAULT_DISPLAY, configuration);
+ }
}
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 dd67e48ef353..af9c6ba5c162 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
@@ -21,9 +21,10 @@ 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.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.Display.DEFAULT_DISPLAY;
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.createTestTaskContainer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -34,8 +35,10 @@ import static org.mockito.Mockito.mock;
import android.app.Activity;
import android.content.Intent;
+import android.content.res.Configuration;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
+import android.window.TaskFragmentParentInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -66,7 +69,7 @@ public class TaskContainerTest {
@Test
public void testIsTaskBoundsInitialized() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
assertFalse(taskContainer.isTaskBoundsInitialized());
@@ -77,7 +80,7 @@ public class TaskContainerTest {
@Test
public void testSetTaskBounds() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
assertFalse(taskContainer.setTaskBounds(new Rect()));
@@ -87,30 +90,24 @@ public class TaskContainerTest {
}
@Test
- public void testIsWindowingModeInitialized() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
-
- assertFalse(taskContainer.isWindowingModeInitialized());
-
- taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
-
- assertTrue(taskContainer.isWindowingModeInitialized());
- }
-
- @Test
public void testGetWindowingModeForSplitTaskFragment() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
final Rect splitBounds = new Rect(0, 0, 500, 1000);
+ final Configuration configuration = new Configuration();
assertEquals(WINDOWING_MODE_MULTI_WINDOW,
taskContainer.getWindowingModeForSplitTaskFragment(splitBounds));
- taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, true /* visible */));
assertEquals(WINDOWING_MODE_MULTI_WINDOW,
taskContainer.getWindowingModeForSplitTaskFragment(splitBounds));
- taskContainer.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, true /* visible */));
assertEquals(WINDOWING_MODE_FREEFORM,
taskContainer.getWindowingModeForSplitTaskFragment(splitBounds));
@@ -123,22 +120,27 @@ public class TaskContainerTest {
@Test
public void testIsInPictureInPicture() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
+ final Configuration configuration = new Configuration();
assertFalse(taskContainer.isInPictureInPicture());
- taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, true /* visible */));
assertFalse(taskContainer.isInPictureInPicture());
- taskContainer.setWindowingMode(WINDOWING_MODE_PINNED);
+ configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED);
+ taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, true /* visible */));
assertTrue(taskContainer.isInPictureInPicture());
}
@Test
public void testIsEmpty() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
assertTrue(taskContainer.isEmpty());
@@ -155,7 +157,7 @@ public class TaskContainerTest {
@Test
public void testGetTopTaskFragmentContainer() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
assertNull(taskContainer.getTopTaskFragmentContainer());
final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */,
@@ -169,7 +171,7 @@ public class TaskContainerTest {
@Test
public void testGetTopNonFinishingActivity() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
assertNull(taskContainer.getTopNonFinishingActivity());
final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class);
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 28c2773e25cb..35415d816d8b 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
@@ -16,9 +16,11 @@
package androidx.window.extensions.embedding;
-import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.junit.Assert.assertEquals;
@@ -36,7 +38,6 @@ import static org.mockito.Mockito.never;
import android.app.Activity;
import android.content.Intent;
import android.os.Binder;
-import android.os.Handler;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
@@ -62,32 +63,34 @@ import java.util.List;
* Build/Install/Run:
* atest WMJetpackUnitTests:TaskFragmentContainerTest
*/
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TaskFragmentContainerTest {
@Mock
private SplitPresenter mPresenter;
- @Mock
private SplitController mController;
@Mock
private TaskFragmentInfo mInfo;
@Mock
- private Handler mHandler;
+ private WindowContainerTransaction mTransaction;
private Activity mActivity;
private Intent mIntent;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- doReturn(mHandler).when(mController).getHandler();
+ mController = new SplitController();
+ spyOn(mController);
mActivity = createMockActivity();
mIntent = new Intent();
}
@Test
public void testNewContainer() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
// One of the activity and the intent must be non-null
assertThrows(IllegalArgumentException.class,
@@ -100,44 +103,43 @@ public class TaskFragmentContainerTest {
@Test
public void testFinish() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
final TaskFragmentContainer container = new TaskFragmentContainer(mActivity,
null /* pendingAppearedIntent */, taskContainer, mController);
doReturn(container).when(mController).getContainerWithActivity(mActivity);
- final WindowContainerTransaction wct = new WindowContainerTransaction();
// Only remove the activity, but not clear the reference until appeared.
- container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+ container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
- verify(mActivity).finish();
+ verify(mTransaction).finishActivity(mActivity.getActivityToken());
verify(mPresenter, never()).deleteTaskFragment(any(), any());
verify(mController, never()).removeContainer(any());
// Calling twice should not finish activity again.
- clearInvocations(mActivity);
- container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+ clearInvocations(mTransaction);
+ container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
- verify(mActivity, never()).finish();
+ verify(mTransaction, never()).finishActivity(any());
verify(mPresenter, never()).deleteTaskFragment(any(), any());
verify(mController, never()).removeContainer(any());
// Remove all references after the container has appeared in server.
doReturn(new ArrayList<>()).when(mInfo).getActivities();
- container.setInfo(mInfo);
- container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+ container.setInfo(mTransaction, mInfo);
+ container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
- verify(mActivity, never()).finish();
- verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken());
+ verify(mTransaction, never()).finishActivity(any());
+ verify(mPresenter).deleteTaskFragment(mTransaction, container.getTaskFragmentToken());
verify(mController).removeContainer(container);
}
@Test
public void testFinish_notFinishActivityThatIsReparenting() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity,
null /* pendingAppearedIntent */, taskContainer, mController);
final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity);
- container0.setInfo(info);
+ container0.setInfo(mTransaction, info);
// Request to reparent the activity to a new TaskFragment.
final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity,
null /* pendingAppearedIntent */, taskContainer, mController);
@@ -147,14 +149,14 @@ public class TaskFragmentContainerTest {
// The activity is requested to be reparented, so don't finish it.
container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
- verify(mActivity, never()).finish();
+ verify(mTransaction, never()).finishActivity(any());
verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken());
verify(mController).removeContainer(container0);
}
@Test
public void testSetInfo() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
// Pending activity should be cleared when it has appeared on server side.
final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity,
null /* pendingAppearedIntent */, taskContainer, mController);
@@ -163,7 +165,7 @@ public class TaskFragmentContainerTest {
final TaskFragmentInfo info0 = createMockTaskFragmentInfo(pendingActivityContainer,
mActivity);
- pendingActivityContainer.setInfo(info0);
+ pendingActivityContainer.setInfo(mTransaction, info0);
assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty());
@@ -175,14 +177,14 @@ public class TaskFragmentContainerTest {
final TaskFragmentInfo info1 = createMockTaskFragmentInfo(pendingIntentContainer,
mActivity);
- pendingIntentContainer.setInfo(info1);
+ pendingIntentContainer.setInfo(mTransaction, info1);
assertNull(pendingIntentContainer.getPendingAppearedIntent());
}
@Test
public void testIsWaitingActivityAppear() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
mIntent, taskContainer, mController);
@@ -191,49 +193,51 @@ public class TaskFragmentContainerTest {
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
doReturn(new ArrayList<>()).when(info).getActivities();
doReturn(true).when(info).isEmpty();
- container.setInfo(info);
+ container.setInfo(mTransaction, info);
assertTrue(container.isWaitingActivityAppear());
doReturn(false).when(info).isEmpty();
- container.setInfo(info);
+ container.setInfo(mTransaction, info);
assertFalse(container.isWaitingActivityAppear());
}
@Test
public void testAppearEmptyTimeout() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any());
+ final TaskContainer taskContainer = createTestTaskContainer();
final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
mIntent, taskContainer, mController);
assertNull(container.mAppearEmptyTimeout);
- // Not set if it is not appeared empty.
- final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
- doReturn(new ArrayList<>()).when(info).getActivities();
- doReturn(false).when(info).isEmpty();
- container.setInfo(info);
-
- assertNull(container.mAppearEmptyTimeout);
-
// Set timeout if the first info set is empty.
+ final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
container.mInfo = null;
doReturn(true).when(info).isEmpty();
- container.setInfo(info);
+ container.setInfo(mTransaction, info);
assertNotNull(container.mAppearEmptyTimeout);
+ // Not set if it is not appeared empty.
+ doReturn(new ArrayList<>()).when(info).getActivities();
+ doReturn(false).when(info).isEmpty();
+ container.setInfo(mTransaction, info);
+
+ assertNull(container.mAppearEmptyTimeout);
+
// Remove timeout after the container becomes non-empty.
doReturn(false).when(info).isEmpty();
- container.setInfo(info);
+ container.setInfo(mTransaction, info);
assertNull(container.mAppearEmptyTimeout);
// Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout.
container.mInfo = null;
+ container.setPendingAppearedIntent(mIntent);
doReturn(true).when(info).isEmpty();
- container.setInfo(info);
+ container.setInfo(mTransaction, info);
container.mAppearEmptyTimeout.run();
assertNull(container.mAppearEmptyTimeout);
@@ -242,7 +246,7 @@ public class TaskFragmentContainerTest {
@Test
public void testCollectNonFinishingActivities() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
mIntent, taskContainer, mController);
List<Activity> activities = container.collectNonFinishingActivities();
@@ -259,7 +263,7 @@ public class TaskFragmentContainerTest {
final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(),
activity1.getActivityToken());
doReturn(runningActivities).when(mInfo).getActivities();
- container.setInfo(mInfo);
+ container.setInfo(mTransaction, mInfo);
activities = container.collectNonFinishingActivities();
assertEquals(3, activities.size());
@@ -270,7 +274,7 @@ public class TaskFragmentContainerTest {
@Test
public void testAddPendingActivity() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
mIntent, taskContainer, mController);
container.addPendingAppearedActivity(mActivity);
@@ -283,8 +287,20 @@ public class TaskFragmentContainerTest {
}
@Test
+ public void testIsAbove() {
+ final TaskContainer taskContainer = createTestTaskContainer();
+ final TaskFragmentContainer container0 = new TaskFragmentContainer(null /* activity */,
+ mIntent, taskContainer, mController);
+ final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */,
+ mIntent, taskContainer, mController);
+
+ assertTrue(container1.isAbove(container0));
+ assertFalse(container0.isAbove(container1));
+ }
+
+ @Test
public void testGetBottomMostActivity() {
- final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskContainer taskContainer = createTestTaskContainer();
final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
mIntent, taskContainer, mController);
container.addPendingAppearedActivity(mActivity);
@@ -294,11 +310,30 @@ public class TaskFragmentContainerTest {
final Activity activity = createMockActivity();
final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken());
doReturn(runningActivities).when(mInfo).getActivities();
- container.setInfo(mInfo);
+ container.setInfo(mTransaction, mInfo);
assertEquals(activity, container.getBottomMostActivity());
}
+ @Test
+ public void testOnActivityDestroyed() {
+ final TaskContainer taskContainer = createTestTaskContainer();
+ final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+ mIntent, taskContainer, mController);
+ container.addPendingAppearedActivity(mActivity);
+ final List<IBinder> activities = new ArrayList<>();
+ activities.add(mActivity.getActivityToken());
+ doReturn(activities).when(mInfo).getActivities();
+ container.setInfo(mTransaction, mInfo);
+
+ assertTrue(container.hasActivity(mActivity.getActivityToken()));
+
+ taskContainer.onActivityDestroyed(mActivity);
+
+ // It should not contain the destroyed Activity.
+ assertFalse(container.hasActivity(mActivity.getActivityToken()));
+ }
+
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
final Activity activity = mock(Activity.class);
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index f54ab08d8a8a..2c766d81d611 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/res/color/decor_button_dark_color.xml b/libs/WindowManager/Shell/res/color/decor_button_dark_color.xml
new file mode 100644
index 000000000000..bf325bd84c00
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/decor_button_dark_color.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item app:state_task_focused="true" android:color="#FF000000" />
+ <item android:color="#33000000" />
+</selector>
diff --git a/libs/WindowManager/Shell/res/color/decor_button_light_color.xml b/libs/WindowManager/Shell/res/color/decor_button_light_color.xml
new file mode 100644
index 000000000000..2e48bca7786a
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/decor_button_light_color.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item app:state_task_focused="true" android:color="#FFFFFFFF" />
+ <item android:color="#33FFFFFF" />
+</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/decor_caption_title_color.xml b/libs/WindowManager/Shell/res/color/decor_caption_title_color.xml
new file mode 100644
index 000000000000..1ecc13e4da38
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/decor_caption_title_color.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <!-- Fading the to 85% blackness -->
+ <item app:state_task_focused="true" android:color="#D8D8D8" />
+ <!-- Fading the to 95% blackness -->
+ <item android:color="#F2F2F2" />
+</selector>
diff --git a/libs/WindowManager/Shell/res/color/taskbar_background.xml b/libs/WindowManager/Shell/res/color/taskbar_background.xml
index 329e5b9b31a0..b3d260299106 100644
--- a/libs/WindowManager/Shell/res/color/taskbar_background.xml
+++ b/libs/WindowManager/Shell/res/color/taskbar_background.xml
@@ -14,6 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
+<!-- Should be the same as in packages/apps/Launcher3/res/color-v31/taskbar_background.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="35" />
+ <item android:color="@android:color/system_neutral1_500" android:lStar="15" />
</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml b/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml
new file mode 100644
index 000000000000..8207365a737d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<shape android:shape="rectangle"
+ android:tintMode="multiply"
+ android:tint="@color/decor_caption_title_color"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="?android:attr/colorPrimary" />
+</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml
new file mode 100644
index 000000000000..f2f1a1d55dee
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32.0dp"
+ android:height="32.0dp"
+ android:viewportWidth="32.0"
+ android:viewportHeight="32.0"
+ android:tint="@color/decor_button_dark_color"
+ >
+ <group android:scaleX="0.5"
+ android:scaleY="0.5"
+ android:translateX="8.0"
+ android:translateY="8.0" >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M6.9,4.0l-2.9,2.9 9.1,9.1 -9.1,9.200001 2.9,2.799999 9.1,-9.1 9.1,9.1 2.9,-2.799999 -9.1,-9.200001 9.1,-9.1 -2.9,-2.9 -9.1,9.2z"/>
+ </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml
new file mode 100644
index 000000000000..ab4e29ac97e5
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32.0dp"
+ android:height="32.0dp"
+ android:viewportWidth="32.0"
+ android:viewportHeight="32.0"
+ android:tint="@color/decor_button_dark_color">
+ <group android:scaleX="0.5"
+ android:scaleY="0.5"
+ android:translateX="8.0"
+ android:translateY="8.0" >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M2.0,4.0l0.0,16.0l28.0,0.0L30.0,4.0L2.0,4.0zM26.0,16.0L6.0,16.0L6.0,8.0l20.0,0.0L26.0,16.0z"/>
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M2.0,24.0l28.0,0.0l0.0,4.0l-28.0,0.0z"/>
+ </group>
+</vector>
+
+
diff --git a/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml
new file mode 100644
index 000000000000..0bcaa530dc80
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white" android:pathData="M6,21V19H18V21Z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_letterboxed_app.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml
index 6fcd1de892a3..ddfb5c27e701 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_letterboxed_app.xml
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml
@@ -15,15 +15,12 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/letterbox_education_dialog_icon_size"
- android:height="@dimen/letterbox_education_dialog_icon_size"
- android:viewportWidth="48"
- android:viewportHeight="48">
+ android:width="@dimen/letterbox_education_dialog_title_icon_width"
+ android:height="@dimen/letterbox_education_dialog_title_icon_height"
+ android:viewportWidth="45"
+ android:viewportHeight="44">
+
<path
android:fillColor="@color/letterbox_education_accent_primary"
- android:fillType="evenOdd"
- android:pathData="M2 8C0.895431 8 0 8.89543 0 10V38C0 39.1046 0.895431 40 2 40H46C47.1046 40 48 39.1046 48 38V10C48 8.89543 47.1046 8 46 8H2ZM44 12H4V36H44V12Z" />
- <path
- android:fillColor="@color/letterbox_education_accent_primary"
- android:pathData="M 17 14 L 31 14 Q 32 14 32 15 L 32 33 Q 32 34 31 34 L 17 34 Q 16 34 16 33 L 16 15 Q 16 14 17 14 Z" />
+ android:pathData="M11 40H19C19 42.2 17.2 44 15 44C12.8 44 11 42.2 11 40ZM7 38L23 38V34L7 34L7 38ZM30 19C30 26.64 24.68 30.72 22.46 32L7.54 32C5.32 30.72 0 26.64 0 19C0 10.72 6.72 4 15 4C23.28 4 30 10.72 30 19ZM26 19C26 12.94 21.06 8 15 8C8.94 8 4 12.94 4 19C4 23.94 6.98 26.78 8.7 28L21.3 28C23.02 26.78 26 23.94 26 19ZM39.74 14.74L37 16L39.74 17.26L41 20L42.26 17.26L45 16L42.26 14.74L41 12L39.74 14.74ZM35 12L36.88 7.88L41 6L36.88 4.12L35 0L33.12 4.12L29 6L33.12 7.88L35 12Z" />
</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml
index cbfcfd06e3b7..22a8f39ca687 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml
@@ -15,18 +15,16 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/letterbox_education_dialog_icon_size"
- android:height="@dimen/letterbox_education_dialog_icon_size"
- android:viewportWidth="48"
- android:viewportHeight="48">
+ android:width="@dimen/letterbox_education_dialog_icon_width"
+ android:height="@dimen/letterbox_education_dialog_icon_height"
+ android:viewportWidth="40"
+ android:viewportHeight="32">
+
<path
android:fillColor="@color/letterbox_education_text_secondary"
android:fillType="evenOdd"
- android:pathData="M2 8C0.895431 8 0 8.89543 0 10V38C0 39.1046 0.895431 40 2 40H46C47.1046 40 48 39.1046 48 38V10C48 8.89543 47.1046 8 46 8H2ZM44 12H4V36H44V12Z" />
+ android:pathData="M4 0C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H36C38.2091 32 40 30.2091 40 28V4C40 1.79086 38.2091 0 36 0H4ZM36 4H4V28H36V4Z" />
<path
android:fillColor="@color/letterbox_education_text_secondary"
- android:pathData="M 14 22 H 30 V 26 H 14 V 22 Z" />
- <path
- android:fillColor="@color/letterbox_education_text_secondary"
- android:pathData="M26 16L34 24L26 32V16Z" />
+ android:pathData="M19.98 8L17.16 10.82L20.32 14L12 14V18H20.32L17.14 21.18L19.98 24L28 16.02L19.98 8Z" />
</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_screen_rotation.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_screen_rotation.xml
deleted file mode 100644
index 469eb1e14849..000000000000
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_screen_rotation.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ 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.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/letterbox_education_dialog_icon_size"
- android:height="@dimen/letterbox_education_dialog_icon_size"
- android:viewportWidth="48"
- android:viewportHeight="48">
- <path
- android:fillColor="@color/letterbox_education_text_secondary"
- android:fillType="evenOdd"
- android:pathData="M22.56 2H26C37.02 2 46 10.98 46 22H42C42 14.44 36.74 8.1 29.7 6.42L31.74 10L28.26 12L22.56 2ZM22 46H25.44L19.74 36L16.26 38L18.3 41.58C11.26 39.9 6 33.56 6 26H2C2 37.02 10.98 46 22 46ZM20.46 12L36 27.52L27.54 36L12 20.48L20.46 12ZM17.64 9.18C18.42 8.4 19.44 8 20.46 8C21.5 8 22.52 8.4 23.3 9.16L38.84 24.7C40.4 26.26 40.4 28.78 38.84 30.34L30.36 38.82C29.58 39.6 28.56 40 27.54 40C26.52 40 25.5 39.6 24.72 38.82L9.18 23.28C7.62 21.72 7.62 19.2 9.18 17.64L17.64 9.18Z" />
-</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml
index dcb8aed05c9c..15e65f716b20 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml
@@ -15,18 +15,12 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/letterbox_education_dialog_icon_size"
- android:height="@dimen/letterbox_education_dialog_icon_size"
- android:viewportWidth="48"
- android:viewportHeight="48">
+ android:width="@dimen/letterbox_education_dialog_icon_width"
+ android:height="@dimen/letterbox_education_dialog_icon_height"
+ android:viewportWidth="40"
+ android:viewportHeight="32">
+
<path
android:fillColor="@color/letterbox_education_text_secondary"
- android:fillType="evenOdd"
- android:pathData="M2 8C0.895431 8 0 8.89543 0 10V38C0 39.1046 0.895431 40 2 40H46C47.1046 40 48 39.1046 48 38V10C48 8.89543 47.1046 8 46 8H2ZM44 12H4V36H44V12Z" />
- <path
- android:fillColor="@color/letterbox_education_text_secondary"
- android:pathData="M6 16C6 14.8954 6.89543 14 8 14H21C22.1046 14 23 14.8954 23 16V32C23 33.1046 22.1046 34 21 34H8C6.89543 34 6 33.1046 6 32V16Z" />
- <path
- android:fillColor="@color/letterbox_education_text_secondary"
- android:pathData="M25 16C25 14.8954 25.8954 14 27 14H40C41.1046 14 42 14.8954 42 16V32C42 33.1046 41.1046 34 40 34H27C25.8954 34 25 33.1046 25 32V16Z" />
+ android:pathData="M40 28L40 4C40 1.8 38.2 -7.86805e-08 36 -1.74846e-07L26 -6.11959e-07C23.8 -7.08124e-07 22 1.8 22 4L22 28C22 30.2 23.8 32 26 32L36 32C38.2 32 40 30.2 40 28ZM14 28L4 28L4 4L14 4L14 28ZM18 28L18 4C18 1.8 16.2 -1.04033e-06 14 -1.1365e-06L4 -1.57361e-06C1.8 -1.66978e-06 -7.86805e-08 1.8 -1.74846e-07 4L-1.22392e-06 28C-1.32008e-06 30.2 1.8 32 4 32L14 32C16.2 32 18 30.2 18 28Z" />
</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml
index cb516cdbe49b..df5985c605d1 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml
@@ -30,7 +30,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
- android:gravity="center"/>
+ android:gravity="center"
+ android:clipChildren="false"/>
<LinearLayout
android:id="@+id/bubble_overflow_empty_state"
diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
new file mode 100644
index 000000000000..d183e42c173b
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<com.android.wm.shell.windowdecor.WindowDecorLinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/caption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ android:background="@drawable/decor_caption_title">
+ <Button
+ android:id="@+id/minimize_window"
+ android:visibility="gone"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_margin="5dp"
+ android:padding="4dp"
+ android:layout_gravity="top|end"
+ android:contentDescription="@string/maximize_button_text"
+ android:background="@drawable/decor_minimize_button_dark"
+ android:duplicateParentState="true"/>
+ <Button
+ android:id="@+id/maximize_window"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_margin="5dp"
+ android:padding="4dp"
+ android:layout_gravity="center_vertical|end"
+ android:contentDescription="@string/maximize_button_text"
+ android:background="@drawable/decor_maximize_button_dark"
+ android:duplicateParentState="true"/>
+ <Button
+ android:id="@+id/close_window"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_margin="5dp"
+ android:padding="4dp"
+ android:layout_gravity="center_vertical|end"
+ android:contentDescription="@string/close_button_text"
+ android:background="@drawable/decor_close_button_dark"
+ android:duplicateParentState="true"/>
+</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml
index cd1d99ae58b0..c65f24d84e37 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml
@@ -26,7 +26,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:layout_marginBottom="12dp"/>
+ android:layout_marginBottom="20dp"/>
<TextView
android:id="@+id/letterbox_education_dialog_action_text"
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml
index 95923763d889..3a44eb9089dd 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml
@@ -50,13 +50,16 @@
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
- android:padding="24dp">
+ android:paddingTop="32dp"
+ android:paddingBottom="32dp"
+ android:paddingLeft="56dp"
+ android:paddingRight="56dp">
<ImageView
- android:layout_width="@dimen/letterbox_education_dialog_icon_size"
- android:layout_height="@dimen/letterbox_education_dialog_icon_size"
- android:layout_marginBottom="12dp"
- android:src="@drawable/letterbox_education_ic_letterboxed_app"/>
+ android:layout_width="@dimen/letterbox_education_dialog_title_icon_width"
+ android:layout_height="@dimen/letterbox_education_dialog_title_icon_height"
+ android:layout_marginBottom="17dp"
+ android:src="@drawable/letterbox_education_ic_light_bulb"/>
<TextView
android:id="@+id/letterbox_education_dialog_title"
@@ -68,16 +71,6 @@
android:textColor="@color/compat_controls_text"
android:textSize="24sp"/>
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
- android:lineSpacingExtra="4sp"
- android:text="@string/letterbox_education_dialog_subtext"
- android:textAlignment="center"
- android:textColor="@color/letterbox_education_text_secondary"
- android:textSize="14sp"/>
-
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -88,16 +81,16 @@
<com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogActionLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- app:icon="@drawable/letterbox_education_ic_screen_rotation"
- app:text="@string/letterbox_education_screen_rotation_text"/>
+ app:icon="@drawable/letterbox_education_ic_reposition"
+ app:text="@string/letterbox_education_reposition_text"/>
<com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogActionLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart=
"@dimen/letterbox_education_dialog_space_between_actions"
- app:icon="@drawable/letterbox_education_ic_reposition"
- app:text="@string/letterbox_education_reposition_text"/>
+ app:icon="@drawable/letterbox_education_ic_split_screen"
+ app:text="@string/letterbox_education_split_screen_text"/>
</LinearLayout>
@@ -105,7 +98,7 @@
android:id="@+id/letterbox_education_dialog_dismiss_button"
android:layout_width="match_parent"
android:layout_height="56dp"
- android:layout_marginTop="48dp"
+ android:layout_marginTop="40dp"
android:background=
"@drawable/letterbox_education_dismiss_button_background_ripple"
android:text="@string/letterbox_education_got_it"
diff --git a/libs/WindowManager/Shell/res/values-af/strings_tv.xml b/libs/WindowManager/Shell/res/values-af/strings_tv.xml
index 6187ea46769c..c87bec093cca 100644
--- a/libs/WindowManager/Shell/res/values-af/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-af/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Beeld-in-beeld"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Titellose program)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Maak toe"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Maak PIP toe"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Volskerm"</string>
- <string name="pip_move" msgid="158770205886688553">"Skuif"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Vou uit"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Vou in"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Skuif PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Vou PIP uit"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Vou PIP in"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Dubbeldruk "<annotation icon="home_icon">" TUIS "</annotation>" vir kontroles"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Prent-in-prent-kieslys"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Skuif links"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Skuif regs"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Skuif op"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Skuif af"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klaar"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-am/strings_tv.xml b/libs/WindowManager/Shell/res/values-am/strings_tv.xml
index 74ce49ef078e..d23353858de6 100644
--- a/libs/WindowManager/Shell/res/values-am/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-am/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ስዕል-ላይ-ስዕል"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ርዕስ የሌለው ፕሮግራም)"</string>
- <string name="pip_close" msgid="2955969519031223530">"ዝጋ"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIPን ዝጋ"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"ሙሉ ማያ ገጽ"</string>
- <string name="pip_move" msgid="158770205886688553">"ውሰድ"</string>
- <string name="pip_expand" msgid="1051966011679297308">"ዘርጋ"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"ሰብስብ"</string>
+ <string name="pip_move" msgid="1544227837964635439">"ፒአይፒ ውሰድ"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"ፒአይፒን ዘርጋ"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"ፒአይፒን ሰብስብ"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" ለመቆጣጠሪያዎች "<annotation icon="home_icon">"መነሻ"</annotation>"ን ሁለቴ ይጫኑ"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"የስዕል-ላይ-ስዕል ምናሌ።"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ወደ ግራ ውሰድ"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ወደ ቀኝ ውሰድ"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ወደ ላይ ውሰድ"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ወደ ታች ውሰድ"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ተጠናቅቋል"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml
index 9c195a7386a9..a1ceda5fc987 100644
--- a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"نافذة ضمن النافذة"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ليس هناك عنوان للبرنامج)"</string>
- <string name="pip_close" msgid="2955969519031223530">"إغلاق"</string>
+ <string name="pip_close" msgid="9135220303720555525">"‏إغلاق PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"ملء الشاشة"</string>
- <string name="pip_move" msgid="158770205886688553">"نقل"</string>
- <string name="pip_expand" msgid="1051966011679297308">"توسيع"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"تصغير"</string>
+ <string name="pip_move" msgid="1544227837964635439">"‏نقل نافذة داخل النافذة (PIP)"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"‏توسيع نافذة داخل النافذة (PIP)"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"‏تصغير نافذة داخل النافذة (PIP)"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" انقر مرتين على "<annotation icon="home_icon">" الصفحة الرئيسية "</annotation>" للوصول لعناصر التحكم."</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"قائمة نافذة ضمن النافذة"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"نقل لليسار"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"نقل لليمين"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"نقل للأعلى"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"نقل للأسفل"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"تمّ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-as/strings_tv.xml b/libs/WindowManager/Shell/res/values-as/strings_tv.xml
index 816b5b1c79dc..8d7bd9f6a27e 100644
--- a/libs/WindowManager/Shell/res/values-as/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-as/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"চিত্ৰৰ ভিতৰত চিত্ৰ"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(শিৰোনামবিহীন কাৰ্যক্ৰম)"</string>
- <string name="pip_close" msgid="2955969519031223530">"বন্ধ কৰক"</string>
+ <string name="pip_close" msgid="9135220303720555525">"পিপ বন্ধ কৰক"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"সম্পূৰ্ণ স্ক্ৰীন"</string>
- <string name="pip_move" msgid="158770205886688553">"স্থানান্তৰ কৰক"</string>
- <string name="pip_expand" msgid="1051966011679297308">"বিস্তাৰ কৰক"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"সংকোচন কৰক"</string>
+ <string name="pip_move" msgid="1544227837964635439">"পিপ স্থানান্তৰ কৰক"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"পিপ বিস্তাৰ কৰক"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"পিপ সংকোচন কৰক"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" নিয়ন্ত্ৰণৰ বাবে "<annotation icon="home_icon">" গৃহপৃষ্ঠা "</annotation>" বুটামত দুবাৰ হেঁচক"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"চিত্ৰৰ ভিতৰৰ চিত্ৰ মেনু।"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাওঁফাললৈ নিয়ক"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"সোঁফাললৈ নিয়ক"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ওপৰলৈ নিয়ক"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"তললৈ নিয়ক"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"হ’ল"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-az/strings_tv.xml b/libs/WindowManager/Shell/res/values-az/strings_tv.xml
index ccb7a7069ad8..87c46fa41a01 100644
--- a/libs/WindowManager/Shell/res/values-az/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-az/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Şəkil-içində-Şəkil"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Başlıqsız proqram)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Bağlayın"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP bağlayın"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string>
- <string name="pip_move" msgid="158770205886688553">"Köçürün"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Genişləndirin"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Yığcamlaşdırın"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP tətbiq edin"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP-ni genişləndirin"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP-ni yığcamlaşdırın"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Nizamlayıcılar üçün "<annotation icon="home_icon">" ƏSAS SƏHİFƏ "</annotation>" süçimini iki dəfə basın"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Şəkildə şəkil menyusu."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola köçürün"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa köçürün"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Yuxarı köçürün"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Aşağı köçürün"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hazırdır"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml
index 51a1262b1de7..c87f30611a07 100644
--- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Ceo ekran"</string>
- <string name="pip_move" msgid="158770205886688553">"Premesti"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Skupi"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Premesti sliku u slici"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Proširi sliku u slici"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Skupi sliku u slici"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" HOME "</annotation>" za kontrole"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni Slika u slici."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomerite nalevo"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomerite nadesno"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomerite nagore"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomerite nadole"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-be/strings_tv.xml b/libs/WindowManager/Shell/res/values-be/strings_tv.xml
index 15a353c649d6..3566bc372820 100644
--- a/libs/WindowManager/Shell/res/values-be/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-be/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Відарыс у відарысе"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Праграма без назвы)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Закрыць"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Закрыць PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Поўнаэкранны рэжым"</string>
- <string name="pip_move" msgid="158770205886688553">"Перамясціць"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Разгарнуць"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Згарнуць"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Перамясціць PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Разгарнуць відарыс у відарысе"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Згарнуць відарыс у відарысе"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Двойчы націсніце "<annotation icon="home_icon">" ГАЛОЎНЫ ЭКРАН "</annotation>" для пераходу ў налады"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню рэжыму \"Відарыс у відарысе\"."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перамясціць улева"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перамясціць управа"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Перамясціць уверх"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Перамясціць уніз"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Гатова"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml
index 2b27a6927077..91049fd2cf02 100644
--- a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картина в картината"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без заглавие)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Затваряне"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Затваряне на PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Цял екран"</string>
- <string name="pip_move" msgid="158770205886688553">"Преместване"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Разгъване"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Свиване"</string>
+ <string name="pip_move" msgid="1544227837964635439">"„Картина в картина“: Преместв."</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Разгъване на прозореца за PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Свиване на прозореца за PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" За достъп до контролите натиснете 2 пъти "<annotation icon="home_icon">"НАЧАЛО"</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню за функцията „Картина в картината“."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Преместване наляво"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Преместване надясно"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Преместване нагоре"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Преместване надолу"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml
index 23c8ffabeede..792708d128a5 100644
--- a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ছবির-মধ্যে-ছবি"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(শিরোনামহীন প্রোগ্রাম)"</string>
- <string name="pip_close" msgid="2955969519031223530">"বন্ধ করুন"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP বন্ধ করুন"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"পূর্ণ স্ক্রিন"</string>
- <string name="pip_move" msgid="158770205886688553">"সরান"</string>
- <string name="pip_expand" msgid="1051966011679297308">"বড় করুন"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"আড়াল করুন"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP সরান"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP বড় করুন"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP আড়াল করুন"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" কন্ট্রোলের জন্য "<annotation icon="home_icon">" হোম "</annotation>" বোতামে ডবল প্রেস করুন"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ছবির-মধ্যে-ছবি মেনু।"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাঁদিকে সরান"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ডানদিকে সরান"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"উপরে তুলুন"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"নিচে নামান"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"হয়ে গেছে"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml
index f10b62e65e97..ae01c641cc43 100644
--- a/libs/WindowManager/Shell/res/values-bs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bs/strings.xml
@@ -35,7 +35,7 @@
<string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava dijeljenje ekrana."</string>
<string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće raditi na sekundarnom ekranu."</string>
<string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim ekranima."</string>
- <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik podijeljenog ekrana"</string>
+ <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik ekrana"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lijevo cijeli ekran"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Lijevo 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevo 50%"</string>
diff --git a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml
index 443fd620fd65..b7f0dca1b5a5 100644
--- a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Zatvori sliku u slici"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli ekran"</string>
- <string name="pip_move" msgid="158770205886688553">"Premjesti"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Suzi"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Pokreni sliku u slici"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Proširi sliku u slici"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Suzi sliku u slici"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" POČETNI EKRAN "</annotation>" za kontrole"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za način rada slika u slici."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomjeranje ulijevo"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomjeranje udesno"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomjeranje nagore"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomjeranje nadolje"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml
index 94ba0db7e978..1c560c7afa06 100644
--- a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sense títol)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Tanca"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Tanca PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string>
- <string name="pip_move" msgid="158770205886688553">"Mou"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Desplega"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Replega"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Mou pantalla en pantalla"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Desplega pantalla en pantalla"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Replega pantalla en pantalla"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Prem dos cops "<annotation icon="home_icon">" INICI "</annotation>" per accedir als controls"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla en pantalla."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mou cap a l\'esquerra"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mou cap a la dreta"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mou cap amunt"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mou cap avall"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fet"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml
index 3ed85dce0433..9a8cc2b4d70e 100644
--- a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz v obraze"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Bez názvu)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Zavřít"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Ukončit obraz v obraze (PIP)"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string>
- <string name="pip_move" msgid="158770205886688553">"Přesunout"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Rozbalit"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Sbalit"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Přesunout PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Rozbalit PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Sbalit PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Ovládací prvky zobrazíte dvojitým stisknutím "<annotation icon="home_icon">"tlačítka plochy"</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Nabídka režimu obrazu v obraze"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Přesunout doleva"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Přesunout doprava"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Přesunout nahoru"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Přesunout dolů"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hotovo"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-da/strings_tv.xml b/libs/WindowManager/Shell/res/values-da/strings_tv.xml
index 09024428a825..cba660ac723c 100644
--- a/libs/WindowManager/Shell/res/values-da/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-da/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Integreret billede"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program uden titel)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Luk"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Luk integreret billede"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Fuld skærm"</string>
- <string name="pip_move" msgid="158770205886688553">"Flyt"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Udvid"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Flyt PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Udvid PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Skjul PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Tryk to gange på "<annotation icon="home_icon">" HJEM "</annotation>" for at se indstillinger"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu for integreret billede."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flyt til venstre"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flyt til højre"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flyt op"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flyt ned"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Udfør"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-de/strings_tv.xml b/libs/WindowManager/Shell/res/values-de/strings_tv.xml
index 18535c9d9338..02a1b66eb63f 100644
--- a/libs/WindowManager/Shell/res/values-de/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-de/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bild im Bild"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Kein Sendungsname gefunden)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Schließen"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP schließen"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Vollbild"</string>
- <string name="pip_move" msgid="158770205886688553">"Bewegen"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Maximieren"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Minimieren"</string>
+ <string name="pip_move" msgid="1544227837964635439">"BiB verschieben"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"BiB maximieren"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"BiB minimieren"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Für Steuerelemente zweimal "<annotation icon="home_icon">"STARTBILDSCHIRMTASTE"</annotation>" drücken"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menü „Bild im Bild“."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Nach links bewegen"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Nach rechts bewegen"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Nach oben bewegen"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Nach unten bewegen"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fertig"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-el/strings_tv.xml b/libs/WindowManager/Shell/res/values-el/strings_tv.xml
index 5f8a004b0a1f..24cd030cd754 100644
--- a/libs/WindowManager/Shell/res/values-el/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-el/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Δεν υπάρχει τίτλος προγράμματος)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Κλείσιμο"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Κλείσιμο PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Πλήρης οθόνη"</string>
- <string name="pip_move" msgid="158770205886688553">"Μετακίνηση"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Ανάπτυξη"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Σύμπτυξη"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Μετακίνηση PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Ανάπτυξη PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Σύμπτυξη PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Πατήστε δύο φορές "<annotation icon="home_icon">" ΑΡΧΙΚΗ ΟΘΟΝΗ "</annotation>" για στοιχεία ελέγχου"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Μενού λειτουργίας Picture-in-Picture."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Μετακίνηση αριστερά"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Μετακίνηση δεξιά"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Μετακίνηση επάνω"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Μετακίνηση κάτω"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Τέλος"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml
index 839789b22a1c..82257b42814d 100644
--- a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Close"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string>
- <string name="pip_move" msgid="158770205886688553">"Move"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Expand"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml
index 839789b22a1c..82257b42814d 100644
--- a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Close"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string>
- <string name="pip_move" msgid="158770205886688553">"Move"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Expand"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml
index 839789b22a1c..82257b42814d 100644
--- a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Close"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string>
- <string name="pip_move" msgid="158770205886688553">"Move"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Expand"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml
index 839789b22a1c..82257b42814d 100644
--- a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Close"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string>
- <string name="pip_move" msgid="158770205886688553">"Move"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Expand"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml
index 507e066e3812..a6e494cfed3c 100644
--- a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‏‏‏‎‎‎‎‏‎‎‎‏‏‏‎‏‎‎‏‎‏‎‏‏‎‎‏‎‎‏‏‏‎‎‎‎‎‏‏‎‎‏‏‎‎‎‎‏‎‎‎‎‎‎‎‏‏‎Picture-in-Picture‎‏‎‎‏‎"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‏‎‎‎‏‎‎‏‏‏‎‎‏‎‏‎‎‎‏‏‏‏‎‏‏‎‎‏‎‏‏‎‎‏‏‏‏‏‎‏‎‏‎‏‎‎‎‏‎‏‎‏‏‏‎(No title program)‎‏‎‎‏‎"</string>
- <string name="pip_close" msgid="2955969519031223530">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‎‎‎‎‎‏‎‏‏‎‏‏‎‏‏‎‏‎‎‏‏‏‎‏‏‎‏‏‏‏‎‎‏‎‏‏‏‏‎‏‎‎‎‎‎‏‎‎‏‏‏‎‏‎‏‎‎Close‎‏‎‎‏‎"</string>
+ <string name="pip_close" msgid="9135220303720555525">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‏‏‎‏‎‎‏‎‎‏‏‏‎‏‏‏‎‎‏‏‏‏‎‎‎‎‏‏‎‎‏‏‎‎‏‎‎‏‎‎‎‎‎‎‎‏‎‏‎Close PIP‎‏‎‎‏‎"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‎‎‎‎‎‎‎‎‏‏‎‏‎‎‎‏‏‎‎‎‎‎‎‏‏‏‏‎‎‎‎‏‎‏‎‎‏‎‎‎‎‎‎‎‏‎‎‏‏‎‎‏‏‎‏‎‎Full screen‎‏‎‎‏‎"</string>
- <string name="pip_move" msgid="158770205886688553">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‎‎‏‏‎‏‎‎‎‎‎‏‎‎‎‎‏‎‏‎‏‎‎‏‎‎‏‏‏‎‏‎‏‏‎‎‏‎‏‎‏‎‏‎‏‏‎‏‎‎‏‎‏‎‎‏‎Move‎‏‎‎‏‎"</string>
- <string name="pip_expand" msgid="1051966011679297308">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‎‎‏‏‎‎‏‎‏‎‏‎‏‎‏‏‎‎‎‎‎‏‏‏‎‎‏‏‎‎‏‏‏‏‎‏‏‎‏‏‏‎‎‏‏‏‏‎‎‎‏‏‏‎‎‎Expand‎‏‎‎‏‎"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‎‎‏‎‏‎‏‏‎‏‎‎‏‎‏‎‎‎‏‏‏‎‎‎‎‎‎‎‏‏‎‎‏‏‏‎‎‎‎‏‎‏‎‏‏‎‎‏‏‏‏‎‏‎‏‎‎Collapse‎‏‎‎‏‎"</string>
+ <string name="pip_move" msgid="1544227837964635439">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‏‎‏‏‎‏‏‏‎‎‎‏‏‎‎‏‏‎‎‎‎‏‎‎‏‎‏‏‏‎‏‏‎‎‎‏‎‎‏‏‎‏‏‎‎‏‏‎‏‎‎‏‎‏‏‏‏‎Move PIP‎‏‎‎‏‎"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‎‎‏‎‏‏‏‏‎‎‏‎‏‏‏‎‏‎‎‏‏‎‏‎‏‏‏‎‎‏‏‏‏‎‎‏‎‏‎‏‎‏‎‎‏‏‎‏‏‎‎‎‏‎‎Expand PIP‎‏‎‎‏‎"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‎‎‎‏‏‏‏‏‏‎‎‏‎‎‎‎‎‎‎‎‏‎‎‎‎‏‎‎‏‎‏‏‎‏‏‎‏‏‏‏‎‎Collapse PIP‎‏‎‎‏‎"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" ‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‏‎‎‏‎‏‎‎‏‏‏‏‎‎‎‏‏‏‎‏‏‏‎‏‎‎‎‎‎‎‏‎‏‏‎‏‏‏‎‏‎ Double press ‎‏‎‎‏‏‎"<annotation icon="home_icon">"‎‏‎‎‏‏‏‎ HOME ‎‏‎‎‏‏‎"</annotation>"‎‏‎‎‏‏‏‎ for controls‎‏‎‎‏‎"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‏‎‏‏‏‎‏‎‏‏‎‎‎‎‎‎‎‎‏‏‎‎‏‎‏‏‎‏‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‏‎‏‎‎‏‏‏‎‎Picture-in-Picture menu.‎‏‎‎‏‎"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‎‏‏‎‎‎‎‎‎‏‏‏‎‎‏‎‎‎‎‎‎‎‏‏‏‎‎‏‎‎‎‎‎‏‎‏‎‎‏‏‎‎‎‏‎‏‎‎‏‏‏‏‎Move left‎‏‎‎‏‎"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‏‏‎‎‎‏‎‎‎‏‏‏‏‎‎‎‎‏‎‏‎‏‎‏‎‏‎‎‎‏‎‎‏‏‎‎‏‏‏‏‎‎‏‎‏‎‏‎‏‎‏‎‏‎‎‎‎‎Move right‎‏‎‎‏‎"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‎‏‏‎‏‎‏‏‏‎‏‏‏‏‏‎‎‏‏‏‎‎‏‏‎‏‏‏‎‏‏‎‎‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‏‏‎‎‎‎‎‏‏‏‎Move up‎‏‎‎‏‎"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‏‎‎‎‏‏‎‏‎‎‏‏‏‎‎‎‏‎‏‏‏‎‏‏‎‏‎‎‎‏‏‎‏‏‎‏‏‎‏‎‏‎‏‎‏‏‏‏‎‎‏‏‏‏‎‎‎Move down‎‏‎‎‏‎"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‏‎‏‎‎‎‏‎‎‏‎‏‎‏‎‏‏‏‏‏‏‎‏‏‎‏‏‎‎‎‎‎‏‏‎‎‎‏‎‏‏‏‎‏‏‎‎‏‎‏‎‏‎‎‏‎‎Done‎‏‎‎‏‎"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml
index a2c27b79e04c..458f6b15b857 100644
--- a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Sin título de programa)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Cerrar"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Cerrar PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string>
- <string name="pip_move" msgid="158770205886688553">"Mover"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Expandir"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Mover PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Maximizar PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Minimizar PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Presiona dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla en pantalla"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover hacia arriba"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover hacia abajo"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Listo"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml
index 39990dc8cb0c..974960708190 100644
--- a/libs/WindowManager/Shell/res/values-es/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings.xml
@@ -46,10 +46,10 @@
<string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Superior 50%"</string>
<string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Superior 30%"</string>
<string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pantalla inferior completa"</string>
- <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Usar modo Una mano"</string>
+ <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Usar Modo una mano"</string>
<string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para salir, desliza el dedo hacia arriba desde la parte inferior de la pantalla o toca cualquier zona que haya encima de la aplicación"</string>
- <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar modo Una mano"</string>
- <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Salir del modo Una mano"</string>
+ <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar Modo una mano"</string>
+ <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Salir del Modo una mano"</string>
<string name="bubbles_settings_button_description" msgid="1301286017420516912">"Ajustes de las burbujas de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
<string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menú adicional"</string>
<string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Volver a añadir a la pila"</string>
@@ -63,7 +63,7 @@
<string name="bubble_dismiss_text" msgid="8816558050659478158">"Cerrar burbuja"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar conversación en burbuja"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatea con burbujas"</string>
- <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamados \"burbujas\". Toca una burbuja para abrirla. Arrástrala para moverla."</string>
+ <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamadas \"burbujas\". Toca una burbuja para abrirla. Arrástrala para moverla."</string>
<string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controla las burbujas"</string>
<string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toca Gestionar para desactivar las burbujas de esta aplicación"</string>
<string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Entendido"</string>
diff --git a/libs/WindowManager/Shell/res/values-es/strings_tv.xml b/libs/WindowManager/Shell/res/values-es/strings_tv.xml
index 75db421ec405..0a690984dac5 100644
--- a/libs/WindowManager/Shell/res/values-es/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Imagen en imagen"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sin título)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Cerrar"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Cerrar PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string>
- <string name="pip_move" msgid="158770205886688553">"Mover"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Mostrar"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Mover imagen en imagen"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Mostrar imagen en imagen"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Ocultar imagen en imagen"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Pulsa dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de imagen en imagen."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover hacia arriba"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover hacia abajo"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hecho"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-et/strings_tv.xml b/libs/WindowManager/Shell/res/values-et/strings_tv.xml
index e8fcb180c0c4..dc0232303a70 100644
--- a/libs/WindowManager/Shell/res/values-et/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-et/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pilt pildis"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programmi pealkiri puudub)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Sule"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Sule PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Täisekraan"</string>
- <string name="pip_move" msgid="158770205886688553">"Teisalda"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Laienda"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Ahenda"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Teisalda PIP-režiimi"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Laienda PIP-akent"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Ahenda PIP-aken"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Nuppude nägemiseks vajutage 2 korda nuppu "<annotation icon="home_icon">"AVAKUVA"</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menüü Pilt pildis."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Teisalda vasakule"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Teisalda paremale"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Teisalda üles"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Teisalda alla"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Valmis"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml
index 67b9a433dc03..caa335a96222 100644
--- a/libs/WindowManager/Shell/res/values-eu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-eu/strings.xml
@@ -23,7 +23,7 @@
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Sartu pantaila zatituan"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menua"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"Pantaila txiki gainjarrian dago <xliff:g id="NAME">%s</xliff:g>"</string>
- <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea nahi ez baduzu, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string>
+ <string name="pip_notification_message" msgid="8854051911700302620">"Ez baduzu nahi <xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string>
<string name="pip_play" msgid="3496151081459417097">"Erreproduzitu"</string>
<string name="pip_pause" msgid="690688849510295232">"Pausatu"</string>
<string name="pip_skip_to_next" msgid="8403429188794867653">"Joan hurrengora"</string>
diff --git a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml
index 07d75d2de9cd..bce06da2c66f 100644
--- a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantaila txiki gainjarria"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa izengabea)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Itxi"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Itxi PIPa"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Pantaila osoa"</string>
- <string name="pip_move" msgid="158770205886688553">"Mugitu"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Zabaldu"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Tolestu"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Mugitu pantaila txiki gainjarria"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Zabaldu pantaila txiki gainjarria"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Tolestu pantaila txiki gainjarria"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Kontrolatzeko aukerak atzitzeko, sakatu birritan "<annotation icon="home_icon">" HASIERA "</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pantaila txiki gainjarriaren menua."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Eraman ezkerrera"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Eraman eskuinera"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Eraman gora"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Eraman behera"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Eginda"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml
index 03f51d01a3a8..ff9a03c6cefb 100644
--- a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"تصویر در تصویر"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(برنامه بدون عنوان)"</string>
- <string name="pip_close" msgid="2955969519031223530">"بستن"</string>
+ <string name="pip_close" msgid="9135220303720555525">"‏بستن PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"تمام صفحه"</string>
- <string name="pip_move" msgid="158770205886688553">"انتقال"</string>
- <string name="pip_expand" msgid="1051966011679297308">"گسترده کردن"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"جمع کردن"</string>
+ <string name="pip_move" msgid="1544227837964635439">"‏انتقال PIP (تصویر در تصویر)"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"گسترده کردن «تصویر در تصویر»"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"جمع کردن «تصویر در تصویر»"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" برای کنترل‌ها، دکمه "<annotation icon="home_icon">"صفحه اصلی"</annotation>" را دوبار فشار دهید"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"منوی تصویر در تصویر."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"انتقال به‌چپ"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"انتقال به‌راست"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"انتقال به‌بالا"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"انتقال به‌پایین"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"تمام"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml
index 24ab7d99e180..3e8bf9032780 100644
--- a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Kuva kuvassa"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nimetön)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Sulje"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Sulje PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Koko näyttö"</string>
- <string name="pip_move" msgid="158770205886688553">"Siirrä"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Laajenna"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Tiivistä"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Siirrä PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Laajenna PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Tiivistä PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Asetukset: paina "<annotation icon="home_icon">"ALOITUSNÄYTTÖPAINIKETTA"</annotation>" kahdesti"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kuva kuvassa ‑valikko."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Siirrä vasemmalle"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Siirrä oikealle"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Siirrä ylös"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Siirrä alas"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Valmis"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml
index 87651ec711d9..66e13b89c64b 100644
--- a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Incrustation d\'image"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Aucun programme de titre)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Fermer"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Fermer mode IDI"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string>
- <string name="pip_move" msgid="158770205886688553">"Déplacer"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Développer"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Déplacer l\'image incrustée"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Développer l\'image incrustée"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Réduire l\'image incrustée"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Appuyez deux fois sur "<annotation icon="home_icon">" ACCUEIL "</annotation>" pour les commandes"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu d\'incrustation d\'image."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Déplacer vers le haut"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Déplacer vers le bas"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"OK"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml
index 07475055f03e..b3e22af0a3e3 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings.xml
@@ -63,7 +63,7 @@
<string name="bubble_dismiss_text" msgid="8816558050659478158">"Fermer la bulle"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne pas afficher la conversation dans une bulle"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatter en utilisant des bulles"</string>
- <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes ou de bulles. Appuyez sur la bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string>
+ <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes ou bulles. Appuyez sur la bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string>
<string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Contrôlez les bulles à tout moment"</string>
<string name="bubbles_user_education_manage" msgid="3460756219946517198">"Appuyez sur \"Gérer\" pour désactiver les bulles de cette application"</string>
<string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml
index 37863fb82295..ed9baf5b6215 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programme sans titre)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Fermer"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Fermer mode PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string>
- <string name="pip_move" msgid="158770205886688553">"Déplacer"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Développer"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Déplacer le PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Développer la fenêtre PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Réduire la fenêtre PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Menu de commandes : appuyez deux fois sur "<annotation icon="home_icon">"ACCUEIL"</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu \"Picture-in-picture\"."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Déplacer vers le haut"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Déplacer vers le bas"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"OK"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml
index 5d6de76c4deb..a057434d7853 100644
--- a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla superposta"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sen título)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Pechar"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Pechar PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string>
- <string name="pip_move" msgid="158770205886688553">"Mover"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Despregar"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Mover pantalla superposta"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Despregar pantalla superposta"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Contraer pantalla superposta"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Preme "<annotation icon="home_icon">"INICIO"</annotation>" dúas veces para acceder aos controis"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla superposta."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover cara á esquerda"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover cara á dereita"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover cara arriba"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover cara abaixo"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Feito"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml
index 6c1b9db73582..d9525910e4c6 100644
--- a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ચિત્રમાં-ચિત્ર"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(કોઈ ટાઇટલ પ્રોગ્રામ નથી)"</string>
- <string name="pip_close" msgid="2955969519031223530">"બંધ કરો"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP બંધ કરો"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"પૂર્ણ સ્ક્રીન"</string>
- <string name="pip_move" msgid="158770205886688553">"ખસેડો"</string>
- <string name="pip_expand" msgid="1051966011679297308">"મોટું કરો"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"નાનું કરો"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP ખસેડો"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP મોટી કરો"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP નાની કરો"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" નિયંત્રણો માટે "<annotation icon="home_icon">" હોમ "</annotation>" બટન પર બે વાર દબાવો"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ચિત્રમાં ચિત્ર મેનૂ."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ડાબે ખસેડો"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"જમણે ખસેડો"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ઉપર ખસેડો"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"નીચે ખસેડો"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"થઈ ગયું"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml
index a5fcb97d1418..36b11514c7e5 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings.xml
@@ -65,9 +65,9 @@
<string name="bubbles_user_education_title" msgid="2112319053732691899">"बबल्स का इस्तेमाल करके चैट करें"</string>
<string name="bubbles_user_education_description" msgid="4215862563054175407">"नई बातचीत फ़्लोटिंग आइकॉन या बबल्स की तरह दिखेंगी. बबल को खोलने के लिए टैप करें. इसे एक जगह से दूसरी जगह ले जाने के लिए खींचें और छोड़ें."</string>
<string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"जब चाहें, बबल्स को कंट्रोल करें"</string>
- <string name="bubbles_user_education_manage" msgid="3460756219946517198">"इस ऐप्लिकेशन पर बबल्स को बंद करने के लिए \'मैनेज करें\' पर टैप करें"</string>
+ <string name="bubbles_user_education_manage" msgid="3460756219946517198">"इस ऐप्लिकेशन पर बबल्स को बंद करने के लिए \'प्रबंधित करें\' पर टैप करें"</string>
<string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ठीक है"</string>
- <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"हाल ही के कोई बबल्स नहीं हैं"</string>
+ <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"हाल ही के बबल्स मौजूद नहीं हैं"</string>
<string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"हाल ही के बबल्स और हटाए गए बबल्स यहां दिखेंगे"</string>
<string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"मैनेज करें"</string>
diff --git a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml
index e0227253b2dc..d897ac73f80d 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"पिक्चर में पिक्चर"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(कोई शीर्षक कार्यक्रम नहीं)"</string>
- <string name="pip_close" msgid="2955969519031223530">"बंद करें"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP बंद करें"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"फ़ुल स्‍क्रीन"</string>
- <string name="pip_move" msgid="158770205886688553">"ले जाएं"</string>
- <string name="pip_expand" msgid="1051966011679297308">"बड़ा करें"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"छोटा करें"</string>
+ <string name="pip_move" msgid="1544227837964635439">"पीआईपी को दूसरी जगह लेकर जाएं"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"पीआईपी विंडो को बड़ा करें"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"पीआईपी विंडो को छोटा करें"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" कंट्रोल मेन्यू पर जाने के लिए, "<annotation icon="home_icon">" होम बटन"</annotation>" दो बार दबाएं"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"पिक्चर में पिक्चर मेन्यू."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बाईं ओर ले जाएं"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दाईं ओर ले जाएं"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ऊपर ले जाएं"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"नीचे ले जाएं"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"हो गया"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml
index a09e6e805f63..8f5f3164c4d7 100644
--- a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli zaslon"</string>
- <string name="pip_move" msgid="158770205886688553">"Premjesti"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Sažmi"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Premjesti PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Proširi PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Sažmi PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">"POČETNI ZASLON"</annotation>" za kontrole"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izbornik slike u slici."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomaknite ulijevo"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomaknite udesno"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomaknite prema gore"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomaknite prema dolje"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml
index 5e065c2ad4e7..fc8d79589121 100644
--- a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Kép a képben"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Cím nélküli program)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Bezárás"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP bezárása"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Teljes képernyő"</string>
- <string name="pip_move" msgid="158770205886688553">"Áthelyezés"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Kibontás"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Összecsukás"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP áthelyezése"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Kép a képben kibontása"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Kép a képben összecsukása"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Vezérlők: "<annotation icon="home_icon">" KEZDŐKÉPERNYŐ "</annotation>" gomb kétszer megnyomva"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kép a képben menü."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mozgatás balra"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mozgatás jobbra"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mozgatás felfelé"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mozgatás lefelé"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Kész"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml
index 7963abf8972b..f5665b8dd166 100644
--- a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Նկար նկարի մեջ"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Առանց վերնագրի ծրագիր)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Փակել"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Փակել PIP-ն"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Լիէկրան"</string>
- <string name="pip_move" msgid="158770205886688553">"Տեղափոխել"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Ծավալել"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Ծալել"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Տեղափոխել PIP-ը"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Ծավալել PIP-ը"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Ծալել PIP-ը"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Կարգավորումների համար կրկնակի սեղմեք "<annotation icon="home_icon">"ԳԼԽԱՎՈՐ ԷԿՐԱՆ"</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"«Նկար նկարի մեջ» ռեժիմի ընտրացանկ։"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Տեղափոխել ձախ"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Տեղափոխել աջ"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Տեղափոխել վերև"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Տեղափոխել ներքև"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Պատրաստ է"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-in/strings_tv.xml b/libs/WindowManager/Shell/res/values-in/strings_tv.xml
index 7d37154bb86c..a1535653f679 100644
--- a/libs/WindowManager/Shell/res/values-in/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-in/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tanpa judul)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Tutup"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Layar penuh"</string>
- <string name="pip_move" msgid="158770205886688553">"Pindahkan"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Luaskan"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Ciutkan"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Pindahkan PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Luaskan PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Ciutkan PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" HOME "</annotation>" untuk membuka kontrol"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture-in-Picture."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pindahkan ke kiri"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pindahkan ke kanan"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pindahkan ke atas"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pindahkan ke bawah"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Selesai"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-is/strings_tv.xml b/libs/WindowManager/Shell/res/values-is/strings_tv.xml
index 1490cb98e034..70ca1afe3aea 100644
--- a/libs/WindowManager/Shell/res/values-is/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-is/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Mynd í mynd"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Efni án titils)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Loka"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Loka mynd í mynd"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Allur skjárinn"</string>
- <string name="pip_move" msgid="158770205886688553">"Færa"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Stækka"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Minnka"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Færa innfellda mynd"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Stækka innfellda mynd"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Minnka innfellda mynd"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Ýttu tvisvar á "<annotation icon="home_icon">" HEIM "</annotation>" til að opna stillingar"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Valmynd fyrir mynd í mynd."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Færa til vinstri"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Færa til hægri"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Færa upp"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Færa niður"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Lokið"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-it/strings_tv.xml b/libs/WindowManager/Shell/res/values-it/strings_tv.xml
index a48516f2588e..cda627517872 100644
--- a/libs/WindowManager/Shell/res/values-it/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-it/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture in picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programma senza titolo)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Chiudi"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Chiudi PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Schermo intero"</string>
- <string name="pip_move" msgid="158770205886688553">"Sposta"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Espandi"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Comprimi"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Sposta PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Espandi PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Comprimi PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Premi due volte "<annotation icon="home_icon">" HOME "</annotation>" per aprire i controlli"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture in picture."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sposta a sinistra"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sposta a destra"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Sposta su"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Sposta giù"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fine"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml
index 2af1896d3c67..30ce97b998ca 100644
--- a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"תמונה בתוך תמונה"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(תוכנית ללא כותרת)"</string>
- <string name="pip_close" msgid="2955969519031223530">"סגירה"</string>
+ <string name="pip_close" msgid="9135220303720555525">"‏סגירת PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"מסך מלא"</string>
- <string name="pip_move" msgid="158770205886688553">"העברה"</string>
- <string name="pip_expand" msgid="1051966011679297308">"הרחבה"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"כיווץ"</string>
+ <string name="pip_move" msgid="1544227837964635439">"‏העברת תמונה בתוך תמונה (PIP)"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"הרחבת חלון תמונה-בתוך-תמונה"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"כיווץ של חלון תמונה-בתוך-תמונה"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" לחיצה כפולה על "<annotation icon="home_icon">" הלחצן הראשי "</annotation>" תציג את אמצעי הבקרה"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"תפריט \'תמונה בתוך תמונה\'."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"הזזה שמאלה"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"הזזה ימינה"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"הזזה למעלה"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"הזזה למטה"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"סיום"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml
index bc7dcb7aa029..e58e7bf6fabc 100644
--- a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ピクチャー イン ピクチャー"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無題の番組)"</string>
- <string name="pip_close" msgid="2955969519031223530">"閉じる"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP を閉じる"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"全画面表示"</string>
- <string name="pip_move" msgid="158770205886688553">"移動"</string>
- <string name="pip_expand" msgid="1051966011679297308">"開く"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"閉じる"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP を移動"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP を開く"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP を閉じる"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" コントロールにアクセス: "<annotation icon="home_icon">" ホーム "</annotation>" を 2 回押します"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ピクチャー イン ピクチャーのメニューです。"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左に移動"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右に移動"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"上に移動"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"下に移動"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完了"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml
index 898dac2aca88..b09686646c8b 100644
--- a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ეკრანი ეკრანში"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(პროგრამის სათაურის გარეშე)"</string>
- <string name="pip_close" msgid="2955969519031223530">"დახურვა"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP-ის დახურვა"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"სრულ ეკრანზე"</string>
- <string name="pip_move" msgid="158770205886688553">"გადაადგილება"</string>
- <string name="pip_expand" msgid="1051966011679297308">"გაშლა"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"ჩაკეცვა"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP გადატანა"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP-ის გაშლა"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP-ის ჩაკეცვა"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" მართვის საშუალებებზე წვდომისთვის ორმაგად დააჭირეთ "<annotation icon="home_icon">" მთავარ ღილაკს "</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"მენიუ „ეკრანი ეკრანში“."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"მარცხნივ გადატანა"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"მარჯვნივ გადატანა"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ზემოთ გადატანა"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ქვემოთ გადატანა"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"მზადაა"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml
index cdf564fb4ca0..7bade0dff0d9 100644
--- a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Суреттегі сурет"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Атаусыз бағдарлама)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Жабу"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP жабу"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Толық экран"</string>
- <string name="pip_move" msgid="158770205886688553">"Жылжыту"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Жаю"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Жию"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP клипін жылжыту"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP терезесін жаю"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP терезесін жию"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Басқару элементтері: "<annotation icon="home_icon">" НЕГІЗГІ ЭКРАН "</annotation>" түймесін екі рет басыңыз."</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"Сурет ішіндегі сурет\" мәзірі."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солға жылжыту"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңға жылжыту"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Жоғары жылжыту"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Төмен жылжыту"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Дайын"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-km/strings_tv.xml b/libs/WindowManager/Shell/res/values-km/strings_tv.xml
index 1a7ae813c1d3..721be1fc1650 100644
--- a/libs/WindowManager/Shell/res/values-km/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-km/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"រូបក្នុងរូប"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(កម្មវិធី​គ្មានចំណងជើង)"</string>
- <string name="pip_close" msgid="2955969519031223530">"បិទ"</string>
+ <string name="pip_close" msgid="9135220303720555525">"បិទ PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"ពេញអេក្រង់"</string>
- <string name="pip_move" msgid="158770205886688553">"ផ្លាស់ទី"</string>
- <string name="pip_expand" msgid="1051966011679297308">"ពង្រីក"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"បង្រួម"</string>
+ <string name="pip_move" msgid="1544227837964635439">"ផ្លាស់ទី PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"ពង្រីក PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"បង្រួម PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" ចុចពីរដងលើ"<annotation icon="home_icon">"ប៊ូតុងដើម"</annotation>" ដើម្បីបើកផ្ទាំងគ្រប់គ្រង"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ម៉ឺនុយ​រូប​ក្នុងរូប"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ផ្លាស់ទី​ទៅ​ឆ្វេង"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ផ្លាស់ទីទៅ​ស្តាំ"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ផ្លាស់ទី​ឡើង​លើ"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ផ្លាស់ទី​ចុះ​ក្រោម"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"រួចរាល់"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml
index 45de068c80a0..8310c8a1169c 100644
--- a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ಶೀರ್ಷಿಕೆ ರಹಿತ ಕಾರ್ಯಕ್ರಮ)"</string>
- <string name="pip_close" msgid="2955969519031223530">"ಮುಚ್ಚಿರಿ"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP ಮುಚ್ಚಿ"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"ಪೂರ್ಣ ಪರದೆ"</string>
- <string name="pip_move" msgid="158770205886688553">"ಸರಿಸಿ"</string>
- <string name="pip_expand" msgid="1051966011679297308">"ವಿಸ್ತೃತಗೊಳಿಸಿ"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"ಕುಗ್ಗಿಸಿ"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP ಅನ್ನು ಸರಿಸಿ"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವನ್ನು ವಿಸ್ತರಿಸಿ"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವನ್ನು ಕುಗ್ಗಿಸಿ"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" ಕಂಟ್ರೋಲ್‌ಗಳಿಗಾಗಿ "<annotation icon="home_icon">" ಹೋಮ್ "</annotation>" ಅನ್ನು ಎರಡು ಬಾರಿ ಒತ್ತಿ"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ ಮೆನು."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ಎಡಕ್ಕೆ ಸರಿಸಿ"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ಬಲಕ್ಕೆ ಸರಿಸಿ"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ಮೇಲಕ್ಕೆ ಸರಿಸಿ"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ಕೆಳಗೆ ಸರಿಸಿ"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ಮುಗಿದಿದೆ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml
index 9e8f1f1258a5..a3e055a515a1 100644
--- a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"PIP 모드"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(제목 없는 프로그램)"</string>
- <string name="pip_close" msgid="2955969519031223530">"닫기"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP 닫기"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"전체화면"</string>
- <string name="pip_move" msgid="158770205886688553">"이동"</string>
- <string name="pip_expand" msgid="1051966011679297308">"펼치기"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"접기"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP 이동"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP 펼치기"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP 접기"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" 제어 메뉴에 액세스하려면 "<annotation icon="home_icon">" 홈 "</annotation>"을 두 번 누르세요."</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"PIP 모드 메뉴입니다."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"왼쪽으로 이동"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"오른쪽으로 이동"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"위로 이동"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"아래로 이동"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"완료"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml
index 19fac5876bb0..887ac52c8e43 100644
--- a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Сүрөттөгү сүрөт"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Аталышы жок программа)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Жабуу"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP\'ти жабуу"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Толук экран"</string>
- <string name="pip_move" msgid="158770205886688553">"Жылдыруу"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Жайып көрсөтүү"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Жыйыштыруу"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP\'ти жылдыруу"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP\'ти жайып көрсөтүү"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP\'ти жыйыштыруу"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Башкаруу элементтерин ачуу үчүн "<annotation icon="home_icon">" БАШКЫ БЕТ "</annotation>" баскычын эки жолу басыңыз"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Сүрөт ичиндеги сүрөт менюсу."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солго жылдыруу"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңго жылдыруу"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Жогору жылдыруу"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Төмөн жылдыруу"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Бүттү"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml
index 6cd0f37c516c..91c4a033356d 100644
--- a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ການສະແດງຜົນຊ້ອນກັນ"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ໂປຣແກຣມບໍ່ມີຊື່)"</string>
- <string name="pip_close" msgid="2955969519031223530">"ປິດ"</string>
+ <string name="pip_close" msgid="9135220303720555525">"ປິດ PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"ເຕັມໜ້າຈໍ"</string>
- <string name="pip_move" msgid="158770205886688553">"ຍ້າຍ"</string>
- <string name="pip_expand" msgid="1051966011679297308">"ຂະຫຍາຍ"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"ຫຍໍ້"</string>
+ <string name="pip_move" msgid="1544227837964635439">"ຍ້າຍ PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"ຂະຫຍາຍ PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"ຫຍໍ້ PIP ລົງ"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" ກົດ "<annotation icon="home_icon">" HOME "</annotation>" ສອງເທື່ອສຳລັບການຄວບຄຸມ"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ເມນູການສະແດງຜົນຊ້ອນກັນ."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ຍ້າຍໄປຊ້າຍ"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ຍ້າຍໄປຂວາ"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ຍ້າຍຂຶ້ນ"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ຍ້າຍລົງ"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ແລ້ວໆ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml
index 52017dca2b94..04265ca01b48 100644
--- a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Vaizdas vaizde"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa be pavadinimo)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Uždaryti"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Uždaryti PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Visas ekranas"</string>
- <string name="pip_move" msgid="158770205886688553">"Perkelti"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Išskleisti"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Sutraukti"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Perkelti PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Iškleisti PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Sutraukti PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Jei reikia valdiklių, dukart paspauskite "<annotation icon="home_icon">"PAGRINDINIS"</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Vaizdo vaizde meniu."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Perkelti kairėn"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Perkelti dešinėn"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Perkelti aukštyn"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Perkelti žemyn"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Atlikta"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml
index 11abac6f6197..8c6191e00833 100644
--- a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Attēls attēlā"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programma bez nosaukuma)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Aizvērt"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Aizvērt PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Pilnekrāna režīms"</string>
- <string name="pip_move" msgid="158770205886688553">"Pārvietot"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Izvērst"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Sakļaut"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Pārvietot attēlu attēlā"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Izvērst “Attēls attēlā” logu"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Sakļaut “Attēls attēlā” logu"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Atvērt vadīklas: divreiz nospiediet pogu "<annotation icon="home_icon">"SĀKUMS"</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izvēlne attēlam attēlā."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pārvietot pa kreisi"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pārvietot pa labi"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pārvietot augšup"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pārvietot lejup"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gatavs"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml
index 21293223b882..beef1fef862b 100644
--- a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Слика во слика"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без наслов)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Затвори"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Цел екран"</string>
- <string name="pip_move" msgid="158770205886688553">"Премести"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Прошири"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Собери"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Премести PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Прошири ја сликата во слика"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Собери ја сликата во слика"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Притиснете двапати на "<annotation icon="home_icon">" HOME "</annotation>" за контроли"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени за „Слика во слика“."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Премести налево"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Премести надесно"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Премести нагоре"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Премести надолу"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml
index 549e39b21101..c2a532d09647 100644
--- a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ചിത്രത്തിനുള്ളിൽ ചിത്രം"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(പേരില്ലാത്ത പ്രോഗ്രാം)"</string>
- <string name="pip_close" msgid="2955969519031223530">"അടയ്ക്കുക"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP അടയ്ക്കുക"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"പൂര്‍ണ്ണ സ്ക്രീന്‍"</string>
- <string name="pip_move" msgid="158770205886688553">"നീക്കുക"</string>
- <string name="pip_expand" msgid="1051966011679297308">"വികസിപ്പിക്കുക"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"ചുരുക്കുക"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP നീക്കുക"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP വികസിപ്പിക്കുക"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP ചുരുക്കുക"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" നിയന്ത്രണങ്ങൾക്കായി "<annotation icon="home_icon">" ഹോം "</annotation>" രണ്ട് തവണ അമർത്തുക"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ചിത്രത്തിനുള്ളിൽ ചിത്രം മെനു."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ഇടത്തേക്ക് നീക്കുക"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"വലത്തേക്ക് നീക്കുക"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"മുകളിലേക്ക് നീക്കുക"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"താഴേക്ക് നീക്കുക"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"പൂർത്തിയായി"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml
index 9a85d96ca602..bf8c59b57359 100644
--- a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Дэлгэц доторх дэлгэц"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Гарчиггүй хөтөлбөр)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Хаах"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP-г хаах"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Бүтэн дэлгэц"</string>
- <string name="pip_move" msgid="158770205886688553">"Зөөх"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Дэлгэх"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Хураах"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP-г зөөх"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP-г дэлгэх"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP-г хураах"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Хяналтад хандах бол "<annotation icon="home_icon">" HOME "</annotation>" дээр хоёр дарна уу"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Дэлгэцэн доторх дэлгэцийн цэс."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Зүүн тийш зөөх"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Баруун тийш зөөх"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Дээш зөөх"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Доош зөөх"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Болсон"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml
index a9779b3a3e89..5d519b7afe9a 100644
--- a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"चित्रात-चित्र"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षक नसलेला कार्यक्रम)"</string>
- <string name="pip_close" msgid="2955969519031223530">"बंद करा"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP बंद करा"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रीन"</string>
- <string name="pip_move" msgid="158770205886688553">"हलवा"</string>
- <string name="pip_expand" msgid="1051966011679297308">"विस्तार करा"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"कोलॅप्स करा"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP हलवा"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP चा विस्तार करा"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP कोलॅप्स करा"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" नियंत्रणांसाठी "<annotation icon="home_icon">" होम "</annotation>" दोनदा दाबा"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"चित्रात-चित्र मेनू."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"डावीकडे हलवा"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"उजवीकडे हलवा"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"वर हलवा"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"खाली हलवा"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"पूर्ण झाले"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml
index 8fe992d9f3b9..08642c47c91a 100644
--- a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Gambar dalam Gambar"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tiada tajuk)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Tutup"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Skrin penuh"</string>
- <string name="pip_move" msgid="158770205886688553">"Alih"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Kembangkan"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Kuncupkan"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Alihkan PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Kembangkan PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Kuncupkan PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" LAMAN UTAMA "</annotation>" untuk mengakses kawalan"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Gambar dalam Gambar."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Alih ke kiri"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Alih ke kanan"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Alih ke atas"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Alih ke bawah"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Selesai"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-my/strings_tv.xml b/libs/WindowManager/Shell/res/values-my/strings_tv.xml
index 105628d8149e..e01daee115ca 100644
--- a/libs/WindowManager/Shell/res/values-my/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-my/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"နှစ်ခုထပ်၍ကြည့်ခြင်း"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ခေါင်းစဉ်မဲ့ အစီအစဉ်)"</string>
- <string name="pip_close" msgid="2955969519031223530">"ပိတ်ရန်"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP ကိုပိတ်ပါ"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"မျက်နှာပြင် အပြည့်"</string>
- <string name="pip_move" msgid="158770205886688553">"ရွှေ့ရန်"</string>
- <string name="pip_expand" msgid="1051966011679297308">"ချဲ့ရန်"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"လျှော့ပြရန်"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP ရွှေ့ရန်"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP ကို ချဲ့ရန်"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP ကို လျှော့ပြပါ"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" ထိန်းချုပ်မှုအတွက် "<annotation icon="home_icon">" ပင်မခလုတ် "</annotation>" နှစ်ချက်နှိပ်ပါ"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"နှစ်ခုထပ်၍ ကြည့်ခြင်းမီနူး။"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ဘယ်သို့ရွှေ့ရန်"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ညာသို့ရွှေ့ရန်"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"အပေါ်သို့ရွှေ့ရန်"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"အောက်သို့ရွှေ့ရန်"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ပြီးပြီ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml
index 2f2fea6eb833..9fd42b2f129c 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings.xml
@@ -63,7 +63,7 @@
<string name="bubble_dismiss_text" msgid="8816558050659478158">"Lukk boblen"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ikke vis samtaler i bobler"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat med bobler"</string>
- <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som flytende ikoner eller bobler. Trykk for å åpne en boble. Dra for å flytte den."</string>
+ <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som flytende ikoner eller bobler. Trykk for å åpne bobler. Dra for å flytte dem."</string>
<string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kontrollér bobler når som helst"</string>
<string name="bubbles_user_education_manage" msgid="3460756219946517198">"Trykk på Administrer for å slå av bobler for denne appen"</string>
<string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Greit"</string>
diff --git a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml
index ca63518df7a5..65ed0b7f5bff 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bilde-i-bilde"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program uten tittel)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Lukk"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Lukk PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Fullskjerm"</string>
- <string name="pip_move" msgid="158770205886688553">"Flytt"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Vis"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Flytt BIB"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Vis BIB"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Skjul BIB"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Dobbelttrykk på "<annotation icon="home_icon">"HJEM"</annotation>" for å åpne kontroller"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bilde-i-bilde-meny."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytt til venstre"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytt til høyre"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flytt opp"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flytt ned"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Ferdig"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml
index 7cbf9e294e7b..d33fed67efb6 100644
--- a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षकविहीन कार्यक्रम)"</string>
- <string name="pip_close" msgid="2955969519031223530">"बन्द गर्नुहोस्"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP लाई बन्द गर्नुहोस्"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रिन"</string>
- <string name="pip_move" msgid="158770205886688553">"सार्नुहोस्"</string>
- <string name="pip_expand" msgid="1051966011679297308">"एक्स्पान्ड गर्नुहोस्"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"कोल्याप्स गर्नुहोस्"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP सार्नुहोस्"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP विन्डो एक्स्पान्ड गर्नु…"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP विन्डो कोल्याप्स गर्नुहोस्"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" कन्ट्रोल मेनु खोल्न "<annotation icon="home_icon">" होम "</annotation>" बटन दुई पटक थिच्नुहोस्"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"picture-in-picture\" मेनु।"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बायाँतिर सार्नुहोस्"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दायाँतिर सार्नुहोस्"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"माथितिर सार्नुहोस्"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"तलतिर सार्नुहोस्"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"सम्पन्न भयो"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml
index 2deaeddc4080..9763c5665ab2 100644
--- a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Scherm-in-scherm"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Naamloos programma)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Sluiten"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP sluiten"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Volledig scherm"</string>
- <string name="pip_move" msgid="158770205886688553">"Verplaatsen"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Uitvouwen"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Samenvouwen"</string>
+ <string name="pip_move" msgid="1544227837964635439">"SIS verplaatsen"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"SIS uitvouwen"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"SIS samenvouwen"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Druk twee keer op "<annotation icon="home_icon">" HOME "</annotation>" voor bedieningselementen"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Scherm-in-scherm-menu."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Naar links verplaatsen"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Naar rechts verplaatsen"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Omhoog verplaatsen"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Omlaag verplaatsen"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klaar"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-or/strings_tv.xml b/libs/WindowManager/Shell/res/values-or/strings_tv.xml
index 0c1d99e4ca71..e0344855bd1f 100644
--- a/libs/WindowManager/Shell/res/values-or/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-or/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ପିକଚର୍-ଇନ୍-ପିକଚର୍"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(କୌଣସି ଟାଇଟଲ୍‍ ପ୍ରୋଗ୍ରାମ୍‍ ନାହିଁ)"</string>
- <string name="pip_close" msgid="2955969519031223530">"ବନ୍ଦ କରନ୍ତୁ"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP ବନ୍ଦ କରନ୍ତୁ"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍‍"</string>
- <string name="pip_move" msgid="158770205886688553">"ମୁଭ କରନ୍ତୁ"</string>
- <string name="pip_expand" msgid="1051966011679297308">"ବିସ୍ତାର କରନ୍ତୁ"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"ସଙ୍କୁଚିତ କରନ୍ତୁ"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIPକୁ ମୁଭ କରନ୍ତୁ"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIPକୁ ବିସ୍ତାର କରନ୍ତୁ"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIPକୁ ସଙ୍କୁଚିତ କରନ୍ତୁ"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ ପାଇଁ "<annotation icon="home_icon">" ହୋମ ବଟନ "</annotation>"କୁ ଦୁଇଥର ଦବାନ୍ତୁ"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ପିକଚର-ଇନ-ପିକଚର ମେନୁ।"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ବାମକୁ ମୁଭ କରନ୍ତୁ"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ଉପରକୁ ମୁଭ କରନ୍ତୁ"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ତଳକୁ ମୁଭ କରନ୍ତୁ"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ହୋଇଗଲା"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml
index a1edde738775..9c01ac3f3cc0 100644
--- a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ਸਿਰਲੇਖ-ਰਹਿਤ ਪ੍ਰੋਗਰਾਮ)"</string>
- <string name="pip_close" msgid="2955969519031223530">"ਬੰਦ ਕਰੋ"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP ਬੰਦ ਕਰੋ"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"ਪੂਰੀ ਸਕ੍ਰੀਨ"</string>
- <string name="pip_move" msgid="158770205886688553">"ਲਿਜਾਓ"</string>
- <string name="pip_expand" msgid="1051966011679297308">"ਵਿਸਤਾਰ ਕਰੋ"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"ਸਮੇਟੋ"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP ਨੂੰ ਲਿਜਾਓ"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP ਦਾ ਵਿਸਤਾਰ ਕਰੋ"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP ਨੂੰ ਸਮੇਟੋ"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" ਕੰਟਰੋਲਾਂ ਲਈ "<annotation icon="home_icon">" ਹੋਮ ਬਟਨ "</annotation>" ਨੂੰ ਦੋ ਵਾਰ ਦਬਾਓ"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ ਮੀਨੂ।"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ਖੱਬੇ ਲਿਜਾਓ"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ਸੱਜੇ ਲਿਜਾਓ"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ਉੱਪਰ ਲਿਜਾਓ"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ਹੇਠਾਂ ਲਿਜਾਓ"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ਹੋ ਗਿਆ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml
index 2bb90addc241..b922e2d5a6ba 100644
--- a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz w obrazie"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez tytułu)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Zamknij"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Zamknij PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Pełny ekran"</string>
- <string name="pip_move" msgid="158770205886688553">"Przenieś"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Rozwiń"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Zwiń"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Przenieś PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Rozwiń PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Zwiń PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Naciśnij dwukrotnie "<annotation icon="home_icon">"EKRAN GŁÓWNY"</annotation>", aby wyświetlić ustawienia"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu funkcji Obraz w obrazie."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Przenieś w lewo"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Przenieś w prawo"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Przenieś w górę"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Przenieś w dół"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotowe"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml
index 14d1c34fd3e8..cc4eb3c32c1f 100644
--- a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(programa sem título)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Fechar"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string>
- <string name="pip_move" msgid="158770205886688553">"Mover"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Abrir"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Abrir picture-in-picture"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Fechar picture-in-picture"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml
index 1ada4508714a..c4ae78d89ba8 100644
--- a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Ecrã no ecrã"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Sem título do programa)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Fechar"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Ecrã inteiro"</string>
- <string name="pip_move" msgid="158770205886688553">"Mover"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Expandir"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Reduzir"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Mover Ecrã no ecrã"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Expandir Ecrã no ecrã"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Reduzir Ecrã no ecrã"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Prima duas vezes "<annotation icon="home_icon">" PÁGINA INICIAL "</annotation>" para controlos"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu de ecrã no ecrã."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml
index 14d1c34fd3e8..cc4eb3c32c1f 100644
--- a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(programa sem título)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Fechar"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string>
- <string name="pip_move" msgid="158770205886688553">"Mover"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Abrir"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Abrir picture-in-picture"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Fechar picture-in-picture"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml
index ba3701dc734c..804d34f980ff 100644
--- a/libs/WindowManager/Shell/res/values-ro/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ro/strings.xml
@@ -17,20 +17,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="pip_phone_close" msgid="5783752637260411309">"Închide"</string>
- <string name="pip_phone_expand" msgid="2579292903468287504">"Extinde"</string>
+ <string name="pip_phone_close" msgid="5783752637260411309">"Închideți"</string>
+ <string name="pip_phone_expand" msgid="2579292903468287504">"Extindeți"</string>
<string name="pip_phone_settings" msgid="5468987116750491918">"Setări"</string>
- <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accesează ecranul împărțit"</string>
+ <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accesați ecranul împărțit"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> este în modul picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Dacă nu doriți ca <xliff:g id="NAME">%s</xliff:g> să utilizeze această funcție, atingeți pentru a deschide setările și dezactivați-o."</string>
- <string name="pip_play" msgid="3496151081459417097">"Redă"</string>
- <string name="pip_pause" msgid="690688849510295232">"Întrerupe"</string>
+ <string name="pip_play" msgid="3496151081459417097">"Redați"</string>
+ <string name="pip_pause" msgid="690688849510295232">"Întrerupeți"</string>
<string name="pip_skip_to_next" msgid="8403429188794867653">"Treceți la următorul"</string>
<string name="pip_skip_to_prev" msgid="7172158111196394092">"Treceți la cel anterior"</string>
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionați"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stocați"</string>
- <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulează stocarea"</string>
+ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulați stocarea"</string>
<string name="dock_forced_resizable" msgid="1749750436092293116">"Este posibil ca aplicația să nu funcționeze cu ecranul împărțit."</string>
<string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplicația nu acceptă ecranul împărțit."</string>
<string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Este posibil ca aplicația să nu funcționeze pe un ecran secundar."</string>
@@ -48,19 +48,19 @@
<string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Partea de jos pe ecran complet"</string>
<string name="one_handed_tutorial_title" msgid="4583241688067426350">"Folosirea modului cu o mână"</string>
<string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pentru a ieși, glisați în sus din partea de jos a ecranului sau atingeți oriunde deasupra ferestrei aplicației"</string>
- <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Activează modul cu o mână"</string>
+ <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Activați modul cu o mână"</string>
<string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Părăsiți modul cu o mână"</string>
<string name="bubbles_settings_button_description" msgid="1301286017420516912">"Setări pentru baloanele <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
<string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Suplimentar"</string>
- <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Adaugă înapoi în stivă"</string>
+ <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Adăugați înapoi în stivă"</string>
<string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
<string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g> și încă <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string>
- <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mută în stânga sus"</string>
- <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mută în dreapta sus"</string>
- <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mută în stânga jos"</string>
- <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mută în dreapta jos"</string>
+ <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mutați în stânga sus"</string>
+ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mutați în dreapta sus"</string>
+ <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mutați în stânga jos"</string>
+ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mutați în dreapta jos"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"Setări <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
- <string name="bubble_dismiss_text" msgid="8816558050659478158">"Închide balonul"</string>
+ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Închideți balonul"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nu afișați conversația în balon"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat cu baloane"</string>
<string name="bubbles_user_education_description" msgid="4215862563054175407">"Conversațiile noi apar ca pictograme flotante sau baloane. Atingeți pentru a deschide balonul. Trageți pentru a-l muta."</string>
@@ -70,15 +70,15 @@
<string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nu există baloane recente"</string>
<string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Baloanele recente și baloanele respinse vor apărea aici"</string>
<string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string>
- <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionează"</string>
+ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionați"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balonul a fost respins."</string>
<string name="restart_button_description" msgid="5887656107651190519">"Atingeți ca să reporniți aplicația și să treceți în modul ecran complet."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Aveți probleme cu camera foto?\nAtingeți pentru a reîncadra"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nu ați remediat problema?\nAtingeți pentru a reveni"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nu aveți probleme cu camera foto? Atingeți pentru a închide."</string>
<string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Unele aplicații funcționează cel mai bine în orientarea portret"</string>
- <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Încearcă una dintre aceste opțiuni pentru a profita din plin de spațiu"</string>
- <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rotește dispozitivul pentru a trece în modul ecran complet"</string>
- <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Atinge de două ori lângă o aplicație pentru a o repoziționa"</string>
+ <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Încercați una dintre aceste opțiuni pentru a profita din plin de spațiu"</string>
+ <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rotiți dispozitivul pentru a trece în modul ecran complet"</string>
+ <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Atingeți de două ori lângă o aplicație pentru a o repoziționa"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml
index 36df2864a752..86a30f49df15 100644
--- a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program fără titlu)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Închide"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Închideți PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Ecran complet"</string>
- <string name="pip_move" msgid="158770205886688553">"Mută"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Extinde"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Restrânge"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Apasă de două ori "<annotation icon="home_icon">"butonul ecran de pornire"</annotation>" pentru comenzi"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meniu picture-in-picture."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mută spre stânga"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mută spre dreapta"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mută în sus"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mută în jos"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gata"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Mutați fereastra PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Extindeți fereastra PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Restrângeți fereastra PIP"</string>
+ <string name="pip_edu_text" msgid="3672999496647508701">" Apăsați de două ori "<annotation icon="home_icon">"butonul ecran de pornire"</annotation>" pentru comenzi"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml
index e7f55ec1bc57..08623e1e69c5 100644
--- a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картинка в картинке"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Без названия)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Закрыть"</string>
+ <string name="pip_close" msgid="9135220303720555525">"\"Кадр в кадре\" – выйти"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Во весь экран"</string>
- <string name="pip_move" msgid="158770205886688553">"Переместить"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Развернуть"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Свернуть"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Переместить PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Развернуть PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Свернуть PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Элементы управления: дважды нажмите "<annotation icon="home_icon">" кнопку главного экрана "</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"Картинка в картинке\"."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Переместить влево"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Переместить вправо"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Переместить вверх"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Переместить вниз"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-si/strings_tv.xml b/libs/WindowManager/Shell/res/values-si/strings_tv.xml
index 5478ce5d3d40..fbb0ebba0623 100644
--- a/libs/WindowManager/Shell/res/values-si/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-si/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"පින්තූරය-තුළ-පින්තූරය"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(මාතෘකාවක් නැති වැඩසටහන)"</string>
- <string name="pip_close" msgid="2955969519031223530">"වසන්න"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP වසන්න"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"සම්පූර්ණ තිරය"</string>
- <string name="pip_move" msgid="158770205886688553">"ගෙන යන්න"</string>
- <string name="pip_expand" msgid="1051966011679297308">"දිග හරින්න"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"හකුළන්න"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP ගෙන යන්න"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP දිග හරින්න"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP හකුළන්න"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" පාලන සඳහා "<annotation icon="home_icon">" මුල් පිටුව "</annotation>" දෙවරක් ඔබන්න"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"පින්තූරය තුළ පින්තූරය මෙනුව"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"වමට ගෙන යන්න"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"දකුණට ගෙන යන්න"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ඉහළට ගෙන යන්න"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"පහළට ගෙන යන්න"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"නිමයි"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml
index 1df43afca2da..81cb0eafc759 100644
--- a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz v obraze"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez názvu)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Zavrieť"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Zavrieť režim PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string>
- <string name="pip_move" msgid="158770205886688553">"Presunúť"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Rozbaliť"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Zbaliť"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Presunúť obraz v obraze"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Rozbaliť obraz v obraze"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Zbaliť obraz v obraze"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Ovládanie zobraz. dvoj. stlač. "<annotation icon="home_icon">" TLAČIDLA PLOCHY "</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Ponuka obrazu v obraze."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Posunúť doľava"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Posunúť doprava"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Posunúť nahor"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Posunúť nadol"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hotovo"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml
index 88fc8325aa01..060aaa0ce647 100644
--- a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika v sliki"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program brez naslova)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Zapri"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Zapri način PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Celozaslonsko"</string>
- <string name="pip_move" msgid="158770205886688553">"Premakni"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Razširi"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Strni"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Premakni sliko v sliki"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Razširi sliko v sliki"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Strni sliko v sliki"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Za kontrolnike dvakrat pritisnite gumb za "<annotation icon="home_icon">" ZAČETNI ZASLON "</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za sliko v sliki"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Premakni levo"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Premakni desno"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Premakni navzgor"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Premakni navzdol"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Končano"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml
index 58687e5867fe..9bfdb6a3edd8 100644
--- a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Figurë brenda figurës"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program pa titull)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Mbyll"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Mbyll PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Ekrani i plotë"</string>
- <string name="pip_move" msgid="158770205886688553">"Lëviz"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Zgjero"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Palos"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Zhvendos PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Zgjero PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Palos PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Trokit dy herë "<annotation icon="home_icon">" KREU "</annotation>" për kontrollet"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyja e \"Figurës brenda figurës\"."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Lëviz majtas"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Lëviz djathtas"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Lëviz lart"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Lëviz poshtë"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"U krye"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml
index e850979174a3..6bc5c87bab48 100644
--- a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Слика у слици"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програм без наслова)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Затвори"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Цео екран"</string>
- <string name="pip_move" msgid="158770205886688553">"Премести"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Прошири"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Скупи"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Премести слику у слици"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Прошири слику у слици"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Скупи слику у слици"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Двапут притисните "<annotation icon="home_icon">" HOME "</annotation>" за контроле"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени Слика у слици."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Померите налево"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Померите надесно"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Померите нагоре"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Померите надоле"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml
index d3a9c3de66db..b3465ab1db85 100644
--- a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bild-i-bild"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Namnlöst program)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Stäng"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Stäng PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Helskärm"</string>
- <string name="pip_move" msgid="158770205886688553">"Flytta"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Utöka"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Komprimera"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Flytta BIB"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Utöka bild-i-bild"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Komprimera bild-i-bild"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Tryck snabbt två gånger på "<annotation icon="home_icon">" HEM "</annotation>" för kontroller"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bild-i-bild-meny."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytta åt vänster"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytta åt höger"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flytta uppåt"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flytta nedåt"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klar"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml
index 7b9a310ff0b6..baff49ed821a 100644
--- a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pachika Picha Ndani ya Picha Nyingine"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programu isiyo na jina)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Funga"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Funga PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Skrini nzima"</string>
- <string name="pip_move" msgid="158770205886688553">"Hamisha"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Panua"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Kunja"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Kuhamisha PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Panua PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Kunja PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Bonyeza mara mbili kitufe cha "<annotation icon="home_icon">" UKURASA WA KWANZA "</annotation>" kupata vidhibiti"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyu ya kipengele cha kupachika picha ndani ya picha nyingine."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sogeza kushoto"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sogeza kulia"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Sogeza juu"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Sogeza chini"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Imemaliza"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml
index e201401e2e35..4439e299c919 100644
--- a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"பிக்ச்சர்-இன்-பிக்ச்சர்"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(தலைப்பு இல்லை)"</string>
- <string name="pip_close" msgid="2955969519031223530">"மூடுக"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIPஐ மூடு"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"முழுத்திரை"</string>
- <string name="pip_move" msgid="158770205886688553">"நகர்த்து"</string>
- <string name="pip_expand" msgid="1051966011679297308">"விரி"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"சுருக்கு"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIPபை நகர்த்து"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIPபை விரிவாக்கு"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIPபைச் சுருக்கு"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" கட்டுப்பாடுகள்: "<annotation icon="home_icon">" முகப்பு "</annotation>" பட்டனை இருமுறை அழுத்துக"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"பிக்ச்சர்-இன்-பிக்ச்சர் மெனு."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"இடப்புறம் நகர்த்து"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"வலப்புறம் நகர்த்து"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"மேலே நகர்த்து"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"கீழே நகர்த்து"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"முடிந்தது"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-te/strings_tv.xml b/libs/WindowManager/Shell/res/values-te/strings_tv.xml
index 6284d90cb11f..35579346615f 100644
--- a/libs/WindowManager/Shell/res/values-te/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-te/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"పిక్చర్-ఇన్-పిక్చర్"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(శీర్షిక లేని ప్రోగ్రామ్)"</string>
- <string name="pip_close" msgid="2955969519031223530">"మూసివేయండి"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIPని మూసివేయి"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"ఫుల్-స్క్రీన్‌"</string>
- <string name="pip_move" msgid="158770205886688553">"తరలించండి"</string>
- <string name="pip_expand" msgid="1051966011679297308">"విస్తరించండి"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"కుదించండి"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIPను తరలించండి"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIPని విస్తరించండి"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIPని కుదించండి"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" కంట్రోల్స్ కోసం "<annotation icon="home_icon">" HOME "</annotation>" బటన్ రెండుసార్లు నొక్కండి"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"పిక్చర్-ఇన్-పిక్చర్ మెనూ."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ఎడమ వైపుగా జరపండి"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"కుడి వైపుగా జరపండి"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"పైకి జరపండి"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"కిందికి జరపండి"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"పూర్తయింది"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-television/config.xml b/libs/WindowManager/Shell/res/values-television/config.xml
index 86ca65526336..cc0333efd82b 100644
--- a/libs/WindowManager/Shell/res/values-television/config.xml
+++ b/libs/WindowManager/Shell/res/values-television/config.xml
@@ -43,4 +43,13 @@
<!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself
if a custom action is present before closing it. -->
<integer name="config_pipForceCloseDelay">5000</integer>
+
+ <!-- Animation duration when exit starting window: fade out icon -->
+ <integer name="starting_window_app_reveal_icon_fade_out_duration">0</integer>
+
+ <!-- Animation duration when exit starting window: reveal app -->
+ <integer name="starting_window_app_reveal_anim_delay">0</integer>
+
+ <!-- Animation duration when exit starting window: reveal app -->
+ <integer name="starting_window_app_reveal_anim_duration">0</integer>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-th/strings_tv.xml b/libs/WindowManager/Shell/res/values-th/strings_tv.xml
index 27cf56c6e154..0a07d157ec6f 100644
--- a/libs/WindowManager/Shell/res/values-th/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-th/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"การแสดงภาพซ้อนภาพ"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ไม่มีชื่อรายการ)"</string>
- <string name="pip_close" msgid="2955969519031223530">"ปิด"</string>
+ <string name="pip_close" msgid="9135220303720555525">"ปิด PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"เต็มหน้าจอ"</string>
- <string name="pip_move" msgid="158770205886688553">"ย้าย"</string>
- <string name="pip_expand" msgid="1051966011679297308">"ขยาย"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"ยุบ"</string>
+ <string name="pip_move" msgid="1544227837964635439">"ย้าย PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"ขยาย PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"ยุบ PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" กดปุ่ม "<annotation icon="home_icon">" หน้าแรก "</annotation>" สองครั้งเพื่อเปิดการควบคุม"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"เมนูการแสดงภาพซ้อนภาพ"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ย้ายไปทางซ้าย"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ย้ายไปทางขวา"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ย้ายขึ้น"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ย้ายลง"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"เสร็จ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml
index 4cc050bebe5b..9a11a38fa492 100644
--- a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Walang pamagat na programa)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Isara"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Isara ang PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string>
- <string name="pip_move" msgid="158770205886688553">"Ilipat"</string>
- <string name="pip_expand" msgid="1051966011679297308">"I-expand"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"I-collapse"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Ilipat ang PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"I-expand ang PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"I-collapse ang PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" I-double press ang "<annotation icon="home_icon">" HOME "</annotation>" para sa mga kontrol"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu ng Picture-in-Picture."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Ilipat pakaliwa"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Ilipat pakanan"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Itaas"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Ibaba"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Tapos na"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml
index 69bb608061e4..bf4bc6f1fff7 100644
--- a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pencere İçinde Pencere"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Başlıksız program)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Kapat"</string>
+ <string name="pip_close" msgid="9135220303720555525">"PIP\'yi kapat"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string>
- <string name="pip_move" msgid="158770205886688553">"Taşı"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Genişlet"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Daralt"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Kontroller için "<annotation icon="home_icon">" ANA SAYFA "</annotation>" düğmesine iki kez basın"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pencere içinde pencere menüsü."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola taşı"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa taşı"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Yukarı taşı"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Aşağı taşı"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Bitti"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIP\'yi taşı"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP penceresini genişlet"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP penceresini daralt"</string>
+ <string name="pip_edu_text" msgid="3672999496647508701">" Kontroller için "<annotation icon="home_icon">" ANA SAYFA "</annotation>"\'ya iki kez basın"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml
index 81a8285c58cf..7e9f54e68f54 100644
--- a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картинка в картинці"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без назви)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Закрити"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Закрити PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"На весь екран"</string>
- <string name="pip_move" msgid="158770205886688553">"Перемістити"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Розгорнути"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Згорнути"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Перемістити картинку в картинці"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Розгорнути картинку в картинці"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Згорнути картинку в картинці"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Відкрити елементи керування: двічі натисніть "<annotation icon="home_icon">"HOME"</annotation></string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"картинка в картинці\""</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перемістити ліворуч"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перемістити праворуч"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Перемістити вгору"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Перемістити вниз"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml
index e83885772f2d..c2ef69ff1488 100644
--- a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"تصویر میں تصویر"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(بلا عنوان پروگرام)"</string>
- <string name="pip_close" msgid="2955969519031223530">"بند کریں"</string>
+ <string name="pip_close" msgid="9135220303720555525">"‏PIP بند کریں"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"فُل اسکرین"</string>
- <string name="pip_move" msgid="158770205886688553">"منتقل کریں"</string>
- <string name="pip_expand" msgid="1051966011679297308">"پھیلائیں"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"سکیڑیں"</string>
+ <string name="pip_move" msgid="1544227837964635439">"‏PIP کو منتقل کریں"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"‏PIP کو پھیلائیں"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"‏PIP کو سکیڑیں"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" کنٹرولز کے لیے "<annotation icon="home_icon">"ہوم "</annotation>" بٹن کو دو بار دبائیں"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"تصویر میں تصویر کا مینو۔"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"دائیں منتقل کریں"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"بائیں منتقل کریں"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"اوپر منتقل کریں"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"نیچے منتقل کریں"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ہو گیا"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml
index da953356628c..9ab95c80aa25 100644
--- a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Tasvir ustida tasvir"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nomsiz)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Yopish"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Kadr ichida kadr – chiqish"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Butun ekran"</string>
- <string name="pip_move" msgid="158770205886688553">"Boshqa joyga olish"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Yoyish"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Yopish"</string>
+ <string name="pip_move" msgid="1544227837964635439">"PIPni siljitish"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"PIP funksiyasini yoyish"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"PIP funksiyasini yopish"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Boshqaruv uchun "<annotation icon="home_icon">"ASOSIY"</annotation>" tugmani ikki marta bosing"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Tasvir ustida tasvir menyusi."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Chapga olish"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Oʻngga olish"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Tepaga olish"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pastga olish"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Tayyor"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml
index 1f9260fdcff0..146376d3cab6 100644
--- a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Hình trong hình"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Không có chương trình tiêu đề)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Đóng"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Đóng PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Toàn màn hình"</string>
- <string name="pip_move" msgid="158770205886688553">"Di chuyển"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Mở rộng"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Thu gọn"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Di chuyển PIP (Ảnh trong ảnh)"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Mở rộng PIP (Ảnh trong ảnh)"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Thu gọn PIP (Ảnh trong ảnh)"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Nhấn đúp vào nút "<annotation icon="home_icon">" MÀN HÌNH CHÍNH "</annotation>" để mở trình đơn điều khiển"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Trình đơn hình trong hình."</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Di chuyển sang trái"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Di chuyển sang phải"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Di chuyển lên"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Di chuyển xuống"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Xong"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml
index 399d639fe70f..55407d2c699d 100644
--- a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"画中画"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(节目没有标题)"</string>
- <string name="pip_close" msgid="2955969519031223530">"关闭"</string>
+ <string name="pip_close" msgid="9135220303720555525">"关闭画中画"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"全屏"</string>
- <string name="pip_move" msgid="158770205886688553">"移动"</string>
- <string name="pip_expand" msgid="1051966011679297308">"展开"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"收起"</string>
+ <string name="pip_move" msgid="1544227837964635439">"移动画中画窗口"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"展开 PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"收起 PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" 按两次"<annotation icon="home_icon">"主屏幕"</annotation>"按钮可查看相关控件"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"画中画菜单。"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左移"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右移"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"上移"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"下移"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml
index acbc26d033cd..15e278d8ecc2 100644
--- a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"畫中畫"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(沒有標題的節目)"</string>
- <string name="pip_close" msgid="2955969519031223530">"關閉"</string>
+ <string name="pip_close" msgid="9135220303720555525">"關閉 PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string>
- <string name="pip_move" msgid="158770205886688553">"移動"</string>
- <string name="pip_expand" msgid="1051966011679297308">"展開"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"收合"</string>
+ <string name="pip_move" msgid="1544227837964635439">"移動畫中畫"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"展開畫中畫"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"收合畫中畫"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">" 主畫面按鈕"</annotation>"即可顯示控制項"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"畫中畫選單。"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"向上移"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"向下移"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml
index f8c683ec3a60..0b17b31d23d0 100644
--- a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"子母畫面"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無標題的節目)"</string>
- <string name="pip_close" msgid="2955969519031223530">"關閉"</string>
+ <string name="pip_close" msgid="9135220303720555525">"關閉子母畫面"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string>
- <string name="pip_move" msgid="158770205886688553">"移動"</string>
- <string name="pip_expand" msgid="1051966011679297308">"展開"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"收合"</string>
+ <string name="pip_move" msgid="1544227837964635439">"移動子母畫面"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"展開子母畫面"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"收合子母畫面"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">"主畫面按鈕"</annotation>"即可顯示控制選項"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"子母畫面選單。"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移動"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移動"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"向上移動"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"向下移動"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml
index 20243a9dfc9c..dad8c8128222 100644
--- a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml
@@ -19,16 +19,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"Isithombe-esithombeni"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Alukho uhlelo lwesihloko)"</string>
- <string name="pip_close" msgid="2955969519031223530">"Vala"</string>
+ <string name="pip_close" msgid="9135220303720555525">"Vala i-PIP"</string>
<string name="pip_fullscreen" msgid="7278047353591302554">"Iskrini esigcwele"</string>
- <string name="pip_move" msgid="158770205886688553">"Hambisa"</string>
- <string name="pip_expand" msgid="1051966011679297308">"Nweba"</string>
- <string name="pip_collapse" msgid="3903295106641385962">"Goqa"</string>
+ <string name="pip_move" msgid="1544227837964635439">"Hambisa i-PIP"</string>
+ <string name="pip_expand" msgid="7605396312689038178">"Nweba i-PIP"</string>
+ <string name="pip_collapse" msgid="5732233773786896094">"Goqa i-PIP"</string>
<string name="pip_edu_text" msgid="3672999496647508701">" Chofoza kabili "<annotation icon="home_icon">" IKHAYA"</annotation>" mayelana nezilawuli"</string>
- <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Imenyu yesithombe-esithombeni"</string>
- <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Yisa kwesokunxele"</string>
- <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Yisa kwesokudla"</string>
- <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Khuphula"</string>
- <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Yehlisa"</string>
- <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Kwenziwe"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/attrs.xml b/libs/WindowManager/Shell/res/values/attrs.xml
index 4aaeef8afcb0..2aad4c1c1805 100644
--- a/libs/WindowManager/Shell/res/values/attrs.xml
+++ b/libs/WindowManager/Shell/res/values/attrs.xml
@@ -19,4 +19,8 @@
<attr name="icon" format="reference" />
<attr name="text" format="string" />
</declare-styleable>
+
+ <declare-styleable name="MessageState">
+ <attr name="state_task_focused" format="boolean"/>
+ </declare-styleable>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index f03b7f66cdc8..30c3d50ed8ad 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -19,6 +19,10 @@
by the resources of the app using the Shell library. -->
<bool name="config_enableShellMainThread">false</bool>
+ <!-- Determines whether to register the shell task organizer on init.
+ TODO(b/238217847): This config is temporary until we refactor the base WMComponent. -->
+ <bool name="config_registerShellTaskOrganizerOnInit">true</bool>
+
<!-- Animation duration for PIP when entering. -->
<integer name="config_pipEnterAnimationDuration">425</integer>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 1dac9caba01e..0bc70857a113 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -81,6 +81,9 @@
<!-- The width and height of the background for custom action in PiP menu. -->
<dimen name="pip_custom_close_bg_size">32dp</dimen>
+ <!-- Extra padding between picture-in-picture windows and any registered keep clear areas. -->
+ <dimen name="pip_keep_clear_areas_padding">16dp</dimen>
+
<dimen name="dismiss_target_x_size">24dp</dimen>
<dimen name="floating_dismiss_bottom_margin">50dp</dimen>
@@ -246,8 +249,17 @@
<!-- The corner radius of the letterbox education dialog. -->
<dimen name="letterbox_education_dialog_corner_radius">28dp</dimen>
- <!-- The size of an icon in the letterbox education dialog. -->
- <dimen name="letterbox_education_dialog_icon_size">48dp</dimen>
+ <!-- The width of the top icon in the letterbox education dialog. -->
+ <dimen name="letterbox_education_dialog_title_icon_width">45dp</dimen>
+
+ <!-- The height of the top icon in the letterbox education dialog. -->
+ <dimen name="letterbox_education_dialog_title_icon_height">44dp</dimen>
+
+ <!-- The width of an icon in the letterbox education dialog. -->
+ <dimen name="letterbox_education_dialog_icon_width">40dp</dimen>
+
+ <!-- The height of an icon in the letterbox education dialog. -->
+ <dimen name="letterbox_education_dialog_icon_height">32dp</dimen>
<!-- The fixed width of the dialog if there is enough space in the parent. -->
<dimen name="letterbox_education_dialog_width">472dp</dimen>
@@ -285,4 +297,28 @@
when the pinned stack size is overridden by app via minWidth/minHeight.
-->
<dimen name="overridable_minimal_size_pip_resizable_task">48dp</dimen>
+
+ <!-- The size of the drag handle / menu shown along with a floating task. -->
+ <dimen name="floating_task_menu_size">32dp</dimen>
+
+ <!-- The size of menu items in the floating task menu. -->
+ <dimen name="floating_task_menu_item_size">24dp</dimen>
+
+ <!-- The horizontal margin of menu items in the floating task menu. -->
+ <dimen name="floating_task_menu_item_padding">5dp</dimen>
+
+ <!-- The width of visible floating view region when stashed. -->
+ <dimen name="floating_task_stash_offset">32dp</dimen>
+
+ <!-- The amount of elevation for a floating task. -->
+ <dimen name="floating_task_elevation">8dp</dimen>
+
+ <!-- The amount of padding around the bottom and top of the task. -->
+ <dimen name="floating_task_vertical_padding">8dp</dimen>
+
+ <!-- The normal size of the dismiss target. -->
+ <dimen name="floating_task_dismiss_circle_size">150dp</dimen>
+
+ <!-- The smaller size of the dismiss target (shrinks when something is in the target). -->
+ <dimen name="floating_dismiss_circle_small">120dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index a24311fb1f21..1d1162daf249 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -157,7 +157,7 @@
<string name="accessibility_bubble_dismissed">Bubble dismissed.</string>
<!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] -->
- <string name="restart_button_description">Tap to restart this app and go full screen.</string>
+ <string name="restart_button_description">Tap to restart this app for a better view.</string>
<!-- Description of the camera compat button for applying stretched issues treatment in the hint for
compatibility control. [CHAR LIMIT=NONE] -->
@@ -172,18 +172,25 @@
<string name="camera_compat_dismiss_button_description">No camera issues? Tap to dismiss.</string>
<!-- The title of the letterbox education dialog. [CHAR LIMIT=NONE] -->
- <string name="letterbox_education_dialog_title">Some apps work best in portrait</string>
+ <string name="letterbox_education_dialog_title">See and do more</string>
- <!-- The subtext of the letterbox education dialog. [CHAR LIMIT=NONE] -->
- <string name="letterbox_education_dialog_subtext">Try one of these options to make the most of your space</string>
-
- <!-- Description of the rotate screen action. [CHAR LIMIT=NONE] -->
- <string name="letterbox_education_screen_rotation_text">Rotate your device to go full screen</string>
+ <!-- Description of the split screen action. [CHAR LIMIT=NONE] -->
+ <string name="letterbox_education_split_screen_text">Drag in another app for split-screen</string>
<!-- Description of the reposition app action. [CHAR LIMIT=NONE] -->
- <string name="letterbox_education_reposition_text">Double-tap next to an app to reposition it</string>
+ <string name="letterbox_education_reposition_text">Double-tap outside an app to reposition it</string>
<!-- Button text for dismissing the letterbox education dialog. [CHAR LIMIT=20] -->
<string name="letterbox_education_got_it">Got it</string>
+ <!-- Accessibility description of the letterbox education toast expand to dialog button. [CHAR LIMIT=NONE] -->
+ <string name="letterbox_education_expand_button_description">Expand for more information.</string>
+
+ <!-- Freeform window caption strings -->
+ <!-- Accessibility text for the maximize window button [CHAR LIMIT=NONE] -->
+ <string name="maximize_button_text">Maximize</string>
+ <!-- Accessibility text for the minimize window button [CHAR LIMIT=NONE] -->
+ <string name="minimize_button_text">Minimize</string>
+ <!-- Accessibility text for the close window button [CHAR LIMIT=NONE] -->
+ <string name="close_button_text">Close</string>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
new file mode 100644
index 000000000000..d2760022a015
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+import com.android.wm.shell.protolog.ShellProtoLogImpl;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellInit;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+/**
+ * Controls the {@link ShellProtoLogImpl} in WMShell via adb shell commands.
+ *
+ * Use with {@code adb shell dumpsys activity service SystemUIService WMShell protolog ...}.
+ */
+public class ProtoLogController implements ShellCommandHandler.ShellCommandActionHandler {
+ private final ShellCommandHandler mShellCommandHandler;
+ private final ShellProtoLogImpl mShellProtoLog;
+
+ public ProtoLogController(ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler) {
+ shellInit.addInitCallback(this::onInit, this);
+ mShellCommandHandler = shellCommandHandler;
+ mShellProtoLog = ShellProtoLogImpl.getSingleInstance();
+ }
+
+ void onInit() {
+ mShellCommandHandler.addCommandCallback("protolog", this, this);
+ }
+
+ @Override
+ public boolean onShellCommand(String[] args, PrintWriter pw) {
+ switch (args[0]) {
+ case "status": {
+ pw.println(mShellProtoLog.getStatus());
+ return true;
+ }
+ case "start": {
+ mShellProtoLog.startProtoLog(pw);
+ return true;
+ }
+ case "stop": {
+ mShellProtoLog.stopProtoLog(pw, true /* writeToFile */);
+ return true;
+ }
+ case "enable-text": {
+ String[] groups = Arrays.copyOfRange(args, 1, args.length);
+ int result = mShellProtoLog.startTextLogging(groups, pw);
+ if (result == 0) {
+ pw.println("Starting logging on groups: " + Arrays.toString(groups));
+ return true;
+ }
+ return false;
+ }
+ case "disable-text": {
+ String[] groups = Arrays.copyOfRange(args, 1, args.length);
+ int result = mShellProtoLog.stopTextLogging(groups, pw);
+ if (result == 0) {
+ pw.println("Stopping logging on groups: " + Arrays.toString(groups));
+ return true;
+ }
+ return false;
+ }
+ case "enable": {
+ String[] groups = Arrays.copyOfRange(args, 1, args.length);
+ return mShellProtoLog.startTextLogging(groups, pw) == 0;
+ }
+ case "disable": {
+ String[] groups = Arrays.copyOfRange(args, 1, args.length);
+ return mShellProtoLog.stopTextLogging(groups, pw) == 0;
+ }
+ default: {
+ pw.println("Invalid command: " + args[0]);
+ printShellCommandHelp(pw, "");
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public void printShellCommandHelp(PrintWriter pw, String prefix) {
+ pw.println(prefix + "status");
+ pw.println(prefix + " Get current ProtoLog status.");
+ pw.println(prefix + "start");
+ pw.println(prefix + " Start proto logging.");
+ pw.println(prefix + "stop");
+ pw.println(prefix + " Stop proto logging and flush to file.");
+ pw.println(prefix + "enable [group...]");
+ pw.println(prefix + " Enable proto logging for given groups.");
+ pw.println(prefix + "disable [group...]");
+ pw.println(prefix + " Disable proto logging for given groups.");
+ pw.println(prefix + "enable-text [group...]");
+ pw.println(prefix + " Enable logcat logging for given groups.");
+ pw.println(prefix + "disable-text [group...]");
+ pw.println(prefix + " Disable logcat logging for given groups.");
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
index 14ba9df93f24..b085b73d78ce 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
@@ -16,14 +16,20 @@
package com.android.wm.shell;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.app.WindowConfiguration;
import android.util.SparseArray;
import android.view.SurfaceControl;
import android.window.DisplayAreaAppearedInfo;
import android.window.DisplayAreaInfo;
import android.window.DisplayAreaOrganizer;
+import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
+import com.android.internal.protolog.common.ProtoLog;
+
import java.io.PrintWriter;
import java.util.List;
import java.util.concurrent.Executor;
@@ -85,6 +91,8 @@ public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer {
}
mDisplayAreasInfo.remove(displayId);
+ mLeashes.get(displayId).release();
+ mLeashes.remove(displayId);
}
@Override
@@ -100,10 +108,44 @@ public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer {
mDisplayAreasInfo.put(displayId, displayAreaInfo);
}
+ /**
+ * Create a {@link WindowContainerTransaction} to update display windowing mode.
+ *
+ * @param displayId display id to update windowing mode for
+ * @param windowingMode target {@link WindowConfiguration.WindowingMode}
+ * @return {@link WindowContainerTransaction} with pending operation to set windowing mode
+ */
+ public WindowContainerTransaction prepareWindowingModeChange(int displayId,
+ @WindowConfiguration.WindowingMode int windowingMode) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId);
+ if (displayAreaInfo == null) {
+ ProtoLog.e(WM_SHELL_DESKTOP_MODE,
+ "unable to update windowing mode for display %d display not found", displayId);
+ return wct;
+ }
+
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId,
+ displayAreaInfo.configuration.windowConfiguration.getWindowingMode(),
+ windowingMode);
+
+ wct.setWindowingMode(displayAreaInfo.token, windowingMode);
+ return wct;
+ }
+
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
final String childPrefix = innerPrefix + " ";
pw.println(prefix + this);
+
+ for (int i = 0; i < mDisplayAreasInfo.size(); i++) {
+ int displayId = mDisplayAreasInfo.keyAt(i);
+ DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId);
+ int windowingMode =
+ displayAreaInfo.configuration.windowConfiguration.getWindowingMode();
+ pw.println(innerPrefix + "# displayId=" + displayId + " wmMode=" + windowingMode);
+ }
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
index 9230c22c5d95..ca977ed2cb94 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
@@ -179,6 +179,14 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer {
}
/**
+ * Returns the {@link DisplayAreaInfo} of the {@link DisplayAreaInfo#displayId}.
+ */
+ @Nullable
+ public DisplayAreaInfo getDisplayAreaInfo(int displayId) {
+ return mDisplayAreasInfo.get(displayId);
+ }
+
+ /**
* Applies the {@link DisplayAreaInfo} to the {@link DisplayAreaContext} specified by
* {@link DisplayAreaInfo#displayId}.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java
deleted file mode 100644
index 73fd6931066d..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell;
-
-import com.android.wm.shell.common.annotations.ExternalThread;
-
-import java.io.PrintWriter;
-
-/**
- * An entry point into the shell for dumping shell internal state and running adb commands.
- *
- * Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}.
- */
-@ExternalThread
-public interface ShellCommandHandler {
- /**
- * Dumps the shell state.
- */
- void dump(PrintWriter pw);
-
- /**
- * Handles a shell command.
- */
- boolean handleCommand(final String[] args, PrintWriter pw);
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
deleted file mode 100644
index 06f4367752fb..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell;
-
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
-
-import com.android.wm.shell.apppairs.AppPairsController;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController;
-import com.android.wm.shell.onehanded.OneHandedController;
-import com.android.wm.shell.pip.Pip;
-import com.android.wm.shell.recents.RecentTasksController;
-import com.android.wm.shell.splitscreen.SplitScreenController;
-
-import java.io.PrintWriter;
-import java.util.Optional;
-
-/**
- * An entry point into the shell for dumping shell internal state and running adb commands.
- *
- * Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}.
- */
-public final class ShellCommandHandlerImpl {
- private static final String TAG = ShellCommandHandlerImpl.class.getSimpleName();
-
- private final Optional<LegacySplitScreenController> mLegacySplitScreenOptional;
- private final Optional<SplitScreenController> mSplitScreenOptional;
- private final Optional<Pip> mPipOptional;
- private final Optional<OneHandedController> mOneHandedOptional;
- private final Optional<HideDisplayCutoutController> mHideDisplayCutout;
- private final Optional<AppPairsController> mAppPairsOptional;
- private final Optional<RecentTasksController> mRecentTasks;
- private final ShellTaskOrganizer mShellTaskOrganizer;
- private final KidsModeTaskOrganizer mKidsModeTaskOrganizer;
- private final ShellExecutor mMainExecutor;
- private final HandlerImpl mImpl = new HandlerImpl();
-
- public ShellCommandHandlerImpl(
- ShellTaskOrganizer shellTaskOrganizer,
- KidsModeTaskOrganizer kidsModeTaskOrganizer,
- Optional<LegacySplitScreenController> legacySplitScreenOptional,
- Optional<SplitScreenController> splitScreenOptional,
- Optional<Pip> pipOptional,
- Optional<OneHandedController> oneHandedOptional,
- Optional<HideDisplayCutoutController> hideDisplayCutout,
- Optional<AppPairsController> appPairsOptional,
- Optional<RecentTasksController> recentTasks,
- ShellExecutor mainExecutor) {
- mShellTaskOrganizer = shellTaskOrganizer;
- mKidsModeTaskOrganizer = kidsModeTaskOrganizer;
- mRecentTasks = recentTasks;
- mLegacySplitScreenOptional = legacySplitScreenOptional;
- mSplitScreenOptional = splitScreenOptional;
- mPipOptional = pipOptional;
- mOneHandedOptional = oneHandedOptional;
- mHideDisplayCutout = hideDisplayCutout;
- mAppPairsOptional = appPairsOptional;
- mMainExecutor = mainExecutor;
- }
-
- public ShellCommandHandler asShellCommandHandler() {
- return mImpl;
- }
-
- /** Dumps WM Shell internal state. */
- private void dump(PrintWriter pw) {
- mShellTaskOrganizer.dump(pw, "");
- pw.println();
- pw.println();
- mPipOptional.ifPresent(pip -> pip.dump(pw));
- mLegacySplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw));
- mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw));
- mHideDisplayCutout.ifPresent(hideDisplayCutout -> hideDisplayCutout.dump(pw));
- pw.println();
- pw.println();
- mAppPairsOptional.ifPresent(appPairs -> appPairs.dump(pw, ""));
- pw.println();
- pw.println();
- mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw, ""));
- pw.println();
- pw.println();
- mRecentTasks.ifPresent(recentTasks -> recentTasks.dump(pw, ""));
- pw.println();
- pw.println();
- mKidsModeTaskOrganizer.dump(pw, "");
- }
-
-
- /** Returns {@code true} if command was found and executed. */
- private boolean handleCommand(final String[] args, PrintWriter pw) {
- if (args.length < 2) {
- // Argument at position 0 is "WMShell".
- return false;
- }
- switch (args[1]) {
- case "pair":
- return runPair(args, pw);
- case "unpair":
- return runUnpair(args, pw);
- case "moveToSideStage":
- return runMoveToSideStage(args, pw);
- case "removeFromSideStage":
- return runRemoveFromSideStage(args, pw);
- case "setSideStagePosition":
- return runSetSideStagePosition(args, pw);
- case "help":
- return runHelp(pw);
- default:
- return false;
- }
- }
-
- private boolean runPair(String[] args, PrintWriter pw) {
- if (args.length < 4) {
- // First two arguments are "WMShell" and command name.
- pw.println("Error: two task ids should be provided as arguments");
- return false;
- }
- final int taskId1 = new Integer(args[2]);
- final int taskId2 = new Integer(args[3]);
- mAppPairsOptional.ifPresent(appPairs -> appPairs.pair(taskId1, taskId2));
- return true;
- }
-
- private boolean runUnpair(String[] args, PrintWriter pw) {
- if (args.length < 3) {
- // First two arguments are "WMShell" and command name.
- pw.println("Error: task id should be provided as an argument");
- return false;
- }
- final int taskId = new Integer(args[2]);
- mAppPairsOptional.ifPresent(appPairs -> appPairs.unpair(taskId));
- return true;
- }
-
- private boolean runMoveToSideStage(String[] args, PrintWriter pw) {
- if (args.length < 3) {
- // First arguments are "WMShell" and command name.
- pw.println("Error: task id should be provided as arguments");
- return false;
- }
- final int taskId = new Integer(args[2]);
- final int sideStagePosition = args.length > 3
- ? new Integer(args[3]) : SPLIT_POSITION_BOTTOM_OR_RIGHT;
- mSplitScreenOptional.ifPresent(split -> split.moveToSideStage(taskId, sideStagePosition));
- return true;
- }
-
- private boolean runRemoveFromSideStage(String[] args, PrintWriter pw) {
- if (args.length < 3) {
- // First arguments are "WMShell" and command name.
- pw.println("Error: task id should be provided as arguments");
- return false;
- }
- final int taskId = new Integer(args[2]);
- mSplitScreenOptional.ifPresent(split -> split.removeFromSideStage(taskId));
- return true;
- }
-
- private boolean runSetSideStagePosition(String[] args, PrintWriter pw) {
- if (args.length < 3) {
- // First arguments are "WMShell" and command name.
- pw.println("Error: side stage position should be provided as arguments");
- return false;
- }
- final int position = new Integer(args[2]);
- mSplitScreenOptional.ifPresent(split -> split.setSideStagePosition(position));
- return true;
- }
-
- private boolean runHelp(PrintWriter pw) {
- pw.println("Window Manager Shell commands:");
- pw.println(" help");
- pw.println(" Print this help text.");
- pw.println(" <no arguments provided>");
- pw.println(" Dump Window Manager Shell internal state");
- pw.println(" pair <taskId1> <taskId2>");
- pw.println(" unpair <taskId>");
- pw.println(" Pairs/unpairs tasks with given ids.");
- pw.println(" moveToSideStage <taskId> <SideStagePosition>");
- pw.println(" Move a task with given id in split-screen mode.");
- pw.println(" removeFromSideStage <taskId>");
- pw.println(" Remove a task with given id in split-screen mode.");
- pw.println(" setSideStageOutline <true/false>");
- pw.println(" Enable/Disable outline on the side-stage.");
- pw.println(" setSideStagePosition <SideStagePosition>");
- pw.println(" Sets the position of the side-stage.");
- return true;
- }
-
- private class HandlerImpl implements ShellCommandHandler {
- @Override
- public void dump(PrintWriter pw) {
- try {
- mMainExecutor.executeBlocking(() -> ShellCommandHandlerImpl.this.dump(pw));
- } catch (InterruptedException e) {
- throw new RuntimeException("Failed to dump the Shell in 2s", e);
- }
- }
-
- @Override
- public boolean handleCommand(String[] args, PrintWriter pw) {
- try {
- boolean[] result = new boolean[1];
- mMainExecutor.executeBlocking(() -> {
- result[0] = ShellCommandHandlerImpl.this.handleCommand(args, pw);
- });
- return result[0];
- } catch (InterruptedException e) {
- throw new RuntimeException("Failed to handle Shell command in 2s", e);
- }
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
deleted file mode 100644
index 62fb840d29d1..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell;
-
-import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
-
-import com.android.wm.shell.apppairs.AppPairsController;
-import com.android.wm.shell.bubbles.BubbleController;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.draganddrop.DragAndDropController;
-import com.android.wm.shell.freeform.FreeformTaskListener;
-import com.android.wm.shell.fullscreen.FullscreenTaskListener;
-import com.android.wm.shell.fullscreen.FullscreenUnfoldController;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
-import com.android.wm.shell.pip.phone.PipTouchHandler;
-import com.android.wm.shell.recents.RecentTasksController;
-import com.android.wm.shell.splitscreen.SplitScreenController;
-import com.android.wm.shell.startingsurface.StartingWindowController;
-import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.unfold.UnfoldTransitionHandler;
-
-import java.util.Optional;
-
-/**
- * The entry point implementation into the shell for initializing shell internal state.
- */
-public class ShellInitImpl {
- private static final String TAG = ShellInitImpl.class.getSimpleName();
-
- private final DisplayController mDisplayController;
- private final DisplayImeController mDisplayImeController;
- private final DisplayInsetsController mDisplayInsetsController;
- private final DragAndDropController mDragAndDropController;
- private final ShellTaskOrganizer mShellTaskOrganizer;
- private final KidsModeTaskOrganizer mKidsModeTaskOrganizer;
- private final Optional<BubbleController> mBubblesOptional;
- private final Optional<SplitScreenController> mSplitScreenOptional;
- private final Optional<AppPairsController> mAppPairsOptional;
- private final Optional<PipTouchHandler> mPipTouchHandlerOptional;
- private final FullscreenTaskListener mFullscreenTaskListener;
- private final Optional<FullscreenUnfoldController> mFullscreenUnfoldController;
- private final Optional<UnfoldTransitionHandler> mUnfoldTransitionHandler;
- private final Optional<FreeformTaskListener> mFreeformTaskListenerOptional;
- private final ShellExecutor mMainExecutor;
- private final Transitions mTransitions;
- private final StartingWindowController mStartingWindow;
- private final Optional<RecentTasksController> mRecentTasks;
-
- private final InitImpl mImpl = new InitImpl();
-
- public ShellInitImpl(
- DisplayController displayController,
- DisplayImeController displayImeController,
- DisplayInsetsController displayInsetsController,
- DragAndDropController dragAndDropController,
- ShellTaskOrganizer shellTaskOrganizer,
- KidsModeTaskOrganizer kidsModeTaskOrganizer,
- Optional<BubbleController> bubblesOptional,
- Optional<SplitScreenController> splitScreenOptional,
- Optional<AppPairsController> appPairsOptional,
- Optional<PipTouchHandler> pipTouchHandlerOptional,
- FullscreenTaskListener fullscreenTaskListener,
- Optional<FullscreenUnfoldController> fullscreenUnfoldTransitionController,
- Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
- Optional<FreeformTaskListener> freeformTaskListenerOptional,
- Optional<RecentTasksController> recentTasks,
- Transitions transitions,
- StartingWindowController startingWindow,
- ShellExecutor mainExecutor) {
- mDisplayController = displayController;
- mDisplayImeController = displayImeController;
- mDisplayInsetsController = displayInsetsController;
- mDragAndDropController = dragAndDropController;
- mShellTaskOrganizer = shellTaskOrganizer;
- mKidsModeTaskOrganizer = kidsModeTaskOrganizer;
- mBubblesOptional = bubblesOptional;
- mSplitScreenOptional = splitScreenOptional;
- mAppPairsOptional = appPairsOptional;
- mFullscreenTaskListener = fullscreenTaskListener;
- mPipTouchHandlerOptional = pipTouchHandlerOptional;
- mFullscreenUnfoldController = fullscreenUnfoldTransitionController;
- mUnfoldTransitionHandler = unfoldTransitionHandler;
- mFreeformTaskListenerOptional = freeformTaskListenerOptional;
- mRecentTasks = recentTasks;
- mTransitions = transitions;
- mMainExecutor = mainExecutor;
- mStartingWindow = startingWindow;
- }
-
- public ShellInit asShellInit() {
- return mImpl;
- }
-
- private void init() {
- // Start listening for display and insets changes
- mDisplayController.initialize();
- mDisplayInsetsController.initialize();
- mDisplayImeController.startMonitorDisplays();
-
- // Setup the shell organizer
- mShellTaskOrganizer.addListenerForType(
- mFullscreenTaskListener, TASK_LISTENER_TYPE_FULLSCREEN);
- mShellTaskOrganizer.initStartingWindow(mStartingWindow);
- mShellTaskOrganizer.registerOrganizer();
-
- mAppPairsOptional.ifPresent(AppPairsController::onOrganizerRegistered);
- mSplitScreenOptional.ifPresent(SplitScreenController::onOrganizerRegistered);
- mBubblesOptional.ifPresent(BubbleController::initialize);
-
- // Bind the splitscreen impl to the drag drop controller
- mDragAndDropController.initialize(mSplitScreenOptional);
-
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- mTransitions.register(mShellTaskOrganizer);
- mUnfoldTransitionHandler.ifPresent(UnfoldTransitionHandler::init);
- }
-
- // TODO(b/181599115): This should really be the pip controller, but until we can provide the
- // controller instead of the feature interface, can just initialize the touch handler if
- // needed
- mPipTouchHandlerOptional.ifPresent((handler) -> handler.init());
-
- // Initialize optional freeform
- mFreeformTaskListenerOptional.ifPresent(f ->
- mShellTaskOrganizer.addListenerForType(
- f, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM));
-
- mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init);
- mRecentTasks.ifPresent(RecentTasksController::init);
-
- // Initialize kids mode task organizer
- mKidsModeTaskOrganizer.initialize(mStartingWindow);
- }
-
- @ExternalThread
- private class InitImpl implements ShellInit {
- @Override
- public void init() {
- try {
- mMainExecutor.executeBlocking(() -> ShellInitImpl.this.init());
- } catch (InterruptedException e) {
- throw new RuntimeException("Failed to initialize the Shell in 2s", e);
- }
- }
- }
-}
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 31f0ef0192ae..43679364b443 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -16,13 +16,16 @@
package com.android.wm.shell;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
+import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -30,7 +33,6 @@ import android.annotation.Nullable;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
import android.app.WindowConfiguration;
-import android.content.Context;
import android.content.LocusId;
import android.content.pm.ActivityInfo;
import android.graphics.Rect;
@@ -46,6 +48,7 @@ import android.window.StartingWindowInfo;
import android.window.StartingWindowRemovalInfo;
import android.window.TaskAppearedInfo;
import android.window.TaskOrganizer;
+import android.window.WindowContainerTransaction;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
@@ -55,6 +58,9 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.compatui.CompatUIController;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.startingsurface.StartingWindowController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -70,6 +76,7 @@ import java.util.function.Consumer;
*/
public class ShellTaskOrganizer extends TaskOrganizer implements
CompatUIController.CompatUICallback {
+ private static final String TAG = "ShellTaskOrganizer";
// Intentionally using negative numbers here so the positive numbers can be used
// for task id specific listeners that will be added later.
@@ -88,8 +95,6 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
})
public @interface TaskListenerType {}
- private static final String TAG = "ShellTaskOrganizer";
-
/**
* Callbacks for when the tasks change in the system.
*/
@@ -175,42 +180,62 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
@Nullable
private final CompatUIController mCompatUI;
+ @NonNull
+ private final ShellCommandHandler mShellCommandHandler;
+
@Nullable
private final Optional<RecentTasksController> mRecentTasks;
@Nullable
- private RunningTaskInfo mLastFocusedTaskInfo;
+ private final UnfoldAnimationController mUnfoldAnimationController;
- public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) {
- this(null /* taskOrganizerController */, mainExecutor, context, null /* compatUI */,
- Optional.empty() /* recentTasksController */);
- }
+ @Nullable
+ private RunningTaskInfo mLastFocusedTaskInfo;
- public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
- CompatUIController compatUI) {
- this(null /* taskOrganizerController */, mainExecutor, context, compatUI,
- Optional.empty() /* recentTasksController */);
+ public ShellTaskOrganizer(ShellExecutor mainExecutor) {
+ this(null /* shellInit */, null /* shellCommandHandler */,
+ null /* taskOrganizerController */, null /* compatUI */,
+ Optional.empty() /* unfoldAnimationController */,
+ Optional.empty() /* recentTasksController */,
+ mainExecutor);
}
- public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
- CompatUIController compatUI,
- Optional<RecentTasksController> recentTasks) {
- this(null /* taskOrganizerController */, mainExecutor, context, compatUI,
- recentTasks);
+ public ShellTaskOrganizer(ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ @Nullable CompatUIController compatUI,
+ Optional<UnfoldAnimationController> unfoldAnimationController,
+ Optional<RecentTasksController> recentTasks,
+ ShellExecutor mainExecutor) {
+ this(shellInit, shellCommandHandler, null /* taskOrganizerController */, compatUI,
+ unfoldAnimationController, recentTasks, mainExecutor);
}
@VisibleForTesting
- protected ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController,
- ShellExecutor mainExecutor, Context context, @Nullable CompatUIController compatUI,
- Optional<RecentTasksController> recentTasks) {
+ protected ShellTaskOrganizer(ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ITaskOrganizerController taskOrganizerController,
+ @Nullable CompatUIController compatUI,
+ Optional<UnfoldAnimationController> unfoldAnimationController,
+ Optional<RecentTasksController> recentTasks,
+ ShellExecutor mainExecutor) {
super(taskOrganizerController, mainExecutor);
+ mShellCommandHandler = shellCommandHandler;
mCompatUI = compatUI;
mRecentTasks = recentTasks;
- if (compatUI != null) {
- compatUI.setCompatUICallback(this);
+ mUnfoldAnimationController = unfoldAnimationController.orElse(null);
+ if (shellInit != null) {
+ shellInit.addInitCallback(this::onInit, this);
}
}
+ private void onInit() {
+ mShellCommandHandler.addDumpCallback(this::dump, this);
+ if (mCompatUI != null) {
+ mCompatUI.setCompatUICallback(this);
+ }
+ registerOrganizer();
+ }
+
@Override
public List<TaskAppearedInfo> registerOrganizer() {
synchronized (mLock) {
@@ -437,8 +462,12 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
if (listener != null) {
listener.onTaskAppeared(info.getTaskInfo(), info.getLeash());
}
+ if (mUnfoldAnimationController != null) {
+ mUnfoldAnimationController.onTaskAppeared(info.getTaskInfo(), info.getLeash());
+ }
notifyLocusVisibilityIfNeeded(info.getTaskInfo());
notifyCompatUI(info.getTaskInfo(), listener);
+ mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskAdded(info.getTaskInfo()));
}
/**
@@ -458,6 +487,11 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
synchronized (mLock) {
ProtoLog.v(WM_SHELL_TASK_ORG, "Task info changed taskId=%d", taskInfo.taskId);
+
+ if (mUnfoldAnimationController != null) {
+ mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+ }
+
final TaskAppearedInfo data = mTasks.get(taskInfo.taskId);
final TaskListener oldListener = getTaskListener(data.getTaskInfo());
final TaskListener newListener = getTaskListener(taskInfo);
@@ -482,7 +516,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
|| (taskInfo.topActivityType == WindowConfiguration.ACTIVITY_TYPE_HOME
&& taskInfo.isVisible);
final boolean focusTaskChanged = (mLastFocusedTaskInfo == null
- || mLastFocusedTaskInfo.taskId != taskInfo.taskId) && isFocusedOrHome;
+ || mLastFocusedTaskInfo.taskId != taskInfo.taskId
+ || mLastFocusedTaskInfo.getWindowingMode() != taskInfo.getWindowingMode())
+ && isFocusedOrHome;
if (focusTaskChanged) {
for (int i = 0; i < mFocusListeners.size(); i++) {
mFocusListeners.valueAt(i).onFocusTaskChanged(taskInfo);
@@ -507,8 +543,13 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
public void onTaskVanished(RunningTaskInfo taskInfo) {
synchronized (mLock) {
ProtoLog.v(WM_SHELL_TASK_ORG, "Task vanished taskId=%d", taskInfo.taskId);
+ if (mUnfoldAnimationController != null) {
+ mUnfoldAnimationController.onTaskVanished(taskInfo);
+ }
+
final int taskId = taskInfo.taskId;
- final TaskListener listener = getTaskListener(mTasks.get(taskId).getTaskInfo());
+ final TaskAppearedInfo appearedInfo = mTasks.get(taskId);
+ final TaskListener listener = getTaskListener(appearedInfo.getTaskInfo());
mTasks.remove(taskId);
if (listener != null) {
listener.onTaskVanished(taskInfo);
@@ -518,6 +559,11 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
notifyCompatUI(taskInfo, null /* taskListener */);
// Notify the recent tasks that a task has been removed
mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskRemoved(taskInfo));
+
+ if (!ENABLE_SHELL_TRANSITIONS && (appearedInfo.getLeash() != null)) {
+ // Preemptively clean up the leash only if shell transitions are not enabled
+ appearedInfo.getLeash().release();
+ }
}
}
@@ -647,6 +693,57 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
taskListener.reparentChildSurfaceToTask(taskId, sc, t);
}
+ /**
+ * Create a {@link WindowContainerTransaction} to clear task bounds.
+ *
+ * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to
+ * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}.
+ *
+ * @param displayId display id for tasks that will have bounds cleared
+ * @return {@link WindowContainerTransaction} with pending operations to clear bounds
+ */
+ public WindowContainerTransaction prepareClearBoundsForStandardTasks(int displayId) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ for (int i = 0; i < mTasks.size(); i++) {
+ RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+ if ((taskInfo.displayId == displayId) && (taskInfo.getActivityType()
+ == WindowConfiguration.ACTIVITY_TYPE_STANDARD)) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s",
+ taskInfo.token, taskInfo);
+ wct.setBounds(taskInfo.token, null);
+ }
+ }
+ return wct;
+ }
+
+ /**
+ * Create a {@link WindowContainerTransaction} to clear task level freeform setting.
+ *
+ * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to
+ * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}.
+ *
+ * @param displayId display id for tasks that will have windowing mode reset to {@link
+ * WindowConfiguration#WINDOWING_MODE_UNDEFINED}
+ * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode
+ */
+ public WindowContainerTransaction prepareClearFreeformForStandardTasks(int displayId) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ for (int i = 0; i < mTasks.size(); i++) {
+ RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+ if (taskInfo.displayId == displayId
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
+ && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token,
+ taskInfo);
+ wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ }
+ }
+ return wct;
+ }
+
private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
int event) {
ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
@@ -773,7 +870,18 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
final int key = mTasks.keyAt(i);
final TaskAppearedInfo info = mTasks.valueAt(i);
final TaskListener listener = getTaskListener(info.getTaskInfo());
- pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener);
+ final int windowingMode = info.getTaskInfo().getWindowingMode();
+ String pkg = "";
+ if (info.getTaskInfo().baseActivity != null) {
+ pkg = info.getTaskInfo().baseActivity.getPackageName();
+ }
+ Rect bounds = info.getTaskInfo().getConfiguration().windowConfiguration.getBounds();
+ boolean running = info.getTaskInfo().isRunning;
+ boolean visible = info.getTaskInfo().isVisible;
+ boolean focused = info.getTaskInfo().isFocused;
+ pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener
+ + " wmMode=" + windowingMode + " pkg=" + pkg + " bounds=" + bounds
+ + " running=" + running + " visible=" + visible + " focused=" + focused);
}
pw.println();
@@ -783,6 +891,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
final TaskListener listener = mLaunchCookieToListener.valueAt(i);
pw.println(innerPrefix + "#" + i + " cookie=" + key + " listener=" + listener);
}
+
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
index d28a68a42b2b..d76ad3d27c70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
@@ -54,7 +54,10 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
/** Callback for listening task state. */
public interface Listener {
- /** Called when the container is ready for launching activities. */
+ /**
+ * Only called once when the surface has been created & the container is ready for
+ * launching activities.
+ */
default void onInitialized() {}
/** Called when the container can no longer launch activities. */
@@ -80,12 +83,13 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
private final SyncTransactionQueue mSyncQueue;
private final TaskViewTransitions mTaskViewTransitions;
- private ActivityManager.RunningTaskInfo mTaskInfo;
+ protected ActivityManager.RunningTaskInfo mTaskInfo;
private WindowContainerToken mTaskToken;
private SurfaceControl mTaskLeash;
private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
private boolean mSurfaceCreated;
private boolean mIsInitialized;
+ private boolean mNotifiedForInitialized;
private Listener mListener;
private Executor mListenerExecutor;
private Region mObscuredTouchRegion;
@@ -110,6 +114,13 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
mGuard.open("release");
}
+ /**
+ * @return {@code True} when the TaskView's surface has been created, {@code False} otherwise.
+ */
+ public boolean isInitialized() {
+ return mIsInitialized;
+ }
+
/** Until all users are converted, we may have mixed-use (eg. Car). */
private boolean isUsingShellTransitions() {
return mTaskViewTransitions != null && Transitions.ENABLE_SHELL_TRANSITIONS;
@@ -269,11 +280,17 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
resetTaskInfo();
});
mGuard.close();
- if (mListener != null && mIsInitialized) {
+ mIsInitialized = false;
+ notifyReleased();
+ }
+
+ /** Called when the {@link TaskView} has been released. */
+ protected void notifyReleased() {
+ if (mListener != null && mNotifiedForInitialized) {
mListenerExecutor.execute(() -> {
mListener.onReleased();
});
- mIsInitialized = false;
+ mNotifiedForInitialized = false;
}
}
@@ -407,12 +424,8 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceCreated = true;
- if (mListener != null && !mIsInitialized) {
- mIsInitialized = true;
- mListenerExecutor.execute(() -> {
- mListener.onInitialized();
- });
- }
+ mIsInitialized = true;
+ notifyInitialized();
mShellExecutor.execute(() -> {
if (mTaskToken == null) {
// Nothing to update, task is not yet available
@@ -430,6 +443,16 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
});
}
+ /** Called when the {@link TaskView} is initialized. */
+ protected void notifyInitialized() {
+ if (mListener != null && !mNotifiedForInitialized) {
+ mNotifiedForInitialized = true;
+ mListenerExecutor.execute(() -> {
+ mListener.onInitialized();
+ });
+ }
+ }
+
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mTaskToken == null) {
@@ -494,7 +517,9 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
}
- ActivityManager.RunningTaskInfo getTaskInfo() {
+ /** Returns the task info for the task in the TaskView. */
+ @Nullable
+ public ActivityManager.RunningTaskInfo getTaskInfo() {
return mTaskInfo;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
new file mode 100644
index 000000000000..591e3476ecd9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -0,0 +1,210 @@
+/*
+ * 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.activityembedding;
+
+import static android.graphics.Matrix.MTRANS_X;
+import static android.graphics.Matrix.MTRANS_Y;
+
+import android.annotation.CallSuper;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Wrapper to handle the ActivityEmbedding animation update in one
+ * {@link SurfaceControl.Transaction}.
+ */
+class ActivityEmbeddingAnimationAdapter {
+
+ /**
+ * If {@link #mOverrideLayer} is set to this value, we don't want to override the surface layer.
+ */
+ private static final int LAYER_NO_OVERRIDE = -1;
+
+ @NonNull
+ final Animation mAnimation;
+ @NonNull
+ final TransitionInfo.Change mChange;
+ @NonNull
+ final SurfaceControl mLeash;
+ /** Area in absolute coordinate that the animation surface shouldn't go beyond. */
+ @NonNull
+ private final Rect mWholeAnimationBounds = new Rect();
+
+ @NonNull
+ final Transformation mTransformation = new Transformation();
+ @NonNull
+ final float[] mMatrix = new float[9];
+ @NonNull
+ final float[] mVecs = new float[4];
+ @NonNull
+ final Rect mRect = new Rect();
+ private boolean mIsFirstFrame = true;
+ private int mOverrideLayer = LAYER_NO_OVERRIDE;
+
+ ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
+ @NonNull TransitionInfo.Change change) {
+ this(animation, change, change.getLeash(), change.getEndAbsBounds());
+ }
+
+ /**
+ * @param leash the surface to animate, which is not necessary the same as
+ * {@link TransitionInfo.Change#getLeash()}, it can be a screenshot for example.
+ * @param wholeAnimationBounds area in absolute coordinate that the animation surface shouldn't
+ * go beyond.
+ */
+ ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
+ @NonNull TransitionInfo.Change change, @NonNull SurfaceControl leash,
+ @NonNull Rect wholeAnimationBounds) {
+ mAnimation = animation;
+ mChange = change;
+ mLeash = leash;
+ mWholeAnimationBounds.set(wholeAnimationBounds);
+ }
+
+ /**
+ * Surface layer to be set at the first frame of the animation. We will not set the layer if it
+ * is set to {@link #LAYER_NO_OVERRIDE}.
+ */
+ final void overrideLayer(int layer) {
+ mOverrideLayer = layer;
+ }
+
+ /** Called on frame update. */
+ final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
+ if (mIsFirstFrame) {
+ t.show(mLeash);
+ if (mOverrideLayer != LAYER_NO_OVERRIDE) {
+ t.setLayer(mLeash, mOverrideLayer);
+ }
+ mIsFirstFrame = false;
+ }
+
+ // Extract the transformation to the current time.
+ mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()),
+ mTransformation);
+ t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+ onAnimationUpdateInner(t);
+ }
+
+ /** To be overridden by subclasses to adjust the animation surface change. */
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ // Update the surface position and alpha.
+ final Point offset = mChange.getEndRelOffset();
+ mTransformation.getMatrix().postTranslate(offset.x, offset.y);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+
+ // Get current surface bounds in absolute coordinate.
+ // positionX/Y are in local coordinate, so minus the local offset to get the slide amount.
+ final int positionX = Math.round(mMatrix[MTRANS_X]);
+ final int positionY = Math.round(mMatrix[MTRANS_Y]);
+ final Rect cropRect = new Rect(mChange.getEndAbsBounds());
+ cropRect.offset(positionX - offset.x, positionY - offset.y);
+
+ // Store the current offset of the surface top left from (0,0) in absolute coordinate.
+ final int offsetX = cropRect.left;
+ final int offsetY = cropRect.top;
+
+ // Intersect to make sure the animation happens within the whole animation bounds.
+ if (!cropRect.intersect(mWholeAnimationBounds)) {
+ // Hide the surface when it is outside of the animation area.
+ t.setAlpha(mLeash, 0);
+ }
+
+ // cropRect is in absolute coordinate, so we need to translate it to surface top left.
+ cropRect.offset(-offsetX, -offsetY);
+ t.setCrop(mLeash, cropRect);
+ }
+
+ /** Called after animation finished. */
+ @CallSuper
+ void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+ onAnimationUpdate(t, mAnimation.getDuration());
+ }
+
+ final long getDurationHint() {
+ return mAnimation.computeDurationHint();
+ }
+
+ /**
+ * Should be used for the animation of the snapshot of a {@link TransitionInfo.Change} that has
+ * size change.
+ */
+ static class SnapshotAdapter extends ActivityEmbeddingAnimationAdapter {
+
+ SnapshotAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
+ @NonNull SurfaceControl snapshotLeash) {
+ super(animation, change, snapshotLeash, change.getEndAbsBounds());
+ }
+
+ @Override
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ // Snapshot should always be placed at the top left of the animation leash.
+ mTransformation.getMatrix().postTranslate(0, 0);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+ }
+
+ @Override
+ void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+ super.onAnimationEnd(t);
+ // Remove the screenshot leash after animation is finished.
+ if (mLeash.isValid()) {
+ t.remove(mLeash);
+ }
+ }
+ }
+
+ /**
+ * Should be used for the animation of the {@link TransitionInfo.Change} that has size change.
+ */
+ static class BoundsChangeAdapter extends ActivityEmbeddingAnimationAdapter {
+
+ BoundsChangeAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change) {
+ super(animation, change);
+ }
+
+ @Override
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ final Point offset = mChange.getEndRelOffset();
+ mTransformation.getMatrix().postTranslate(offset.x, offset.y);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+
+ // The following applies an inverse scale to the clip-rect so that it crops "after" the
+ // scale instead of before.
+ mVecs[1] = mVecs[2] = 0;
+ mVecs[0] = mVecs[3] = 1;
+ mTransformation.getMatrix().mapVectors(mVecs);
+ mVecs[0] = 1.f / mVecs[0];
+ mVecs[3] = 1.f / mVecs[3];
+ final Rect clipRect = mTransformation.getClipRect();
+ mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
+ mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
+ mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
+ mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
+ t.setCrop(mLeash, mRect);
+ }
+ }
+}
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
new file mode 100644
index 000000000000..756d80204833
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -0,0 +1,347 @@
+/*
+ * 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.activityembedding;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
+import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
+
+import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.common.ScreenshotUtils;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/** To run the ActivityEmbedding animations. */
+class ActivityEmbeddingAnimationRunner {
+
+ private static final String TAG = "ActivityEmbeddingAnimR";
+
+ private final ActivityEmbeddingController mController;
+ @VisibleForTesting
+ final ActivityEmbeddingAnimationSpec mAnimationSpec;
+
+ ActivityEmbeddingAnimationRunner(@NonNull Context context,
+ @NonNull ActivityEmbeddingController controller) {
+ mController = controller;
+ mAnimationSpec = new ActivityEmbeddingAnimationSpec(context);
+ }
+
+ /** Creates and starts animation for ActivityEmbedding transition. */
+ void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final Animator animator = createAnimator(info, startTransaction, finishTransaction,
+ () -> mController.onAnimationFinished(transition));
+ startTransaction.apply();
+ animator.start();
+ }
+
+ /**
+ * Sets transition animation scale settings value.
+ * @param scale The setting value of transition animation scale.
+ */
+ void setAnimScaleSetting(float scale) {
+ mAnimationSpec.setAnimScaleSetting(scale);
+ }
+
+ /** Creates the animator for the given {@link TransitionInfo}. */
+ @VisibleForTesting
+ @NonNull
+ Animator createAnimator(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Runnable animationFinishCallback) {
+ final List<ActivityEmbeddingAnimationAdapter> adapters =
+ createAnimationAdapters(info, startTransaction, finishTransaction);
+ long duration = 0;
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ duration = Math.max(duration, adapter.getDurationHint());
+ }
+ final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+ animator.setDuration(duration);
+ animator.addUpdateListener((anim) -> {
+ // Update all adapters in the same transaction.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ adapter.onAnimationUpdate(t, animator.getCurrentPlayTime());
+ }
+ t.apply();
+ });
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ adapter.onAnimationEnd(t);
+ }
+ t.apply();
+ animationFinishCallback.run();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ });
+ return animator;
+ }
+
+ /**
+ * Creates list of {@link ActivityEmbeddingAnimationAdapter} to handle animations on all window
+ * changes.
+ */
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ boolean isChangeTransition = false;
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) {
+ // Skip the animation if the windows are behind an app starting window.
+ return new ArrayList<>();
+ }
+ if (!isChangeTransition && change.getMode() == TRANSIT_CHANGE
+ && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
+ isChangeTransition = true;
+ }
+ }
+ if (isChangeTransition) {
+ return createChangeAnimationAdapters(info, startTransaction);
+ }
+ if (Transitions.isClosingType(info.getType())) {
+ return createCloseAnimationAdapters(info, startTransaction, finishTransaction);
+ }
+ return createOpenAnimationAdapters(info, startTransaction, finishTransaction);
+ }
+
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters(
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction,
+ true /* isOpening */, mAnimationSpec::loadOpenAnimation);
+ }
+
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters(
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction,
+ false /* isOpening */, mAnimationSpec::loadCloseAnimation);
+ }
+
+ /**
+ * Creates {@link ActivityEmbeddingAnimationAdapter} for OPEN and CLOSE types of transition.
+ * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type.
+ */
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters(
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction, boolean isOpening,
+ @NonNull AnimationProvider animationProvider) {
+ // We need to know if the change window is only a partial of the whole animation screen.
+ // If so, we will need to adjust it to make the whole animation screen looks like one.
+ final List<TransitionInfo.Change> openingChanges = new ArrayList<>();
+ final List<TransitionInfo.Change> closingChanges = new ArrayList<>();
+ final Rect openingWholeScreenBounds = new Rect();
+ final Rect closingWholeScreenBounds = new Rect();
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (Transitions.isOpeningType(change.getMode())) {
+ openingChanges.add(change);
+ openingWholeScreenBounds.union(change.getEndAbsBounds());
+ } else {
+ closingChanges.add(change);
+ closingWholeScreenBounds.union(change.getEndAbsBounds());
+ }
+ }
+
+ // For OPEN transition, open windows should be above close windows.
+ // For CLOSE transition, open windows should be below close windows.
+ int offsetLayer = TYPE_LAYER_OFFSET;
+ final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
+ for (TransitionInfo.Change change : openingChanges) {
+ final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
+ info, change, startTransaction, finishTransaction, animationProvider,
+ openingWholeScreenBounds);
+ if (isOpening) {
+ adapter.overrideLayer(offsetLayer++);
+ }
+ adapters.add(adapter);
+ }
+ for (TransitionInfo.Change change : closingChanges) {
+ final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
+ info, change, startTransaction, finishTransaction, animationProvider,
+ closingWholeScreenBounds);
+ if (!isOpening) {
+ adapter.overrideLayer(offsetLayer++);
+ }
+ adapters.add(adapter);
+ }
+ return adapters;
+ }
+
+ @NonNull
+ private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter(
+ @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull AnimationProvider animationProvider, @NonNull Rect wholeAnimationBounds) {
+ final Animation animation = animationProvider.get(info, change, wholeAnimationBounds);
+ // We may want to show a background color for open/close transition.
+ final int backgroundColor = getTransitionBackgroundColorIfSet(info, change, animation,
+ 0 /* defaultColor */);
+ if (backgroundColor != 0) {
+ addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction,
+ finishTransaction);
+ }
+ return new ActivityEmbeddingAnimationAdapter(animation, change, change.getLeash(),
+ wholeAnimationBounds);
+ }
+
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createChangeAnimationAdapters(
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+ final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
+ final Set<TransitionInfo.Change> handledChanges = new ArraySet<>();
+
+ // For the first iteration, we prepare the animation for the change type windows. This is
+ // needed because there may be window that is reparented while resizing. In such case, we
+ // will do the following:
+ // 1. Capture a screenshot from the Activity surface.
+ // 2. Attach the screenshot surface to the top of TaskFragment (Activity's parent) surface.
+ // 3. Animate the TaskFragment using Activity Change info (start/end bounds).
+ // This is because the TaskFragment surface/change won't contain the Activity's before its
+ // reparent.
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getMode() != TRANSIT_CHANGE
+ || change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
+ continue;
+ }
+
+ // This is the window with bounds change.
+ handledChanges.add(change);
+ final WindowContainerToken parentToken = change.getParent();
+ TransitionInfo.Change boundsAnimationChange = change;
+ if (parentToken != null) {
+ // When the parent window is also included in the transition as an opening window,
+ // we would like to animate the parent window instead.
+ final TransitionInfo.Change parentChange = info.getChange(parentToken);
+ if (parentChange != null && Transitions.isOpeningType(parentChange.getMode())) {
+ // We won't create a separate animation for the parent, but to animate the
+ // parent for the child resizing.
+ handledChanges.add(parentChange);
+ boundsAnimationChange = parentChange;
+ }
+ }
+
+ final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change,
+ boundsAnimationChange.getEndAbsBounds());
+
+ // Create a screenshot based on change, but attach it to the top of the
+ // boundsAnimationChange.
+ final SurfaceControl screenshotLeash = getOrCreateScreenshot(change,
+ boundsAnimationChange, startTransaction);
+ if (screenshotLeash != null) {
+ // Adapter for the starting screenshot leash.
+ // The screenshot leash will be removed in SnapshotAdapter#onAnimationEnd
+ adapters.add(new ActivityEmbeddingAnimationAdapter.SnapshotAdapter(
+ animations[0], change, screenshotLeash));
+ } else {
+ Log.e(TAG, "Failed to take screenshot for change=" + change);
+ }
+ // Adapter for the ending bounds changed leash.
+ adapters.add(new ActivityEmbeddingAnimationAdapter.BoundsChangeAdapter(
+ animations[1], boundsAnimationChange));
+ }
+
+ // Handle the other windows that don't have bounds change in the same transition.
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (handledChanges.contains(change)) {
+ // Skip windows that we have already handled in the previous iteration.
+ continue;
+ }
+
+ final Animation animation;
+ if (change.getParent() != null
+ && handledChanges.contains(info.getChange(change.getParent()))) {
+ // No-op if it will be covered by the changing parent window.
+ animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
+ } else if (Transitions.isClosingType(change.getMode())) {
+ animation = mAnimationSpec.createChangeBoundsCloseAnimation(change);
+ } else {
+ animation = mAnimationSpec.createChangeBoundsOpenAnimation(change);
+ }
+ adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change));
+ }
+ return adapters;
+ }
+
+ /**
+ * Takes a screenshot of the given {@code screenshotChange} surface if WM Core hasn't taken one.
+ * The screenshot leash should be attached to the {@code animationChange} surface which we will
+ * animate later.
+ */
+ @Nullable
+ private SurfaceControl getOrCreateScreenshot(@NonNull TransitionInfo.Change screenshotChange,
+ @NonNull TransitionInfo.Change animationChange,
+ @NonNull SurfaceControl.Transaction t) {
+ final SurfaceControl screenshotLeash = screenshotChange.getSnapshot();
+ if (screenshotLeash != null) {
+ // If WM Core has already taken a screenshot, make sure it is reparented to the
+ // animation leash.
+ t.reparent(screenshotLeash, animationChange.getLeash());
+ return screenshotLeash;
+ }
+
+ // If WM Core hasn't taken a screenshot, take a screenshot now.
+ final Rect cropBounds = new Rect(screenshotChange.getStartAbsBounds());
+ cropBounds.offsetTo(0, 0);
+ return ScreenshotUtils.takeScreenshot(t, screenshotChange.getLeash(),
+ animationChange.getLeash(), cropBounds, Integer.MAX_VALUE);
+ }
+
+ /** To provide an {@link Animation} based on the transition infos. */
+ private interface AnimationProvider {
+ Animation get(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change,
+ @NonNull Rect animationBounds);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
new file mode 100644
index 000000000000..eb6ac7615266
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -0,0 +1,233 @@
+/*
+ * 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.activityembedding;
+
+
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.ClipRectAnimation;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.policy.TransitionAnimation;
+import com.android.wm.shell.transition.Transitions;
+
+/** Animation spec for ActivityEmbedding transition. */
+// TODO(b/206557124): provide an easier way to customize animation
+class ActivityEmbeddingAnimationSpec {
+
+ private static final String TAG = "ActivityEmbeddingAnimSpec";
+ private static final int CHANGE_ANIMATION_DURATION = 517;
+ private static final int CHANGE_ANIMATION_FADE_DURATION = 80;
+ private static final int CHANGE_ANIMATION_FADE_OFFSET = 30;
+
+ private final Context mContext;
+ private final TransitionAnimation mTransitionAnimation;
+ private final Interpolator mFastOutExtraSlowInInterpolator;
+ private final LinearInterpolator mLinearInterpolator;
+ private float mTransitionAnimationScaleSetting;
+
+ ActivityEmbeddingAnimationSpec(@NonNull Context context) {
+ mContext = context;
+ mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
+ mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.fast_out_extra_slow_in);
+ mLinearInterpolator = new LinearInterpolator();
+ }
+
+ /**
+ * Sets transition animation scale settings value.
+ * @param scale The setting value of transition animation scale.
+ */
+ void setAnimScaleSetting(float scale) {
+ mTransitionAnimationScaleSetting = scale;
+ }
+
+ /** For window that doesn't need to be animated. */
+ @NonNull
+ static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) {
+ // Noop but just keep the window showing/hiding.
+ final float alpha = Transitions.isClosingType(change.getMode()) ? 0f : 1f;
+ return new AlphaAnimation(alpha, alpha);
+ }
+
+ /** Animation for window that is opening in a change transition. */
+ @NonNull
+ Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change) {
+ final Rect bounds = change.getEndAbsBounds();
+ final Point offset = change.getEndRelOffset();
+ // The window will be animated in from left or right depends on its position.
+ final int startLeft = offset.x == 0 ? -bounds.width() : bounds.width();
+
+ // The position should be 0-based as we will post translate in
+ // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0);
+ animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+ animation.setDuration(CHANGE_ANIMATION_DURATION);
+ animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ /** Animation for window that is closing in a change transition. */
+ @NonNull
+ Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change) {
+ final Rect bounds = change.getEndAbsBounds();
+ final Point offset = change.getEndRelOffset();
+ // The window will be animated out to left or right depends on its position.
+ final int endLeft = offset.x == 0 ? -bounds.width() : bounds.width();
+
+ // The position should be 0-based as we will post translate in
+ // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Animation animation = new TranslateAnimation(0, endLeft, 0, 0);
+ animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+ animation.setDuration(CHANGE_ANIMATION_DURATION);
+ animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ /**
+ * Animation for window that is changing (bounds change) in a change transition.
+ * @return the return array always has two elements. The first one is for the start leash, and
+ * the second one is for the end leash.
+ */
+ @NonNull
+ Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change,
+ @NonNull Rect parentBounds) {
+ // Both start bounds and end bounds are in screen coordinates. We will post translate
+ // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Rect startBounds = change.getStartAbsBounds();
+ final Rect endBounds = change.getEndAbsBounds();
+ float scaleX = ((float) startBounds.width()) / endBounds.width();
+ float scaleY = ((float) startBounds.height()) / endBounds.height();
+ // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
+ // be scaled up with its parent.
+ float startScaleX = 1.f / scaleX;
+ float startScaleY = 1.f / scaleY;
+
+ // The start leash will be fade out.
+ final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
+ final Animation startAlpha = new AlphaAnimation(1f, 0f);
+ startAlpha.setInterpolator(mLinearInterpolator);
+ startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
+ startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
+ startSet.addAnimation(startAlpha);
+ final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
+ startScaleY);
+ startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
+ startScale.setDuration(CHANGE_ANIMATION_DURATION);
+ startSet.addAnimation(startScale);
+ startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
+ endBounds.height());
+ startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+ // The end leash will be moved into the end position while scaling.
+ final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
+ endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
+ final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
+ endScale.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(endScale);
+ // The position should be 0-based as we will post translate in
+ // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
+ 0, 0);
+ endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(endTranslate);
+ // The end leash is resizing, we should update the window crop based on the clip rect.
+ final Rect startClip = new Rect(startBounds);
+ final Rect endClip = new Rect(endBounds);
+ startClip.offsetTo(0, 0);
+ endClip.offsetTo(0, 0);
+ final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
+ clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(clipAnim);
+ endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
+ parentBounds.height());
+ endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+ return new Animation[]{startSet, endSet};
+ }
+
+ @NonNull
+ Animation loadOpenAnimation(@NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
+ final boolean isEnter = Transitions.isOpeningType(change.getMode());
+ final Animation animation;
+ // TODO(b/207070762): Implement edgeExtension version
+ if (shouldShowBackdrop(info, change)) {
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? com.android.internal.R.anim.task_fragment_clear_top_open_enter
+ : com.android.internal.R.anim.task_fragment_clear_top_open_exit);
+ } else {
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? com.android.internal.R.anim.task_fragment_open_enter
+ : com.android.internal.R.anim.task_fragment_open_exit);
+ }
+ // Use the whole animation bounds instead of the change bounds, so that when multiple change
+ // targets are opening at the same time, the animation applied to each will be the same.
+ // Otherwise, we may see gap between the activities that are launching together.
+ animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
+ wholeAnimationBounds.width(), wholeAnimationBounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ @NonNull
+ Animation loadCloseAnimation(@NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
+ final boolean isEnter = Transitions.isOpeningType(change.getMode());
+ final Animation animation;
+ // TODO(b/207070762): Implement edgeExtension version
+ if (shouldShowBackdrop(info, change)) {
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? com.android.internal.R.anim.task_fragment_clear_top_close_enter
+ : com.android.internal.R.anim.task_fragment_clear_top_close_exit);
+ } else {
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? com.android.internal.R.anim.task_fragment_close_enter
+ : com.android.internal.R.anim.task_fragment_close_exit);
+ }
+ // Use the whole animation bounds instead of the change bounds, so that when multiple change
+ // targets are closing at the same time, the animation applied to each will be the same.
+ // Otherwise, we may see gap between the activities that are finishing together.
+ animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
+ wholeAnimationBounds.width(), wholeAnimationBounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ private boolean shouldShowBackdrop(@NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change change) {
+ final Animation a = loadAttributeAnimation(info, change, WALLPAPER_TRANSITION_NONE,
+ mTransitionAnimation);
+ return a != null && a.getShowBackdrop();
+ }
+}
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
new file mode 100644
index 000000000000..521a65cc4df6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -0,0 +1,134 @@
+/*
+ * 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.activityembedding;
+
+import static android.window.TransitionInfo.FLAG_FILLS_TASK;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.os.IBinder;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+/**
+ * Responsible for handling ActivityEmbedding related transitions.
+ */
+public class ActivityEmbeddingController implements Transitions.TransitionHandler {
+
+ private final Context mContext;
+ @VisibleForTesting
+ final Transitions mTransitions;
+ @VisibleForTesting
+ final ActivityEmbeddingAnimationRunner mAnimationRunner;
+
+ /**
+ * Keeps track of the currently-running transition callback associated with each transition
+ * token.
+ */
+ private final ArrayMap<IBinder, Transitions.TransitionFinishCallback> mTransitionCallbacks =
+ new ArrayMap<>();
+
+ private ActivityEmbeddingController(@NonNull Context context, @NonNull ShellInit shellInit,
+ @NonNull Transitions transitions) {
+ mContext = requireNonNull(context);
+ mTransitions = requireNonNull(transitions);
+ mAnimationRunner = new ActivityEmbeddingAnimationRunner(context, this);
+
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ /**
+ * Creates {@link ActivityEmbeddingController}, returns {@code null} if the feature is not
+ * supported.
+ */
+ @Nullable
+ public static ActivityEmbeddingController create(@NonNull Context context,
+ @NonNull ShellInit shellInit, @NonNull Transitions transitions) {
+ return Transitions.ENABLE_SHELL_TRANSITIONS
+ ? new ActivityEmbeddingController(context, shellInit, transitions)
+ : null;
+ }
+
+ /** Registers to handle transitions. */
+ public void onInit() {
+ mTransitions.addHandler(this);
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ boolean containsEmbeddingSplit = false;
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) {
+ // Only animate the transition if all changes are in a Task with ActivityEmbedding.
+ return false;
+ }
+ if (!containsEmbeddingSplit && !change.hasFlags(FLAG_FILLS_TASK)) {
+ // Whether the Task contains any ActivityEmbedding split before or after the
+ // transition.
+ containsEmbeddingSplit = true;
+ }
+ }
+ if (!containsEmbeddingSplit) {
+ // Let the system to play the default animation if there is no ActivityEmbedding split
+ // window. This allows to play the app customized animation when there is no embedding,
+ // such as the device is in a folded state.
+ return false;
+ }
+
+ // Start ActivityEmbedding animation.
+ mTransitionCallbacks.put(transition, finishCallback);
+ mAnimationRunner.startAnimation(transition, info, startTransaction, finishTransaction);
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ return null;
+ }
+
+ @Override
+ public void setAnimScaleSetting(float scale) {
+ mAnimationRunner.setAnimScaleSetting(scale);
+ }
+
+ /** Called when the animation is finished. */
+ void onAnimationFinished(@NonNull IBinder transition) {
+ final Transitions.TransitionFinishCallback callback =
+ mTransitionCallbacks.remove(transition);
+ if (callback == null) {
+ throw new IllegalStateException("No finish callback found");
+ }
+ callback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ }
+}
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 2aead9392e59..a0dde6ad168d 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
@@ -53,6 +53,19 @@ public class Interpolators {
public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
/**
+ * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
+ * is disappearing e.g. when moving off screen.
+ */
+ public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator(
+ 0.3f, 0f, 0.8f, 0.15f);
+
+ /**
+ * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
+ * is appearing e.g. when coming from off screen
+ */
+ public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
+ 0.05f, 0.7f, 0.1f, 1f);
+ /**
* Interpolator to be used when animating a move based on a click. Pair with enough duration.
*/
public static final Interpolator TOUCH_RESPONSE = new PathInterpolator(0.3f, 0f, 0.1f, 1f);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
index b483fe03e80f..ee8c41417458 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
@@ -22,7 +22,6 @@ import android.view.View
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FlingAnimation
import androidx.dynamicanimation.animation.FloatPropertyCompat
-import androidx.dynamicanimation.animation.FrameCallbackScheduler
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
@@ -125,12 +124,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
private var defaultFling: FlingConfig = globalDefaultFling
/**
- * FrameCallbackScheduler to use if it need custom FrameCallbackScheduler, if this is null,
- * it will use the default FrameCallbackScheduler in the DynamicAnimation.
- */
- private var customScheduler: FrameCallbackScheduler? = null
-
- /**
* Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to
* the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add
* just one permanent update and end listener to the DynamicAnimations.
@@ -454,14 +447,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
this.defaultFling = defaultFling
}
- /**
- * Set the custom FrameCallbackScheduler for all aniatmion in this animator. Set this with null for
- * restoring to default FrameCallbackScheduler.
- */
- fun setCustomScheduler(scheduler: FrameCallbackScheduler) {
- this.customScheduler = scheduler
- }
-
/** Starts the animations! */
fun start() {
startAction()
@@ -511,12 +496,9 @@ class PhysicsAnimator<T> private constructor (target: T) {
// springs) on this property before flinging.
cancel(animatedProperty)
- // Apply the custom animation scheduler if it not null
- val flingAnim = getFlingAnimation(animatedProperty, target)
- flingAnim.scheduler = customScheduler ?: flingAnim.scheduler
-
// Apply the configuration and start the animation.
- flingAnim.also { flingConfig.applyToAnimation(it) }.start()
+ getFlingAnimation(animatedProperty, target)
+ .also { flingConfig.applyToAnimation(it) }.start()
}
}
@@ -529,18 +511,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
// Apply the configuration and start the animation.
val springAnim = getSpringAnimation(animatedProperty, target)
- // If customScheduler is exist and has not been set to the animation,
- // it should set here.
- if (customScheduler != null &&
- springAnim.scheduler != customScheduler) {
- // Cancel the animation before set animation handler
- if (springAnim.isRunning) {
- cancel(animatedProperty)
- }
- // Apply the custom scheduler handler if it not null
- springAnim.scheduler = customScheduler ?: springAnim.scheduler
- }
-
// Apply the configuration and start the animation.
springConfig.applyToAnimation(springAnim)
animationStartActions.add(springAnim::start)
@@ -596,12 +566,9 @@ class PhysicsAnimator<T> private constructor (target: T) {
}
}
- // Apply the custom animation scheduler if it not null
- val springAnim = getSpringAnimation(animatedProperty, target)
- springAnim.scheduler = customScheduler ?: springAnim.scheduler
-
// Apply the configuration and start the spring animation.
- springAnim.also { springConfig.applyToAnimation(it) }.start()
+ getSpringAnimation(animatedProperty, target)
+ .also { springConfig.applyToAnimation(it) }.start()
}
}
})
@@ -829,8 +796,12 @@ class PhysicsAnimator<T> private constructor (target: T) {
/** Cancels all in progress animations on all properties. */
fun cancel() {
- cancelAction(flingAnimations.keys)
- cancelAction(springAnimations.keys)
+ if (flingAnimations.size > 0) {
+ cancelAction(flingAnimations.keys)
+ }
+ if (springAnimations.size > 0) {
+ cancelAction(springAnimations.keys)
+ }
}
/** Cancels in progress animations on the provided properties only. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
deleted file mode 100644
index 3f0b01bef0ce..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.apppairs;
-
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
-
-import android.app.ActivityManager;
-import android.view.SurfaceControl;
-import android.view.SurfaceSession;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.SurfaceUtils;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.split.SplitLayout;
-import com.android.wm.shell.common.split.SplitWindowManager;
-
-import java.io.PrintWriter;
-
-/**
- * An app-pairs consisting of {@link #mRootTaskInfo} that acts as the hierarchy parent of
- * {@link #mTaskInfo1} and {@link #mTaskInfo2} in the pair.
- * Also includes all UI for managing the pair like the divider.
- */
-class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayoutHandler {
- private static final String TAG = AppPair.class.getSimpleName();
-
- private ActivityManager.RunningTaskInfo mRootTaskInfo;
- private SurfaceControl mRootTaskLeash;
- private ActivityManager.RunningTaskInfo mTaskInfo1;
- private SurfaceControl mTaskLeash1;
- private ActivityManager.RunningTaskInfo mTaskInfo2;
- private SurfaceControl mTaskLeash2;
- private SurfaceControl mDimLayer1;
- private SurfaceControl mDimLayer2;
- private final SurfaceSession mSurfaceSession = new SurfaceSession();
-
- private final AppPairsController mController;
- private final SyncTransactionQueue mSyncQueue;
- private final DisplayController mDisplayController;
- private final DisplayImeController mDisplayImeController;
- private final DisplayInsetsController mDisplayInsetsController;
- private SplitLayout mSplitLayout;
-
- private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
- new SplitWindowManager.ParentContainerCallbacks() {
- @Override
- public void attachToParentSurface(SurfaceControl.Builder b) {
- b.setParent(mRootTaskLeash);
- }
-
- @Override
- public void onLeashReady(SurfaceControl leash) {
- mSyncQueue.runInSync(t -> t
- .show(leash)
- .setLayer(leash, Integer.MAX_VALUE)
- .setPosition(leash,
- mSplitLayout.getDividerBounds().left,
- mSplitLayout.getDividerBounds().top));
- }
- };
-
- AppPair(AppPairsController controller) {
- mController = controller;
- mSyncQueue = controller.getSyncTransactionQueue();
- mDisplayController = controller.getDisplayController();
- mDisplayImeController = controller.getDisplayImeController();
- mDisplayInsetsController = controller.getDisplayInsetsController();
- }
-
- int getRootTaskId() {
- return mRootTaskInfo != null ? mRootTaskInfo.taskId : INVALID_TASK_ID;
- }
-
- private int getTaskId1() {
- return mTaskInfo1 != null ? mTaskInfo1.taskId : INVALID_TASK_ID;
- }
-
- private int getTaskId2() {
- return mTaskInfo2 != null ? mTaskInfo2.taskId : INVALID_TASK_ID;
- }
-
- boolean contains(int taskId) {
- return taskId == getRootTaskId() || taskId == getTaskId1() || taskId == getTaskId2();
- }
-
- boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
- ProtoLog.v(WM_SHELL_TASK_ORG, "pair task1=%d task2=%d in AppPair=%s",
- task1.taskId, task2.taskId, this);
-
- if (!task1.supportsMultiWindow || !task2.supportsMultiWindow) {
- ProtoLog.e(WM_SHELL_TASK_ORG,
- "Can't pair tasks that doesn't support multi window, "
- + "task1.supportsMultiWindow=%b, task2.supportsMultiWindow=%b",
- task1.supportsMultiWindow, task2.supportsMultiWindow);
- return false;
- }
-
- mTaskInfo1 = task1;
- mTaskInfo2 = task2;
- mSplitLayout = new SplitLayout(TAG + "SplitDivider",
- mDisplayController.getDisplayContext(mRootTaskInfo.displayId),
- mRootTaskInfo.configuration, this /* layoutChangeListener */,
- mParentContainerCallbacks, mDisplayImeController, mController.getTaskOrganizer(),
- SplitLayout.PARALLAX_DISMISSING);
- mDisplayInsetsController.addInsetsChangedListener(mRootTaskInfo.displayId, mSplitLayout);
-
- final WindowContainerToken token1 = task1.token;
- final WindowContainerToken token2 = task2.token;
- final WindowContainerTransaction wct = new WindowContainerTransaction();
-
- wct.setHidden(mRootTaskInfo.token, false)
- .reparent(token1, mRootTaskInfo.token, true /* onTop */)
- .reparent(token2, mRootTaskInfo.token, true /* onTop */)
- .setWindowingMode(token1, WINDOWING_MODE_MULTI_WINDOW)
- .setWindowingMode(token2, WINDOWING_MODE_MULTI_WINDOW)
- .setBounds(token1, mSplitLayout.getBounds1())
- .setBounds(token2, mSplitLayout.getBounds2())
- // Moving the root task to top after the child tasks were repareted , or the root
- // task cannot be visible and focused.
- .reorder(mRootTaskInfo.token, true);
- mController.getTaskOrganizer().applyTransaction(wct);
- return true;
- }
-
- void unpair() {
- unpair(null /* toTopToken */);
- }
-
- private void unpair(@Nullable WindowContainerToken toTopToken) {
- final WindowContainerToken token1 = mTaskInfo1.token;
- final WindowContainerToken token2 = mTaskInfo2.token;
- final WindowContainerTransaction wct = new WindowContainerTransaction();
-
- // Reparent out of this container and reset windowing mode.
- wct.setHidden(mRootTaskInfo.token, true)
- .reorder(mRootTaskInfo.token, false)
- .reparent(token1, null, token1 == toTopToken /* onTop */)
- .reparent(token2, null, token2 == toTopToken /* onTop */)
- .setWindowingMode(token1, WINDOWING_MODE_UNDEFINED)
- .setWindowingMode(token2, WINDOWING_MODE_UNDEFINED);
- mController.getTaskOrganizer().applyTransaction(wct);
-
- mTaskInfo1 = null;
- mTaskInfo2 = null;
- mSplitLayout.release();
- mSplitLayout = null;
- }
-
- @Override
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- if (mRootTaskInfo == null || taskInfo.taskId == mRootTaskInfo.taskId) {
- mRootTaskInfo = taskInfo;
- mRootTaskLeash = leash;
- } else if (taskInfo.taskId == getTaskId1()) {
- mTaskInfo1 = taskInfo;
- mTaskLeash1 = leash;
- mSyncQueue.runInSync(t -> mDimLayer1 =
- SurfaceUtils.makeDimLayer(t, mTaskLeash1, "Dim layer", mSurfaceSession));
- } else if (taskInfo.taskId == getTaskId2()) {
- mTaskInfo2 = taskInfo;
- mTaskLeash2 = leash;
- mSyncQueue.runInSync(t -> mDimLayer2 =
- SurfaceUtils.makeDimLayer(t, mTaskLeash2, "Dim layer", mSurfaceSession));
- } else {
- throw new IllegalStateException("Unknown task=" + taskInfo.taskId);
- }
-
- if (mTaskLeash1 == null || mTaskLeash2 == null) return;
-
- mSplitLayout.init();
-
- mSyncQueue.runInSync(t -> t
- .show(mRootTaskLeash)
- .show(mTaskLeash1)
- .show(mTaskLeash2)
- .setPosition(mTaskLeash1,
- mTaskInfo1.positionInParent.x,
- mTaskInfo1.positionInParent.y)
- .setPosition(mTaskLeash2,
- mTaskInfo2.positionInParent.x,
- mTaskInfo2.positionInParent.y));
- }
-
- @Override
- public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- if (!taskInfo.supportsMultiWindow) {
- // Dismiss AppPair if the task no longer supports multi window.
- mController.unpair(mRootTaskInfo.taskId);
- return;
- }
- if (taskInfo.taskId == getRootTaskId()) {
- if (mRootTaskInfo.isVisible != taskInfo.isVisible) {
- mSyncQueue.runInSync(t -> {
- if (taskInfo.isVisible) {
- t.show(mRootTaskLeash);
- } else {
- t.hide(mRootTaskLeash);
- }
- });
- }
- mRootTaskInfo = taskInfo;
-
- if (mSplitLayout != null
- && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) {
- onLayoutSizeChanged(mSplitLayout);
- }
- } else if (taskInfo.taskId == getTaskId1()) {
- mTaskInfo1 = taskInfo;
- } else if (taskInfo.taskId == getTaskId2()) {
- mTaskInfo2 = taskInfo;
- } else {
- throw new IllegalStateException("Unknown task=" + taskInfo.taskId);
- }
- }
-
- @Override
- public int getSplitItemPosition(WindowContainerToken token) {
- if (token == null) {
- return SPLIT_POSITION_UNDEFINED;
- }
-
- if (token.equals(mTaskInfo1.getToken())) {
- return SPLIT_POSITION_TOP_OR_LEFT;
- } else if (token.equals(mTaskInfo2.getToken())) {
- return SPLIT_POSITION_BOTTOM_OR_RIGHT;
- }
-
- return SPLIT_POSITION_UNDEFINED;
- }
-
- @Override
- public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- if (taskInfo.taskId == getRootTaskId()) {
- // We don't want to release this object back to the pool since the root task went away.
- mController.unpair(mRootTaskInfo.taskId, false /* releaseToPool */);
- } else if (taskInfo.taskId == getTaskId1()) {
- mController.unpair(mRootTaskInfo.taskId);
- mSyncQueue.runInSync(t -> t.remove(mDimLayer1));
- } else if (taskInfo.taskId == getTaskId2()) {
- mController.unpair(mRootTaskInfo.taskId);
- mSyncQueue.runInSync(t -> t.remove(mDimLayer2));
- }
- }
-
- @Override
- public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
- b.setParent(findTaskSurface(taskId));
- }
-
- @Override
- public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc,
- SurfaceControl.Transaction t) {
- t.reparent(sc, findTaskSurface(taskId));
- }
-
- private SurfaceControl findTaskSurface(int taskId) {
- if (getRootTaskId() == taskId) {
- return mRootTaskLeash;
- } else if (getTaskId1() == taskId) {
- return mTaskLeash1;
- } else if (getTaskId2() == taskId) {
- return mTaskLeash2;
- } else {
- throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
- }
- }
-
- @Override
- public void dump(@NonNull PrintWriter pw, String prefix) {
- final String innerPrefix = prefix + " ";
- final String childPrefix = innerPrefix + " ";
- pw.println(prefix + this);
- if (mRootTaskInfo != null) {
- pw.println(innerPrefix + "Root taskId=" + mRootTaskInfo.taskId
- + " winMode=" + mRootTaskInfo.getWindowingMode());
- }
- if (mTaskInfo1 != null) {
- pw.println(innerPrefix + "1 taskId=" + mTaskInfo1.taskId
- + " winMode=" + mTaskInfo1.getWindowingMode());
- }
- if (mTaskInfo2 != null) {
- pw.println(innerPrefix + "2 taskId=" + mTaskInfo2.taskId
- + " winMode=" + mTaskInfo2.getWindowingMode());
- }
- }
-
- @Override
- public String toString() {
- return TAG + "#" + getRootTaskId();
- }
-
- @Override
- public void onSnappedToDismiss(boolean snappedToEnd) {
- unpair(snappedToEnd ? mTaskInfo1.token : mTaskInfo2.token /* toTopToken */);
- }
-
- @Override
- public void onLayoutPositionChanging(SplitLayout layout) {
- mSyncQueue.runInSync(t ->
- layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2,
- true /* applyResizingOffset */));
- }
-
- @Override
- public void onLayoutSizeChanging(SplitLayout layout) {
- mSyncQueue.runInSync(t ->
- layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2,
- true /* applyResizingOffset */));
- }
-
- @Override
- public void onLayoutSizeChanged(SplitLayout layout) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- layout.applyTaskChanges(wct, mTaskInfo1, mTaskInfo2);
- mSyncQueue.queue(wct);
- mSyncQueue.runInSync(t ->
- layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2,
- false /* applyResizingOffset */));
- }
-
- @Override
- public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, mTaskInfo1, mTaskInfo2);
- mController.getTaskOrganizer().applyTransaction(wct);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java
deleted file mode 100644
index a9b1dbc3c23b..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.apppairs;
-
-import android.app.ActivityManager;
-
-import androidx.annotation.NonNull;
-
-import com.android.wm.shell.common.annotations.ExternalThread;
-
-import java.io.PrintWriter;
-
-/**
- * Interface to engage app pairs feature.
- */
-@ExternalThread
-public interface AppPairs {
- /** Pairs indicated tasks. */
- boolean pair(int task1, int task2);
- /** Pairs indicated tasks. */
- boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2);
- /** Unpairs any app-pair containing this task id. */
- void unpair(int taskId);
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java
deleted file mode 100644
index 53234ab971d6..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.apppairs;
-
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
-
-import android.app.ActivityManager;
-import android.util.Slog;
-import android.util.SparseArray;
-
-import androidx.annotation.NonNull;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-import java.io.PrintWriter;
-
-/**
- * Class manages app-pairs multitasking mode and implements the main interface {@link AppPairs}.
- */
-public class AppPairsController {
- private static final String TAG = AppPairsController.class.getSimpleName();
-
- private final ShellTaskOrganizer mTaskOrganizer;
- private final SyncTransactionQueue mSyncQueue;
- private final ShellExecutor mMainExecutor;
- private final AppPairsImpl mImpl = new AppPairsImpl();
-
- private AppPairsPool mPairsPool;
- // Active app-pairs mapped by root task id key.
- private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>();
- private final DisplayController mDisplayController;
- private final DisplayImeController mDisplayImeController;
- private final DisplayInsetsController mDisplayInsetsController;
-
- public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue,
- DisplayController displayController, ShellExecutor mainExecutor,
- DisplayImeController displayImeController,
- DisplayInsetsController displayInsetsController) {
- mTaskOrganizer = organizer;
- mSyncQueue = syncQueue;
- mDisplayController = displayController;
- mDisplayImeController = displayImeController;
- mDisplayInsetsController = displayInsetsController;
- mMainExecutor = mainExecutor;
- }
-
- public AppPairs asAppPairs() {
- return mImpl;
- }
-
- public void onOrganizerRegistered() {
- if (mPairsPool == null) {
- setPairsPool(new AppPairsPool(this));
- }
- }
-
- @VisibleForTesting
- public void setPairsPool(AppPairsPool pool) {
- mPairsPool = pool;
- }
-
- public boolean pair(int taskId1, int taskId2) {
- final ActivityManager.RunningTaskInfo task1 = mTaskOrganizer.getRunningTaskInfo(taskId1);
- final ActivityManager.RunningTaskInfo task2 = mTaskOrganizer.getRunningTaskInfo(taskId2);
- if (task1 == null || task2 == null) {
- return false;
- }
- return pair(task1, task2);
- }
-
- public boolean pair(ActivityManager.RunningTaskInfo task1,
- ActivityManager.RunningTaskInfo task2) {
- return pairInner(task1, task2) != null;
- }
-
- @VisibleForTesting
- public AppPair pairInner(
- @NonNull ActivityManager.RunningTaskInfo task1,
- @NonNull ActivityManager.RunningTaskInfo task2) {
- final AppPair pair = mPairsPool.acquire();
- if (!pair.pair(task1, task2)) {
- mPairsPool.release(pair);
- return null;
- }
-
- mActiveAppPairs.put(pair.getRootTaskId(), pair);
- return pair;
- }
-
- public void unpair(int taskId) {
- unpair(taskId, true /* releaseToPool */);
- }
-
- public void unpair(int taskId, boolean releaseToPool) {
- AppPair pair = mActiveAppPairs.get(taskId);
- if (pair == null) {
- for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) {
- final AppPair candidate = mActiveAppPairs.valueAt(i);
- if (candidate.contains(taskId)) {
- pair = candidate;
- break;
- }
- }
- }
- if (pair == null) {
- ProtoLog.v(WM_SHELL_TASK_ORG, "taskId %d isn't isn't in an app-pair.", taskId);
- return;
- }
-
- ProtoLog.v(WM_SHELL_TASK_ORG, "unpair taskId=%d pair=%s", taskId, pair);
- mActiveAppPairs.remove(pair.getRootTaskId());
- pair.unpair();
- if (releaseToPool) {
- mPairsPool.release(pair);
- }
- }
-
- ShellTaskOrganizer getTaskOrganizer() {
- return mTaskOrganizer;
- }
-
- SyncTransactionQueue getSyncTransactionQueue() {
- return mSyncQueue;
- }
-
- DisplayController getDisplayController() {
- return mDisplayController;
- }
-
- DisplayImeController getDisplayImeController() {
- return mDisplayImeController;
- }
-
- DisplayInsetsController getDisplayInsetsController() {
- return mDisplayInsetsController;
- }
-
- public void dump(@NonNull PrintWriter pw, String prefix) {
- final String innerPrefix = prefix + " ";
- final String childPrefix = innerPrefix + " ";
- pw.println(prefix + this);
-
- for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) {
- mActiveAppPairs.valueAt(i).dump(pw, childPrefix);
- }
-
- if (mPairsPool != null) {
- mPairsPool.dump(pw, prefix);
- }
- }
-
- @Override
- public String toString() {
- return TAG + "#" + mActiveAppPairs.size();
- }
-
- private class AppPairsImpl implements AppPairs {
- @Override
- public boolean pair(int task1, int task2) {
- boolean[] result = new boolean[1];
- try {
- mMainExecutor.executeBlocking(() -> {
- result[0] = AppPairsController.this.pair(task1, task2);
- });
- } catch (InterruptedException e) {
- Slog.e(TAG, "Failed to pair tasks: " + task1 + ", " + task2);
- }
- return result[0];
- }
-
- @Override
- public boolean pair(ActivityManager.RunningTaskInfo task1,
- ActivityManager.RunningTaskInfo task2) {
- boolean[] result = new boolean[1];
- try {
- mMainExecutor.executeBlocking(() -> {
- result[0] = AppPairsController.this.pair(task1, task2);
- });
- } catch (InterruptedException e) {
- Slog.e(TAG, "Failed to pair tasks: " + task1 + ", " + task2);
- }
- return result[0];
- }
-
- @Override
- public void unpair(int taskId) {
- mMainExecutor.execute(() -> {
- AppPairsController.this.unpair(taskId);
- });
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java
deleted file mode 100644
index 5c6037ea0702..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.apppairs;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
-
-import androidx.annotation.NonNull;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.common.ProtoLog;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-
-/**
- * Class that manager pool of {@link AppPair} objects. Helps reduce the need to call system_server
- * to create a root task for the app-pair when needed since we always have one ready to go.
- */
-class AppPairsPool {
- private static final String TAG = AppPairsPool.class.getSimpleName();
-
- @VisibleForTesting
- final AppPairsController mController;
- // The pool
- private final ArrayList<AppPair> mPool = new ArrayList();
-
- AppPairsPool(AppPairsController controller) {
- mController = controller;
- incrementPool();
- }
-
- AppPair acquire() {
- final AppPair entry = mPool.remove(mPool.size() - 1);
- ProtoLog.v(WM_SHELL_TASK_ORG, "acquire entry.taskId=%s listener=%s size=%d",
- entry.getRootTaskId(), entry, mPool.size());
- if (mPool.size() == 0) {
- incrementPool();
- }
- return entry;
- }
-
- void release(AppPair entry) {
- mPool.add(entry);
- ProtoLog.v(WM_SHELL_TASK_ORG, "release entry.taskId=%s listener=%s size=%d",
- entry.getRootTaskId(), entry, mPool.size());
- }
-
- @VisibleForTesting
- void incrementPool() {
- ProtoLog.v(WM_SHELL_TASK_ORG, "incrementPool size=%d", mPool.size());
- final AppPair entry = new AppPair(mController);
- // TODO: multi-display...
- mController.getTaskOrganizer().createRootTask(
- DEFAULT_DISPLAY, WINDOWING_MODE_FULLSCREEN, entry);
- mPool.add(entry);
- }
-
- @VisibleForTesting
- int poolSize() {
- return mPool.size();
- }
-
- public void dump(@NonNull PrintWriter pw, String prefix) {
- final String innerPrefix = prefix + " ";
- final String childPrefix = innerPrefix + " ";
- pw.println(prefix + this);
- for (int i = mPool.size() - 1; i >= 0; --i) {
- mPool.get(i).dump(pw, childPrefix);
- }
- }
-
- @Override
- public String toString() {
- return TAG + "#" + mPool.size();
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS
deleted file mode 100644
index 4d9b520e3f0e..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# WM shell sub-modules apppair owner
-chenghsiuchang@google.com
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 8c0affb0a432..86f9d5b534f4 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
@@ -29,6 +29,13 @@ import com.android.wm.shell.common.annotations.ExternalThread;
public interface BackAnimation {
/**
+ * Returns a binder that can be passed to an external process to update back animations.
+ */
+ default IBackAnimation createExternalInterface() {
+ return null;
+ }
+
+ /**
* Called when a {@link MotionEvent} is generated by a back gesture.
*
* @param touchX the X touch position of the {@link MotionEvent}.
@@ -47,13 +54,6 @@ public interface BackAnimation {
void setTriggerBack(boolean triggerBack);
/**
- * Returns a binder that can be passed to an external process to update back animations.
- */
- default IBackAnimation createExternalInterface() {
- return null;
- }
-
- /**
* Sets the threshold values that defining edge swipe behavior.
* @param triggerThreshold the min threshold to trigger back.
* @param progressThreshold the max threshold to keep progressing back animation.
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 0cf2b28921e1..33ecdd88fad3 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
@@ -16,6 +16,9 @@
package com.android.wm.shell.back;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
@@ -27,21 +30,29 @@ import android.app.WindowConfiguration;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
-import android.graphics.Point;
-import android.graphics.PointF;
import android.hardware.HardwareBuffer;
+import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Handler;
+import android.os.IBinder;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings.Global;
import android.util.Log;
+import android.view.IWindowFocusObserver;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
+import android.window.BackAnimationAdaptor;
import android.window.BackEvent;
import android.window.BackNavigationInfo;
+import android.window.IBackAnimationRunner;
+import android.window.IBackNaviAnimationController;
import android.window.IOnBackInvokedCallback;
import com.android.internal.annotations.VisibleForTesting;
@@ -50,6 +61,7 @@ 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.sysui.ShellInit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -68,28 +80,22 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private static final int PROGRESS_THRESHOLD = SystemProperties
.getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
+ // TODO (b/241808055) Find a appropriate time to remove during refactor
+ private static final boolean USE_TRANSITION =
+ SystemProperties.getInt("persist.wm.debug.predictive_back_ani_trans", 1) != 0;
/**
* Max duration to wait for a transition to finish before accepting another gesture start
* request.
*/
private static final long MAX_TRANSITION_DURATION = 2000;
- /**
- * Location of the initial touch event of the back gesture.
- */
- private final PointF mInitTouchLocation = new PointF();
-
- /**
- * Raw delta between {@link #mInitTouchLocation} and the last touch location.
- */
- private final Point mTouchEventDelta = new Point();
- private final ShellExecutor mShellExecutor;
-
/** True when a back gesture is ongoing */
private boolean mBackGestureStarted = false;
/** Tracks if an uninterruptible transition is in progress */
private boolean mTransitionInProgress = false;
+ /** Tracks if we should start the back gesture on the next motion move event */
+ private boolean mShouldStartOnNextMoveEvent = false;
/** @see #setTriggerBack(boolean) */
private boolean mTriggerBack;
@@ -98,6 +104,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private final SurfaceControl.Transaction mTransaction;
private final IActivityTaskManager mActivityTaskManager;
private final Context mContext;
+ private final ContentResolver mContentResolver;
+ private final ShellExecutor mShellExecutor;
+ private final Handler mBgHandler;
@Nullable
private IOnBackInvokedCallback mBackToLauncherCallback;
private float mTriggerThreshold;
@@ -107,17 +116,133 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mTransitionInProgress = false;
};
+ private RemoteAnimationTarget mAnimationTarget;
+ IBackAnimationRunner mIBackAnimationRunner;
+ private IBackNaviAnimationController mBackAnimationController;
+ private BackAnimationAdaptor mBackAnimationAdaptor;
+
+ private boolean mWaitingAnimationStart;
+ private final TouchTracker mTouchTracker = new TouchTracker();
+ private final CachingBackDispatcher mCachingBackDispatcher = new CachingBackDispatcher();
+
+ @VisibleForTesting
+ final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() {
+ @Override
+ public void focusGained(IBinder inputToken) { }
+ @Override
+ public void focusLost(IBinder inputToken) {
+ mShellExecutor.execute(() -> {
+ if (!mBackGestureStarted || mTransitionInProgress) {
+ // If an uninterruptible transition is already in progress, we should ignore
+ // this due to the transition may cause focus lost. (alpha = 0)
+ return;
+ }
+ setTriggerBack(false);
+ onGestureFinished(false);
+ });
+ }
+ };
+
+ /**
+ * Helper class to record the touch location for gesture start and latest.
+ */
+ private static class TouchTracker {
+ /**
+ * Location of the latest touch event
+ */
+ private float mLatestTouchX;
+ private float mLatestTouchY;
+ private int mSwipeEdge;
+
+ /**
+ * Location of the initial touch event of the back gesture.
+ */
+ private float mInitTouchX;
+ private float mInitTouchY;
+
+ void update(float touchX, float touchY, int swipeEdge) {
+ mLatestTouchX = touchX;
+ mLatestTouchY = touchY;
+ mSwipeEdge = swipeEdge;
+ }
+
+ void setGestureStartLocation(float touchX, float touchY) {
+ mInitTouchX = touchX;
+ mInitTouchY = touchY;
+ }
+
+ int getDeltaFromGestureStart(float touchX) {
+ return Math.round(touchX - mInitTouchX);
+ }
+
+ void reset() {
+ mInitTouchX = 0;
+ mInitTouchY = 0;
+ }
+ }
+
+ /**
+ * Cache the temporary callback and trigger result if gesture was finish before received
+ * BackAnimationRunner#onAnimationStart/cancel, so there can continue play the animation.
+ */
+ private class CachingBackDispatcher {
+ private IOnBackInvokedCallback mOnBackCallback;
+ private boolean mTriggerBack;
+ // Whether we are waiting to receive onAnimationStart
+ private boolean mWaitingAnimation;
+
+ void startWaitingAnimation() {
+ mWaitingAnimation = true;
+ }
+
+ boolean set(IOnBackInvokedCallback callback, boolean triggerBack) {
+ if (mWaitingAnimation) {
+ mOnBackCallback = callback;
+ mTriggerBack = triggerBack;
+ return true;
+ }
+ return false;
+ }
+
+ boolean consume() {
+ boolean consumed = false;
+ if (mWaitingAnimation && mOnBackCallback != null) {
+ if (mTriggerBack) {
+ final BackEvent backFinish = new BackEvent(
+ mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 1,
+ mTouchTracker.mSwipeEdge, mAnimationTarget);
+ dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
+ dispatchOnBackInvoked(mOnBackCallback);
+ } else {
+ final BackEvent backFinish = new BackEvent(
+ mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 0,
+ mTouchTracker.mSwipeEdge, mAnimationTarget);
+ dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
+ dispatchOnBackCancelled(mOnBackCallback);
+ }
+ startTransition();
+ consumed = true;
+ }
+ mOnBackCallback = null;
+ mWaitingAnimation = false;
+ return consumed;
+ }
+ }
+
public BackAnimationController(
+ @NonNull ShellInit shellInit,
@NonNull @ShellMainThread ShellExecutor shellExecutor,
@NonNull @ShellBackgroundThread Handler backgroundHandler,
Context context) {
- this(shellExecutor, backgroundHandler, new SurfaceControl.Transaction(),
+ this(shellInit, shellExecutor, backgroundHandler, new SurfaceControl.Transaction(),
ActivityTaskManager.getService(), context, context.getContentResolver());
}
@VisibleForTesting
- BackAnimationController(@NonNull @ShellMainThread ShellExecutor shellExecutor,
- @NonNull @ShellBackgroundThread Handler handler,
+ BackAnimationController(
+ @NonNull ShellInit shellInit,
+ @NonNull @ShellMainThread ShellExecutor shellExecutor,
+ @NonNull @ShellBackgroundThread Handler bgHandler,
@NonNull SurfaceControl.Transaction transaction,
@NonNull IActivityTaskManager activityTaskManager,
Context context, ContentResolver contentResolver) {
@@ -125,7 +250,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mTransaction = transaction;
mActivityTaskManager = activityTaskManager;
mContext = context;
- setupAnimationDeveloperSettingsObserver(contentResolver, handler);
+ mContentResolver = contentResolver;
+ mBgHandler = bgHandler;
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
}
private void setupAnimationDeveloperSettingsObserver(
@@ -233,6 +364,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
@VisibleForTesting
void setBackToLauncherCallback(IOnBackInvokedCallback callback) {
mBackToLauncherCallback = callback;
+ if (USE_TRANSITION) {
+ createAdaptor();
+ }
}
private void clearBackToLauncherCallback() {
@@ -241,15 +375,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
@VisibleForTesting
void onBackToLauncherAnimationFinished() {
- if (mBackNavigationInfo != null) {
- IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
- if (mTriggerBack) {
+ final boolean triggerBack = mTriggerBack;
+ IOnBackInvokedCallback callback = mBackNavigationInfo != null
+ ? mBackNavigationInfo.getOnBackInvokedCallback() : null;
+ // Make sure the notification sequence should be controller > client.
+ finishAnimation();
+ if (callback != null) {
+ if (triggerBack) {
dispatchOnBackInvoked(callback);
} else {
dispatchOnBackCancelled(callback);
}
}
- finishAnimation();
}
/**
@@ -261,34 +398,45 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
if (mTransitionInProgress) {
return;
}
- if (keyAction == MotionEvent.ACTION_MOVE) {
+
+ mTouchTracker.update(touchX, touchY, swipeEdge);
+ if (keyAction == MotionEvent.ACTION_DOWN) {
if (!mBackGestureStarted) {
+ mShouldStartOnNextMoveEvent = true;
+ }
+ } else if (keyAction == MotionEvent.ACTION_MOVE) {
+ if (!mBackGestureStarted && mShouldStartOnNextMoveEvent) {
// Let the animation initialized here to make sure the onPointerDownOutsideFocus
// could be happened when ACTION_DOWN, it may change the current focus that we
// would access it when startBackNavigation.
- initAnimation(touchX, touchY);
+ onGestureStarted(touchX, touchY);
+ mShouldStartOnNextMoveEvent = false;
}
onMove(touchX, touchY, swipeEdge);
} else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"Finishing gesture with event action: %d", keyAction);
- onGestureFinished();
+ if (keyAction == MotionEvent.ACTION_CANCEL) {
+ mTriggerBack = false;
+ }
+ onGestureFinished(true);
}
}
- private void initAnimation(float touchX, float touchY) {
+ private void onGestureStarted(float touchX, float touchY) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted);
if (mBackGestureStarted || mBackNavigationInfo != null) {
Log.e(TAG, "Animation is being initialized but is already started.");
finishAnimation();
}
- mInitTouchLocation.set(touchX, touchY);
+ mTouchTracker.setGestureStartLocation(touchX, touchY);
mBackGestureStarted = true;
try {
boolean requestAnimation = mEnableAnimations.get();
- mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation);
+ mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation,
+ mFocusObserver, mBackAnimationAdaptor);
onBackNavigationInfoReceived(mBackNavigationInfo);
} catch (RemoteException remoteException) {
Log.e(TAG, "Failed to initAnimation", remoteException);
@@ -300,11 +448,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo);
if (backNavigationInfo == null) {
Log.e(TAG, "Received BackNavigationInfo is null.");
- finishAnimation();
return;
}
int backType = backNavigationInfo.getType();
IOnBackInvokedCallback targetCallback = null;
+ final boolean dispatchToLauncher = shouldDispatchToLauncher(backType);
if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) {
HardwareBuffer hardwareBuffer = backNavigationInfo.getScreenshotHardwareBuffer();
if (hardwareBuffer != null) {
@@ -312,12 +460,17 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
backNavigationInfo.getTaskWindowConfiguration());
}
mTransaction.apply();
- } else if (shouldDispatchToLauncher(backType)) {
+ } else if (dispatchToLauncher) {
targetCallback = mBackToLauncherCallback;
+ if (USE_TRANSITION) {
+ mCachingBackDispatcher.startWaitingAnimation();
+ }
} else if (backType == BackNavigationInfo.TYPE_CALLBACK) {
targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
}
- dispatchOnBackStarted(targetCallback);
+ if (!USE_TRANSITION || !dispatchToLauncher) {
+ dispatchOnBackStarted(targetCallback);
+ }
}
/**
@@ -356,36 +509,89 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
if (!mBackGestureStarted || mBackNavigationInfo == null) {
return;
}
- int deltaX = Math.round(touchX - mInitTouchLocation.x);
+ int deltaX = mTouchTracker.getDeltaFromGestureStart(touchX);
float progressThreshold = PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold;
float progress = Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1);
- int backType = mBackNavigationInfo.getType();
- RemoteAnimationTarget animationTarget = mBackNavigationInfo.getDepartingAnimationTarget();
+ if (USE_TRANSITION) {
+ if (mBackAnimationController != null && mAnimationTarget != null) {
+ final BackEvent backEvent = new BackEvent(
+ touchX, touchY, progress, swipeEdge, mAnimationTarget);
+ dispatchOnBackProgressed(mBackToLauncherCallback, backEvent);
+ }
+ } else {
+ int backType = mBackNavigationInfo.getType();
+ RemoteAnimationTarget animationTarget =
+ mBackNavigationInfo.getDepartingAnimationTarget();
+
+ BackEvent backEvent = new BackEvent(
+ touchX, touchY, progress, swipeEdge, animationTarget);
+ IOnBackInvokedCallback targetCallback = null;
+ if (shouldDispatchToLauncher(backType)) {
+ targetCallback = mBackToLauncherCallback;
+ } else if (backType == BackNavigationInfo.TYPE_CROSS_TASK
+ || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) {
+ // TODO(208427216) Run the actual animation
+ } else if (backType == BackNavigationInfo.TYPE_CALLBACK) {
+ targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
+ }
+ dispatchOnBackProgressed(targetCallback, backEvent);
+ }
+ }
- BackEvent backEvent = new BackEvent(
- touchX, touchY, progress, swipeEdge, animationTarget);
- IOnBackInvokedCallback targetCallback = null;
- if (shouldDispatchToLauncher(backType)) {
- targetCallback = mBackToLauncherCallback;
- } else if (backType == BackNavigationInfo.TYPE_CROSS_TASK
- || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) {
- // TODO(208427216) Run the actual animation
- } else if (backType == BackNavigationInfo.TYPE_CALLBACK) {
- targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
+ private void injectBackKey() {
+ sendBackEvent(KeyEvent.ACTION_DOWN);
+ sendBackEvent(KeyEvent.ACTION_UP);
+ }
+
+ private void sendBackEvent(int action) {
+ final long when = SystemClock.uptimeMillis();
+ final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, 0 /* repeat */,
+ 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
+ KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
+ InputDevice.SOURCE_KEYBOARD);
+
+ ev.setDisplayId(mContext.getDisplay().getDisplayId());
+ if (!InputManager.getInstance()
+ .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
+ Log.e(TAG, "Inject input event fail");
}
- dispatchOnBackProgressed(targetCallback, backEvent);
}
- private void onGestureFinished() {
+ private void onGestureFinished(boolean fromTouch) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
- if (!mBackGestureStarted || mBackNavigationInfo == null) {
+ if (!mBackGestureStarted) {
+ finishAnimation();
return;
}
+
+ if (fromTouch) {
+ // Let touch reset the flag otherwise it will start a new back navigation and refresh
+ // the info when received a new move event.
+ mBackGestureStarted = false;
+ }
+
+ if (mTransitionInProgress) {
+ return;
+ }
+
+ if (mBackNavigationInfo == null) {
+ // No focus window found or core are running recents animation, inject back key as
+ // legacy behavior.
+ if (mTriggerBack) {
+ injectBackKey();
+ }
+ finishAnimation();
+ return;
+ }
+
int backType = mBackNavigationInfo.getType();
boolean shouldDispatchToLauncher = shouldDispatchToLauncher(backType);
IOnBackInvokedCallback targetCallback = shouldDispatchToLauncher
? mBackToLauncherCallback
: mBackNavigationInfo.getOnBackInvokedCallback();
+ if (mCachingBackDispatcher.set(targetCallback, mTriggerBack)) {
+ return;
+ }
if (shouldDispatchToLauncher) {
startTransition();
}
@@ -403,7 +609,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private boolean shouldDispatchToLauncher(int backType) {
return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
&& mBackToLauncherCallback != null
- && mEnableAnimations.get();
+ && mEnableAnimations.get()
+ && mBackNavigationInfo != null
+ && ((USE_TRANSITION && mBackNavigationInfo.isPrepareRemoteAnimation())
+ || mBackNavigationInfo.getDepartingAnimationTarget() != null);
}
private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) {
@@ -468,29 +677,43 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private void finishAnimation() {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishAnimation()");
- mBackGestureStarted = false;
- mTouchEventDelta.set(0, 0);
- mInitTouchLocation.set(0, 0);
+ mTouchTracker.reset();
BackNavigationInfo backNavigationInfo = mBackNavigationInfo;
boolean triggerBack = mTriggerBack;
mBackNavigationInfo = null;
mTriggerBack = false;
+ mShouldStartOnNextMoveEvent = false;
if (backNavigationInfo == null) {
return;
}
- RemoteAnimationTarget animationTarget = backNavigationInfo.getDepartingAnimationTarget();
- if (animationTarget != null) {
- if (animationTarget.leash != null && animationTarget.leash.isValid()) {
- mTransaction.remove(animationTarget.leash);
+
+ if (!USE_TRANSITION) {
+ RemoteAnimationTarget animationTarget = backNavigationInfo
+ .getDepartingAnimationTarget();
+ if (animationTarget != null) {
+ if (animationTarget.leash != null && animationTarget.leash.isValid()) {
+ mTransaction.remove(animationTarget.leash);
+ }
}
+ SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface();
+ if (screenshotSurface != null && screenshotSurface.isValid()) {
+ mTransaction.remove(screenshotSurface);
+ }
+ mTransaction.apply();
}
- SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface();
- if (screenshotSurface != null && screenshotSurface.isValid()) {
- mTransaction.remove(screenshotSurface);
- }
- mTransaction.apply();
stopTransition();
backNavigationInfo.onBackNavigationFinished(triggerBack);
+ if (USE_TRANSITION) {
+ final IBackNaviAnimationController controller = mBackAnimationController;
+ if (controller != null) {
+ try {
+ controller.finish(triggerBack);
+ } catch (RemoteException r) {
+ // Oh no!
+ }
+ }
+ mBackAnimationController = null;
+ }
}
private void startTransition() {
@@ -508,4 +731,50 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mShellExecutor.removeCallbacks(mResetTransitionRunnable);
mTransitionInProgress = false;
}
+
+ private void createAdaptor() {
+ mIBackAnimationRunner = new IBackAnimationRunner.Stub() {
+ @Override
+ public void onAnimationCancelled() {
+ // no op for now
+ }
+ @Override // Binder interface
+ public void onAnimationStart(IBackNaviAnimationController controller, int type,
+ RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps) {
+ mShellExecutor.execute(() -> {
+ mBackAnimationController = controller;
+ for (int i = 0; i < apps.length; i++) {
+ final RemoteAnimationTarget target = apps[i];
+ if (MODE_CLOSING == target.mode) {
+ mAnimationTarget = target;
+ } else if (MODE_OPENING == target.mode) {
+ // TODO Home activity should handle the visibility for itself
+ // once it finish relayout for orientation change
+ SurfaceControl.Transaction tx =
+ new SurfaceControl.Transaction();
+ tx.setAlpha(target.leash, 1);
+ tx.apply();
+ }
+ }
+ // TODO animation target should be passed at onBackStarted
+ dispatchOnBackStarted(mBackToLauncherCallback);
+ // TODO This is Workaround for LauncherBackAnimationController, there will need
+ // to dispatch onBackProgressed twice(startBack & updateBackProgress) to
+ // initialize the animation data, for now that would happen when onMove
+ // called, but there will no expected animation if the down -> up gesture
+ // happen very fast which ACTION_MOVE only happen once.
+ final BackEvent backInit = new BackEvent(
+ mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 0,
+ mTouchTracker.mSwipeEdge, mAnimationTarget);
+ dispatchOnBackProgressed(mBackToLauncherCallback, backInit);
+ if (!mCachingBackDispatcher.consume()) {
+ dispatchOnBackProgressed(mBackToLauncherCallback, backInit);
+ }
+ });
+ }
+ };
+ mBackAnimationAdaptor = new BackAnimationAdaptor(mIBackAnimationRunner,
+ BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 31fc6a5be589..922472a26113 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -59,6 +59,8 @@ import java.util.concurrent.Executor;
public class Bubble implements BubbleViewProvider {
private static final String TAG = "Bubble";
+ public static final String KEY_APP_BUBBLE = "key_app_bubble";
+
private final String mKey;
@Nullable
private final String mGroupKey;
@@ -164,13 +166,22 @@ public class Bubble implements BubbleViewProvider {
private PendingIntent mDeleteIntent;
/**
+ * Used only for a special bubble in the stack that has the key {@link #KEY_APP_BUBBLE}.
+ * There can only be one of these bubbles in the stack and this intent will be populated for
+ * that bubble.
+ */
+ @Nullable
+ private Intent mAppIntent;
+
+ /**
* Create a bubble with limited information based on given {@link ShortcutInfo}.
* Note: Currently this is only being used when the bubble is persisted to disk.
*/
@VisibleForTesting(visibility = PRIVATE)
public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo,
final int desiredHeight, final int desiredHeightResId, @Nullable final String title,
- int taskId, @Nullable final String locus, Executor mainExecutor) {
+ int taskId, @Nullable final String locus, Executor mainExecutor,
+ final Bubbles.BubbleMetadataFlagListener listener) {
Objects.requireNonNull(key);
Objects.requireNonNull(shortcutInfo);
mMetadataShortcutId = shortcutInfo.getId();
@@ -188,11 +199,28 @@ public class Bubble implements BubbleViewProvider {
mShowBubbleUpdateDot = false;
mMainExecutor = mainExecutor;
mTaskId = taskId;
+ mBubbleMetadataFlagListener = listener;
+ }
+
+ public Bubble(Intent intent,
+ UserHandle user,
+ Executor mainExecutor) {
+ mKey = KEY_APP_BUBBLE;
+ mGroupKey = null;
+ mLocusId = null;
+ mFlags = 0;
+ mUser = user;
+ mShowBubbleUpdateDot = false;
+ mMainExecutor = mainExecutor;
+ mTaskId = INVALID_TASK_ID;
+ mAppIntent = intent;
+ mDesiredHeight = Integer.MAX_VALUE;
+ mPackageName = intent.getPackage();
}
@VisibleForTesting(visibility = PRIVATE)
public Bubble(@NonNull final BubbleEntry entry,
- @Nullable final Bubbles.BubbleMetadataFlagListener listener,
+ final Bubbles.BubbleMetadataFlagListener listener,
final Bubbles.PendingIntentCanceledListener intentCancelListener,
Executor mainExecutor) {
mKey = entry.getKey();
@@ -415,6 +443,9 @@ public class Bubble implements BubbleViewProvider {
mShortcutInfo = info.shortcutInfo;
mAppName = info.appName;
+ if (mTitle == null) {
+ mTitle = mAppName;
+ }
mFlyoutMessage = info.flyoutMessage;
mBadgeBitmap = info.badgeBitmap;
@@ -452,6 +483,7 @@ public class Bubble implements BubbleViewProvider {
*/
void setEntry(@NonNull final BubbleEntry entry) {
Objects.requireNonNull(entry);
+ boolean showingDotPreviously = showDot();
mLastUpdated = entry.getStatusBarNotification().getPostTime();
mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification();
mPackageName = entry.getStatusBarNotification().getPackageName();
@@ -498,6 +530,10 @@ public class Bubble implements BubbleViewProvider {
mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot();
mShouldSuppressNotificationList = entry.shouldSuppressNotificationList();
mShouldSuppressPeek = entry.shouldSuppressPeek();
+ if (showingDotPreviously != showDot()) {
+ // This will update the UI if needed
+ setShowDot(showDot());
+ }
}
@Nullable
@@ -513,7 +549,7 @@ public class Bubble implements BubbleViewProvider {
* @return the last time this bubble was updated or accessed, whichever is most recent.
*/
long getLastActivity() {
- return Math.max(mLastUpdated, mLastAccessed);
+ return isAppBubble() ? Long.MAX_VALUE : Math.max(mLastUpdated, mLastAccessed);
}
/**
@@ -712,6 +748,15 @@ public class Bubble implements BubbleViewProvider {
return mDeleteIntent;
}
+ @Nullable
+ Intent getAppBubbleIntent() {
+ return mAppIntent;
+ }
+
+ boolean isAppBubble() {
+ return KEY_APP_BUBBLE.equals(mKey);
+ }
+
Intent getSettingsIntent(final Context context) {
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
@@ -816,7 +861,7 @@ public class Bubble implements BubbleViewProvider {
/**
* Description of current bubble state.
*/
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ public void dump(@NonNull PrintWriter pw) {
pw.print("key: "); pw.println(mKey);
pw.print(" showInShade: "); pw.println(showInShade());
pw.print(" showDot: "); pw.println(showDot());
@@ -825,8 +870,9 @@ public class Bubble implements BubbleViewProvider {
pw.print(" desiredHeight: "); pw.println(getDesiredHeightString());
pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification());
pw.print(" autoExpand: "); pw.println(shouldAutoExpand());
+ pw.print(" bubbleMetadataFlagListener null: " + (mBubbleMetadataFlagListener == null));
if (mExpandedView != null) {
- mExpandedView.dump(pw, args);
+ mExpandedView.dump(pw);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
index 4eeb20769e09..d6803e8052c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
@@ -19,14 +19,14 @@ package com.android.wm.shell.bubbles;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
-import android.graphics.Paint;
+import android.graphics.Color;
import android.graphics.Path;
+import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.ShadowGenerator;
import com.android.wm.shell.R;
/**
@@ -44,78 +44,77 @@ public class BubbleBadgeIconFactory extends BaseIconFactory {
* will include the workprofile indicator on the badge if appropriate.
*/
BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) {
- ShadowGenerator shadowGenerator = new ShadowGenerator(mIconBitmapSize);
- Bitmap userBadgedBitmap = createIconBitmap(userBadgedAppIcon, 1f, mIconBitmapSize);
-
if (userBadgedAppIcon instanceof AdaptiveIconDrawable) {
- userBadgedBitmap = Bitmap.createScaledBitmap(
- getCircleBitmap((AdaptiveIconDrawable) userBadgedAppIcon, /* size */
- userBadgedAppIcon.getIntrinsicWidth()),
- mIconBitmapSize, mIconBitmapSize, /* filter */ true);
+ AdaptiveIconDrawable ad = (AdaptiveIconDrawable) userBadgedAppIcon;
+ userBadgedAppIcon = new CircularAdaptiveIcon(ad.getBackground(), ad.getForeground());
}
-
if (isImportantConversation) {
- final float ringStrokeWidth = mContext.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.importance_ring_stroke_width);
- final int importantConversationColor = mContext.getResources().getColor(
+ userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon);
+ }
+ Bitmap userBadgedBitmap = createIconBitmap(
+ userBadgedAppIcon, 1, BITMAP_GENERATION_MODE_WITH_SHADOW);
+ return createIconBitmap(userBadgedBitmap);
+ }
+
+ private class CircularRingDrawable extends CircularAdaptiveIcon {
+
+ final int mImportantConversationColor;
+ final Rect mTempBounds = new Rect();
+
+ final Drawable mDr;
+
+ CircularRingDrawable(Drawable dr) {
+ super(null, null);
+ mDr = dr;
+ mImportantConversationColor = mContext.getResources().getColor(
R.color.important_conversation, null);
- Bitmap badgeAndRing = Bitmap.createBitmap(userBadgedBitmap.getWidth(),
- userBadgedBitmap.getHeight(), userBadgedBitmap.getConfig());
- Canvas c = new Canvas(badgeAndRing);
-
- Paint ringPaint = new Paint();
- ringPaint.setStyle(Paint.Style.FILL);
- ringPaint.setColor(importantConversationColor);
- ringPaint.setAntiAlias(true);
- c.drawCircle(c.getWidth() / 2, c.getHeight() / 2, c.getWidth() / 2, ringPaint);
-
- final int bitmapTop = (int) ringStrokeWidth;
- final int bitmapLeft = (int) ringStrokeWidth;
- final int bitmapWidth = c.getWidth() - 2 * (int) ringStrokeWidth;
- final int bitmapHeight = c.getHeight() - 2 * (int) ringStrokeWidth;
-
- Bitmap scaledBitmap = Bitmap.createScaledBitmap(userBadgedBitmap, bitmapWidth,
- bitmapHeight, /* filter */ true);
- c.drawBitmap(scaledBitmap, bitmapTop, bitmapLeft, /* paint */null);
-
- shadowGenerator.recreateIcon(Bitmap.createBitmap(badgeAndRing), c);
- return createIconBitmap(badgeAndRing);
- } else {
- Canvas c = new Canvas();
- c.setBitmap(userBadgedBitmap);
- shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c);
- return createIconBitmap(userBadgedBitmap);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ int save = canvas.save();
+ canvas.clipPath(getIconMask());
+ canvas.drawColor(mImportantConversationColor);
+ int ringStrokeWidth = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.importance_ring_stroke_width);
+ mTempBounds.set(getBounds());
+ mTempBounds.inset(ringStrokeWidth, ringStrokeWidth);
+ mDr.setBounds(mTempBounds);
+ mDr.draw(canvas);
+ canvas.restoreToCount(save);
}
}
- private Bitmap getCircleBitmap(AdaptiveIconDrawable icon, int size) {
- Drawable foreground = icon.getForeground();
- Drawable background = icon.getBackground();
- Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas();
- canvas.setBitmap(bitmap);
-
- // Clip canvas to circle.
- Path circlePath = new Path();
- circlePath.addCircle(/* x */ size / 2f,
- /* y */ size / 2f,
- /* radius */ size / 2f,
- Path.Direction.CW);
- canvas.clipPath(circlePath);
-
- // Draw background.
- background.setBounds(0, 0, size, size);
- background.draw(canvas);
-
- // Draw foreground. The foreground and background drawables are derived from adaptive icons
- // Some icon shapes fill more space than others, so adaptive icons are normalized to about
- // the same size. This size is smaller than the original bounds, so we estimate
- // the difference in this offset.
- int offset = size / 5;
- foreground.setBounds(-offset, -offset, size + offset, size + offset);
- foreground.draw(canvas);
-
- canvas.setBitmap(null);
- return bitmap;
+ private static class CircularAdaptiveIcon extends AdaptiveIconDrawable {
+
+ final Path mPath = new Path();
+
+ CircularAdaptiveIcon(Drawable bg, Drawable fg) {
+ super(bg, fg);
+ }
+
+ @Override
+ public Path getIconMask() {
+ mPath.reset();
+ Rect bounds = getBounds();
+ mPath.addOval(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW);
+ return mPath;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ int save = canvas.save();
+ canvas.clipPath(getIconMask());
+
+ canvas.drawColor(Color.BLACK);
+ Drawable d;
+ if ((d = getBackground()) != null) {
+ d.draw(canvas);
+ }
+ if ((d = getForeground()) != null) {
+ d.draw(canvas);
+ }
+ canvas.restoreToCount(save);
+ }
}
}
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 f427a2c4bc95..93413dbe7e5f 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
@@ -25,6 +25,7 @@ import static android.view.View.VISIBLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_BOTTOM;
@@ -69,28 +70,22 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
-import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
-import android.util.Slog;
import android.util.SparseArray;
-import android.util.SparseSetArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
-import android.window.WindowContainerTransaction;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.logging.UiEventLogger;
import com.android.internal.statusbar.IStatusBarService;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.TaskViewTransitions;
import com.android.wm.shell.WindowManagerShellWrapper;
-import com.android.wm.shell.common.DisplayChangeController;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
@@ -103,14 +98,20 @@ 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.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
@@ -121,7 +122,7 @@ import java.util.function.IntConsumer;
*
* The controller manages addition, removal, and visible state of bubbles on screen.
*/
-public class BubbleController {
+public class BubbleController implements ConfigurationChangeListener {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
@@ -157,6 +158,8 @@ public class BubbleController {
private final DisplayController mDisplayController;
private final TaskViewTransitions mTaskViewTransitions;
private final SyncTransactionQueue mSyncQueue;
+ private final ShellController mShellController;
+ private final ShellCommandHandler mShellCommandHandler;
// Used to post to main UI thread
private final ShellExecutor mMainExecutor;
@@ -176,8 +179,8 @@ public class BubbleController {
private int mCurrentUserId;
// Current profiles of the user (e.g. user with a workprofile)
private SparseArray<UserInfo> mCurrentProfiles;
- // Saves notification keys of active bubbles when users are switched.
- private final SparseSetArray<String> mSavedBubbleKeysPerUser;
+ // Saves data about active bubbles when users are switched.
+ private final SparseArray<UserBubbleData> mSavedUserBubbleData;
// Used when ranking updates occur and we check if things should bubble / unbubble
private NotificationListenerService.Ranking mTmpRanking;
@@ -224,44 +227,11 @@ public class BubbleController {
/** Drag and drop controller to register listener for onDragStarted. */
private DragAndDropController mDragAndDropController;
- /**
- * Creates an instance of the BubbleController.
- */
- public static BubbleController create(Context context,
- @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
- FloatingContentCoordinator floatingContentCoordinator,
- @Nullable IStatusBarService statusBarService,
- WindowManager windowManager,
- WindowManagerShellWrapper windowManagerShellWrapper,
- UserManager userManager,
- LauncherApps launcherApps,
- TaskStackListenerImpl taskStackListener,
- UiEventLogger uiEventLogger,
- ShellTaskOrganizer organizer,
- DisplayController displayController,
- Optional<OneHandedController> oneHandedOptional,
- DragAndDropController dragAndDropController,
- @ShellMainThread ShellExecutor mainExecutor,
- @ShellMainThread Handler mainHandler,
- @ShellBackgroundThread ShellExecutor bgExecutor,
- TaskViewTransitions taskViewTransitions,
- SyncTransactionQueue syncQueue) {
- BubbleLogger logger = new BubbleLogger(uiEventLogger);
- BubblePositioner positioner = new BubblePositioner(context, windowManager);
- BubbleData data = new BubbleData(context, logger, positioner, mainExecutor);
- return new BubbleController(context, data, synchronizer, floatingContentCoordinator,
- new BubbleDataRepository(context, launcherApps, mainExecutor),
- statusBarService, windowManager, windowManagerShellWrapper, userManager,
- launcherApps, logger, taskStackListener, organizer, positioner, displayController,
- oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
- taskViewTransitions, syncQueue);
- }
-
- /**
- * Testing constructor.
- */
- @VisibleForTesting
- protected BubbleController(Context context,
+
+ public BubbleController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
BubbleData data,
@Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
FloatingContentCoordinator floatingContentCoordinator,
@@ -284,6 +254,8 @@ public class BubbleController {
TaskViewTransitions taskViewTransitions,
SyncTransactionQueue syncQueue) {
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
+ mShellController = shellController;
mLauncherApps = launcherApps;
mBarService = statusBarService == null
? IStatusBarService.Stub.asInterface(
@@ -304,7 +276,7 @@ public class BubbleController {
mCurrentUserId = ActivityManager.getCurrentUser();
mBubblePositioner = positioner;
mBubbleData = data;
- mSavedBubbleKeysPerUser = new SparseSetArray<>();
+ mSavedUserBubbleData = new SparseArray<>();
mBubbleIconFactory = new BubbleIconFactory(context);
mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(context);
mDisplayController = displayController;
@@ -312,6 +284,7 @@ public class BubbleController {
mOneHandedOptional = oneHandedOptional;
mDragAndDropController = dragAndDropController;
mSyncQueue = syncQueue;
+ shellInit.addInitCallback(this::onInit, this);
}
private void registerOneHandedState(OneHandedController oneHanded) {
@@ -333,9 +306,10 @@ public class BubbleController {
});
}
- public void initialize() {
+ protected void onInit() {
mBubbleData.setListener(mBubbleDataListener);
mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
+ mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
mBubbleData.setPendingIntentCancelledListener(bubble -> {
if (bubble.getBubbleIntent() == null) {
@@ -435,17 +409,13 @@ public class BubbleController {
});
mDisplayController.addDisplayChangingController(
- new DisplayChangeController.OnDisplayChangingListener() {
- @Override
- public void onRotateDisplay(int displayId, int fromRotation, int toRotation,
- WindowContainerTransaction t) {
- // This is triggered right before the rotation is applied
- if (fromRotation != toRotation) {
- if (mStackView != null) {
- // Layout listener set on stackView will update the positioner
- // once the rotation is applied
- mStackView.onOrientationChanged();
- }
+ (displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> {
+ // This is triggered right before the rotation is applied
+ if (fromRotation != toRotation) {
+ if (mStackView != null) {
+ // Layout listener set on stackView will update the positioner
+ // once the rotation is applied
+ mStackView.onOrientationChanged();
}
}
});
@@ -456,6 +426,16 @@ public class BubbleController {
// Clear out any persisted bubbles on disk that no longer have a valid user.
List<UserInfo> users = mUserManager.getAliveUsers();
mDataRepository.sanitizeBubbles(users);
+
+ // Init profiles
+ SparseArray<UserInfo> userProfiles = new SparseArray<>();
+ for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) {
+ userProfiles.put(user.id, user);
+ }
+ mCurrentProfiles = userProfiles;
+
+ mShellController.addConfigurationChangeListener(this);
+ mShellCommandHandler.addDumpCallback(this::dump, this);
}
@VisibleForTesting
@@ -563,7 +543,6 @@ public class BubbleController {
if (mNotifEntryToExpandOnShadeUnlock != null) {
expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
- mNotifEntryToExpandOnShadeUnlock = null;
}
updateStack();
@@ -809,11 +788,13 @@ public class BubbleController {
*/
private void saveBubbles(@UserIdInt int userId) {
// First clear any existing keys that might be stored.
- mSavedBubbleKeysPerUser.remove(userId);
+ mSavedUserBubbleData.remove(userId);
+ UserBubbleData userBubbleData = new UserBubbleData();
// Add in all active bubbles for the current user.
for (Bubble bubble : mBubbleData.getBubbles()) {
- mSavedBubbleKeysPerUser.add(userId, bubble.getKey());
+ userBubbleData.add(bubble.getKey(), bubble.showInShade());
}
+ mSavedUserBubbleData.put(userId, userBubbleData);
}
/**
@@ -822,25 +803,27 @@ public class BubbleController {
* @param userId the id of the user
*/
private void restoreBubbles(@UserIdInt int userId) {
- ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId);
- if (savedBubbleKeys == null) {
+ UserBubbleData savedBubbleData = mSavedUserBubbleData.get(userId);
+ if (savedBubbleData == null) {
// There were no bubbles saved for this used.
return;
}
- mSysuiProxy.getShouldRestoredEntries(savedBubbleKeys, (entries) -> {
+ mSysuiProxy.getShouldRestoredEntries(savedBubbleData.getKeys(), (entries) -> {
mMainExecutor.execute(() -> {
for (BubbleEntry e : entries) {
if (canLaunchInTaskView(mContext, e)) {
- updateBubble(e, true /* suppressFlyout */, false /* showInShade */);
+ boolean showInShade = savedBubbleData.isShownInShade(e.getKey());
+ updateBubble(e, true /* suppressFlyout */, showInShade);
}
}
});
});
// Finally, remove the entries for this user now that bubbles are restored.
- mSavedBubbleKeysPerUser.remove(userId);
+ mSavedUserBubbleData.remove(userId);
}
- private void updateForThemeChanges() {
+ @Override
+ public void onThemeChanged() {
if (mStackView != null) {
mStackView.onThemeChanged();
}
@@ -860,7 +843,8 @@ public class BubbleController {
}
}
- private void onConfigChanged(Configuration newConfig) {
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
if (mBubblePositioner != null) {
mBubblePositioner.update();
}
@@ -885,6 +869,19 @@ public class BubbleController {
}
}
+ private void onNotificationPanelExpandedChanged(boolean expanded) {
+ if (DEBUG_BUBBLE_GESTURE) {
+ Log.d(TAG, "onNotificationPanelExpandedChanged: expanded=" + expanded);
+ }
+ if (mStackView != null && mStackView.isExpanded()) {
+ if (expanded) {
+ mStackView.stopMonitoringSwipeUpGesture();
+ } else {
+ mStackView.startMonitoringSwipeUpGesture();
+ }
+ }
+ }
+
private void setSysuiProxy(Bubbles.SysuiProxy proxy) {
mSysuiProxy = proxy;
}
@@ -932,15 +929,6 @@ public class BubbleController {
return (isSummary && isSuppressedSummary) || isSuppressedBubble;
}
- private void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback) {
- if (mBubbleData.isSummarySuppressed(groupKey)) {
- mBubbleData.removeSuppressedSummary(groupKey);
- if (callback != null) {
- callback.accept(mBubbleData.getSummaryKey(groupKey));
- }
- }
- }
-
/** Promote the provided bubble from the overflow view. */
public void promoteBubbleFromOverflow(Bubble bubble) {
mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
@@ -1013,7 +1001,33 @@ public class BubbleController {
*/
@VisibleForTesting
public void updateBubble(BubbleEntry notif) {
- updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
+ int bubbleUserId = notif.getStatusBarNotification().getUserId();
+ if (isCurrentProfile(bubbleUserId)) {
+ updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
+ } else {
+ // Skip update, but store it in user bubbles so it gets restored after user switch
+ mSavedUserBubbleData.get(bubbleUserId, new UserBubbleData()).add(notif.getKey(),
+ true /* shownInShade */);
+ if (DEBUG_BUBBLE_CONTROLLER) {
+ Log.d(TAG,
+ "Ignore update to bubble for not active user. Bubble userId=" + bubbleUserId
+ + " current userId=" + mCurrentUserId);
+ }
+ }
+ }
+
+ /**
+ * Adds a bubble for a specific intent. These bubbles are <b>not</b> backed by a notification
+ * and remain until the user dismisses the bubble or bubble stack. Only one intent bubble
+ * is supported at a time.
+ *
+ * @param intent the intent to display in the bubble expanded view.
+ */
+ public void addAppBubble(Intent intent) {
+ if (intent == null || intent.getPackage() == null) return;
+ Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
+ b.setShouldAutoExpand(true);
+ inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
}
/**
@@ -1050,18 +1064,28 @@ public class BubbleController {
public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) {
// If this is an interruptive notif, mark that it's interrupted
mSysuiProxy.setNotificationInterruption(notif.getKey());
- if (!notif.getRanking().isTextChanged()
+ boolean isNonInterruptiveNotExpanding = !notif.getRanking().isTextChanged()
&& (notif.getBubbleMetadata() != null
- && !notif.getBubbleMetadata().getAutoExpandBubble())
+ && !notif.getBubbleMetadata().getAutoExpandBubble());
+ if (isNonInterruptiveNotExpanding
&& mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) {
// Update the bubble but don't promote it out of overflow
Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey());
- b.setEntry(notif);
+ if (notif.isBubble()) {
+ notif.setFlagBubble(false);
+ }
+ updateNotNotifyingEntry(b, notif, showInShade);
+ } else if (mBubbleData.hasAnyBubbleWithKey(notif.getKey())
+ && isNonInterruptiveNotExpanding) {
+ Bubble b = mBubbleData.getAnyBubbleWithkey(notif.getKey());
+ if (b != null) {
+ updateNotNotifyingEntry(b, notif, showInShade);
+ }
} else if (mBubbleData.isSuppressedWithLocusId(notif.getLocusId())) {
// Update the bubble but don't promote it out of overflow
Bubble b = mBubbleData.getSuppressedBubbleWithKey(notif.getKey());
if (b != null) {
- b.setEntry(notif);
+ updateNotNotifyingEntry(b, notif, showInShade);
}
} else {
Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
@@ -1071,13 +1095,28 @@ public class BubbleController {
if (bubble.shouldAutoExpand()) {
bubble.setShouldAutoExpand(false);
}
+ mImpl.mCachedState.updateBubbleSuppressedState(bubble);
} else {
inflateAndAdd(bubble, suppressFlyout, showInShade);
}
}
}
- void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
+ void updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade) {
+ boolean showInShadeBefore = b.showInShade();
+ boolean isBubbleSelected = Objects.equals(b, mBubbleData.getSelectedBubble());
+ boolean isBubbleExpandedAndSelected = isStackExpanded() && isBubbleSelected;
+ b.setEntry(entry);
+ boolean suppress = isBubbleExpandedAndSelected || !showInShade || !b.showInShade();
+ b.setSuppressNotification(suppress);
+ b.setShowDot(!isBubbleExpandedAndSelected);
+ if (showInShadeBefore != b.showInShade()) {
+ mImpl.mCachedState.updateBubbleSuppressedState(b);
+ }
+ }
+
+ @VisibleForTesting
+ public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
// Lazy init stack view when a bubble is created
ensureStackViewCreated();
bubble.setInflateSynchronously(mInflateSynchronously);
@@ -1106,7 +1145,10 @@ public class BubbleController {
}
@VisibleForTesting
- public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
+ public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) {
+ if (!fromSystem) {
+ return;
+ }
// shouldBubbleUp checks canBubble & for bubble metadata
boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry);
if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
@@ -1167,9 +1209,9 @@ public class BubbleController {
// notification, so that the bubble will be re-created if shouldBubbleUp returns
// true.
mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP);
- } else if (entry != null && mTmpRanking.isBubble() && !isActive) {
+ } else if (entry != null && mTmpRanking.isBubble() && !isActiveOrInOverflow) {
entry.setFlagBubble(true);
- onEntryUpdated(entry, shouldBubbleUp);
+ onEntryUpdated(entry, shouldBubbleUp, /* fromSystem= */ true);
}
}
}
@@ -1309,19 +1351,7 @@ public class BubbleController {
}
mSysuiProxy.updateNotificationBubbleButton(bubble.getKey());
}
-
}
- mSysuiProxy.getPendingOrActiveEntry(bubble.getKey(), (entry) -> {
- mMainExecutor.execute(() -> {
- if (entry != null) {
- final String groupKey = entry.getStatusBarNotification().getGroupKey();
- if (getBubblesInGroup(groupKey).isEmpty()) {
- // Time to potentially remove the summary
- mSysuiProxy.notifyMaybeCancelSummary(bubble.getKey());
- }
- }
- });
- });
}
mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository);
@@ -1342,23 +1372,24 @@ public class BubbleController {
mStackView.setBubbleSuppressed(update.unsuppressedBubble, false);
}
+ boolean collapseStack = update.expandedChanged && !update.expanded;
+
// At this point, the correct bubbles are inflated in the stack.
// Make sure the order in bubble data is reflected in bubble row.
if (update.orderChanged && mStackView != null) {
mDataRepository.addBubbles(mCurrentUserId, update.bubbles);
- mStackView.updateBubbleOrder(update.bubbles);
+ // if the stack is going to be collapsed, do not update pointer position
+ // after reordering
+ mStackView.updateBubbleOrder(update.bubbles, !collapseStack);
}
- if (update.expandedChanged && !update.expanded) {
+ if (collapseStack) {
mStackView.setExpanded(false);
mSysuiProxy.requestNotificationShadeTopUi(false, TAG);
}
if (update.selectionChanged && mStackView != null) {
mStackView.setSelectedBubble(update.selectedBubble);
- if (update.selectedBubble != null) {
- mSysuiProxy.updateNotificationSuppression(update.selectedBubble.getKey());
- }
}
// Expanding? Apply this last.
@@ -1417,7 +1448,6 @@ public class BubbleController {
// in the shade, it is essentially removed.
Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey());
if (bubbleChild != null) {
- mSysuiProxy.removeNotificationEntry(bubbleChild.getKey());
bubbleChild.setSuppressNotification(true);
bubbleChild.setShowDot(false /* show */);
}
@@ -1468,16 +1498,29 @@ public class BubbleController {
}
/**
+ * Check if notification panel is in an expanded state.
+ * Makes a call to System UI process and delivers the result via {@code callback} on the
+ * WM Shell main thread.
+ *
+ * @param callback callback that has the result of notification panel expanded state
+ */
+ public void isNotificationPanelExpanded(Consumer<Boolean> callback) {
+ mSysuiProxy.isNotificationPanelExpand(expanded ->
+ mMainExecutor.execute(() -> callback.accept(expanded)));
+ }
+
+ /**
* Description of current bubble state.
*/
- private void dump(PrintWriter pw, String[] args) {
+ private void dump(PrintWriter pw, String prefix) {
pw.println("BubbleController state:");
- mBubbleData.dump(pw, args);
+ mBubbleData.dump(pw);
pw.println();
if (mStackView != null) {
- mStackView.dump(pw, args);
+ mStackView.dump(pw);
}
pw.println();
+ mImpl.mCachedState.dump(pw);
}
/**
@@ -1546,7 +1589,7 @@ public class BubbleController {
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
mBubblePositioner.setImeVisible(imeVisible, imeHeight);
if (mStackView != null) {
- mStackView.animateForIme(imeVisible);
+ mStackView.setImeVisible(imeVisible);
}
}
}
@@ -1662,28 +1705,12 @@ public class BubbleController {
}
@Override
- public boolean isStackExpanded() {
- return mCachedState.isStackExpanded();
- }
-
- @Override
@Nullable
public Bubble getBubbleWithShortcutId(String shortcutId) {
return mCachedState.getBubbleWithShortcutId(shortcutId);
}
@Override
- public void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
- Executor callbackExecutor) {
- mMainExecutor.execute(() -> {
- Consumer<String> cb = callback != null
- ? (key) -> callbackExecutor.execute(() -> callback.accept(key))
- : null;
- BubbleController.this.removeSuppressedSummaryIfNecessary(groupKey, cb);
- });
- }
-
- @Override
public void collapseStack() {
mMainExecutor.execute(() -> {
BubbleController.this.collapseStack();
@@ -1691,13 +1718,6 @@ public class BubbleController {
}
@Override
- public void updateForThemeChanges() {
- mMainExecutor.execute(() -> {
- BubbleController.this.updateForThemeChanges();
- });
- }
-
- @Override
public void expandStackAndSelectBubble(BubbleEntry entry) {
mMainExecutor.execute(() -> {
BubbleController.this.expandStackAndSelectBubble(entry);
@@ -1719,13 +1739,6 @@ public class BubbleController {
}
@Override
- public void openBubbleOverflow() {
- mMainExecutor.execute(() -> {
- BubbleController.this.openBubbleOverflow();
- });
- }
-
- @Override
public boolean handleDismissalInterception(BubbleEntry entry,
@Nullable List<BubbleEntry> children, IntConsumer removeCallback,
Executor callbackExecutor) {
@@ -1759,9 +1772,9 @@ public class BubbleController {
}
@Override
- public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
+ public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) {
mMainExecutor.execute(() -> {
- BubbleController.this.onEntryUpdated(entry, shouldBubbleUp);
+ BubbleController.this.onEntryUpdated(entry, shouldBubbleUp, fromSystem);
});
}
@@ -1836,22 +1849,38 @@ public class BubbleController {
}
@Override
- public void onConfigChanged(Configuration newConfig) {
- mMainExecutor.execute(() -> {
- BubbleController.this.onConfigChanged(newConfig);
- });
+ public void onNotificationPanelExpandedChanged(boolean expanded) {
+ mMainExecutor.execute(
+ () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded));
}
+ }
- @Override
- public void dump(PrintWriter pw, String[] args) {
- try {
- mMainExecutor.executeBlocking(() -> {
- BubbleController.this.dump(pw, args);
- mCachedState.dump(pw);
- });
- } catch (InterruptedException e) {
- Slog.e(TAG, "Failed to dump BubbleController in 2s");
- }
+ /**
+ * Bubble data that is stored per user.
+ * Used to store and restore active bubbles during user switching.
+ */
+ private static class UserBubbleData {
+ private final Map<String, Boolean> mKeyToShownInShadeMap = new HashMap<>();
+
+ /**
+ * Add bubble key and whether it should be shown in notification shade
+ */
+ void add(String key, boolean shownInShade) {
+ mKeyToShownInShadeMap.put(key, shownInShade);
+ }
+
+ /**
+ * Get all bubble keys stored for this user
+ */
+ Set<String> getKeys() {
+ return mKeyToShownInShadeMap.keySet();
+ }
+
+ /**
+ * Check if this bubble with the given key should be shown in the notification shade
+ */
+ boolean isShownInShade(String key) {
+ return mKeyToShownInShadeMap.get(key);
}
}
}
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 fa86c8436647..af31391fec96 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
@@ -158,7 +158,6 @@ public class BubbleData {
@Nullable
private Listener mListener;
- @Nullable
private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
private Bubbles.PendingIntentCanceledListener mCancelledListener;
@@ -1136,7 +1135,7 @@ public class BubbleData {
/**
* Description of current bubble data state.
*/
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.print("selected: ");
pw.println(mSelectedBubble != null
? mSelectedBubble.getKey()
@@ -1147,13 +1146,13 @@ public class BubbleData {
pw.print("stack bubble count: ");
pw.println(mBubbles.size());
for (Bubble bubble : mBubbles) {
- bubble.dump(pw, args);
+ bubble.dump(pw);
}
pw.print("overflow bubble count: ");
pw.println(mOverflowBubbles.size());
for (Bubble bubble : mOverflowBubbles) {
- bubble.dump(pw, args);
+ bubble.dump(pw);
}
pw.print("summaryKeys: ");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
index 97560f44fb06..3a5961462c87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
@@ -25,6 +25,7 @@ import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LA
import android.content.pm.UserInfo
import android.os.UserHandle
import android.util.Log
+import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener
import com.android.wm.shell.bubbles.storage.BubbleEntity
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.bubbles.storage.BubbleVolatileRepository
@@ -47,6 +48,13 @@ internal class BubbleDataRepository(
private val ioScope = CoroutineScope(Dispatchers.IO)
private var job: Job? = null
+ // For use in Bubble construction.
+ private lateinit var bubbleMetadataFlagListener: BubbleMetadataFlagListener
+
+ fun setSuppressionChangedListener(listener: BubbleMetadataFlagListener) {
+ bubbleMetadataFlagListener = listener
+ }
+
/**
* Adds the bubble in memory, then persists the snapshot after adding the bubble to disk
* asynchronously.
@@ -197,7 +205,8 @@ internal class BubbleDataRepository(
entity.title,
entity.taskId,
entity.locus,
- mainExecutor
+ mainExecutor,
+ bubbleMetadataFlagListener
)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
index dc2ace949f0c..dce6b56261ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
@@ -46,6 +46,9 @@ public class BubbleDebugConfig {
static final boolean DEBUG_OVERFLOW = false;
static final boolean DEBUG_USER_EDUCATION = false;
static final boolean DEBUG_POSITIONER = false;
+ public static final boolean DEBUG_COLLAPSE_ANIMATOR = false;
+ static final boolean DEBUG_BUBBLE_GESTURE = false;
+ public static boolean DEBUG_EXPANDED_VIEW_DRAGGING = false;
private static final boolean FORCE_SHOW_USER_EDUCATION = false;
private static final String FORCE_SHOW_USER_EDUCATION_SETTING =
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 b8bf1a8e497e..5ea370b65407 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
@@ -43,10 +43,13 @@ import android.graphics.CornerPathEffect;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Picture;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.os.RemoteException;
import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.IntProperty;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
@@ -75,6 +78,62 @@ import java.io.PrintWriter;
public class BubbleExpandedView extends LinearLayout {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;
+ /** {@link IntProperty} for updating bottom clip */
+ public static final IntProperty<BubbleExpandedView> BOTTOM_CLIP_PROPERTY =
+ new IntProperty<BubbleExpandedView>("bottomClip") {
+ @Override
+ public void setValue(BubbleExpandedView expandedView, int value) {
+ expandedView.setBottomClip(value);
+ }
+
+ @Override
+ public Integer get(BubbleExpandedView expandedView) {
+ return expandedView.mBottomClip;
+ }
+ };
+
+ /** {@link FloatProperty} for updating taskView or overflow alpha */
+ public static final FloatProperty<BubbleExpandedView> CONTENT_ALPHA =
+ new FloatProperty<BubbleExpandedView>("contentAlpha") {
+ @Override
+ public void setValue(BubbleExpandedView expandedView, float value) {
+ expandedView.setContentAlpha(value);
+ }
+
+ @Override
+ public Float get(BubbleExpandedView expandedView) {
+ return expandedView.getContentAlpha();
+ }
+ };
+
+ /** {@link FloatProperty} for updating background and pointer alpha */
+ public static final FloatProperty<BubbleExpandedView> BACKGROUND_ALPHA =
+ new FloatProperty<BubbleExpandedView>("backgroundAlpha") {
+ @Override
+ public void setValue(BubbleExpandedView expandedView, float value) {
+ expandedView.setBackgroundAlpha(value);
+ }
+
+ @Override
+ public Float get(BubbleExpandedView expandedView) {
+ return expandedView.getAlpha();
+ }
+ };
+
+ /** {@link FloatProperty} for updating manage button alpha */
+ public static final FloatProperty<BubbleExpandedView> MANAGE_BUTTON_ALPHA =
+ new FloatProperty<BubbleExpandedView>("manageButtonAlpha") {
+ @Override
+ public void setValue(BubbleExpandedView expandedView, float value) {
+ expandedView.mManageButton.setAlpha(value);
+ }
+
+ @Override
+ public Float get(BubbleExpandedView expandedView) {
+ return expandedView.mManageButton.getAlpha();
+ }
+ };
+
// The triangle pointing to the expanded view
private View mPointerView;
@Nullable private int[] mExpandedViewContainerLocation;
@@ -90,7 +149,7 @@ public class BubbleExpandedView extends LinearLayout {
/**
* Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
- * {@link #mIsAlphaAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
+ * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
* value until the animation ends.
*/
private boolean mIsContentVisible = false;
@@ -99,12 +158,13 @@ public class BubbleExpandedView extends LinearLayout {
* Whether we're animating the {@code TaskView}'s alpha value. If so, we will hold off on
* applying alpha changes from {@link #setContentVisibility} until the animation ends.
*/
- private boolean mIsAlphaAnimating = false;
+ private boolean mIsAnimating = false;
private int mPointerWidth;
private int mPointerHeight;
private float mPointerRadius;
private float mPointerOverlap;
+ private final PointF mPointerPos = new PointF();
private CornerPathEffect mPointerEffect;
private ShapeDrawable mCurrentPointer;
private ShapeDrawable mTopPointer;
@@ -113,11 +173,13 @@ public class BubbleExpandedView extends LinearLayout {
private float mCornerRadius = 0f;
private int mBackgroundColorFloating;
private boolean mUsingMaxHeight;
-
+ private int mTopClip = 0;
+ private int mBottomClip = 0;
@Nullable private Bubble mBubble;
private PendingIntent mPendingIntent;
// TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead
private boolean mIsOverflow;
+ private boolean mIsClipping;
private BubbleController mController;
private BubbleStackView mStackView;
@@ -162,15 +224,23 @@ public class BubbleExpandedView extends LinearLayout {
try {
options.setTaskAlwaysOnTop(true);
options.setLaunchedFromBubble(true);
- if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
+
+ Intent fillInIntent = new Intent();
+ // Apply flags to make behaviour match documentLaunchMode=always.
+ fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+
+ if (mBubble.isAppBubble()) {
+ PendingIntent pi = PendingIntent.getActivity(mContext, 0,
+ mBubble.getAppBubbleIntent(),
+ PendingIntent.FLAG_MUTABLE,
+ null);
+ mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
+ } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
options.setApplyActivityFlagsForBubbles(true);
mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
options, launchBounds);
} else {
- Intent fillInIntent = new Intent();
- // Apply flags to make behaviour match documentLaunchMode=always.
- fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
if (mBubble != null) {
mBubble.setIntentActive();
}
@@ -268,7 +338,8 @@ public class BubbleExpandedView extends LinearLayout {
mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
- outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
+ Rect clip = new Rect(0, mTopClip, view.getWidth(), view.getHeight() - mBottomClip);
+ outline.setRoundRect(clip, mCornerRadius);
}
});
mExpandedViewContainer.setClipToOutline(true);
@@ -300,9 +371,9 @@ public class BubbleExpandedView extends LinearLayout {
// they should not collapse the stack (which all other touches on areas around the AV
// would do).
if (motionEvent.getRawY() >= avBounds.top
- && motionEvent.getRawY() <= avBounds.bottom
- && (motionEvent.getRawX() < avBounds.left
- || motionEvent.getRawX() > avBounds.right)) {
+ && motionEvent.getRawY() <= avBounds.bottom
+ && (motionEvent.getRawX() < avBounds.left
+ || motionEvent.getRawX() > avBounds.right)) {
return true;
}
@@ -384,7 +455,7 @@ public class BubbleExpandedView extends LinearLayout {
}
void applyThemeAttrs() {
- final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
android.R.attr.dialogCornerRadius,
android.R.attr.colorBackgroundFloating});
boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
@@ -429,7 +500,7 @@ public class BubbleExpandedView extends LinearLayout {
* ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
* being dragged out, the manage menu) this is set to false, otherwise it should be true.
*/
- void setSurfaceZOrderedOnTop(boolean onTop) {
+ public void setSurfaceZOrderedOnTop(boolean onTop) {
if (mTaskView == null) {
return;
}
@@ -510,12 +581,12 @@ public class BubbleExpandedView extends LinearLayout {
}
/**
- * Whether we are currently animating the {@code TaskView}'s alpha value. If this is set to
+ * Whether we are currently animating the {@code TaskView}. If this is set to
* true, calls to {@link #setContentVisibility} will not be applied until this is set to false
* again.
*/
- void setAlphaAnimating(boolean animating) {
- mIsAlphaAnimating = animating;
+ public void setAnimating(boolean animating) {
+ mIsAnimating = animating;
// If we're done animating, apply the correct
if (!animating) {
@@ -524,18 +595,139 @@ public class BubbleExpandedView extends LinearLayout {
}
/**
- * Sets the alpha of the underlying {@code TaskView}, since changing the expanded view's alpha
- * does not affect the {@code TaskView} since it uses a Surface.
+ * 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.
+ *
+ * @return alpha for the content being shown
*/
- void setTaskViewAlpha(float alpha) {
+ public float getContentAlpha() {
+ if (mIsOverflow) {
+ return mOverflowView.getAlpha();
+ }
if (mTaskView != null) {
+ return mTaskView.getAlpha();
+ }
+ return 1f;
+ }
+
+ /**
+ * Set alpha of the underlying {@code TaskView} if this view is for a bubble.
+ * Or set alpha for the overflow view if this view is for overflow.
+ *
+ * Changing expanded view's alpha does not affect the {@code TaskView} since it uses a Surface.
+ */
+ public void setContentAlpha(float alpha) {
+ if (mIsOverflow) {
+ mOverflowView.setAlpha(alpha);
+ } else if (mTaskView != null) {
mTaskView.setAlpha(alpha);
}
+ }
+
+ /**
+ * Sets the alpha of the background and the pointer view.
+ */
+ public void setBackgroundAlpha(float alpha) {
mPointerView.setAlpha(alpha);
setAlpha(alpha);
}
/**
+ * Set translation Y for the expanded view content.
+ * Excludes manage button and pointer.
+ */
+ public void setContentTranslationY(float translationY) {
+ mExpandedViewContainer.setTranslationY(translationY);
+
+ // Left or right pointer can become detached when moving the view up
+ if (translationY <= 0 && (isShowingLeftPointer() || isShowingRightPointer())) {
+ // Y coordinate where the pointer would start to get detached from the expanded view.
+ // Takes into account bottom clipping and rounded corners
+ float detachPoint =
+ mExpandedViewContainer.getBottom() - mBottomClip - mCornerRadius + translationY;
+ float pointerBottom = mPointerPos.y + mPointerHeight;
+ // If pointer bottom is past detach point, move it in by that many pixels
+ float horizontalShift = 0;
+ if (pointerBottom > detachPoint) {
+ horizontalShift = pointerBottom - detachPoint;
+ }
+ if (isShowingLeftPointer()) {
+ // Move left pointer right
+ movePointerBy(horizontalShift, 0);
+ } else {
+ // Move right pointer left
+ movePointerBy(-horizontalShift, 0);
+ }
+ // Hide pointer if it is moved by entire width
+ mPointerView.setVisibility(
+ horizontalShift > mPointerWidth ? View.INVISIBLE : View.VISIBLE);
+ }
+ }
+
+ /**
+ * Update alpha value for the manage button
+ */
+ public void setManageButtonAlpha(float alpha) {
+ mManageButton.setAlpha(alpha);
+ }
+
+ /**
+ * Set {@link #setTranslationY(float) translationY} for the manage button
+ */
+ public void setManageButtonTranslationY(float translationY) {
+ mManageButton.setTranslationY(translationY);
+ }
+
+ /**
+ * Set top clipping for the view
+ */
+ public void setTopClip(int clip) {
+ mTopClip = clip;
+ onContainerClipUpdate();
+ }
+
+ /**
+ * Set bottom clipping for the view
+ */
+ public void setBottomClip(int clip) {
+ mBottomClip = clip;
+ onContainerClipUpdate();
+ }
+
+ private void onContainerClipUpdate() {
+ if (mTopClip == 0 && mBottomClip == 0) {
+ if (mIsClipping) {
+ mIsClipping = false;
+ if (mTaskView != null) {
+ mTaskView.setClipBounds(null);
+ mTaskView.setEnableSurfaceClipping(false);
+ }
+ mExpandedViewContainer.invalidateOutline();
+ }
+ } else {
+ if (!mIsClipping) {
+ mIsClipping = true;
+ if (mTaskView != null) {
+ mTaskView.setEnableSurfaceClipping(true);
+ }
+ }
+ mExpandedViewContainer.invalidateOutline();
+ if (mTaskView != null) {
+ mTaskView.setClipBounds(new Rect(0, mTopClip, mTaskView.getWidth(),
+ mTaskView.getHeight() - mBottomClip));
+ }
+ }
+ }
+
+ /**
+ * Move pointer from base position
+ */
+ public void movePointerBy(float x, float y) {
+ mPointerView.setTranslationX(mPointerPos.x + x);
+ mPointerView.setTranslationY(mPointerPos.y + y);
+ }
+
+ /**
* Set visibility of contents in the expanded state.
*
* @param visibility {@code true} if the contents should be visible on the screen.
@@ -543,13 +735,13 @@ public class BubbleExpandedView extends LinearLayout {
* Note that this contents visibility doesn't affect visibility at {@link android.view.View},
* and setting {@code false} actually means rendering the contents in transparent.
*/
- void setContentVisibility(boolean visibility) {
+ public void setContentVisibility(boolean visibility) {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "setContentVisibility: visibility=" + visibility
+ " bubble=" + getBubbleKey());
}
mIsContentVisible = visibility;
- if (mTaskView != null && !mIsAlphaAnimating) {
+ if (mTaskView != null && !mIsAnimating) {
mTaskView.setAlpha(visibility ? 1f : 0f);
mPointerView.setAlpha(visibility ? 1f : 0f);
}
@@ -560,6 +752,44 @@ public class BubbleExpandedView extends LinearLayout {
return mTaskView;
}
+ @VisibleForTesting
+ public BubbleOverflowContainerView getOverflow() {
+ return mOverflowView;
+ }
+
+
+ /**
+ * Return content height: taskView or overflow.
+ * Takes into account clippings set by {@link #setTopClip(int)} and {@link #setBottomClip(int)}
+ *
+ * @return if bubble is for overflow, return overflow height, otherwise return taskView height
+ */
+ public int getContentHeight() {
+ if (mIsOverflow) {
+ return mOverflowView.getHeight() - mTopClip - mBottomClip;
+ }
+ if (mTaskView != null) {
+ return mTaskView.getHeight() - mTopClip - mBottomClip;
+ }
+ return 0;
+ }
+
+ /**
+ * Return bottom position of the content on screen
+ *
+ * @return if bubble is for overflow, return value for overflow, otherwise taskView
+ */
+ public int getContentBottomOnScreen() {
+ Rect out = new Rect();
+ if (mIsOverflow) {
+ mOverflowView.getBoundsOnScreen(out);
+ }
+ if (mTaskView != null) {
+ mTaskView.getBoundsOnScreen(out);
+ }
+ return out.bottom;
+ }
+
int getTaskId() {
return mTaskId;
}
@@ -687,7 +917,9 @@ public class BubbleExpandedView extends LinearLayout {
mTaskView.onLocationChanged();
}
if (mIsOverflow) {
- mOverflowView.show();
+ post(() -> {
+ mOverflowView.show();
+ });
}
}
@@ -730,38 +962,59 @@ public class BubbleExpandedView extends LinearLayout {
post(() -> {
mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer;
updatePointerView();
- float pointerY;
- float pointerX;
if (showVertically) {
- pointerY = bubbleCenter - (mPointerWidth / 2f);
+ mPointerPos.y = bubbleCenter - (mPointerWidth / 2f);
if (!isRtl) {
- pointerX = onLeft
+ mPointerPos.x = onLeft
? -mPointerHeight + mPointerOverlap
: getWidth() - mPaddingRight - mPointerOverlap;
} else {
- pointerX = onLeft
+ mPointerPos.x = onLeft
? -(getWidth() - mPaddingLeft - mPointerOverlap)
: mPointerHeight - mPointerOverlap;
}
} else {
- pointerY = mPointerOverlap;
+ mPointerPos.y = mPointerOverlap;
if (!isRtl) {
- pointerX = bubbleCenter - (mPointerWidth / 2f);
+ mPointerPos.x = bubbleCenter - (mPointerWidth / 2f);
} else {
- pointerX = -(getWidth() - mPaddingLeft - bubbleCenter) + (mPointerWidth / 2f);
+ mPointerPos.x = -(getWidth() - mPaddingLeft - bubbleCenter)
+ + (mPointerWidth / 2f);
}
}
if (animate) {
- mPointerView.animate().translationX(pointerX).translationY(pointerY).start();
+ mPointerView.animate().translationX(mPointerPos.x).translationY(
+ mPointerPos.y).start();
} else {
- mPointerView.setTranslationY(pointerY);
- mPointerView.setTranslationX(pointerX);
+ mPointerView.setTranslationY(mPointerPos.y);
+ mPointerView.setTranslationX(mPointerPos.x);
mPointerView.setVisibility(VISIBLE);
}
});
}
/**
+ * Return true if pointer is shown on the left
+ */
+ public boolean isShowingLeftPointer() {
+ return mCurrentPointer == mLeftPointer;
+ }
+
+ /**
+ * Return true if pointer is shown on the right
+ */
+ public boolean isShowingRightPointer() {
+ return mCurrentPointer == mRightPointer;
+ }
+
+ /**
+ * Return width of the current pointer
+ */
+ public int getPointerWidth() {
+ return mPointerWidth;
+ }
+
+ /**
* Position of the manage button displayed in the expanded view. Used for placing user
* education about the manage button.
*/
@@ -799,7 +1052,7 @@ public class BubbleExpandedView extends LinearLayout {
/**
* Description of current expanded view state.
*/
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ public void dump(@NonNull PrintWriter pw) {
pw.print("BubbleExpandedView");
pw.print(" taskId: "); pw.println(mTaskId);
pw.print(" stackView: "); pw.println(mStackView);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
index 9d3bf34895d3..5dab8a071f76 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
@@ -65,4 +66,19 @@ public class BubbleIconFactory extends BaseIconFactory {
return null;
}
}
+
+ /**
+ * Creates the bitmap for the provided drawable and returns the scale used for
+ * drawing the actual drawable.
+ */
+ public Bitmap createIconBitmap(@NonNull Drawable icon, float[] outScale) {
+ if (outScale == null) {
+ outScale = new float[1];
+ }
+ icon = normalizeAndWrapToAdaptiveIcon(icon,
+ true /* shrinkNonAdaptiveIcons */,
+ null /* outscale */,
+ outScale);
+ return createIconBitmap(icon, outScale[0], BITMAP_GENERATION_MODE_WITH_SHADOW);
+ }
}
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 fcd0ed7308ef..9aa285fff19c 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
@@ -167,7 +167,10 @@ public class BubbleOverflowContainerView extends LinearLayout {
void updateOverflow() {
Resources res = getResources();
- final int columns = res.getInteger(R.integer.bubbles_overflow_columns);
+ int columns = (int) Math.round(getWidth()
+ / (res.getDimension(R.dimen.bubble_name_width)));
+ columns = columns > 0 ? columns : res.getInteger(R.integer.bubbles_overflow_columns);
+
mRecyclerView.setLayoutManager(
new OverflowGridLayoutManager(getContext(), columns));
if (mRecyclerView.getItemDecorationCount() == 0) {
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 e9729e45731b..dbad5df9cf56 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
@@ -16,6 +16,8 @@
package com.android.wm.shell.bubbles;
+import static android.view.View.LAYOUT_DIRECTION_RTL;
+
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.IntDef;
@@ -28,7 +30,6 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;
import android.view.Surface;
-import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
@@ -366,6 +367,14 @@ public class BubblePositioner {
return mImeVisible ? mImeHeight : 0;
}
+ /** Return top position of the IME if it's visible */
+ public int getImeTop() {
+ if (mImeVisible) {
+ return getScreenRect().bottom - getImeHeight() - getInsets().bottom;
+ }
+ return 0;
+ }
+
/** Sets whether the IME is visible. **/
public void setImeVisible(boolean visible, int height) {
mImeVisible = visible;
@@ -557,16 +566,30 @@ public class BubblePositioner {
* @return the position of the bubble on-screen when the stack is expanded.
*/
public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) {
- final float positionInRow = index * (mBubbleSize + mSpacingBetweenBubbles);
+ boolean showBubblesVertically = showBubblesVertically();
+ boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection()
+ == LAYOUT_DIRECTION_RTL;
+
+ int onScreenIndex;
+ if (showBubblesVertically || !isRtl) {
+ onScreenIndex = index;
+ } else {
+ // If bubbles are shown horizontally, check if RTL language is used.
+ // If RTL is active, position first bubble on the right and last on the left.
+ // Last bubble has screen index 0 and first bubble has max screen index value.
+ onScreenIndex = state.numberOfBubbles - 1 - index;
+ }
+
+ final float positionInRow = onScreenIndex * (mBubbleSize + mSpacingBetweenBubbles);
final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles);
- final float centerPosition = showBubblesVertically()
+ final float centerPosition = showBubblesVertically
? mPositionRect.centerY()
: mPositionRect.centerX();
// alignment - centered on the edge
final float rowStart = centerPosition - (expandedStackSize / 2f);
float x;
float y;
- if (showBubblesVertically()) {
+ if (showBubblesVertically) {
int inset = mExpandedViewLargeScreenInsetClosestEdge;
y = rowStart + positionInRow;
int left = mIsLargeScreen
@@ -583,8 +606,8 @@ public class BubblePositioner {
x = rowStart + positionInRow;
}
- if (showBubblesVertically() && mImeVisible) {
- return new PointF(x, getExpandedBubbleYForIme(index, state));
+ if (showBubblesVertically && mImeVisible) {
+ return new PointF(x, getExpandedBubbleYForIme(onScreenIndex, state));
}
return new PointF(x, y);
}
@@ -693,7 +716,7 @@ public class BubblePositioner {
// Start on the left if we're in LTR, right otherwise.
final boolean startOnLeft =
mContext.getResources().getConfiguration().getLayoutDirection()
- != View.LAYOUT_DIRECTION_RTL;
+ != LAYOUT_DIRECTION_RTL;
final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset(
R.dimen.bubble_stack_starting_offset_y);
// TODO: placement bug here because mPositionRect doesn't handle the overhanging edge
@@ -749,4 +772,21 @@ public class BubblePositioner {
public void setPinnedLocation(PointF point) {
mPinLocation = point;
}
+
+ /**
+ * Navigation bar has an area where system gestures can be started from.
+ *
+ * @return {@link Rect} for system navigation bar gesture zone
+ */
+ public Rect getNavBarGestureZone() {
+ // Gesture zone height from the bottom
+ int gestureZoneHeight = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.navigation_bar_gesture_height);
+ Rect screen = getScreenRect();
+ return new Rect(
+ screen.left,
+ screen.bottom - gestureZoneHeight,
+ screen.right,
+ screen.bottom);
+ }
}
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 0a334140d616..be100bb1dd34 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
@@ -21,6 +21,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
@@ -44,6 +45,7 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
+import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
import android.view.Choreographer;
@@ -56,6 +58,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
+import android.view.WindowManagerPolicyConstants;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.FrameLayout;
@@ -75,8 +78,12 @@ import com.android.internal.util.FrameworkStatsLog;
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;
+import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationController;
+import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerImpl;
+import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerStub;
import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout;
import com.android.wm.shell.bubbles.animation.StackAnimationController;
import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -89,6 +96,7 @@ import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -97,6 +105,15 @@ import java.util.stream.Collectors;
*/
public class BubbleStackView extends FrameLayout
implements ViewTreeObserver.OnComputeInternalInsetsListener {
+ /**
+ * Set to {@code true} to enable home gesture handling in bubbles
+ */
+ public static final boolean HOME_GESTURE_ENABLED =
+ SystemProperties.getBoolean("persist.wm.debug.bubbles_home_gesture", true);
+
+ public static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE =
+ SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true);
+
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
/** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
@@ -123,6 +140,9 @@ public class BubbleStackView extends FrameLayout
private static final float SCRIM_ALPHA = 0.6f;
+ /** Minimum alpha value for scrim when alpha is being changed via drag */
+ private static final float MIN_SCRIM_ALPHA_FOR_DRAG = 0.2f;
+
/**
* How long to wait to animate the stack temporarily invisible after a drag/flyout hide
* animation ends, if we are in fact temporarily invisible.
@@ -148,7 +168,7 @@ public class BubbleStackView extends FrameLayout
* Handler to use for all delayed animations - this way, we can easily cancel them before
* starting a new animation.
*/
- private final ShellExecutor mDelayedAnimationExecutor;
+ private final ShellExecutor mMainExecutor;
private Runnable mDelayedAnimation;
/**
@@ -197,8 +217,10 @@ public class BubbleStackView extends FrameLayout
private PhysicsAnimationLayout mBubbleContainer;
private StackAnimationController mStackAnimationController;
private ExpandedAnimationController mExpandedAnimationController;
+ private ExpandedViewAnimationController mExpandedViewAnimationController;
private View mScrim;
+ private boolean mScrimAnimating;
private View mManageMenuScrim;
private FrameLayout mExpandedViewContainer;
@@ -276,8 +298,11 @@ public class BubbleStackView extends FrameLayout
*/
private int mPointerIndexDown = -1;
+ @Nullable
+ private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker;
+
/** Description of current animation controller state. */
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.println("Stack view state:");
String bubblesOnScreen = BubbleDebugConfig.formatBubblesString(
@@ -291,8 +316,8 @@ public class BubbleStackView extends FrameLayout
pw.print(" expandedContainerMatrix: ");
pw.println(mExpandedViewContainer.getAnimationMatrix());
- mStackAnimationController.dump(pw, args);
- mExpandedAnimationController.dump(pw, args);
+ mStackAnimationController.dump(pw);
+ mExpandedAnimationController.dump(pw);
if (mExpandedBubble != null) {
pw.println("Expanded bubble state:");
@@ -693,6 +718,90 @@ public class BubbleStackView extends FrameLayout
}
};
+ /** Touch listener set on the whole view that forwards event to the swipe up listener. */
+ private final RelativeTouchListener mContainerSwipeListener = new RelativeTouchListener() {
+ @Override
+ public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
+ // Pass move event on to swipe listener
+ mSwipeUpListener.onDown(ev.getX(), ev.getY());
+ return true;
+ }
+
+ @Override
+ public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+ float viewInitialY, float dx, float dy) {
+ // Pass move event on to swipe listener
+ mSwipeUpListener.onMove(dx, dy);
+ }
+
+ @Override
+ public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+ float viewInitialY, float dx, float dy, float velX, float velY) {
+ // Pass up even on to swipe listener
+ mSwipeUpListener.onUp(velX, velY);
+ }
+ };
+
+ /** MotionEventListener that listens from home gesture swipe event. */
+ private final MotionEventListener mSwipeUpListener = new MotionEventListener() {
+ @Override
+ public void onDown(float x, float y) {}
+
+ @Override
+ public void onMove(float dx, float dy) {
+ if ((mManageEduView != null && mManageEduView.getVisibility() == VISIBLE)
+ || isStackEduShowing()) {
+ return;
+ }
+
+ if (mShowingManage) {
+ showManageMenu(false /* show */);
+ }
+ // Only allow up, normalize for up direction
+ float collapsed = -Math.min(dy, 0);
+ mExpandedViewAnimationController.updateDrag((int) collapsed);
+
+ // Update scrim
+ if (!mScrimAnimating) {
+ mScrim.setAlpha(getScrimAlphaForDrag(collapsed));
+ }
+ }
+
+ @Override
+ public void onCancel() {
+ mExpandedViewAnimationController.animateBackToExpanded();
+ }
+
+ @Override
+ public void onUp(float velX, float velY) {
+ mExpandedViewAnimationController.setSwipeVelocity(velY);
+ if (mExpandedViewAnimationController.shouldCollapse()) {
+ // Update data first and start the animation when we are processing change
+ mBubbleData.setExpanded(false);
+ } else {
+ mExpandedViewAnimationController.animateBackToExpanded();
+
+ // Update scrim
+ if (!mScrimAnimating) {
+ showScrim(true);
+ }
+ }
+ }
+
+ private float getScrimAlphaForDrag(float dragAmount) {
+ // dragAmount should be negative as we allow scroll up only
+ if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ float alphaRange = SCRIM_ALPHA - MIN_SCRIM_ALPHA_FOR_DRAG;
+
+ int dragMax = mExpandedBubble.getExpandedView().getContentHeight();
+ float dragFraction = dragAmount / dragMax;
+
+ return Math.max(SCRIM_ALPHA - alphaRange * dragFraction, MIN_SCRIM_ALPHA_FOR_DRAG);
+ }
+ return SCRIM_ALPHA;
+ }
+ };
+
/** Click listener set on the flyout, which expands the stack when the flyout is tapped. */
private OnClickListener mFlyoutClickListener = new OnClickListener() {
@Override
@@ -766,7 +875,7 @@ public class BubbleStackView extends FrameLayout
ShellExecutor mainExecutor) {
super(context);
- mDelayedAnimationExecutor = mainExecutor;
+ mMainExecutor = mainExecutor;
mBubbleController = bubbleController;
mBubbleData = data;
@@ -796,6 +905,14 @@ public class BubbleStackView extends FrameLayout
mExpandedAnimationController = new ExpandedAnimationController(mPositioner,
onBubbleAnimatedOut, this);
+
+ if (HOME_GESTURE_ENABLED) {
+ mExpandedViewAnimationController =
+ new ExpandedViewAnimationControllerImpl(context, mPositioner);
+ } else {
+ mExpandedViewAnimationController = new ExpandedViewAnimationControllerStub();
+ }
+
mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
// Force LTR by default since most of the Bubbles UI is positioned manually by the user, or
@@ -971,7 +1088,7 @@ public class BubbleStackView extends FrameLayout
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
// We need to be Z ordered on top in order for alpha animations to work.
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
- mExpandedBubble.getExpandedView().setAlphaAnimating(true);
+ mExpandedBubble.getExpandedView().setAnimating(true);
}
}
@@ -985,14 +1102,15 @@ public class BubbleStackView extends FrameLayout
// = 0f remains in effect.
&& !mExpandedViewTemporarilyHidden) {
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
- mExpandedBubble.getExpandedView().setAlphaAnimating(false);
+ mExpandedBubble.getExpandedView().setAnimating(false);
}
}
});
mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
- mExpandedBubble.getExpandedView().setTaskViewAlpha(
- (float) valueAnimator.getAnimatedValue());
+ float alpha = (float) valueAnimator.getAnimatedValue();
+ mExpandedBubble.getExpandedView().setContentAlpha(alpha);
+ mExpandedBubble.getExpandedView().setBackgroundAlpha(alpha);
}
});
@@ -1152,7 +1270,7 @@ public class BubbleStackView extends FrameLayout
}
final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION);
final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext))
- && mExpandedBubble != null;
+ && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null;
if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
Log.d(TAG, "Show manage edu: " + shouldShow);
}
@@ -1708,7 +1826,7 @@ public class BubbleStackView extends FrameLayout
/**
* Update bubble order and pointer position.
*/
- public void updateBubbleOrder(List<Bubble> bubbles) {
+ public void updateBubbleOrder(List<Bubble> bubbles, boolean updatePointerPositoion) {
final Runnable reorder = () -> {
for (int i = 0; i < bubbles.size(); i++) {
Bubble bubble = bubbles.get(i);
@@ -1724,7 +1842,10 @@ public class BubbleStackView extends FrameLayout
.map(b -> b.getIconView()).collect(Collectors.toList());
mStackAnimationController.animateReorder(bubbleViews, reorder);
}
- updatePointerPosition(false /* forIme */);
+
+ if (updatePointerPositoion) {
+ updatePointerPosition(false /* forIme */);
+ }
}
/**
@@ -1795,6 +1916,7 @@ public class BubbleStackView extends FrameLayout
private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) {
final BubbleViewProvider previouslySelected = mExpandedBubble;
mExpandedBubble = bubbleToSelect;
+ mExpandedViewAnimationController.setExpandedView(mExpandedBubble.getExpandedView());
if (mIsExpanded) {
hideCurrentInputMethod();
@@ -1843,12 +1965,19 @@ public class BubbleStackView extends FrameLayout
return;
}
+ boolean wasExpanded = mIsExpanded;
+
hideCurrentInputMethod();
mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand);
- if (mIsExpanded) {
- animateCollapse();
+ if (wasExpanded) {
+ stopMonitoringSwipeUpGesture();
+ if (HOME_GESTURE_ENABLED) {
+ animateCollapse();
+ } else {
+ animateCollapseWithScale();
+ }
logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
} else {
animateExpansion();
@@ -1856,11 +1985,58 @@ public class BubbleStackView extends FrameLayout
logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
logBubbleEvent(mExpandedBubble,
FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
+ if (HOME_GESTURE_ENABLED) {
+ mBubbleController.isNotificationPanelExpanded(notifPanelExpanded -> {
+ if (!notifPanelExpanded && mIsExpanded) {
+ startMonitoringSwipeUpGesture();
+ }
+ });
+ }
}
notifyExpansionChanged(mExpandedBubble, mIsExpanded);
}
/**
+ * Monitor for swipe up gesture that is used to collapse expanded view
+ */
+ void startMonitoringSwipeUpGesture() {
+ if (DEBUG_BUBBLE_GESTURE) {
+ Log.d(TAG, "startMonitoringSwipeUpGesture");
+ }
+ stopMonitoringSwipeUpGestureInternal();
+
+ if (isGestureNavEnabled()) {
+ mBubblesNavBarGestureTracker = new BubblesNavBarGestureTracker(mContext, mPositioner);
+ mBubblesNavBarGestureTracker.start(mSwipeUpListener);
+ setOnTouchListener(mContainerSwipeListener);
+ }
+ }
+
+ private boolean isGestureNavEnabled() {
+ return mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_navBarInteractionMode)
+ == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
+ }
+
+ /**
+ * Stop monitoring for swipe up gesture
+ */
+ void stopMonitoringSwipeUpGesture() {
+ if (DEBUG_BUBBLE_GESTURE) {
+ Log.d(TAG, "stopMonitoringSwipeUpGesture");
+ }
+ stopMonitoringSwipeUpGestureInternal();
+ }
+
+ private void stopMonitoringSwipeUpGestureInternal() {
+ if (mBubblesNavBarGestureTracker != null) {
+ mBubblesNavBarGestureTracker.stop();
+ mBubblesNavBarGestureTracker = null;
+ setOnTouchListener(null);
+ }
+ }
+
+ /**
* Called when back press occurs while bubbles are expanded.
*/
public void onBackPressed() {
@@ -1982,15 +2158,28 @@ public class BubbleStackView extends FrameLayout
}
private void showScrim(boolean show) {
+ AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mScrimAnimating = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mScrimAnimating = false;
+ }
+ };
if (show) {
mScrim.animate()
.setInterpolator(ALPHA_IN)
.alpha(SCRIM_ALPHA)
+ .setListener(listener)
.start();
} else {
mScrim.animate()
.alpha(0f)
.setInterpolator(ALPHA_OUT)
+ .setListener(listener)
.start();
}
}
@@ -2072,11 +2261,12 @@ public class BubbleStackView extends FrameLayout
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
if (mExpandedBubble.getExpandedView() != null) {
- mExpandedBubble.getExpandedView().setTaskViewAlpha(0f);
+ mExpandedBubble.getExpandedView().setContentAlpha(0f);
+ mExpandedBubble.getExpandedView().setBackgroundAlpha(0f);
// We'll be starting the alpha animation after a slight delay, so set this flag early
// here.
- mExpandedBubble.getExpandedView().setAlphaAnimating(true);
+ mExpandedBubble.getExpandedView().setAnimating(true);
}
mDelayedAnimation = () -> {
@@ -2114,10 +2304,10 @@ public class BubbleStackView extends FrameLayout
})
.start();
};
- mDelayedAnimationExecutor.executeDelayed(mDelayedAnimation, startDelay);
+ mMainExecutor.executeDelayed(mDelayedAnimation, startDelay);
}
- private void animateCollapse() {
+ private void animateCollapseWithScale() {
cancelDelayedExpandCollapseSwitchAnimations();
if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
@@ -2217,6 +2407,68 @@ public class BubbleStackView extends FrameLayout
.start();
}
+ private void animateCollapse() {
+ cancelDelayedExpandCollapseSwitchAnimations();
+
+ if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+ mManageEduView.hide();
+ }
+
+ mIsExpanded = false;
+ mIsExpansionAnimating = true;
+
+ showScrim(false);
+
+ mBubbleContainer.cancelAllAnimations();
+
+ // If we were in the middle of swapping, the animating-out surface would have been scaling
+ // to zero - finish it off.
+ PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
+ mAnimatingOutSurfaceContainer.setScaleX(0f);
+ mAnimatingOutSurfaceContainer.setScaleY(0f);
+
+ // Let the expanded animation controller know that it shouldn't animate child adds/reorders
+ // since we're about to animate collapsed.
+ mExpandedAnimationController.notifyPreparingToCollapse();
+
+ final Runnable collapseBackToStack = () -> mExpandedAnimationController.collapseBackToStack(
+ mStackAnimationController
+ .getStackPositionAlongNearestHorizontalEdge()
+ /* collapseTo */,
+ () -> mBubbleContainer.setActiveController(mStackAnimationController));
+
+ final Runnable after = () -> {
+ final BubbleViewProvider previouslySelected = mExpandedBubble;
+ // TODO(b/231350255): investigate why this call is needed here
+ beforeExpandedViewAnimation();
+ if (mManageEduView != null) {
+ mManageEduView.hide();
+ }
+
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "animateCollapse");
+ Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
+ mExpandedBubble));
+ }
+ updateOverflowVisibility();
+ updateZOrder();
+ updateBadges(true /* setBadgeForCollapsedStack */);
+ afterExpandedViewAnimation();
+ if (previouslySelected != null) {
+ previouslySelected.setTaskViewVisibility(false);
+ }
+ mExpandedViewAnimationController.reset();
+ };
+ mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after);
+ 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
+ // animating. But updates the internal state for the content to be hidden after
+ // animation completes.
+ mExpandedBubble.getExpandedView().setContentVisibility(false);
+ }
+ }
+
private void animateSwitchBubbles() {
// If we're no longer expanded, this is meaningless.
if (!mIsExpanded) {
@@ -2277,7 +2529,7 @@ public class BubbleStackView extends FrameLayout
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
- mDelayedAnimationExecutor.executeDelayed(() -> {
+ mMainExecutor.executeDelayed(() -> {
if (!mIsExpanded) {
mIsBubbleSwitchAnimating = false;
return;
@@ -2308,7 +2560,7 @@ public class BubbleStackView extends FrameLayout
* animating flags for those animations.
*/
private void cancelDelayedExpandCollapseSwitchAnimations() {
- mDelayedAnimationExecutor.removeCallbacks(mDelayedAnimation);
+ mMainExecutor.removeCallbacks(mDelayedAnimation);
mIsExpansionAnimating = false;
mIsBubbleSwitchAnimating = false;
@@ -2332,9 +2584,18 @@ public class BubbleStackView extends FrameLayout
/**
* Updates the stack based for IME changes. When collapsed it'll move the stack if it
* overlaps where they IME would be. When expanded it'll shift the expanded bubbles
- * if they might overlap with the IME (this only happens for large screens).
+ * if they might overlap with the IME (this only happens for large screens)
+ * and clip the expanded view.
*/
- public void animateForIme(boolean visible) {
+ public void setImeVisible(boolean visible) {
+ if (HOME_GESTURE_ENABLED) {
+ setImeVisibleInternal(visible);
+ } else {
+ setImeVisibleWithoutClipping(visible);
+ }
+ }
+
+ private void setImeVisibleWithoutClipping(boolean visible) {
if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) {
// This will update the animation so the bubbles move to position for the IME
mExpandedAnimationController.expandFromStack(() -> {
@@ -2385,6 +2646,62 @@ public class BubbleStackView extends FrameLayout
}
}
+ private void setImeVisibleInternal(boolean visible) {
+ if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) {
+ // This will update the animation so the bubbles move to position for the IME
+ mExpandedAnimationController.expandFromStack(() -> {
+ updatePointerPosition(false /* forIme */);
+ afterExpandedViewAnimation();
+ mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
+ } /* after */);
+ return;
+ }
+
+ if (!mIsExpanded && getBubbleCount() > 0) {
+ final float stackDestinationY =
+ mStackAnimationController.animateForImeVisibility(visible);
+
+ // How far the stack is animating due to IME, we'll just animate the flyout by that
+ // much too.
+ final float stackDy =
+ stackDestinationY - mStackAnimationController.getStackPosition().y;
+
+ // If the flyout is visible, translate it along with the bubble stack.
+ if (mFlyout.getVisibility() == VISIBLE) {
+ PhysicsAnimator.getInstance(mFlyout)
+ .spring(DynamicAnimation.TRANSLATION_Y,
+ mFlyout.getTranslationY() + stackDy,
+ FLYOUT_IME_ANIMATION_SPRING_CONFIG)
+ .start();
+ }
+ }
+
+ if (mIsExpanded) {
+ mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
+ if (mPositioner.showBubblesVertically()
+ && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex,
+ getState()).y;
+ float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY);
+ mExpandedBubble.getExpandedView().setImeVisible(visible);
+ if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) {
+ mExpandedViewContainer.animate().translationY(newExpandedViewTop);
+ }
+ List<Animator> animList = new ArrayList();
+ for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
+ View child = mBubbleContainer.getChildAt(i);
+ float transY = mPositioner.getExpandedBubbleXY(i, getState()).y;
+ ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY);
+ animList.add(anim);
+ }
+ updatePointerPosition(true /* forIme */);
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(animList);
+ set.start();
+ }
+ }
+ }
+
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) {
@@ -2473,8 +2790,10 @@ public class BubbleStackView extends FrameLayout
private void dismissBubbleIfExists(@Nullable BubbleViewProvider bubble) {
if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
- if (mIsExpanded && mBubbleData.getBubbles().size() > 1) {
- // If we have more than 1 bubble we will perform the switch animation
+ if (mIsExpanded && mBubbleData.getBubbles().size() > 1
+ && Objects.equals(bubble, mExpandedBubble)) {
+ // If we have more than 1 bubble and it's the current bubble being dismissed,
+ // we will perform the switch animation
mIsBubbleSwitchAnimating = true;
}
mBubbleData.dismissBubbleWithKey(bubble.getKey(), Bubbles.DISMISS_USER_GESTURE);
@@ -2820,7 +3139,7 @@ public class BubbleStackView extends FrameLayout
&& mExpandedBubble.getExpandedView() != null) {
BubbleExpandedView bev = mExpandedBubble.getExpandedView();
bev.setContentVisibility(false);
- bev.setAlphaAnimating(!mIsExpansionAnimating);
+ bev.setAnimating(!mIsExpansionAnimating);
mExpandedViewContainerMatrix.setScaleX(0f);
mExpandedViewContainerMatrix.setScaleY(0f);
mExpandedViewContainerMatrix.setTranslate(0f, 0f);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index 69762c9bc06a..f437553337ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -195,15 +195,18 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
b.isImportantConversation());
info.badgeBitmap = badgeBitmapInfo.icon;
// Raw badge bitmap never includes the important conversation ring
- info.mRawBadgeBitmap = badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon;
- info.bubbleBitmap = iconFactory.createBadgedIconBitmap(bubbleDrawable).icon;
+ info.mRawBadgeBitmap = b.isImportantConversation()
+ ? badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon
+ : badgeBitmapInfo.icon;
+
+ float[] bubbleBitmapScale = new float[1];
+ info.bubbleBitmap = iconFactory.createIconBitmap(bubbleDrawable, bubbleBitmapScale);
// Dot color & placement
Path iconPath = PathParser.createPathFromPathData(
c.getResources().getString(com.android.internal.R.string.config_icon_mask));
Matrix matrix = new Matrix();
- float scale = iconFactory.getNormalizer().getScale(bubbleDrawable,
- null /* outBounds */, null /* path */, null /* outMaskShape */);
+ float scale = bubbleBitmapScale[0];
float radius = DEFAULT_PATH_SIZE / 2f;
matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
radius /* pivot y */);
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 8a0db0a12711..b3104b518440 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
@@ -23,12 +23,10 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.app.NotificationChannel;
import android.content.pm.UserInfo;
-import android.content.res.Configuration;
import android.os.Bundle;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
-import android.util.ArraySet;
import android.util.Pair;
import android.util.SparseArray;
@@ -37,11 +35,11 @@ import androidx.annotation.Nullable;
import com.android.wm.shell.common.annotations.ExternalThread;
-import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
@@ -92,24 +90,9 @@ public interface Bubbles {
*/
boolean isBubbleExpanded(String key);
- /** @return {@code true} if stack of bubbles is expanded or not. */
- boolean isStackExpanded();
-
- /**
- * Removes a group key indicating that the summary for this group should no longer be
- * suppressed.
- *
- * @param callback If removed, this callback will be called with the summary key of the group
- */
- void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
- Executor callbackExecutor);
-
/** Tell the stack of bubbles to collapse. */
void collapseStack();
- /** Tell the controller need update its UI to fit theme. */
- void updateForThemeChanges();
-
/**
* Request the stack expand if needed, then select the specified Bubble as current.
* If no bubble exists for this entry, one is created.
@@ -134,9 +117,6 @@ public interface Bubbles {
/** Called for any taskbar changes. */
void onTaskbarChanged(Bundle b);
- /** Open the overflow view. */
- void openBubbleOverflow();
-
/**
* We intercept notification entries (including group summaries) dismissed by the user when
* there is an active bubble associated with it. We do this so that developers can still
@@ -175,8 +155,9 @@ public interface Bubbles {
*
* @param entry the {@link BubbleEntry} by the notification.
* @param shouldBubbleUp {@code true} if this notification should bubble up.
+ * @param fromSystem {@code true} if this update is from NotificationManagerService.
*/
- void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp);
+ void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem);
/**
* Called when new notification entry removed.
@@ -214,6 +195,11 @@ public interface Bubbles {
int modificationType);
/**
+ * Called when notification panel is expanded or collapsed
+ */
+ void onNotificationPanelExpandedChanged(boolean expanded);
+
+ /**
* Called when the status bar has become visible or invisible (either permanently or
* temporarily).
*/
@@ -250,16 +236,6 @@ public interface Bubbles {
*/
void onUserRemoved(int removedUserId);
- /**
- * Called when config changed.
- *
- * @param newConfig the new config.
- */
- void onConfigChanged(Configuration newConfig);
-
- /** Description of current bubble state. */
- void dump(PrintWriter pw, String[] args);
-
/** Listener to find out about stack expansion / collapse events. */
interface BubbleExpandListener {
/**
@@ -285,11 +261,11 @@ public interface Bubbles {
/** Callback to tell SysUi components execute some methods. */
interface SysuiProxy {
- void isNotificationShadeExpand(Consumer<Boolean> callback);
+ void isNotificationPanelExpand(Consumer<Boolean> callback);
void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback);
- void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys,
+ void getShouldRestoredEntries(Set<String> savedBubbleKeys,
Consumer<List<BubbleEntry>> callback);
void setNotificationInterruption(String key);
@@ -300,14 +276,8 @@ public interface Bubbles {
void notifyInvalidateNotifications(String reason);
- void notifyMaybeCancelSummary(String key);
-
- void removeNotificationEntry(String key);
-
void updateNotificationBubbleButton(String key);
- void updateNotificationSuppression(String key);
-
void onStackExpandChanged(boolean shouldExpand);
void onManageMenuExpandChanged(boolean menuExpanded);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java
new file mode 100644
index 000000000000..e7beeeb06534
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java
@@ -0,0 +1,104 @@
+/*
+ * 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.bubbles;
+
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.InputChannel;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
+
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener;
+
+/**
+ * Set up tracking bubbles gestures that begin in navigation bar
+ */
+class BubblesNavBarGestureTracker {
+
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "BubblesGestureTracker" : TAG_BUBBLES;
+
+ private static final String GESTURE_MONITOR = "bubbles-gesture";
+ private final Context mContext;
+ private final BubblePositioner mPositioner;
+
+ @Nullable
+ private InputMonitor mInputMonitor;
+ @Nullable
+ private InputEventReceiver mInputEventReceiver;
+
+ BubblesNavBarGestureTracker(Context context, BubblePositioner positioner) {
+ mContext = context;
+ mPositioner = positioner;
+ }
+
+ /**
+ * Start tracking gestures
+ *
+ * @param listener listener that is notified of touch events
+ */
+ void start(MotionEventListener listener) {
+ if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) {
+ Log.d(TAG, "start monitoring bubbles swipe up gesture");
+ }
+
+ stopInternal();
+
+ mInputMonitor = InputManager.getInstance().monitorGestureInput(GESTURE_MONITOR,
+ mContext.getDisplayId());
+ InputChannel inputChannel = mInputMonitor.getInputChannel();
+
+ BubblesNavBarMotionEventHandler motionEventHandler =
+ new BubblesNavBarMotionEventHandler(mContext, mPositioner,
+ this::onInterceptTouch, listener);
+ mInputEventReceiver = new BubblesNavBarInputEventReceiver(inputChannel,
+ Choreographer.getInstance(), motionEventHandler);
+ }
+
+ void stop() {
+ if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) {
+ Log.d(TAG, "stop monitoring bubbles swipe up gesture");
+ }
+ stopInternal();
+ }
+
+ private void stopInternal() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ mInputMonitor = null;
+ }
+ }
+
+ private void onInterceptTouch() {
+ if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) {
+ Log.d(TAG, "intercept touch event");
+ }
+ if (mInputMonitor != null) {
+ mInputMonitor.pilferPointers();
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java
new file mode 100644
index 000000000000..45037b87830f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java
@@ -0,0 +1,51 @@
+/*
+ * 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.bubbles;
+
+import android.os.Looper;
+import android.view.BatchedInputEventReceiver;
+import android.view.Choreographer;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+
+/**
+ * Bubbles {@link BatchedInputEventReceiver} for monitoring touches from navbar gesture area
+ */
+class BubblesNavBarInputEventReceiver extends BatchedInputEventReceiver {
+
+ private final BubblesNavBarMotionEventHandler mMotionEventHandler;
+
+ BubblesNavBarInputEventReceiver(InputChannel inputChannel,
+ Choreographer choreographer, BubblesNavBarMotionEventHandler motionEventHandler) {
+ super(inputChannel, Looper.myLooper(), choreographer);
+ mMotionEventHandler = motionEventHandler;
+ }
+
+ @Override
+ public void onInputEvent(InputEvent event) {
+ boolean handled = false;
+ try {
+ if (!(event instanceof MotionEvent)) {
+ return;
+ }
+ handled = mMotionEventHandler.onMotionEvent((MotionEvent) event);
+ } finally {
+ finishInputEvent(event, handled);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java
new file mode 100644
index 000000000000..844526ca0f35
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java
@@ -0,0 +1,175 @@
+/*
+ * 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.bubbles;
+
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Handles {@link MotionEvent}s for bubbles that begin in the nav bar area
+ */
+class BubblesNavBarMotionEventHandler {
+ private static final String TAG =
+ TAG_WITH_CLASS_NAME ? "BubblesNavBarMotionEventHandler" : TAG_BUBBLES;
+ private static final int VELOCITY_UNITS = 1000;
+
+ private final Runnable mOnInterceptTouch;
+ private final MotionEventListener mMotionEventListener;
+ private final int mTouchSlop;
+ private final BubblePositioner mPositioner;
+ private final PointF mTouchDown = new PointF();
+ private boolean mTrackingTouches;
+ private boolean mInterceptingTouches;
+ @Nullable
+ private VelocityTracker mVelocityTracker;
+
+ BubblesNavBarMotionEventHandler(Context context, BubblePositioner positioner,
+ Runnable onInterceptTouch, MotionEventListener motionEventListener) {
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mPositioner = positioner;
+ mOnInterceptTouch = onInterceptTouch;
+ mMotionEventListener = motionEventListener;
+ }
+
+ /**
+ * Handle {@link MotionEvent} and forward it to {@code motionEventListener} defined in
+ * constructor
+ *
+ * @return {@code true} if this {@link MotionEvent} is handled (it started in the gesture area)
+ */
+ public boolean onMotionEvent(MotionEvent motionEvent) {
+ float dx = motionEvent.getX() - mTouchDown.x;
+ float dy = motionEvent.getY() - mTouchDown.y;
+
+ switch (motionEvent.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (isInGestureRegion(motionEvent)) {
+ mTouchDown.set(motionEvent.getX(), motionEvent.getY());
+ mMotionEventListener.onDown(motionEvent.getX(), motionEvent.getY());
+ mTrackingTouches = true;
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (mTrackingTouches) {
+ if (!mInterceptingTouches && Math.hypot(dx, dy) > mTouchSlop) {
+ mInterceptingTouches = true;
+ mOnInterceptTouch.run();
+ }
+ if (mInterceptingTouches) {
+ getVelocityTracker().addMovement(motionEvent);
+ mMotionEventListener.onMove(dx, dy);
+ }
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (mTrackingTouches) {
+ mMotionEventListener.onCancel();
+ finishTracking();
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mTrackingTouches) {
+ if (mInterceptingTouches) {
+ getVelocityTracker().computeCurrentVelocity(VELOCITY_UNITS);
+ mMotionEventListener.onUp(getVelocityTracker().getXVelocity(),
+ getVelocityTracker().getYVelocity());
+ }
+ finishTracking();
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ private boolean isInGestureRegion(MotionEvent ev) {
+ // Only handles touch events beginning in navigation bar system gesture zone
+ if (mPositioner.getNavBarGestureZone().contains((int) ev.getX(), (int) ev.getY())) {
+ if (DEBUG_BUBBLE_GESTURE) {
+ Log.d(TAG, "handling touch y=" + ev.getY()
+ + " navBarGestureZone=" + mPositioner.getNavBarGestureZone());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private VelocityTracker getVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ return mVelocityTracker;
+ }
+
+ private void finishTracking() {
+ mTouchDown.set(0, 0);
+ mTrackingTouches = false;
+ mInterceptingTouches = false;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ /**
+ * Callback for receiving {@link MotionEvent} updates
+ */
+ interface MotionEventListener {
+ /**
+ * Touch down action.
+ *
+ * @param x x coordinate
+ * @param y y coordinate
+ */
+ void onDown(float x, float y);
+
+ /**
+ * Move action.
+ * Reports distance from point reported in {@link #onDown(float, float)}
+ *
+ * @param dx distance moved on x-axis from starting point, in pixels
+ * @param dy distance moved on y-axis from starting point, in pixels
+ */
+ void onMove(float dx, float dy);
+
+ /**
+ * Touch up action.
+ *
+ * @param velX velocity of the move action on x axis
+ * @param velY velocity of the move actin on y axis
+ */
+ void onUp(float velX, float velY);
+
+ /**
+ * Motion action was cancelled.
+ */
+ void onCancel();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
index 063dac3d4109..ab194dfb3ce9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
@@ -24,8 +24,8 @@ import android.util.IntProperty
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
-import android.view.WindowManager
import android.view.WindowInsets
+import android.view.WindowManager
import android.widget.FrameLayout
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
@@ -41,6 +41,7 @@ class DismissView(context: Context) : FrameLayout(context) {
var circle = DismissCircleView(context)
var isShowing = false
+ var targetSizeResId: Int
private val animator = PhysicsAnimator.getInstance(circle)
private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY)
@@ -70,7 +71,8 @@ class DismissView(context: Context) : FrameLayout(context) {
setVisibility(View.INVISIBLE)
setBackgroundDrawable(gradientDrawable)
- val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size)
+ targetSizeResId = R.dimen.dismiss_circle_size
+ val targetSize: Int = resources.getDimensionPixelSize(targetSizeResId)
addView(circle, LayoutParams(targetSize, targetSize,
Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL))
// start with circle offscreen so it's animated up
@@ -126,7 +128,7 @@ class DismissView(context: Context) : FrameLayout(context) {
layoutParams.height = resources.getDimensionPixelSize(
R.dimen.floating_dismiss_gradient_height)
- val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size)
+ val targetSize = resources.getDimensionPixelSize(targetSizeResId)
circle.layoutParams.width = targetSize
circle.layoutParams.height = targetSize
circle.requestLayout()
@@ -153,4 +155,4 @@ class DismissView(context: Context) : FrameLayout(context) {
setPadding(0, 0, 0, navInset.bottom +
resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin))
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt
index cf0cefec401a..ea9d065d5f53 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt
@@ -17,8 +17,6 @@
package com.android.wm.shell.bubbles
import android.graphics.PointF
-import android.os.Handler
-import android.os.Looper
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
@@ -146,6 +144,12 @@ abstract class RelativeTouchListener : View.OnTouchListener {
velocityTracker.clear()
movedEnough = false
}
+
+ MotionEvent.ACTION_CANCEL -> {
+ v.handler.removeCallbacksAndMessages(null)
+ velocityTracker.clear()
+ movedEnough = false
+ }
}
return true
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 573f42468512..b91062f891e8 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
@@ -16,12 +16,17 @@
package com.android.wm.shell.bubbles.animation;
+import static android.view.View.LAYOUT_DIRECTION_RTL;
+
import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
+import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE;
+import static com.android.wm.shell.bubbles.BubbleStackView.HOME_GESTURE_ENABLED;
import android.content.res.Resources;
import android.graphics.Path;
import android.graphics.PointF;
import android.view.View;
+import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -61,7 +66,10 @@ public class ExpandedAnimationController
private static final float DAMPING_RATIO_MEDIUM_LOW_BOUNCY = 0.65f;
/** Stiffness for the expand/collapse path-following animation. */
- private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
+ private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 400;
+
+ /** Stiffness for the expand/collapse animation when home gesture handling is off */
+ private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS_WITHOUT_HOME_GESTURE = 1000;
/**
* Velocity required to dismiss an individual bubble without dragging it into the dismiss
@@ -73,6 +81,11 @@ public class ExpandedAnimationController
new PhysicsAnimator.SpringConfig(
EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY);
+ private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfigWithoutHomeGesture =
+ new PhysicsAnimator.SpringConfig(
+ EXPAND_COLLAPSE_ANIM_STIFFNESS_WITHOUT_HOME_GESTURE,
+ SpringForce.DAMPING_RATIO_NO_BOUNCY);
+
/** Horizontal offset between bubbles, which we need to know to re-stack them. */
private float mStackOffsetPx;
/** Size of each bubble. */
@@ -233,6 +246,11 @@ public class ExpandedAnimationController
};
}
+ boolean showBubblesVertically = mPositioner.showBubblesVertically();
+ final boolean isRtl =
+ mLayout.getContext().getResources().getConfiguration().getLayoutDirection()
+ == LAYOUT_DIRECTION_RTL;
+
// Animate each bubble individually, since each path will end in a different spot.
animationsForChildrenFromIndex(0, (index, animation) -> {
final View bubble = mLayout.getChildAt(index);
@@ -267,9 +285,20 @@ public class ExpandedAnimationController
// right side, the first bubble is traveling to the top left, so it leads. During
// collapse to the left, the first bubble has the shortest travel time back to the stack
// position, so it leads (and vice versa).
- final boolean firstBubbleLeads =
- (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
+ final boolean firstBubbleLeads;
+ if (showBubblesVertically || !isRtl) {
+ firstBubbleLeads =
+ (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
|| (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
+ } else {
+ // For RTL languages, when showing bubbles horizontally, it is reversed. The bubbles
+ // are positioned right to left. This means that when expanding from left, the top
+ // bubble will lead as it will be positioned on the right. And when expanding from
+ // right, the top bubble will have the least travel distance.
+ firstBubbleLeads =
+ (expanding && mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
+ || (!expanding && !mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
+ }
final int startDelay = firstBubbleLeads
? (index * 10)
: ((mLayout.getChildCount() - index) * 10);
@@ -278,11 +307,20 @@ public class ExpandedAnimationController
(firstBubbleLeads && index == 0)
|| (!firstBubbleLeads && index == mLayout.getChildCount() - 1);
+ Interpolator interpolator;
+ if (HOME_GESTURE_ENABLED) {
+ // When home gesture is enabled, we use a different animation timing for collapse
+ interpolator = expanding
+ ? Interpolators.EMPHASIZED_ACCELERATE : Interpolators.EMPHASIZED_DECELERATE;
+ } else {
+ interpolator = Interpolators.LINEAR;
+ }
+
animation
.followAnimatedTargetAlongPath(
path,
EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
- Interpolators.LINEAR /* targetAnimInterpolator */,
+ interpolator /* targetAnimInterpolator */,
isLeadBubble ? mLeadBubbleEndAction : null /* endAction */,
() -> mLeadBubbleEndAction = null /* endAction */)
.withStartDelay(startDelay)
@@ -329,6 +367,7 @@ public class ExpandedAnimationController
mMagnetizedBubbleDraggingOut.setMagnetListener(listener);
mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ mMagnetizedBubbleDraggingOut.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
}
private void springBubbleTo(View bubble, float x, float y) {
@@ -431,7 +470,7 @@ public class ExpandedAnimationController
}
/** Description of current animation controller state. */
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.println("ExpandedAnimationController state:");
pw.print(" isActive: "); pw.println(isActiveController());
pw.print(" animatingExpand: "); pw.println(mAnimatingExpand);
@@ -525,10 +564,16 @@ public class ExpandedAnimationController
finishRemoval.run();
mOnBubbleAnimatedOutAction.run();
} else {
+ PhysicsAnimator.SpringConfig springConfig;
+ if (HOME_GESTURE_ENABLED) {
+ springConfig = mAnimateOutSpringConfig;
+ } else {
+ springConfig = mAnimateOutSpringConfigWithoutHomeGesture;
+ }
PhysicsAnimator.getInstance(child)
.spring(DynamicAnimation.ALPHA, 0f)
- .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig)
- .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig)
+ .spring(DynamicAnimation.SCALE_X, 0f, springConfig)
+ .spring(DynamicAnimation.SCALE_Y, 0f, springConfig)
.withEndActions(finishRemoval, mOnBubbleAnimatedOutAction)
.start();
}
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
new file mode 100644
index 000000000000..8a33780bc8d5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java
@@ -0,0 +1,75 @@
+/*
+ * 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.bubbles.animation;
+
+import com.android.wm.shell.bubbles.BubbleExpandedView;
+
+/**
+ * Animation controller for bubble expanded view collapsing
+ */
+public interface ExpandedViewAnimationController {
+ /**
+ * Set expanded view that this controller is working with.
+ */
+ void setExpandedView(BubbleExpandedView expandedView);
+
+ /**
+ * Set current collapse value, in pixels.
+ *
+ * @param distance pixels that user dragged the view by
+ */
+ void updateDrag(float distance);
+
+ /**
+ * Set current swipe velocity.
+ * Velocity is directional:
+ * <ul>
+ * <li>velocity < 0 means swipe direction is up</li>
+ * <li>velocity > 0 means swipe direction is down</li>
+ * </ul>
+ */
+ void setSwipeVelocity(float velocity);
+
+ /**
+ * Check if view is dragged past collapse threshold or swipe up velocity exceeds min velocity
+ * required to collapse the view
+ */
+ boolean shouldCollapse();
+
+ /**
+ * Animate view to collapsed state
+ *
+ * @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
+ */
+ void animateCollapse(Runnable startStackCollapse, Runnable after);
+
+ /**
+ * Animate the view back to fully expanded state.
+ */
+ void animateBackToExpanded();
+
+ /**
+ * Animate view for IME visibility change
+ */
+ void animateForImeVisibilityChange(boolean visible);
+
+ /**
+ * 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
new file mode 100644
index 000000000000..845dca34b41f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java
@@ -0,0 +1,431 @@
+/*
+ * 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.bubbles.animation;
+
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_COLLAPSE_ANIMATOR;
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_EXPANDED_VIEW_DRAGGING;
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.wm.shell.bubbles.BubbleExpandedView.BACKGROUND_ALPHA;
+import static com.android.wm.shell.bubbles.BubbleExpandedView.BOTTOM_CLIP_PROPERTY;
+import static com.android.wm.shell.bubbles.BubbleExpandedView.CONTENT_ALPHA;
+import static com.android.wm.shell.bubbles.BubbleExpandedView.MANAGE_BUTTON_ALPHA;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.Log;
+import android.view.HapticFeedbackConstants;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.wm.shell.animation.FlingAnimationUtils;
+import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.bubbles.BubbleExpandedView;
+import com.android.wm.shell.bubbles.BubblePositioner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implementation of {@link ExpandedViewAnimationController} that uses a collapse animation to
+ * hide the {@link BubbleExpandedView}
+ */
+public class ExpandedViewAnimationControllerImpl implements ExpandedViewAnimationController {
+
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "ExpandedViewAnimCtrl" : TAG_BUBBLES;
+
+ private static final float COLLAPSE_THRESHOLD = 0.02f;
+
+ private static final int COLLAPSE_DURATION_MS = 250;
+
+ private static final int MANAGE_BUTTON_ANIM_DURATION_MS = 78;
+
+ private static final int CONTENT_OPACITY_ANIM_DELAY_MS = 93;
+ private static final int CONTENT_OPACITY_ANIM_DURATION_MS = 78;
+
+ private static final int BACKGROUND_OPACITY_ANIM_DELAY_MS = 172;
+ private static final int BACKGROUND_OPACITY_ANIM_DURATION_MS = 78;
+
+ /** Animation fraction threshold for content alpha animation when stack collapse should begin */
+ private static final float STACK_COLLAPSE_THRESHOLD = 0.5f;
+
+ private static final FloatPropertyCompat<ExpandedViewAnimationControllerImpl>
+ COLLAPSE_HEIGHT_PROPERTY =
+ new FloatPropertyCompat<ExpandedViewAnimationControllerImpl>("CollapseSpring") {
+ @Override
+ public float getValue(ExpandedViewAnimationControllerImpl controller) {
+ return controller.getCollapsedAmount();
+ }
+
+ @Override
+ public void setValue(ExpandedViewAnimationControllerImpl controller,
+ float value) {
+ controller.setCollapsedAmount(value);
+ }
+ };
+
+ private final int mMinFlingVelocity;
+ private float mSwipeUpVelocity;
+ private float mSwipeDownVelocity;
+ private final BubblePositioner mPositioner;
+ private final FlingAnimationUtils mFlingAnimationUtils;
+ private int mDraggedAmount;
+ private float mCollapsedAmount;
+ @Nullable
+ private BubbleExpandedView mExpandedView;
+ @Nullable
+ private AnimatorSet mCollapseAnimation;
+ private boolean mNotifiedAboutThreshold;
+ private SpringAnimation mBackToExpandedAnimation;
+ @Nullable
+ private ObjectAnimator mBottomClipAnim;
+
+ public ExpandedViewAnimationControllerImpl(Context context, BubblePositioner positioner) {
+ mFlingAnimationUtils = new FlingAnimationUtils(context.getResources().getDisplayMetrics(),
+ COLLAPSE_DURATION_MS / 1000f);
+ mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+ mPositioner = positioner;
+ }
+
+ private static void adjustAnimatorSetDuration(AnimatorSet animatorSet,
+ float durationAdjustment) {
+ for (Animator animator : animatorSet.getChildAnimations()) {
+ animator.setStartDelay((long) (animator.getStartDelay() * durationAdjustment));
+ animator.setDuration((long) (animator.getDuration() * durationAdjustment));
+ }
+ }
+
+ @Override
+ public void setExpandedView(BubbleExpandedView expandedView) {
+ if (mExpandedView != null) {
+ if (DEBUG_COLLAPSE_ANIMATOR) {
+ Log.d(TAG, "updating expandedView, resetting previous");
+ }
+ if (mCollapseAnimation != null) {
+ mCollapseAnimation.cancel();
+ }
+ if (mBackToExpandedAnimation != null) {
+ mBackToExpandedAnimation.cancel();
+ }
+ reset();
+ }
+ mExpandedView = expandedView;
+ }
+
+ @Override
+ public void updateDrag(float distance) {
+ if (mExpandedView != null) {
+ mDraggedAmount = OverScroll.dampedScroll(distance, mExpandedView.getContentHeight());
+
+ if (DEBUG_COLLAPSE_ANIMATOR && DEBUG_EXPANDED_VIEW_DRAGGING) {
+ Log.d(TAG, "updateDrag: distance=" + distance + " dragged=" + mDraggedAmount);
+ }
+
+ setCollapsedAmount(mDraggedAmount);
+
+ if (!mNotifiedAboutThreshold && isPastCollapseThreshold()) {
+ mNotifiedAboutThreshold = true;
+ if (DEBUG_COLLAPSE_ANIMATOR) {
+ Log.d(TAG, "notifying over collapse threshold");
+ }
+ vibrateIfEnabled();
+ }
+ }
+ }
+
+ @Override
+ public void setSwipeVelocity(float velocity) {
+ if (velocity < 0) {
+ mSwipeUpVelocity = Math.abs(velocity);
+ mSwipeDownVelocity = 0;
+ } else {
+ mSwipeUpVelocity = 0;
+ mSwipeDownVelocity = velocity;
+ }
+ }
+
+ @Override
+ public boolean shouldCollapse() {
+ if (mSwipeDownVelocity > mMinFlingVelocity) {
+ // Swipe velocity is positive and over fling velocity.
+ // This is a swipe down, always reset to expanded state, regardless of dragged amount.
+ if (DEBUG_COLLAPSE_ANIMATOR) {
+ Log.d(TAG,
+ "not collapsing expanded view, swipe down velocity: " + mSwipeDownVelocity
+ + " minV: " + mMinFlingVelocity);
+ }
+ return false;
+ }
+
+ if (mSwipeUpVelocity > mMinFlingVelocity) {
+ // Swiping up and over fling velocity, collapse the view.
+ if (DEBUG_COLLAPSE_ANIMATOR) {
+ Log.d(TAG,
+ "collapse expanded view, swipe up velocity: " + mSwipeUpVelocity + " minV: "
+ + mMinFlingVelocity);
+ }
+ return true;
+ }
+
+ if (isPastCollapseThreshold()) {
+ if (DEBUG_COLLAPSE_ANIMATOR) {
+ Log.d(TAG, "collapse expanded view, past threshold, dragged: " + mDraggedAmount);
+ }
+ return true;
+ }
+
+ if (DEBUG_COLLAPSE_ANIMATOR) {
+ Log.d(TAG, "not collapsing expanded view");
+ }
+
+ return false;
+ }
+
+ @Override
+ public void animateCollapse(Runnable startStackCollapse, Runnable after) {
+ if (DEBUG_COLLAPSE_ANIMATOR) {
+ Log.d(TAG,
+ "expandedView animate collapse swipeVel=" + mSwipeUpVelocity + " minFlingVel="
+ + mMinFlingVelocity);
+ }
+ if (mExpandedView != null) {
+ // Mark it as animating immediately to avoid updates to the view before animation starts
+ mExpandedView.setAnimating(true);
+
+ if (mCollapseAnimation != null) {
+ mCollapseAnimation.cancel();
+ }
+ mCollapseAnimation = createCollapseAnimation(mExpandedView, startStackCollapse, after);
+
+ if (mSwipeUpVelocity >= mMinFlingVelocity) {
+ int contentHeight = mExpandedView.getContentHeight();
+
+ // Use a temp animator to get adjusted duration value for swipe.
+ // This new value will be used to adjust animation times proportionally in the
+ // animator set. If we adjust animator set duration directly, all child animations
+ // will get the same animation time.
+ ValueAnimator tempAnimator = new ValueAnimator();
+ mFlingAnimationUtils.applyDismissing(tempAnimator, mCollapsedAmount, contentHeight,
+ mSwipeUpVelocity, (contentHeight - mCollapsedAmount));
+
+ float durationAdjustment =
+ (float) tempAnimator.getDuration() / COLLAPSE_DURATION_MS;
+
+ adjustAnimatorSetDuration(mCollapseAnimation, durationAdjustment);
+ mCollapseAnimation.setInterpolator(tempAnimator.getInterpolator());
+ }
+ mCollapseAnimation.start();
+ }
+ }
+
+ @Override
+ public void animateBackToExpanded() {
+ if (DEBUG_COLLAPSE_ANIMATOR) {
+ Log.d(TAG, "expandedView animate back to expanded");
+ }
+ BubbleExpandedView expandedView = mExpandedView;
+ if (expandedView == null) {
+ return;
+ }
+
+ expandedView.setAnimating(true);
+
+ mBackToExpandedAnimation = new SpringAnimation(this, COLLAPSE_HEIGHT_PROPERTY);
+ mBackToExpandedAnimation.setSpring(new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_LOW)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+ );
+ mBackToExpandedAnimation.addEndListener(new OneTimeEndListener() {
+ @Override
+ public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
+ float velocity) {
+ super.onAnimationEnd(animation, canceled, value, velocity);
+ mNotifiedAboutThreshold = false;
+ mBackToExpandedAnimation = null;
+ expandedView.setAnimating(false);
+ }
+ });
+ mBackToExpandedAnimation.setStartValue(mCollapsedAmount);
+ mBackToExpandedAnimation.animateToFinalPosition(0);
+ }
+
+ @Override
+ public void animateForImeVisibilityChange(boolean visible) {
+ if (mExpandedView != null) {
+ if (mBottomClipAnim != null) {
+ mBottomClipAnim.cancel();
+ }
+ int clip = 0;
+ if (visible) {
+ // Clip the expanded view at the top of the IME view
+ clip = mExpandedView.getContentBottomOnScreen() - mPositioner.getImeTop();
+ // Don't allow negative clip value
+ clip = Math.max(clip, 0);
+ }
+ mBottomClipAnim = ObjectAnimator.ofInt(mExpandedView, BOTTOM_CLIP_PROPERTY, clip);
+ mBottomClipAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mBottomClipAnim = null;
+ }
+ });
+ mBottomClipAnim.start();
+ }
+ }
+
+ @Override
+ public void reset() {
+ if (DEBUG_COLLAPSE_ANIMATOR) {
+ Log.d(TAG, "reset expandedView collapsed state");
+ }
+ if (mExpandedView == null) {
+ return;
+ }
+ mExpandedView.setAnimating(false);
+
+ if (mCollapseAnimation != null) {
+ mCollapseAnimation.cancel();
+ }
+ if (mBackToExpandedAnimation != null) {
+ mBackToExpandedAnimation.cancel();
+ }
+ mExpandedView.setContentAlpha(1);
+ mExpandedView.setBackgroundAlpha(1);
+ mExpandedView.setManageButtonAlpha(1);
+ setCollapsedAmount(0);
+ mExpandedView.setBottomClip(0);
+ mExpandedView.movePointerBy(0, 0);
+ mCollapsedAmount = 0;
+ mDraggedAmount = 0;
+ mSwipeUpVelocity = 0;
+ mSwipeDownVelocity = 0;
+ mNotifiedAboutThreshold = false;
+ }
+
+ private float getCollapsedAmount() {
+ return mCollapsedAmount;
+ }
+
+ private void setCollapsedAmount(float collapsed) {
+ if (mCollapsedAmount != collapsed) {
+ float previous = mCollapsedAmount;
+ mCollapsedAmount = collapsed;
+
+ if (mExpandedView != null) {
+ if (previous == 0) {
+ // View was not collapsed before. Apply z order change
+ mExpandedView.setSurfaceZOrderedOnTop(true);
+ mExpandedView.setAnimating(true);
+ }
+
+ mExpandedView.setTopClip((int) mCollapsedAmount);
+ // Move up with translationY. Use negative collapsed value
+ mExpandedView.setContentTranslationY(-mCollapsedAmount);
+ mExpandedView.setManageButtonTranslationY(-mCollapsedAmount);
+
+ if (mCollapsedAmount == 0) {
+ // View is no longer collapsed. Revert z order change
+ mExpandedView.setSurfaceZOrderedOnTop(false);
+ mExpandedView.setAnimating(false);
+ }
+ }
+ }
+ }
+
+ private boolean isPastCollapseThreshold() {
+ if (mExpandedView != null) {
+ return mDraggedAmount > mExpandedView.getContentHeight() * COLLAPSE_THRESHOLD;
+ }
+ return false;
+ }
+
+ private AnimatorSet createCollapseAnimation(BubbleExpandedView expandedView,
+ Runnable startStackCollapse, Runnable after) {
+ List<Animator> animatorList = new ArrayList<>();
+ animatorList.add(createHeightAnimation(expandedView));
+ animatorList.add(createManageButtonAnimation());
+ ObjectAnimator contentAlphaAnimation = createContentAlphaAnimation();
+ final boolean[] notified = {false};
+ contentAlphaAnimation.addUpdateListener(animation -> {
+ if (!notified[0] && animation.getAnimatedFraction() > STACK_COLLAPSE_THRESHOLD) {
+ notified[0] = true;
+ // Notify bubbles that they can start moving back to the collapsed position
+ startStackCollapse.run();
+ }
+ });
+ animatorList.add(contentAlphaAnimation);
+ animatorList.add(createBackgroundAlphaAnimation());
+
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ after.run();
+ }
+ });
+ animatorSet.playTogether(animatorList);
+ return animatorSet;
+ }
+
+ private ValueAnimator createHeightAnimation(BubbleExpandedView expandedView) {
+ ValueAnimator animator = ValueAnimator.ofInt((int) mCollapsedAmount,
+ expandedView.getContentHeight());
+ animator.setInterpolator(Interpolators.EMPHASIZED_ACCELERATE);
+ animator.setDuration(COLLAPSE_DURATION_MS);
+ animator.addUpdateListener(anim -> setCollapsedAmount((int) anim.getAnimatedValue()));
+ return animator;
+ }
+
+ private ObjectAnimator createManageButtonAnimation() {
+ ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, MANAGE_BUTTON_ALPHA, 0f);
+ animator.setDuration(MANAGE_BUTTON_ANIM_DURATION_MS);
+ animator.setInterpolator(Interpolators.LINEAR);
+ return animator;
+ }
+
+ private ObjectAnimator createContentAlphaAnimation() {
+ ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, CONTENT_ALPHA, 0f);
+ animator.setDuration(CONTENT_OPACITY_ANIM_DURATION_MS);
+ animator.setInterpolator(Interpolators.LINEAR);
+ animator.setStartDelay(CONTENT_OPACITY_ANIM_DELAY_MS);
+ return animator;
+ }
+
+ private ObjectAnimator createBackgroundAlphaAnimation() {
+ ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, BACKGROUND_ALPHA, 0f);
+ animator.setDuration(BACKGROUND_OPACITY_ANIM_DURATION_MS);
+ animator.setInterpolator(Interpolators.LINEAR);
+ animator.setStartDelay(BACKGROUND_OPACITY_ANIM_DELAY_MS);
+ return animator;
+ }
+
+ @SuppressLint("MissingPermission")
+ private void vibrateIfEnabled() {
+ if (mExpandedView != null) {
+ mExpandedView.performHapticFeedback(HapticFeedbackConstants.DRAG_CROSSING);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java
new file mode 100644
index 000000000000..bb8a3aaaf551
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java
@@ -0,0 +1,57 @@
+/*
+ * 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.bubbles.animation;
+
+import com.android.wm.shell.bubbles.BubbleExpandedView;
+
+/**
+ * Stub implementation {@link ExpandedViewAnimationController} that does not animate the
+ * {@link BubbleExpandedView}
+ */
+public class ExpandedViewAnimationControllerStub implements ExpandedViewAnimationController {
+ @Override
+ public void setExpandedView(BubbleExpandedView expandedView) {
+ }
+
+ @Override
+ public void updateDrag(float distance) {
+ }
+
+ @Override
+ public void setSwipeVelocity(float velocity) {
+ }
+
+ @Override
+ public boolean shouldCollapse() {
+ return false;
+ }
+
+ @Override
+ public void animateCollapse(Runnable startStackCollapse, Runnable after) {
+ }
+
+ @Override
+ public void animateBackToExpanded() {
+ }
+
+ @Override
+ public void animateForImeVisibilityChange(boolean visible) {
+ }
+
+ @Override
+ public void reset() {
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java
new file mode 100644
index 000000000000..d4e76ed0282e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java
@@ -0,0 +1,57 @@
+/*
+ * 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.bubbles.animation;
+
+/**
+ * Utility methods for overscroll damping and related effect.
+ *
+ * Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/OverScroll.java
+ */
+public class OverScroll {
+
+ private static final float OVERSCROLL_DAMP_FACTOR = 0.07f;
+
+ /**
+ * This curve determines how the effect of scrolling over the limits of the page diminishes
+ * as the user pulls further and further from the bounds
+ *
+ * @param f The percentage of how much the user has overscrolled.
+ * @return A transformed percentage based on the influence curve.
+ */
+ private static float overScrollInfluenceCurve(float f) {
+ f -= 1.0f;
+ return f * f * f + 1.0f;
+ }
+
+ /**
+ * @param amount The original amount overscrolled.
+ * @param max The maximum amount that the View can overscroll.
+ * @return The dampened overscroll amount.
+ */
+ public static int dampedScroll(float amount, int max) {
+ if (Float.compare(amount, 0) == 0) return 0;
+
+ float f = amount / max;
+ f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
+
+ // Clamp this factor, f, to -1 < f < 1
+ if (Math.abs(f) >= 1) {
+ f /= Math.abs(f);
+ }
+
+ return Math.round(OVERSCROLL_DAMP_FACTOR * f * max);
+ }
+}
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 0a1b4d70fb2b..961722ba9bc0 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
@@ -17,6 +17,7 @@
package com.android.wm.shell.bubbles.animation;
import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
+import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE;
import android.content.ContentResolver;
import android.content.res.Resources;
@@ -431,7 +432,7 @@ public class StackAnimationController extends
}
/** Description of current animation controller state. */
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.println("StackAnimationController state:");
pw.print(" isActive: "); pw.println(isActiveController());
pw.print(" restingStackPos: ");
@@ -1028,6 +1029,7 @@ public class StackAnimationController extends
};
mMagnetizedStack.setHapticsEnabled(true);
mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ mMagnetizedStack.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
}
final ContentResolver contentResolver = mLayout.getContext().getContentResolver();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
index 976fba52b9e2..e0c782d1675b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
@@ -49,6 +49,8 @@ public class DismissCircleView extends FrameLayout {
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
+ final Resources res = getResources();
+ setBackground(res.getDrawable(R.drawable.dismiss_circle_background));
setViewSizes();
}
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 c32733d4f73c..ae1f43320c8b 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
@@ -16,16 +16,19 @@
package com.android.wm.shell.common;
+import android.annotation.Nullable;
import android.os.RemoteException;
import android.util.Slog;
-import android.view.IDisplayWindowRotationCallback;
-import android.view.IDisplayWindowRotationController;
+import android.view.IDisplayChangeWindowCallback;
+import android.view.IDisplayChangeWindowController;
import android.view.IWindowManager;
+import android.window.DisplayAreaInfo;
import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -40,17 +43,22 @@ public class DisplayChangeController {
private final ShellExecutor mMainExecutor;
private final IWindowManager mWmService;
- private final IDisplayWindowRotationController mControllerImpl;
+ private final IDisplayChangeWindowController mControllerImpl;
- private final CopyOnWriteArrayList<OnDisplayChangingListener> mRotationListener =
+ private final CopyOnWriteArrayList<OnDisplayChangingListener> mDisplayChangeListener =
new CopyOnWriteArrayList<>();
- public DisplayChangeController(IWindowManager wmService, ShellExecutor mainExecutor) {
+ public DisplayChangeController(IWindowManager wmService, ShellInit shellInit,
+ ShellExecutor mainExecutor) {
mMainExecutor = mainExecutor;
mWmService = wmService;
- mControllerImpl = new DisplayWindowRotationControllerImpl();
+ mControllerImpl = new DisplayChangeWindowControllerImpl();
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
try {
- mWmService.setDisplayWindowRotationController(mControllerImpl);
+ mWmService.setDisplayChangeWindowController(mControllerImpl);
} catch (RemoteException e) {
throw new RuntimeException("Unable to register rotation controller");
}
@@ -59,63 +67,64 @@ public class DisplayChangeController {
/**
* Adds a display rotation controller.
*/
- public void addRotationListener(OnDisplayChangingListener listener) {
- mRotationListener.add(listener);
+ public void addDisplayChangeListener(OnDisplayChangingListener listener) {
+ mDisplayChangeListener.add(listener);
}
/**
* Removes a display rotation controller.
*/
- public void removeRotationListener(OnDisplayChangingListener listener) {
- mRotationListener.remove(listener);
+ public void removeDisplayChangeListener(OnDisplayChangingListener listener) {
+ mDisplayChangeListener.remove(listener);
}
- /** Query all listeners for changes that should happen on rotation. */
- public void dispatchOnRotateDisplay(WindowContainerTransaction outWct, int displayId,
- final int fromRotation, final int toRotation) {
- for (OnDisplayChangingListener c : mRotationListener) {
- c.onRotateDisplay(displayId, fromRotation, toRotation, outWct);
+ /** Query all listeners for changes that should happen on display change. */
+ public void dispatchOnDisplayChange(WindowContainerTransaction outWct, int displayId,
+ int fromRotation, int toRotation, DisplayAreaInfo newDisplayAreaInfo) {
+ for (OnDisplayChangingListener c : mDisplayChangeListener) {
+ c.onDisplayChange(displayId, fromRotation, toRotation, newDisplayAreaInfo, outWct);
}
}
- private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation,
- IDisplayWindowRotationCallback callback) {
+ private void onDisplayChange(int displayId, int fromRotation, int toRotation,
+ DisplayAreaInfo newDisplayAreaInfo, IDisplayChangeWindowCallback callback) {
WindowContainerTransaction t = new WindowContainerTransaction();
- dispatchOnRotateDisplay(t, displayId, fromRotation, toRotation);
+ dispatchOnDisplayChange(t, displayId, fromRotation, toRotation, newDisplayAreaInfo);
try {
- callback.continueRotateDisplay(toRotation, t);
+ callback.continueDisplayChange(t);
} catch (RemoteException e) {
- Slog.e(TAG, "Failed to continue rotation", e);
+ Slog.e(TAG, "Failed to continue handling display change", e);
}
}
@BinderThread
- private class DisplayWindowRotationControllerImpl
- extends IDisplayWindowRotationController.Stub {
+ private class DisplayChangeWindowControllerImpl
+ extends IDisplayChangeWindowController.Stub {
@Override
- public void onRotateDisplay(int displayId, final int fromRotation,
- final int toRotation, IDisplayWindowRotationCallback callback) {
- mMainExecutor.execute(() -> {
- DisplayChangeController.this.onRotateDisplay(displayId, fromRotation, toRotation,
- callback);
- });
+ public void onDisplayChange(int displayId, int fromRotation, int toRotation,
+ DisplayAreaInfo newDisplayAreaInfo, IDisplayChangeWindowCallback callback) {
+ mMainExecutor.execute(() -> DisplayChangeController.this
+ .onDisplayChange(displayId, fromRotation, toRotation,
+ newDisplayAreaInfo, callback));
}
}
/**
* Give a listener a chance to queue up configuration changes to execute as part of a
- * display rotation. The contents of {@link #onRotateDisplay} must run synchronously.
+ * display rotation. The contents of {@link #onDisplayChange} must run synchronously.
*/
@ShellMainThread
public interface OnDisplayChangingListener {
/**
- * Called before the display is rotated. Contents of this method must run synchronously.
- * @param displayId Id of display that is rotating.
- * @param fromRotation starting rotation of the display.
- * @param toRotation target rotation of the display (after rotating).
+ * Called before the display size has changed.
+ * Contents of this method must run synchronously.
+ * @param displayId display id of the display that is under the change
+ * @param fromRotation rotation before the change
+ * @param toRotation rotation after the change
+ * @param newDisplayAreaInfo display area info after applying the update
* @param t A task transaction to populate.
*/
- void onRotateDisplay(int displayId, int fromRotation, int toRotation,
- WindowContainerTransaction t);
+ void onDisplayChange(int displayId, int fromRotation, int toRotation,
+ @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction t);
}
}
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 4ba32e93fb3d..f07ea751b044 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,6 +34,7 @@ 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.sysui.ShellInit;
import java.util.ArrayList;
import java.util.List;
@@ -57,19 +58,23 @@ public class DisplayController {
private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>();
private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
- public DisplayController(Context context, IWindowManager wmService,
+ public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit,
ShellExecutor mainExecutor) {
mMainExecutor = mainExecutor;
mContext = context;
mWmService = wmService;
- mChangeController = new DisplayChangeController(mWmService, mainExecutor);
+ // TODO: Inject this instead
+ mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor);
mDisplayContainerListener = new DisplayWindowListenerImpl();
+ // Note, add this after DisplaceChangeController is constructed to ensure that is
+ // initialized first
+ shellInit.addInitCallback(this::onInit, this);
}
/**
* Initializes the window listener.
*/
- public void initialize() {
+ public void onInit() {
try {
int[] displayIds = mWmService.registerDisplayWindowListener(mDisplayContainerListener);
for (int i = 0; i < displayIds.length; i++) {
@@ -156,14 +161,14 @@ public class DisplayController {
* Adds a display rotation controller.
*/
public void addDisplayChangingController(OnDisplayChangingListener controller) {
- mChangeController.addRotationListener(controller);
+ mChangeController.addDisplayChangeListener(controller);
}
/**
* Removes a display rotation controller.
*/
public void removeDisplayChangingController(OnDisplayChangingListener controller) {
- mChangeController.removeRotationListener(controller);
+ mChangeController.removeDisplayChangeListener(controller);
}
private void onDisplayAdded(int displayId) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 6a2acf438302..266cf294a950 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -20,6 +20,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
+import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
@@ -43,6 +44,7 @@ import android.view.animation.PathInterpolator;
import androidx.annotation.VisibleForTesting;
import com.android.internal.view.IInputMethodManager;
+import com.android.wm.shell.sysui.ShellInit;
import java.util.ArrayList;
import java.util.concurrent.Executor;
@@ -73,18 +75,24 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>();
- public DisplayImeController(IWindowManager wmService, DisplayController displayController,
+ public DisplayImeController(IWindowManager wmService,
+ ShellInit shellInit,
+ DisplayController displayController,
DisplayInsetsController displayInsetsController,
- Executor mainExecutor, TransactionPool transactionPool) {
+ TransactionPool transactionPool,
+ Executor mainExecutor) {
mWmService = wmService;
mDisplayController = displayController;
mDisplayInsetsController = displayInsetsController;
mMainExecutor = mainExecutor;
mTransactionPool = transactionPool;
+ shellInit.addInitCallback(this::onInit, this);
}
- /** Starts monitor displays changes and set insets controller for each displays. */
- public void startMonitorDisplays() {
+ /**
+ * Starts monitor displays changes and set insets controller for each displays.
+ */
+ public void onInit() {
mDisplayController.addDisplayWindowListener(this);
}
@@ -324,7 +332,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
@Override
- public void topFocusedWindowChanged(String packageName,
+ public void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) {
// Do nothing
}
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 b6705446674a..90a01f8c5295 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.common;
+import android.content.ComponentName;
import android.os.RemoteException;
import android.util.Slog;
import android.util.SparseArray;
@@ -28,6 +29,7 @@ import android.view.InsetsVisibilities;
import androidx.annotation.BinderThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -44,17 +46,20 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners =
new SparseArray<>();
- public DisplayInsetsController(IWindowManager wmService, DisplayController displayController,
+ public DisplayInsetsController(IWindowManager wmService,
+ ShellInit shellInit,
+ DisplayController displayController,
ShellExecutor mainExecutor) {
mWmService = wmService;
mDisplayController = displayController;
mMainExecutor = mainExecutor;
+ shellInit.addInitCallback(this::onInit, this);
}
/**
* Starts listening for insets for each display.
**/
- public void initialize() {
+ public void onInit() {
mDisplayController.addDisplayWindowListener(this);
}
@@ -171,14 +176,14 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
}
}
- private void topFocusedWindowChanged(String packageName,
+ private void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) {
CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
if (listeners == null) {
return;
}
for (OnInsetsChangedListener listener : listeners) {
- listener.topFocusedWindowChanged(packageName, requestedVisibilities);
+ listener.topFocusedWindowChanged(component, requestedVisibilities);
}
}
@@ -186,10 +191,10 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
private class DisplayWindowInsetsControllerImpl
extends IDisplayWindowInsetsController.Stub {
@Override
- public void topFocusedWindowChanged(String packageName,
+ public void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) throws RemoteException {
mMainExecutor.execute(() -> {
- PerDisplay.this.topFocusedWindowChanged(packageName, requestedVisibilities);
+ PerDisplay.this.topFocusedWindowChanged(component, requestedVisibilities);
});
}
@@ -234,10 +239,10 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
/**
* Called when top focused window changes to determine whether or not to take over insets
* control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false.
- * @param packageName The name of the package that is open in the top focussed window.
+ * @param component The application component that is open in the top focussed window.
* @param requestedVisibilities The insets visibilities requested by the focussed window.
*/
- default void topFocusedWindowChanged(String packageName,
+ default void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) {}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
index 47f1e2e18255..96efeeb0c5eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -96,7 +96,8 @@ public class DisplayLayout {
/**
* Different from {@link #equals(Object)}, this method compares the basic geometry properties
- * of two {@link DisplayLayout} objects including width, height, rotation, density and cutout.
+ * of two {@link DisplayLayout} objects including width, height, rotation, density, cutout and
+ * insets.
* @return {@code true} if the given {@link DisplayLayout} is identical geometry wise.
*/
public boolean isSameGeometry(@NonNull DisplayLayout other) {
@@ -104,7 +105,8 @@ public class DisplayLayout {
&& mHeight == other.mHeight
&& mRotation == other.mRotation
&& mDensityDpi == other.mDensityDpi
- && Objects.equals(mCutout, other.mCutout);
+ && Objects.equals(mCutout, other.mCutout)
+ && Objects.equals(mStableInsets, other.mStableInsets);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java
index fd3aa05cfc06..ec344d345139 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java
@@ -18,7 +18,9 @@ package com.android.wm.shell.common;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.Context;
import android.text.TextUtils;
+import android.view.SurfaceControl;
import android.view.View;
import com.android.internal.jank.InteractionJankMonitor;
@@ -44,6 +46,24 @@ public class InteractionJankMonitorUtils {
}
/**
+ * Begin a trace session.
+ *
+ * @param cujType the specific {@link InteractionJankMonitor.CujType}.
+ * @param context the context
+ * @param surface the surface to trace
+ * @param tag the tag to distinguish different flow of same type CUJ.
+ */
+ public static void beginTracing(@InteractionJankMonitor.CujType int cujType,
+ @NonNull Context context, @NonNull SurfaceControl surface, @Nullable String tag) {
+ final InteractionJankMonitor.Configuration.Builder builder =
+ InteractionJankMonitor.Configuration.Builder.withSurface(cujType, context, surface);
+ if (!TextUtils.isEmpty(tag)) {
+ builder.setTag(tag);
+ }
+ InteractionJankMonitor.getInstance().begin(builder);
+ }
+
+ /**
* End a trace session.
*
* @param cujType the specific {@link InteractionJankMonitor.CujType}.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java
index c4bd73ba1b4a..2a1bf0ee42ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java
@@ -28,7 +28,7 @@ import java.util.function.Consumer;
public class ScreenshotUtils {
/**
- * Take a screenshot of the specified SurfaceControl.
+ * Takes a screenshot of the specified SurfaceControl.
*
* @param sc the SurfaceControl to take a screenshot of
* @param crop the crop to use when capturing the screenshot
@@ -49,11 +49,14 @@ public class ScreenshotUtils {
SurfaceControl mScreenshot = null;
SurfaceControl.Transaction mTransaction;
SurfaceControl mSurfaceControl;
+ SurfaceControl mParentSurfaceControl;
int mLayer;
- BufferConsumer(SurfaceControl.Transaction t, SurfaceControl sc, int layer) {
+ BufferConsumer(SurfaceControl.Transaction t, SurfaceControl sc, SurfaceControl parentSc,
+ int layer) {
mTransaction = t;
mSurfaceControl = sc;
+ mParentSurfaceControl = parentSc;
mLayer = layer;
}
@@ -72,7 +75,7 @@ public class ScreenshotUtils {
mTransaction.setBuffer(mScreenshot, buffer.getHardwareBuffer());
mTransaction.setColorSpace(mScreenshot, buffer.getColorSpace());
- mTransaction.reparent(mScreenshot, mSurfaceControl);
+ mTransaction.reparent(mScreenshot, mParentSurfaceControl);
mTransaction.setLayer(mScreenshot, mLayer);
mTransaction.show(mScreenshot);
mTransaction.apply();
@@ -80,7 +83,7 @@ public class ScreenshotUtils {
}
/**
- * Take a screenshot of the specified SurfaceControl.
+ * Takes a screenshot of the specified SurfaceControl.
*
* @param t the transaction used to set changes on the resulting screenshot.
* @param sc the SurfaceControl to take a screenshot of
@@ -91,7 +94,23 @@ public class ScreenshotUtils {
*/
public static SurfaceControl takeScreenshot(SurfaceControl.Transaction t, SurfaceControl sc,
Rect crop, int layer) {
- BufferConsumer consumer = new BufferConsumer(t, sc, layer);
+ return takeScreenshot(t, sc, sc /* parentSc */, crop, layer);
+ }
+
+ /**
+ * Takes a screenshot of the specified SurfaceControl.
+ *
+ * @param t the transaction used to set changes on the resulting screenshot.
+ * @param sc the SurfaceControl to take a screenshot of
+ * @param parentSc the SurfaceControl to attach the screenshot to.
+ * @param crop the crop to use when capturing the screenshot
+ * @param layer the layer to place the screenshot
+ *
+ * @return A SurfaceControl where the screenshot will be attached, or null if failed.
+ */
+ public static SurfaceControl takeScreenshot(SurfaceControl.Transaction t, SurfaceControl sc,
+ SurfaceControl parentSc, Rect crop, int layer) {
+ BufferConsumer consumer = new BufferConsumer(t, sc, parentSc, layer);
captureLayer(sc, crop, consumer);
return consumer.mScreenshot;
}
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 d5875c03ccd2..e270edb800bd 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
@@ -221,8 +221,7 @@ public class SystemWindows {
}
final Display display = mDisplayController.getDisplay(mDisplayId);
SurfaceControlViewHost viewRoot =
- new SurfaceControlViewHost(
- view.getContext(), display, wwm, true /* useSfChoreographer */);
+ new SurfaceControlViewHost(view.getContext(), display, wwm);
attrs.flags |= FLAG_HARDWARE_ACCELERATED;
viewRoot.setView(view, attrs);
mViewRoots.put(view, viewRoot);
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 6305959bb6ac..8bc16bcc9d9d 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
@@ -206,12 +206,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
mSplitLayout = layout;
mSplitWindowManager = splitWindowManager;
mViewHost = viewHost;
- mDividerBounds.set(layout.getDividerBounds());
+ layout.getDividerBounds(mDividerBounds);
onInsetsChanged(insetsState, false /* animate */);
}
void onInsetsChanged(InsetsState insetsState, boolean animate) {
- mTempRect.set(mSplitLayout.getDividerBounds());
+ mSplitLayout.getDividerBounds(mTempRect);
final InsetsSource taskBarInsetsSource =
insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
// Only insets the divider bar with task bar when it's expanded so that the rounded corners
@@ -286,6 +286,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
setTouching();
mStartPos = touchPos;
mMoving = false;
+ mSplitLayout.onStartDragging();
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(event);
@@ -301,7 +302,10 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
releaseTouching();
- if (!mMoving) break;
+ if (!mMoving) {
+ mSplitLayout.onDraggingCancelled();
+ break;
+ }
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(1000 /* units */);
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 484294ab295b..74f8bf9ac863 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
@@ -50,12 +50,15 @@ import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.SurfaceUtils;
+import java.util.function.Consumer;
+
/**
* Handles split decor like showing resizing hint for a specific split.
*/
public class SplitDecorManager extends WindowlessWindowManager {
private static final String TAG = SplitDecorManager.class.getSimpleName();
private static final String RESIZING_BACKGROUND_SURFACE_NAME = "ResizingBackground";
+ private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground";
private static final long FADE_DURATION = 133;
private final IconProvider mIconProvider;
@@ -67,6 +70,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
private SurfaceControl mHostLeash;
private SurfaceControl mIconLeash;
private SurfaceControl mBackgroundLeash;
+ private SurfaceControl mGapBackgroundLeash;
private boolean mShown;
private boolean mIsResizing;
@@ -141,6 +145,10 @@ public class SplitDecorManager extends WindowlessWindowManager {
t.remove(mBackgroundLeash);
mBackgroundLeash = null;
}
+ if (mGapBackgroundLeash != null) {
+ t.remove(mGapBackgroundLeash);
+ mGapBackgroundLeash = null;
+ }
mHostLeash = null;
mIcon = null;
mResizingIconView = null;
@@ -150,7 +158,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
/** Showing resizing hint. */
public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds,
- SurfaceControl.Transaction t) {
+ Rect sideBounds, SurfaceControl.Transaction t) {
if (mResizingIconView == null) {
return;
}
@@ -176,6 +184,19 @@ public class SplitDecorManager extends WindowlessWindowManager {
.setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
}
+ if (mGapBackgroundLeash == null) {
+ final boolean isLandscape = newBounds.height() == sideBounds.height();
+ final int left = isLandscape ? mBounds.width() : 0;
+ final int top = isLandscape ? 0 : mBounds.height();
+ mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
+ GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+ // Fill up another side bounds area.
+ t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask))
+ .setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2)
+ .setPosition(mGapBackgroundLeash, left, top)
+ .setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height());
+ }
+
if (mIcon == null && resizingTask.topActivityInfo != null) {
mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo);
mResizingIconView.setImageDrawable(mIcon);
@@ -193,7 +214,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
newBounds.height() / 2 - mIconSize / 2);
if (animate) {
- startFadeAnimation(show, false /* isResized */);
+ startFadeAnimation(show, null /* finishedConsumer */);
mShown = show;
}
}
@@ -224,14 +245,29 @@ public class SplitDecorManager extends WindowlessWindowManager {
mFadeAnimator.cancel();
}
if (mShown) {
- startFadeAnimation(false /* show */, true /* isResized */);
+ fadeOutDecor(null /* finishedCallback */);
} else {
// Decor surface is hidden so release it directly.
releaseDecor(t);
}
}
- private void startFadeAnimation(boolean show, boolean isResized) {
+ /** Fade-out decor surface with animation end callback, if decor is hidden, run the callback
+ * directly. */
+ public void fadeOutDecor(Runnable finishedCallback) {
+ if (mShown) {
+ startFadeAnimation(false /* show */, transaction -> {
+ releaseDecor(transaction);
+ if (finishedCallback != null) finishedCallback.run();
+ });
+ mShown = false;
+ } else {
+ if (finishedCallback != null) finishedCallback.run();
+ }
+ }
+
+ private void startFadeAnimation(boolean show,
+ Consumer<SurfaceControl.Transaction> finishedConsumer) {
final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
mFadeAnimator = ValueAnimator.ofFloat(0f, 1f);
mFadeAnimator.setDuration(FADE_DURATION);
@@ -249,7 +285,9 @@ public class SplitDecorManager extends WindowlessWindowManager {
@Override
public void onAnimationStart(@NonNull Animator animation) {
if (show) {
- animT.show(mBackgroundLeash).show(mIconLeash).apply();
+ animT.show(mBackgroundLeash).show(mIconLeash).show(mGapBackgroundLeash).apply();
+ } else {
+ animT.hide(mGapBackgroundLeash).apply();
}
}
@@ -263,8 +301,8 @@ public class SplitDecorManager extends WindowlessWindowManager {
animT.hide(mIconLeash);
}
}
- if (isResized) {
- releaseDecor(animT);
+ if (finishedConsumer != null) {
+ finishedConsumer.accept(animT);
}
animT.apply();
animT.close();
@@ -280,6 +318,11 @@ public class SplitDecorManager extends WindowlessWindowManager {
mBackgroundLeash = null;
}
+ if (mGapBackgroundLeash != null) {
+ t.remove(mGapBackgroundLeash);
+ mGapBackgroundLeash = null;
+ }
+
if (mIcon != null) {
mResizingIconView.setVisibility(View.GONE);
mResizingIconView.setImageDrawable(null);
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 c94455d9151a..419e62daf586 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
@@ -24,6 +24,7 @@ import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_RIGHT;
import static android.view.WindowManager.DOCKED_TOP;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE;
import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END;
import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START;
import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR;
@@ -31,9 +32,11 @@ import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -55,7 +58,6 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.policy.DividerSnapAlgorithm;
import com.android.internal.policy.DockedDividerUtils;
import com.android.wm.shell.R;
@@ -78,6 +80,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public static final int PARALLAX_DISMISSING = 1;
public static final int PARALLAX_ALIGN_CENTER = 2;
+ private static final int FLING_RESIZE_DURATION = 250;
+ private static final int FLING_SWITCH_DURATION = 350;
+ private static final int FLING_ENTER_DURATION = 350;
+ private static final int FLING_EXIT_DURATION = 350;
+
private final int mDividerWindowWidth;
private final int mDividerInsets;
private final int mDividerSize;
@@ -85,8 +92,13 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
private final Rect mTempRect = new Rect();
private final Rect mRootBounds = new Rect();
private final Rect mDividerBounds = new Rect();
+ // Bounds1 final position should be always at top or left
private final Rect mBounds1 = new Rect();
+ // Bounds2 final position should be always at bottom or right
private final Rect mBounds2 = new Rect();
+ // The temp bounds outside of display bounds for side stage when split screen inactive to avoid
+ // flicker next time active split screen.
+ private final Rect mInvisibleBounds = new Rect();
private final Rect mWinBounds1 = new Rect();
private final Rect mWinBounds2 = new Rect();
private final SplitLayoutHandler mSplitLayoutHandler;
@@ -135,6 +147,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
resetDividerPosition();
mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide);
+
+ updateInvisibleRect();
}
private int getDividerInsets(Resources resources, Display display) {
@@ -178,6 +192,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
return outBounds;
}
+ /** Gets root bounds of the whole split layout */
+ public Rect getRootBounds() {
+ return new Rect(mRootBounds);
+ }
+
/** Gets bounds of divider window with screen based coordinate. */
public Rect getDividerBounds() {
return new Rect(mDividerBounds);
@@ -190,6 +209,50 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
return outBounds;
}
+ /** Gets bounds of the primary split with screen based coordinate on the param Rect. */
+ public void getBounds1(Rect rect) {
+ rect.set(mBounds1);
+ }
+
+ /** Gets bounds of the primary split with parent based coordinate on the param Rect. */
+ public void getRefBounds1(Rect rect) {
+ getBounds1(rect);
+ rect.offset(-mRootBounds.left, -mRootBounds.top);
+ }
+
+ /** Gets bounds of the secondary split with screen based coordinate on the param Rect. */
+ public void getBounds2(Rect rect) {
+ rect.set(mBounds2);
+ }
+
+ /** Gets bounds of the secondary split with parent based coordinate on the param Rect. */
+ public void getRefBounds2(Rect rect) {
+ getBounds2(rect);
+ rect.offset(-mRootBounds.left, -mRootBounds.top);
+ }
+
+ /** Gets root bounds of the whole split layout on the param Rect. */
+ public void getRootBounds(Rect rect) {
+ rect.set(mRootBounds);
+ }
+
+ /** Gets bounds of divider window with screen based coordinate on the param Rect. */
+ public void getDividerBounds(Rect rect) {
+ rect.set(mDividerBounds);
+ }
+
+ /** Gets bounds of divider window with parent based coordinate on the param Rect. */
+ public void getRefDividerBounds(Rect rect) {
+ getDividerBounds(rect);
+ rect.offset(-mRootBounds.left, -mRootBounds.top);
+ }
+
+ /** Gets bounds size equal to root bounds but outside of screen, used for position side stage
+ * when split inactive to avoid flicker when next time active. */
+ public void getInvisibleBounds(Rect rect) {
+ rect.set(mInvisibleBounds);
+ }
+
/** Returns leash of the current divider bar. */
@Nullable
public SurfaceControl getDividerLeash() {
@@ -209,31 +272,40 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
: (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom));
}
+ private void updateInvisibleRect() {
+ mInvisibleBounds.set(mRootBounds.left, mRootBounds.top,
+ isLandscape() ? mRootBounds.right / 2 : mRootBounds.right,
+ isLandscape() ? mRootBounds.bottom : mRootBounds.bottom / 2);
+ mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0,
+ isLandscape() ? 0 : mRootBounds.bottom);
+ }
+
/** Applies new configuration, returns {@code false} if there's no effect to the layout. */
public boolean updateConfiguration(Configuration configuration) {
- // Always update configuration after orientation changed to make sure to render divider bar
- // with proper resources that matching screen orientation.
- final int orientation = configuration.orientation;
- if (mOrientation != orientation) {
- mContext = mContext.createConfigurationContext(configuration);
- mSplitWindowManager.setConfiguration(configuration);
- mOrientation = orientation;
- }
-
// Update the split bounds when necessary. Besides root bounds changed, split bounds need to
// be updated when the rotation changed to cover the case that users rotated the screen 180
// degrees.
+ // Make sure to render the divider bar with proper resources that matching the screen
+ // orientation.
final int rotation = configuration.windowConfiguration.getRotation();
final Rect rootBounds = configuration.windowConfiguration.getBounds();
- if (mRotation == rotation && mRootBounds.equals(rootBounds)) {
+ final int orientation = configuration.orientation;
+
+ if (mOrientation == orientation
+ && mRotation == rotation
+ && mRootBounds.equals(rootBounds)) {
return false;
}
+ mContext = mContext.createConfigurationContext(configuration);
+ mSplitWindowManager.setConfiguration(configuration);
+ mOrientation = orientation;
mTempRect.set(mRootBounds);
mRootBounds.set(rootBounds);
mRotation = rotation;
mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
initDividerPosition(mTempRect);
+ updateInvisibleRect();
return true;
}
@@ -270,28 +342,35 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
updateBounds(mDividePosition);
}
- /** Updates recording bounds of divider window and both of the splits. */
private void updateBounds(int position) {
- mDividerBounds.set(mRootBounds);
- mBounds1.set(mRootBounds);
- mBounds2.set(mRootBounds);
+ updateBounds(position, mBounds1, mBounds2, mDividerBounds, true /* setEffectBounds */);
+ }
+
+ /** Updates recording bounds of divider window and both of the splits. */
+ private void updateBounds(int position, Rect bounds1, Rect bounds2, Rect dividerBounds,
+ boolean setEffectBounds) {
+ dividerBounds.set(mRootBounds);
+ bounds1.set(mRootBounds);
+ bounds2.set(mRootBounds);
final boolean isLandscape = isLandscape(mRootBounds);
if (isLandscape) {
position += mRootBounds.left;
- mDividerBounds.left = position - mDividerInsets;
- mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth;
- mBounds1.right = position;
- mBounds2.left = mBounds1.right + mDividerSize;
+ dividerBounds.left = position - mDividerInsets;
+ dividerBounds.right = dividerBounds.left + mDividerWindowWidth;
+ bounds1.right = position;
+ bounds2.left = bounds1.right + mDividerSize;
} else {
position += mRootBounds.top;
- mDividerBounds.top = position - mDividerInsets;
- mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth;
- mBounds1.bottom = position;
- mBounds2.top = mBounds1.bottom + mDividerSize;
+ dividerBounds.top = position - mDividerInsets;
+ dividerBounds.bottom = dividerBounds.top + mDividerWindowWidth;
+ bounds1.bottom = position;
+ bounds2.top = bounds1.bottom + mDividerSize;
+ }
+ DockedDividerUtils.sanitizeStackBounds(bounds1, true /** topLeft */);
+ DockedDividerUtils.sanitizeStackBounds(bounds2, false /** topLeft */);
+ if (setEffectBounds) {
+ mSurfaceEffectPolicy.applyDividerPosition(position, isLandscape);
}
- DockedDividerUtils.sanitizeStackBounds(mBounds1, true /** topLeft */);
- DockedDividerUtils.sanitizeStackBounds(mBounds2, false /** topLeft */);
- mSurfaceEffectPolicy.applyDividerPosition(position, isLandscape);
}
/** Inflates {@link DividerView} on the root surface. */
@@ -349,6 +428,13 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
mFreezeDividerWindow = freezeDividerWindow;
}
+ /** Update current layout as divider put on start or end position. */
+ public void setDividerAtBorder(boolean start) {
+ final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position
+ : mDividerSnapAlgorithm.getDismissEndTarget().position;
+ setDividePosition(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.
@@ -393,20 +479,31 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
switch (snapTarget.flag) {
case FLAG_DISMISS_START:
- flingDividePosition(currentPosition, snapTarget.position,
- () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */));
+ flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
+ () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */,
+ EXIT_REASON_DRAG_DIVIDER));
break;
case FLAG_DISMISS_END:
- flingDividePosition(currentPosition, snapTarget.position,
- () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */));
+ flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
+ () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */,
+ EXIT_REASON_DRAG_DIVIDER));
break;
default:
- flingDividePosition(currentPosition, snapTarget.position,
+ flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> setDividePosition(snapTarget.position, true /* applyLayoutChange */));
break;
}
}
+ void onStartDragging() {
+ InteractionJankMonitorUtils.beginTracing(CUJ_SPLIT_SCREEN_RESIZE, mContext,
+ getDividerLeash(), null /* tag */);
+ }
+
+ void onDraggingCancelled() {
+ InteractionJankMonitorUtils.cancelTracing(CUJ_SPLIT_SCREEN_RESIZE);
+ }
+
void onDoubleTappedDivider() {
mSplitLayoutHandler.onDoubleTappedDivider();
}
@@ -423,28 +520,58 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds,
@Nullable Rect stableInsets) {
final boolean isLandscape = isLandscape(rootBounds);
+ final Rect insets = stableInsets != null ? stableInsets : getDisplayInsets(context);
+
+ // Make split axis insets value same as the larger one to avoid bounds1 and bounds2
+ // have difference for avoiding size-compat mode when switching unresizable apps in
+ // landscape while they are letterboxed.
+ if (!isLandscape) {
+ final int largerInsets = Math.max(insets.top, insets.bottom);
+ insets.set(insets.left, largerInsets, insets.right, largerInsets);
+ }
+
return new DividerSnapAlgorithm(
context.getResources(),
rootBounds.width(),
rootBounds.height(),
mDividerSize,
!isLandscape,
- stableInsets != null ? stableInsets : getDisplayInsets(context),
+ insets,
isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
}
+ /** Fling divider from current position to end or start position then exit */
+ public void flingDividerToDismiss(boolean toEnd, int reason) {
+ final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position
+ : mDividerSnapAlgorithm.getDismissStartTarget().position;
+ flingDividePosition(getDividePosition(), 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 */));
+ }
+
@VisibleForTesting
- void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) {
+ void flingDividePosition(int from, int to, int duration,
+ @Nullable Runnable flingFinishedCallback) {
if (from == to) {
// No animation run, still callback to stop resizing.
mSplitLayoutHandler.onLayoutSizeChanged(this);
+
+ if (flingFinishedCallback != null) {
+ flingFinishedCallback.run();
+ }
+ InteractionJankMonitorUtils.endTracing(
+ CUJ_SPLIT_SCREEN_RESIZE);
return;
}
- InteractionJankMonitorUtils.beginTracing(InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE,
- mSplitWindowManager.getDividerView(), "Divider fling");
ValueAnimator animator = ValueAnimator
.ofInt(from, to)
- .setDuration(250);
+ .setDuration(duration);
animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
animator.addUpdateListener(
animation -> updateDivideBounds((int) animation.getAnimatedValue()));
@@ -455,7 +582,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
flingFinishedCallback.run();
}
InteractionJankMonitorUtils.endTracing(
- InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE);
+ CUJ_SPLIT_SCREEN_RESIZE);
}
@Override
@@ -466,6 +593,86 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
animator.start();
}
+ /** Swich both surface position with animation. */
+ public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1,
+ SurfaceControl leash2, Runnable finishCallback) {
+ final boolean isLandscape = isLandscape();
+ final Rect insets = getDisplayInsets(mContext);
+ insets.set(isLandscape ? insets.left : 0, isLandscape ? 0 : insets.top,
+ isLandscape ? insets.right : 0, isLandscape ? 0 : insets.bottom);
+
+ final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
+ isLandscape ? mBounds2.width() : mBounds2.height()).position;
+ final Rect distBounds1 = new Rect();
+ final Rect distBounds2 = new Rect();
+ final Rect distDividerBounds = new Rect();
+ // Compute dist bounds.
+ updateBounds(dividerPos, distBounds2, distBounds1, distDividerBounds,
+ false /* setEffectBounds */);
+ // Offset to real position under root container.
+ distBounds1.offset(-mRootBounds.left, -mRootBounds.top);
+ distBounds2.offset(-mRootBounds.left, -mRootBounds.top);
+ distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top);
+ // DO NOT move to insets area for smooth animation.
+ distBounds1.set(distBounds1.left, distBounds1.top,
+ distBounds1.right - insets.right, distBounds1.bottom - insets.bottom);
+ distBounds2.set(distBounds2.left + insets.left, distBounds2.top + insets.top,
+ distBounds2.right, distBounds2.bottom);
+
+ ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1,
+ false /* alignStart */);
+ ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2,
+ true /* alignStart */);
+ ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(),
+ distDividerBounds, true /* alignStart */);
+
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(animator1, animator2, animator3);
+ set.setDuration(FLING_SWITCH_DURATION);
+ set.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mDividePosition = dividerPos;
+ updateBounds(mDividePosition);
+ finishCallback.run();
+ }
+ });
+ set.start();
+ }
+
+ private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash,
+ Rect start, Rect end, boolean alignStart) {
+ Rect tempStart = new Rect(start);
+ Rect tempEnd = new Rect(end);
+ final float diffX = tempEnd.left - tempStart.left;
+ final float diffY = tempEnd.top - tempStart.top;
+ final float diffWidth = tempEnd.width() - tempStart.width();
+ final float diffHeight = tempEnd.height() - tempStart.height();
+ ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+ animator.addUpdateListener(animation -> {
+ if (leash == null) return;
+
+ final float scale = (float) animation.getAnimatedValue();
+ final float distX = tempStart.left + scale * diffX;
+ final float distY = tempStart.top + scale * diffY;
+ final int width = (int) (tempStart.width() + scale * diffWidth);
+ final int height = (int) (tempStart.height() + scale * diffHeight);
+ if (alignStart) {
+ t.setPosition(leash, distX, distY);
+ t.setWindowCrop(leash, width, height);
+ } else {
+ final int offsetX = width - tempStart.width();
+ final int offsetY = height - tempStart.height();
+ t.setPosition(leash, distX + offsetX, distY + offsetY);
+ mTempRect.set(0, 0, width, height);
+ mTempRect.offsetTo(-offsetX, -offsetY);
+ t.setCrop(leash, mTempRect);
+ }
+ t.apply();
+ });
+ return animator;
+ }
+
private static Rect getDisplayInsets(Context context) {
return context.getSystemService(WindowManager.class)
.getMaximumWindowMetrics()
@@ -504,15 +711,15 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
boolean applyResizingOffset) {
final SurfaceControl dividerLeash = getDividerLeash();
if (dividerLeash != null) {
- mTempRect.set(getRefDividerBounds());
+ getRefDividerBounds(mTempRect);
t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
// Resets layer of divider bar to make sure it is always on top.
t.setLayer(dividerLeash, Integer.MAX_VALUE);
}
- mTempRect.set(getRefBounds1());
+ getRefBounds1(mTempRect);
t.setPosition(leash1, mTempRect.left, mTempRect.top)
.setWindowCrop(leash1, mTempRect.width(), mTempRect.height());
- mTempRect.set(getRefBounds2());
+ getRefBounds2(mTempRect);
t.setPosition(leash2, mTempRect.left, mTempRect.top)
.setWindowCrop(leash2, mTempRect.width(), mTempRect.height());
@@ -560,31 +767,23 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) {
if (offsetX == 0 && offsetY == 0) {
wct.setBounds(taskInfo1.token, mBounds1);
- wct.setAppBounds(taskInfo1.token, null);
wct.setScreenSizeDp(taskInfo1.token,
SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
wct.setBounds(taskInfo2.token, mBounds2);
- wct.setAppBounds(taskInfo2.token, null);
wct.setScreenSizeDp(taskInfo2.token,
SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
} else {
- mTempRect.set(taskInfo1.configuration.windowConfiguration.getBounds());
+ getBounds1(mTempRect);
mTempRect.offset(offsetX, offsetY);
wct.setBounds(taskInfo1.token, mTempRect);
- mTempRect.set(taskInfo1.configuration.windowConfiguration.getAppBounds());
- mTempRect.offset(offsetX, offsetY);
- wct.setAppBounds(taskInfo1.token, mTempRect);
wct.setScreenSizeDp(taskInfo1.token,
taskInfo1.configuration.screenWidthDp,
taskInfo1.configuration.screenHeightDp);
- mTempRect.set(taskInfo2.configuration.windowConfiguration.getBounds());
+ getBounds2(mTempRect);
mTempRect.offset(offsetX, offsetY);
wct.setBounds(taskInfo2.token, mTempRect);
- mTempRect.set(taskInfo2.configuration.windowConfiguration.getAppBounds());
- mTempRect.offset(offsetX, offsetY);
- wct.setAppBounds(taskInfo2.token, mTempRect);
wct.setScreenSizeDp(taskInfo2.token,
taskInfo2.configuration.screenWidthDp,
taskInfo2.configuration.screenHeightDp);
@@ -602,7 +801,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public interface SplitLayoutHandler {
/** Calls when dismissing split. */
- void onSnappedToDismiss(boolean snappedToEnd);
+ void onSnappedToDismiss(boolean snappedToEnd, int reason);
/**
* Calls when resizing the split bounds.
@@ -971,16 +1170,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
boolean adjusted = false;
if (mYOffsetForIme != 0) {
if (dividerLeash != null) {
- mTempRect.set(mDividerBounds);
+ getRefDividerBounds(mTempRect);
mTempRect.offset(0, mYOffsetForIme);
t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
}
- mTempRect.set(mBounds1);
+ getRefBounds1(mTempRect);
mTempRect.offset(0, mYOffsetForIme);
t.setPosition(leash1, mTempRect.left, mTempRect.top);
- mTempRect.set(mBounds2);
+ getRefBounds2(mTempRect);
mTempRect.offset(0, mYOffsetForIme);
t.setPosition(leash2, mTempRect.left, mTempRect.top);
adjusted = true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
index 9b614875119b..b8204d013105 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
@@ -15,6 +15,12 @@
*/
package com.android.wm.shell.common.split;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
+
import android.annotation.IntDef;
/** Helper utility class of methods and constants that are available to be imported in Launcher. */
@@ -44,4 +50,13 @@ public class SplitScreenConstants {
})
public @interface SplitPosition {
}
+
+ public static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD};
+ public static final int[] CONTROLLED_WINDOWING_MODES =
+ {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
+ public static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
+ {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
+
+ /** Flag applied to a transition change to identify it as a divider bar for animation. */
+ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java
deleted file mode 100644
index b87cf47dd93f..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.compatui;
-
-import com.android.wm.shell.common.annotations.ExternalThread;
-
-/**
- * Interface to engage compat UI.
- */
-@ExternalThread
-public interface CompatUI {
- /**
- * Called when the keyguard showing state changes. Removes all compat UIs if the
- * keyguard is now showing.
- *
- * <p>Note that if the keyguard is occluded it will also be considered showing.
- *
- * @param showing indicates if the keyguard is now showing.
- */
- void onKeyguardShowingChanged(boolean showing);
-}
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 99b32a677abe..235fd9c469ea 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
@@ -39,9 +39,11 @@ import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListen
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.annotations.ExternalThread;
import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager;
+import com.android.wm.shell.sysui.KeyguardChangeListener;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import java.lang.ref.WeakReference;
@@ -58,7 +60,7 @@ import dagger.Lazy;
* activities are in compatibility mode.
*/
public class CompatUIController implements OnDisplaysChangedListener,
- DisplayImeController.ImePositionProcessor {
+ DisplayImeController.ImePositionProcessor, KeyguardChangeListener {
/** Callback for compat UI interaction. */
public interface CompatUICallback {
@@ -100,13 +102,13 @@ public class CompatUIController implements OnDisplaysChangedListener,
private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0);
private final Context mContext;
+ private final ShellController mShellController;
private final DisplayController mDisplayController;
private final DisplayInsetsController mDisplayInsetsController;
private final DisplayImeController mImeController;
private final SyncTransactionQueue mSyncQueue;
private final ShellExecutor mMainExecutor;
private final Lazy<Transitions> mTransitionsLazy;
- private final CompatUIImpl mImpl = new CompatUIImpl();
private CompatUICallback mCallback;
@@ -118,6 +120,8 @@ public class CompatUIController implements OnDisplaysChangedListener,
private boolean mKeyguardShowing;
public CompatUIController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
DisplayController displayController,
DisplayInsetsController displayInsetsController,
DisplayImeController imeController,
@@ -125,20 +129,21 @@ public class CompatUIController implements OnDisplaysChangedListener,
ShellExecutor mainExecutor,
Lazy<Transitions> transitionsLazy) {
mContext = context;
+ mShellController = shellController;
mDisplayController = displayController;
mDisplayInsetsController = displayInsetsController;
mImeController = imeController;
mSyncQueue = syncQueue;
mMainExecutor = mainExecutor;
mTransitionsLazy = transitionsLazy;
- mDisplayController.addDisplayWindowListener(this);
- mImeController.addPositionProcessor(this);
mCompatUIHintsState = new CompatUIHintsState();
+ shellInit.addInitCallback(this::onInit, this);
}
- /** Returns implementation of {@link CompatUI}. */
- public CompatUI asCompatUI() {
- return mImpl;
+ private void onInit() {
+ mShellController.addKeyguardChangeListener(this);
+ mDisplayController.addDisplayWindowListener(this);
+ mImeController.addPositionProcessor(this);
}
/** Sets the callback for UI interactions. */
@@ -223,9 +228,10 @@ public class CompatUIController implements OnDisplaysChangedListener,
layout -> layout.updateVisibility(showOnDisplay(displayId)));
}
- @VisibleForTesting
- void onKeyguardShowingChanged(boolean showing) {
- mKeyguardShowing = showing;
+ @Override
+ public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
+ boolean animatingDismiss) {
+ mKeyguardShowing = visible;
// Hide the compat UIs when keyguard is showing.
forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId())));
}
@@ -373,19 +379,6 @@ public class CompatUIController implements OnDisplaysChangedListener,
}
}
- /**
- * The interface for calls from outside the Shell, within the host process.
- */
- @ExternalThread
- private class CompatUIImpl implements CompatUI {
- @Override
- public void onKeyguardShowingChanged(boolean showing) {
- mMainExecutor.execute(() -> {
- CompatUIController.this.onKeyguardShowingChanged(showing);
- });
- }
- }
-
/** An implementation of {@link OnInsetsChangedListener} for a given display id. */
private class PerDisplayOnInsetsChangedListener implements OnInsetsChangedListener {
final int mDisplayId;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java
index 806f795d1015..10b121bbc32c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java
@@ -92,6 +92,8 @@ import javax.inject.Qualifier;
*
* For example, this uses the same setup as above, but the interface provided (if bound) is used
* otherwise the default is created:
+ *
+ * BaseModule:
* @BindsOptionalOf
* @DynamicOverride
* abstract Interface dynamicInterface();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt
deleted file mode 100644
index 1cd69edf7cd2..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-The dagger modules in this directory can be included by the host SysUI using the Shell library for
-explicity injection of Shell components. Apps using this library are not required to use these
-dagger modules for setup, but it is recommended for them to include them as needed.
-
-The modules are currently inherited as such:
-
-+- WMShellBaseModule (common shell features across SysUI)
- |
- +- WMShellModule (handheld)
- |
- +- TvPipModule (tv pip)
- |
- +- TvWMShellModule (tv) \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java
new file mode 100644
index 000000000000..482b19983850
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java
@@ -0,0 +1,38 @@
+/*
+ * 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.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * An annotation to specifically mark the provider that is triggering the creation of independent
+ * shell components that are not created as a part of the dependencies for interfaces passed to
+ * SysUI.
+ *
+ * TODO: This will be removed once we have a more explicit method for specifying components to start
+ * with SysUI
+ */
+@Documented
+@Inherited
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ShellCreateTrigger {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java
new file mode 100644
index 000000000000..31c678968a25
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java
@@ -0,0 +1,38 @@
+/*
+ * 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.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * An annotation for non-base modules to specifically mark the provider that is triggering the
+ * creation of independent shell components that are not created as a part of the dependencies for
+ * interfaces passed to SysUI.
+ *
+ * TODO: This will be removed once we have a more explicit method for specifying components to start
+ * with SysUI
+ */
+@Documented
+@Inherited
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ShellCreateTriggerOverride {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
index 1ea5e21a2c1e..8022e9b1cd81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
@@ -48,6 +48,8 @@ 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.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import java.util.Optional;
@@ -64,6 +66,8 @@ public abstract class TvPipModule {
@Provides
static Optional<Pip> providePip(
Context context,
+ ShellInit shellInit,
+ ShellController shellController,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
@@ -81,6 +85,8 @@ public abstract class TvPipModule {
return Optional.of(
TvPipController.create(
context,
+ shellInit,
+ shellController,
tvPipBoundsState,
tvPipBoundsAlgorithm,
tvPipBoundsController,
@@ -135,12 +141,14 @@ public abstract class TvPipModule {
@WMSingleton
@Provides
static PipTransitionController provideTvPipTransition(
- Transitions transitions, ShellTaskOrganizer shellTaskOrganizer,
+ ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
+ Transitions transitions,
PipAnimationController pipAnimationController,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsState tvPipBoundsState, TvPipMenuController pipMenuController) {
- return new TvPipTransition(tvPipBoundsState, pipMenuController,
- tvPipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer);
+ return new TvPipTransition(shellInit, shellTaskOrganizer, transitions, tvPipBoundsState,
+ pipMenuController, tvPipBoundsAlgorithm, pipAnimationController);
}
@WMSingleton
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 db6131a17114..c25bbbf06dda 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
@@ -24,22 +24,20 @@ import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.SystemProperties;
import android.view.IWindowManager;
+import android.view.WindowManager;
import com.android.internal.logging.UiEventLogger;
import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.ProtoLogController;
+import com.android.wm.shell.R;
import com.android.wm.shell.RootDisplayAreaOrganizer;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
-import com.android.wm.shell.ShellCommandHandler;
-import com.android.wm.shell.ShellCommandHandlerImpl;
-import com.android.wm.shell.ShellInit;
-import com.android.wm.shell.ShellInitImpl;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.TaskViewFactory;
import com.android.wm.shell.TaskViewFactoryController;
import com.android.wm.shell.TaskViewTransitions;
import com.android.wm.shell.WindowManagerShellWrapper;
-import com.android.wm.shell.apppairs.AppPairs;
-import com.android.wm.shell.apppairs.AppPairsController;
+import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.back.BackAnimationController;
import com.android.wm.shell.bubbles.BubbleController;
@@ -58,20 +56,20 @@ 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.compatui.CompatUI;
import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeController;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
-import com.android.wm.shell.draganddrop.DragAndDrop;
import com.android.wm.shell.draganddrop.DragAndDropController;
-import com.android.wm.shell.freeform.FreeformTaskListener;
+import com.android.wm.shell.floating.FloatingTasks;
+import com.android.wm.shell.floating.FloatingTasksController;
+import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
-import com.android.wm.shell.fullscreen.FullscreenUnfoldController;
-import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
@@ -87,12 +85,16 @@ import com.android.wm.shell.startingsurface.StartingSurface;
import com.android.wm.shell.startingsurface.StartingWindowController;
import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm;
import com.android.wm.shell.startingsurface.phone.PhoneStartingWindowTypeAlgorithm;
-import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper;
-import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelperController;
+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.sysui.ShellInterface;
import com.android.wm.shell.transition.ShellTransitions;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
import com.android.wm.shell.unfold.UnfoldTransitionHandler;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.util.Optional;
@@ -120,38 +122,34 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
static DisplayController provideDisplayController(Context context,
- IWindowManager wmService, @ShellMainThread ShellExecutor mainExecutor) {
- return new DisplayController(context, wmService, mainExecutor);
+ IWindowManager wmService,
+ ShellInit shellInit,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return new DisplayController(context, wmService, shellInit, mainExecutor);
}
@WMSingleton
@Provides
- static DisplayInsetsController provideDisplayInsetsController( IWindowManager wmService,
+ static DisplayInsetsController provideDisplayInsetsController(IWindowManager wmService,
+ ShellInit shellInit,
DisplayController displayController,
@ShellMainThread ShellExecutor mainExecutor) {
- return new DisplayInsetsController(wmService, displayController, mainExecutor);
+ return new DisplayInsetsController(wmService, shellInit, displayController,
+ mainExecutor);
}
- // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
- @BindsOptionalOf
- @DynamicOverride
- abstract DisplayImeController optionalDisplayImeController();
-
@WMSingleton
@Provides
static DisplayImeController provideDisplayImeController(
- @DynamicOverride Optional<DisplayImeController> overrideDisplayImeController,
IWindowManager wmService,
+ ShellInit shellInit,
DisplayController displayController,
DisplayInsetsController displayInsetsController,
- @ShellMainThread ShellExecutor mainExecutor,
- TransactionPool transactionPool
+ TransactionPool transactionPool,
+ @ShellMainThread ShellExecutor mainExecutor
) {
- if (overrideDisplayImeController.isPresent()) {
- return overrideDisplayImeController.get();
- }
- return new DisplayImeController(wmService, displayController, displayInsetsController,
- mainExecutor, transactionPool);
+ return new DisplayImeController(wmService, shellInit, displayController,
+ displayInsetsController, transactionPool, mainExecutor);
}
@WMSingleton
@@ -163,56 +161,64 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
static DragAndDropController provideDragAndDropController(Context context,
- DisplayController displayController, UiEventLogger uiEventLogger,
- IconProvider iconProvider, @ShellMainThread ShellExecutor mainExecutor) {
- return new DragAndDropController(context, displayController, uiEventLogger, iconProvider,
- mainExecutor);
- }
-
- @WMSingleton
- @Provides
- static Optional<DragAndDrop> provideDragAndDrop(DragAndDropController dragAndDropController) {
- return Optional.of(dragAndDropController.asDragAndDrop());
+ ShellInit shellInit,
+ ShellController shellController,
+ DisplayController displayController,
+ UiEventLogger uiEventLogger,
+ IconProvider iconProvider,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return new DragAndDropController(context, shellInit, shellController, displayController,
+ uiEventLogger, iconProvider, mainExecutor);
}
@WMSingleton
@Provides
- static ShellTaskOrganizer provideShellTaskOrganizer(@ShellMainThread ShellExecutor mainExecutor,
+ static ShellTaskOrganizer provideShellTaskOrganizer(
Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
CompatUIController compatUI,
- Optional<RecentTasksController> recentTasksOptional
+ Optional<UnfoldAnimationController> unfoldAnimationController,
+ Optional<RecentTasksController> recentTasksOptional,
+ @ShellMainThread ShellExecutor mainExecutor
) {
- return new ShellTaskOrganizer(mainExecutor, context, compatUI, recentTasksOptional);
+ if (!context.getResources().getBoolean(R.bool.config_registerShellTaskOrganizerOnInit)) {
+ // TODO(b/238217847): Force override shell init if registration is disabled
+ shellInit = new ShellInit(mainExecutor);
+ }
+ return new ShellTaskOrganizer(shellInit, shellCommandHandler, compatUI,
+ unfoldAnimationController, recentTasksOptional, mainExecutor);
}
@WMSingleton
@Provides
static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
- @ShellMainThread ShellExecutor mainExecutor,
- @ShellMainThread Handler mainHandler,
Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
SyncTransactionQueue syncTransactionQueue,
DisplayController displayController,
DisplayInsetsController displayInsetsController,
- Optional<RecentTasksController> recentTasksOptional
+ Optional<UnfoldAnimationController> unfoldAnimationController,
+ Optional<RecentTasksController> recentTasksOptional,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellMainThread Handler mainHandler
) {
- return new KidsModeTaskOrganizer(mainExecutor, mainHandler, context, syncTransactionQueue,
- displayController, displayInsetsController, recentTasksOptional);
- }
-
- @WMSingleton
- @Provides static Optional<CompatUI> provideCompatUI(CompatUIController compatUIController) {
- return Optional.of(compatUIController.asCompatUI());
+ return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
+ syncTransactionQueue, displayController, displayInsetsController,
+ unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
}
@WMSingleton
@Provides
static CompatUIController provideCompatUIController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
DisplayController displayController, DisplayInsetsController displayInsetsController,
DisplayImeController imeController, SyncTransactionQueue syncQueue,
@ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy) {
- return new CompatUIController(context, displayController, displayInsetsController,
- imeController, syncQueue, mainExecutor, transitionsLazy);
+ return new CompatUIController(context, shellInit, shellController, displayController,
+ displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy);
}
@WMSingleton
@@ -267,6 +273,22 @@ public abstract class WMShellBaseModule {
return backAnimationController.map(BackAnimationController::getBackAnimationImpl);
}
+ @WMSingleton
+ @Provides
+ static Optional<BackAnimationController> provideBackAnimationController(
+ Context context,
+ ShellInit shellInit,
+ @ShellMainThread ShellExecutor shellExecutor,
+ @ShellBackgroundThread Handler backgroundHandler
+ ) {
+ if (BackAnimationController.IS_ENABLED) {
+ return Optional.of(
+ new BackAnimationController(shellInit, shellExecutor, backgroundHandler,
+ context));
+ }
+ return Optional.empty();
+ }
+
//
// Bubbles (optional feature)
//
@@ -287,24 +309,33 @@ public abstract class WMShellBaseModule {
// Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
@BindsOptionalOf
@DynamicOverride
- abstract FullscreenTaskListener optionalFullscreenTaskListener();
+ abstract FullscreenTaskListener<?> optionalFullscreenTaskListener();
@WMSingleton
@Provides
- static FullscreenTaskListener provideFullscreenTaskListener(
- @DynamicOverride Optional<FullscreenTaskListener> fullscreenTaskListener,
+ static FullscreenTaskListener<?> provideFullscreenTaskListener(
+ @DynamicOverride Optional<FullscreenTaskListener<?>> fullscreenTaskListener,
+ ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
SyncTransactionQueue syncQueue,
- Optional<FullscreenUnfoldController> optionalFullscreenUnfoldController,
- Optional<RecentTasksController> recentTasksOptional) {
+ Optional<RecentTasksController> recentTasksOptional,
+ Optional<WindowDecorViewModel<?>> windowDecorViewModelOptional) {
if (fullscreenTaskListener.isPresent()) {
return fullscreenTaskListener.get();
} else {
- return new FullscreenTaskListener(syncQueue, optionalFullscreenUnfoldController,
- recentTasksOptional);
+ return new FullscreenTaskListener(shellInit, shellTaskOrganizer, syncQueue,
+ recentTasksOptional, windowDecorViewModelOptional);
}
}
//
+ // Window Decoration
+ //
+
+ @BindsOptionalOf
+ abstract WindowDecorViewModel<?> optionalWindowDecorViewModel();
+
+ //
// Unfold transition
//
@@ -314,31 +345,33 @@ public abstract class WMShellBaseModule {
// Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
@BindsOptionalOf
@DynamicOverride
- abstract FullscreenUnfoldController optionalFullscreenUnfoldController();
+ abstract UnfoldAnimationController optionalUnfoldController();
@WMSingleton
@Provides
- static Optional<FullscreenUnfoldController> provideFullscreenUnfoldController(
- @DynamicOverride Optional<FullscreenUnfoldController> fullscreenUnfoldController,
+ static Optional<UnfoldAnimationController> provideUnfoldController(
+ @DynamicOverride Lazy<Optional<UnfoldAnimationController>>
+ fullscreenUnfoldController,
Optional<ShellUnfoldProgressProvider> progressProvider) {
if (progressProvider.isPresent()
&& progressProvider.get() != ShellUnfoldProgressProvider.NO_PROVIDER) {
- return fullscreenUnfoldController;
+ return fullscreenUnfoldController.get();
}
return Optional.empty();
}
+ @BindsOptionalOf
+ @DynamicOverride
+ abstract UnfoldTransitionHandler optionalUnfoldTransitionHandler();
+
@WMSingleton
@Provides
static Optional<UnfoldTransitionHandler> provideUnfoldTransitionHandler(
Optional<ShellUnfoldProgressProvider> progressProvider,
- TransactionPool transactionPool,
- Transitions transitions,
- @ShellMainThread ShellExecutor executor) {
- if (progressProvider.isPresent()) {
- return Optional.of(
- new UnfoldTransitionHandler(progressProvider.get(), transactionPool, executor,
- transitions));
+ @DynamicOverride Lazy<Optional<UnfoldTransitionHandler>> handler) {
+ if (progressProvider.isPresent()
+ && progressProvider.get() != ShellUnfoldProgressProvider.NO_PROVIDER) {
+ return handler.get();
}
return Optional.empty();
}
@@ -350,15 +383,15 @@ public abstract class WMShellBaseModule {
// Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
@BindsOptionalOf
@DynamicOverride
- abstract FreeformTaskListener optionalFreeformTaskListener();
+ abstract FreeformComponents optionalFreeformComponents();
@WMSingleton
@Provides
- static Optional<FreeformTaskListener> provideFreeformTaskListener(
- @DynamicOverride Optional<FreeformTaskListener> freeformTaskListener,
+ static Optional<FreeformComponents> provideFreeformComponents(
+ @DynamicOverride Optional<FreeformComponents> freeformComponents,
Context context) {
- if (FreeformTaskListener.isFreeformEnabled(context)) {
- return freeformTaskListener;
+ if (FreeformComponents.isFreeformEnabled(context)) {
+ return freeformComponents;
}
return Optional.empty();
}
@@ -369,17 +402,15 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static Optional<HideDisplayCutout> provideHideDisplayCutout(
- Optional<HideDisplayCutoutController> hideDisplayCutoutController) {
- return hideDisplayCutoutController.map((controller) -> controller.asHideDisplayCutout());
- }
-
- @WMSingleton
- @Provides
static Optional<HideDisplayCutoutController> provideHideDisplayCutoutController(Context context,
- DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor) {
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ DisplayController displayController,
+ @ShellMainThread ShellExecutor mainExecutor) {
return Optional.ofNullable(
- HideDisplayCutoutController.create(context, displayController, mainExecutor));
+ HideDisplayCutoutController.create(context, shellInit, shellCommandHandler,
+ shellController, displayController, mainExecutor));
}
//
@@ -408,23 +439,6 @@ public abstract class WMShellBaseModule {
}
//
- // Task to Surface communication
- //
-
- @WMSingleton
- @Provides
- static Optional<TaskSurfaceHelper> provideTaskSurfaceHelper(
- Optional<TaskSurfaceHelperController> taskSurfaceController) {
- return taskSurfaceController.map((controller) -> controller.asTaskSurfaceHelper());
- }
-
- @Provides
- static Optional<TaskSurfaceHelperController> provideTaskSurfaceHelperController(
- ShellTaskOrganizer taskOrganizer, @ShellMainThread ShellExecutor mainExecutor) {
- return Optional.ofNullable(new TaskSurfaceHelperController(taskOrganizer, mainExecutor));
- }
-
- //
// Pip (optional feature)
//
@@ -444,8 +458,8 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper() {
- return new PipSurfaceTransactionHelper();
+ static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper(Context context) {
+ return new PipSurfaceTransactionHelper(context);
}
@WMSingleton
@@ -473,11 +487,17 @@ public abstract class WMShellBaseModule {
@Provides
static Optional<RecentTasksController> provideRecentTasksController(
Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
TaskStackListenerImpl taskStackListener,
+ ActivityTaskManager activityTaskManager,
+ Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
@ShellMainThread ShellExecutor mainExecutor
) {
return Optional.ofNullable(
- RecentTasksController.create(context, taskStackListener, mainExecutor));
+ RecentTasksController.create(context, shellInit, shellCommandHandler,
+ taskStackListener, activityTaskManager, desktopModeTaskRepository,
+ mainExecutor));
}
//
@@ -492,12 +512,15 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static Transitions provideTransitions(ShellTaskOrganizer organizer, TransactionPool pool,
- DisplayController displayController, Context context,
+ static Transitions provideTransitions(Context context,
+ ShellInit shellInit,
+ ShellTaskOrganizer organizer,
+ TransactionPool pool,
+ DisplayController displayController,
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellAnimationThread ShellExecutor animExecutor) {
- return new Transitions(organizer, pool, displayController, context, mainExecutor,
+ return new Transitions(context, shellInit, organizer, pool, displayController, mainExecutor,
mainHandler, animExecutor);
}
@@ -561,29 +584,47 @@ public abstract class WMShellBaseModule {
return Optional.empty();
}
- // Legacy split (optional feature)
+ //
+ // Floating tasks
+ //
@WMSingleton
@Provides
- static Optional<LegacySplitScreen> provideLegacySplitScreen(
- Optional<LegacySplitScreenController> splitScreenController) {
- return splitScreenController.map((controller) -> controller.asLegacySplitScreen());
+ static Optional<FloatingTasks> provideFloatingTasks(
+ Optional<FloatingTasksController> floatingTaskController) {
+ return floatingTaskController.map((controller) -> controller.asFloatingTasks());
}
- @BindsOptionalOf
- abstract LegacySplitScreenController optionalLegacySplitScreenController();
-
- // App Pairs (optional feature)
-
@WMSingleton
@Provides
- static Optional<AppPairs> provideAppPairs(Optional<AppPairsController> appPairsController) {
- return appPairsController.map((controller) -> controller.asAppPairs());
+ static Optional<FloatingTasksController> provideFloatingTasksController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ ShellCommandHandler shellCommandHandler,
+ Optional<BubbleController> bubbleController,
+ WindowManager windowManager,
+ ShellTaskOrganizer organizer,
+ TaskViewTransitions taskViewTransitions,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
+ SyncTransactionQueue syncQueue) {
+ if (FloatingTasksController.FLOATING_TASKS_ENABLED) {
+ return Optional.of(new FloatingTasksController(context,
+ shellInit,
+ shellController,
+ shellCommandHandler,
+ bubbleController,
+ windowManager,
+ organizer,
+ taskViewTransitions,
+ mainExecutor,
+ bgExecutor,
+ syncQueue));
+ } else {
+ return Optional.empty();
+ }
}
- @BindsOptionalOf
- abstract AppPairsController optionalAppPairs();
-
//
// Starting window
//
@@ -598,11 +639,13 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
static StartingWindowController provideStartingWindowController(Context context,
+ ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
@ShellSplashscreenThread ShellExecutor splashScreenExecutor,
StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider,
TransactionPool pool) {
- return new StartingWindowController(context, splashScreenExecutor,
- startingWindowTypeAlgorithm, iconProvider, pool);
+ return new StartingWindowController(context, shellInit, shellTaskOrganizer,
+ splashScreenExecutor, startingWindowTypeAlgorithm, iconProvider, pool);
}
// Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
@@ -644,19 +687,96 @@ public abstract class WMShellBaseModule {
taskViewTransitions);
}
+
//
- // Misc
+ // ActivityEmbedding
+ //
+
+ @WMSingleton
+ @Provides
+ static Optional<ActivityEmbeddingController> provideActivityEmbeddingController(
+ Context context,
+ ShellInit shellInit,
+ Transitions transitions) {
+ return Optional.ofNullable(
+ ActivityEmbeddingController.create(context, shellInit, transitions));
+ }
+
+ //
+ // SysUI -> Shell interface
//
@WMSingleton
@Provides
- static ShellInit provideShellInit(ShellInitImpl impl) {
- return impl.asShellInit();
+ static ShellInterface provideShellSysuiCallbacks(
+ @ShellCreateTrigger Object createTrigger,
+ ShellController shellController) {
+ return shellController.asShell();
}
@WMSingleton
@Provides
- static ShellInitImpl provideShellInitImpl(DisplayController displayController,
+ static ShellController provideShellController(ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return new ShellController(shellInit, shellCommandHandler, mainExecutor);
+ }
+
+ //
+ // Desktop mode (optional feature)
+ //
+
+ @WMSingleton
+ @Provides
+ static Optional<DesktopMode> provideDesktopMode(
+ Optional<DesktopModeController> desktopModeController) {
+ return desktopModeController.map(DesktopModeController::asDesktopMode);
+ }
+
+ @BindsOptionalOf
+ @DynamicOverride
+ abstract DesktopModeController optionalDesktopModeController();
+
+ @WMSingleton
+ @Provides
+ static Optional<DesktopModeController> providesDesktopModeController(
+ @DynamicOverride Optional<DesktopModeController> desktopModeController) {
+ if (DesktopModeStatus.IS_SUPPORTED) {
+ return desktopModeController;
+ }
+ return Optional.empty();
+ }
+
+ @BindsOptionalOf
+ @DynamicOverride
+ abstract DesktopModeTaskRepository optionalDesktopModeTaskRepository();
+
+ @WMSingleton
+ @Provides
+ static Optional<DesktopModeTaskRepository> providesDesktopTaskRepository(
+ @DynamicOverride Optional<DesktopModeTaskRepository> desktopModeTaskRepository) {
+ if (DesktopModeStatus.IS_SUPPORTED) {
+ return desktopModeTaskRepository;
+ }
+ return Optional.empty();
+ }
+
+ //
+ // Misc
+ //
+
+ // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
+ @BindsOptionalOf
+ @ShellCreateTriggerOverride
+ abstract Object provideIndependentShellComponentsToCreateOverride();
+
+ // TODO: Temporarily move dependencies to this instead of ShellInit since that is needed to add
+ // the callback. We will be moving to a different explicit startup mechanism in a follow- up CL.
+ @WMSingleton
+ @ShellCreateTrigger
+ @Provides
+ static Object provideIndependentShellComponentsToCreate(
+ DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
DragAndDropController dragAndDropController,
@@ -664,75 +784,40 @@ public abstract class WMShellBaseModule {
KidsModeTaskOrganizer kidsModeTaskOrganizer,
Optional<BubbleController> bubblesOptional,
Optional<SplitScreenController> splitScreenOptional,
- Optional<AppPairsController> appPairsOptional,
+ Optional<Pip> pipOptional,
Optional<PipTouchHandler> pipTouchHandlerOptional,
- FullscreenTaskListener fullscreenTaskListener,
- Optional<FullscreenUnfoldController> appUnfoldTransitionController,
+ FullscreenTaskListener<?> fullscreenTaskListener,
+ Optional<UnfoldAnimationController> unfoldAnimationController,
Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
- Optional<FreeformTaskListener> freeformTaskListener,
+ Optional<FreeformComponents> freeformComponents,
Optional<RecentTasksController> recentTasksOptional,
+ Optional<OneHandedController> oneHandedControllerOptional,
+ Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
+ Optional<ActivityEmbeddingController> activityEmbeddingOptional,
Transitions transitions,
StartingWindowController startingWindow,
- @ShellMainThread ShellExecutor mainExecutor) {
- return new ShellInitImpl(displayController,
- displayImeController,
- displayInsetsController,
- dragAndDropController,
- shellTaskOrganizer,
- kidsModeTaskOrganizer,
- bubblesOptional,
- splitScreenOptional,
- appPairsOptional,
- pipTouchHandlerOptional,
- fullscreenTaskListener,
- appUnfoldTransitionController,
- unfoldTransitionHandler,
- freeformTaskListener,
- recentTasksOptional,
- transitions,
- startingWindow,
- mainExecutor);
+ ProtoLogController protoLogController,
+ @ShellCreateTriggerOverride Optional<Object> overriddenCreateTrigger) {
+ return new Object();
}
- /**
- * Note, this is only optional because we currently pass this to the SysUI component scope and
- * for non-primary users, we may inject a null-optional for that dependency.
- */
@WMSingleton
@Provides
- static Optional<ShellCommandHandler> provideShellCommandHandler(ShellCommandHandlerImpl impl) {
- return Optional.of(impl.asShellCommandHandler());
+ static ShellInit provideShellInit(@ShellMainThread ShellExecutor mainExecutor) {
+ return new ShellInit(mainExecutor);
}
@WMSingleton
@Provides
- static ShellCommandHandlerImpl provideShellCommandHandlerImpl(
- ShellTaskOrganizer shellTaskOrganizer,
- KidsModeTaskOrganizer kidsModeTaskOrganizer,
- Optional<LegacySplitScreenController> legacySplitScreenOptional,
- Optional<SplitScreenController> splitScreenOptional,
- Optional<Pip> pipOptional,
- Optional<OneHandedController> oneHandedOptional,
- Optional<HideDisplayCutoutController> hideDisplayCutout,
- Optional<AppPairsController> appPairsOptional,
- Optional<RecentTasksController> recentTasksOptional,
- @ShellMainThread ShellExecutor mainExecutor) {
- return new ShellCommandHandlerImpl(shellTaskOrganizer, kidsModeTaskOrganizer,
- legacySplitScreenOptional, splitScreenOptional, pipOptional, oneHandedOptional,
- hideDisplayCutout, appPairsOptional, recentTasksOptional, mainExecutor);
+ static ShellCommandHandler provideShellCommandHandler() {
+ return new ShellCommandHandler();
}
@WMSingleton
@Provides
- static Optional<BackAnimationController> provideBackAnimationController(
- Context context,
- @ShellMainThread ShellExecutor shellExecutor,
- @ShellBackgroundThread Handler backgroundHandler
- ) {
- if (BackAnimationController.IS_ENABLED) {
- return Optional.of(
- new BackAnimationController(shellExecutor, backgroundHandler, context));
- }
- return Optional.empty();
+ static ProtoLogController provideProtoLogController(
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler) {
+ return new ProtoLogController(shellInit, shellCommandHandler);
}
}
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 cc741d3896a2..0cc545a7724a 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
@@ -20,21 +20,19 @@ import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static android.os.Process.THREAD_PRIORITY_DISPLAY;
import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
-import android.animation.AnimationHandler;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Trace;
+import android.view.Choreographer;
import androidx.annotation.Nullable;
-import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
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.ChoreographerSfVsync;
import com.android.wm.shell.common.annotations.ExternalMainThread;
import com.android.wm.shell.common.annotations.ShellAnimationThread;
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
@@ -144,6 +142,25 @@ public abstract class WMShellConcurrencyModule {
}
/**
+ * Provide a Shell main-thread {@link Choreographer} with the app vsync.
+ *
+ * @param executor the executor of the shell main thread
+ */
+ @WMSingleton
+ @Provides
+ @ShellMainThread
+ public static Choreographer provideShellMainChoreographer(
+ @ShellMainThread ShellExecutor executor) {
+ try {
+ final Choreographer[] choreographer = new Choreographer[1];
+ executor.executeBlocking(() -> choreographer[0] = Choreographer.getInstance());
+ return choreographer[0];
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to obtain main Choreographer.", e);
+ }
+ }
+
+ /**
* Provide a Shell animation-thread Executor.
*/
@WMSingleton
@@ -175,30 +192,6 @@ public abstract class WMShellConcurrencyModule {
}
/**
- * Provide a Shell main-thread AnimationHandler. The AnimationHandler can be set on
- * {@link android.animation.ValueAnimator}s and will ensure that the animation will run on
- * the Shell main-thread with the SF vsync.
- */
- @WMSingleton
- @Provides
- @ChoreographerSfVsync
- public static AnimationHandler provideShellMainExecutorSfVsyncAnimationHandler(
- @ShellMainThread ShellExecutor mainExecutor) {
- try {
- AnimationHandler handler = new AnimationHandler();
- mainExecutor.executeBlocking(() -> {
- // This is called on the animation thread since it calls
- // Choreographer.getSfInstance() which returns a thread-local Choreographer instance
- // that uses the SF vsync
- handler.setProvider(new SfVsyncFrameCallbackProvider());
- });
- return handler;
- } catch (InterruptedException e) {
- throw new RuntimeException("Failed to initialize SfVsync animation handler in 1s", e);
- }
- }
-
- /**
* Provides a Shell background thread Handler for low priority background tasks.
*/
@WMSingleton
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 b3799e2cf8d9..37a50b611039 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
@@ -16,11 +16,11 @@
package com.android.wm.shell.dagger;
-import android.animation.AnimationHandler;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.os.Handler;
import android.os.UserManager;
+import android.view.Choreographer;
import android.view.WindowManager;
import com.android.internal.jank.InteractionJankMonitor;
@@ -31,8 +31,11 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.TaskViewTransitions;
import com.android.wm.shell.WindowManagerShellWrapper;
-import com.android.wm.shell.apppairs.AppPairsController;
import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.bubbles.BubbleData;
+import com.android.wm.shell.bubbles.BubbleDataRepository;
+import com.android.wm.shell.bubbles.BubbleLogger;
+import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -43,13 +46,16 @@ 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.TransactionPool;
-import com.android.wm.shell.common.annotations.ChoreographerSfVsync;
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.desktopmode.DesktopModeController;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.draganddrop.DragAndDropController;
+import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.freeform.FreeformTaskListener;
-import com.android.wm.shell.fullscreen.FullscreenUnfoldController;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController;
+import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
+import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
+import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
@@ -65,21 +71,35 @@ import com.android.wm.shell.pip.PipTransition;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.pip.PipUiEventLogger;
+import com.android.wm.shell.pip.phone.PhonePipKeepClearAlgorithm;
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.recents.RecentTasksController;
import com.android.wm.shell.splitscreen.SplitScreenController;
-import com.android.wm.shell.splitscreen.StageTaskUnfoldController;
+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.DefaultMixedHandler;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
import com.android.wm.shell.unfold.UnfoldBackgroundController;
-
+import com.android.wm.shell.unfold.UnfoldTransitionHandler;
+import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
+import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;
+import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
+import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition;
+import com.android.wm.shell.unfold.qualifier.UnfoldTransition;
+import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
+
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
-import javax.inject.Provider;
-
+import dagger.Binds;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
@@ -93,16 +113,42 @@ import dagger.Provides;
* dependencies should go into {@link WMShellBaseModule}.
*/
@Module(includes = WMShellBaseModule.class)
-public class WMShellModule {
+public abstract class WMShellModule {
//
// Bubbles
//
+ @WMSingleton
+ @Provides
+ static BubbleLogger provideBubbleLogger(UiEventLogger uiEventLogger) {
+ return new BubbleLogger(uiEventLogger);
+ }
+
+ @WMSingleton
+ @Provides
+ static BubblePositioner provideBubblePositioner(Context context,
+ WindowManager windowManager) {
+ return new BubblePositioner(context, windowManager);
+ }
+
+ @WMSingleton
+ @Provides
+ static BubbleData provideBubbleData(Context context,
+ BubbleLogger logger,
+ BubblePositioner positioner,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return new BubbleData(context, logger, positioner, mainExecutor);
+ }
+
// Note: Handler needed for LauncherApps.register
@WMSingleton
@Provides
static BubbleController provideBubbleController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ BubbleData data,
FloatingContentCoordinator floatingContentCoordinator,
IStatusBarService statusBarService,
WindowManager windowManager,
@@ -110,8 +156,9 @@ public class WMShellModule {
UserManager userManager,
LauncherApps launcherApps,
TaskStackListenerImpl taskStackListener,
- UiEventLogger uiEventLogger,
+ BubbleLogger logger,
ShellTaskOrganizer organizer,
+ BubblePositioner positioner,
DisplayController displayController,
@DynamicOverride Optional<OneHandedController> oneHandedOptional,
DragAndDropController dragAndDropController,
@@ -120,24 +167,90 @@ public class WMShellModule {
@ShellBackgroundThread ShellExecutor bgExecutor,
TaskViewTransitions taskViewTransitions,
SyncTransactionQueue syncQueue) {
- return BubbleController.create(context, null /* synchronizer */,
- floatingContentCoordinator, statusBarService, windowManager,
- windowManagerShellWrapper, userManager, launcherApps, taskStackListener,
- uiEventLogger, organizer, displayController, oneHandedOptional,
- dragAndDropController, mainExecutor, mainHandler, bgExecutor,
+ return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
+ null /* synchronizer */, floatingContentCoordinator,
+ new BubbleDataRepository(context, launcherApps, mainExecutor),
+ statusBarService, windowManager, windowManagerShellWrapper, userManager,
+ launcherApps, logger, taskStackListener, organizer, positioner, displayController,
+ oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
taskViewTransitions, syncQueue);
}
//
+ // Window decoration
+ //
+
+ @WMSingleton
+ @Provides
+ static WindowDecorViewModel<?> provideWindowDecorViewModel(
+ Context context,
+ @ShellMainThread Handler mainHandler,
+ @ShellMainThread Choreographer mainChoreographer,
+ ShellTaskOrganizer taskOrganizer,
+ DisplayController displayController,
+ SyncTransactionQueue syncQueue,
+ @DynamicOverride DesktopModeController desktopModeController) {
+ return new CaptionWindowDecorViewModel(
+ context,
+ mainHandler,
+ mainChoreographer,
+ taskOrganizer,
+ displayController,
+ syncQueue,
+ desktopModeController);
+ }
+
+ //
// Freeform
//
@WMSingleton
@Provides
@DynamicOverride
- static FreeformTaskListener provideFreeformTaskListener(
- SyncTransactionQueue syncQueue) {
- return new FreeformTaskListener(syncQueue);
+ static FreeformComponents provideFreeformComponents(
+ FreeformTaskListener<?> taskListener,
+ FreeformTaskTransitionHandler transitionHandler,
+ FreeformTaskTransitionObserver transitionObserver) {
+ return new FreeformComponents(
+ taskListener, Optional.of(transitionHandler), Optional.of(transitionObserver));
+ }
+
+ @WMSingleton
+ @Provides
+ static FreeformTaskListener<?> provideFreeformTaskListener(
+ Context context,
+ ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
+ Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ WindowDecorViewModel<?> windowDecorViewModel) {
+ // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
+ // override for this controller from the base module
+ ShellInit init = FreeformComponents.isFreeformEnabled(context)
+ ? shellInit
+ : null;
+ return new FreeformTaskListener<>(init, shellTaskOrganizer, desktopModeTaskRepository,
+ windowDecorViewModel);
+ }
+
+ @WMSingleton
+ @Provides
+ static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler(
+ ShellInit shellInit,
+ Transitions transitions,
+ WindowDecorViewModel<?> windowDecorViewModel) {
+ return new FreeformTaskTransitionHandler(shellInit, transitions, windowDecorViewModel);
+ }
+
+ @WMSingleton
+ @Provides
+ static FreeformTaskTransitionObserver provideFreeformTaskTransitionObserver(
+ Context context,
+ ShellInit shellInit,
+ Transitions transitions,
+ FullscreenTaskListener<?> fullscreenTaskListener,
+ FreeformTaskListener<?> freeformTaskListener) {
+ return new FreeformTaskTransitionObserver(
+ context, shellInit, transitions, fullscreenTaskListener, freeformTaskListener);
}
//
@@ -150,12 +263,20 @@ public class WMShellModule {
@Provides
@DynamicOverride
static OneHandedController provideOneHandedController(Context context,
- WindowManager windowManager, DisplayController displayController,
- DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener,
- UiEventLogger uiEventLogger, InteractionJankMonitor jankMonitor,
- @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) {
- return OneHandedController.create(context, windowManager, displayController, displayLayout,
- taskStackListener, jankMonitor, uiEventLogger, mainExecutor, mainHandler);
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ WindowManager windowManager,
+ DisplayController displayController,
+ DisplayLayout displayLayout,
+ TaskStackListenerImpl taskStackListener,
+ UiEventLogger uiEventLogger,
+ InteractionJankMonitor jankMonitor,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellMainThread Handler mainHandler) {
+ return OneHandedController.create(context, shellInit, shellCommandHandler, shellController,
+ windowManager, displayController, displayLayout, taskStackListener, jankMonitor,
+ uiEventLogger, mainExecutor, mainHandler);
}
//
@@ -166,45 +287,26 @@ public class WMShellModule {
@Provides
@DynamicOverride
static SplitScreenController provideSplitScreenController(
+ Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
ShellTaskOrganizer shellTaskOrganizer,
- SyncTransactionQueue syncQueue, Context context,
+ SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
- @ShellMainThread ShellExecutor mainExecutor,
DisplayController displayController,
DisplayImeController displayImeController,
- DisplayInsetsController displayInsetsController, Transitions transitions,
- TransactionPool transactionPool, IconProvider iconProvider,
+ DisplayInsetsController displayInsetsController,
+ DragAndDropController dragAndDropController,
+ Transitions transitions,
+ TransactionPool transactionPool,
+ IconProvider iconProvider,
Optional<RecentTasksController> recentTasks,
- Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) {
- return new SplitScreenController(shellTaskOrganizer, syncQueue, context,
- rootTaskDisplayAreaOrganizer, mainExecutor, displayController, displayImeController,
- displayInsetsController, transitions, transactionPool, iconProvider,
- recentTasks, stageTaskUnfoldControllerProvider);
- }
-
- @WMSingleton
- @Provides
- static LegacySplitScreenController provideLegacySplitScreen(Context context,
- DisplayController displayController, SystemWindows systemWindows,
- DisplayImeController displayImeController, TransactionPool transactionPool,
- ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue,
- TaskStackListenerImpl taskStackListener, Transitions transitions,
- @ShellMainThread ShellExecutor mainExecutor,
- @ChoreographerSfVsync AnimationHandler sfVsyncAnimationHandler) {
- return new LegacySplitScreenController(context, displayController, systemWindows,
- displayImeController, transactionPool, shellTaskOrganizer, syncQueue,
- taskStackListener, transitions, mainExecutor, sfVsyncAnimationHandler);
- }
-
- @WMSingleton
- @Provides
- static AppPairsController provideAppPairs(ShellTaskOrganizer shellTaskOrganizer,
- SyncTransactionQueue syncQueue, DisplayController displayController,
- @ShellMainThread ShellExecutor mainExecutor,
- DisplayImeController displayImeController,
- DisplayInsetsController displayInsetsController) {
- return new AppPairsController(shellTaskOrganizer, syncQueue, displayController,
- mainExecutor, displayImeController, displayInsetsController);
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return new SplitScreenController(context, shellInit, shellCommandHandler, shellController,
+ shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController,
+ displayImeController, displayInsetsController, dragAndDropController, transitions,
+ transactionPool, iconProvider, recentTasks, mainExecutor);
}
//
@@ -213,21 +315,37 @@ public class WMShellModule {
@WMSingleton
@Provides
- static Optional<Pip> providePip(Context context, DisplayController displayController,
- PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm,
- PipBoundsState pipBoundsState, PipMediaController pipMediaController,
- PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer,
- PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController,
+ static Optional<Pip> providePip(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ DisplayController displayController,
+ PipAnimationController pipAnimationController,
+ PipAppOpsListener pipAppOpsListener,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ PhonePipKeepClearAlgorithm pipKeepClearAlgorithm,
+ PipBoundsState pipBoundsState,
+ PipMotionHelper pipMotionHelper,
+ PipMediaController pipMediaController,
+ PhonePipMenuController phonePipMenuController,
+ PipTaskOrganizer pipTaskOrganizer,
+ PipTransitionState pipTransitionState,
+ PipTouchHandler pipTouchHandler,
+ PipTransitionController pipTransitionController,
WindowManagerShellWrapper windowManagerShellWrapper,
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
+ DisplayInsetsController displayInsetsController,
Optional<OneHandedController> oneHandedController,
@ShellMainThread ShellExecutor mainExecutor) {
- return Optional.ofNullable(PipController.create(context, displayController,
- pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState,
- pipMediaController, phonePipMenuController, pipTaskOrganizer,
- pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
- taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor));
+ return Optional.ofNullable(PipController.create(
+ context, shellInit, shellCommandHandler, shellController,
+ displayController, pipAnimationController, pipAppOpsListener, pipBoundsAlgorithm,
+ pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, pipMediaController,
+ phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler,
+ pipTransitionController, windowManagerShellWrapper, taskStackListener,
+ pipParamsChangedForwarder, displayInsetsController, oneHandedController,
+ mainExecutor));
}
@WMSingleton
@@ -244,9 +362,17 @@ public class WMShellModule {
@WMSingleton
@Provides
+ static PhonePipKeepClearAlgorithm providePhonePipKeepClearAlgorithm(Context context) {
+ return new PhonePipKeepClearAlgorithm(context);
+ }
+
+ @WMSingleton
+ @Provides
static PipBoundsAlgorithm providesPipBoundsAlgorithm(Context context,
- PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) {
- return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm);
+ PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm,
+ PhonePipKeepClearAlgorithm pipKeepClearAlgorithm) {
+ return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm,
+ pipKeepClearAlgorithm);
}
// Handler is used by Icon.loadDrawableAsync
@@ -266,14 +392,16 @@ public class WMShellModule {
@WMSingleton
@Provides
static PipTouchHandler providePipTouchHandler(Context context,
- PhonePipMenuController menuPhoneController, PipBoundsAlgorithm pipBoundsAlgorithm,
+ ShellInit shellInit,
+ PhonePipMenuController menuPhoneController,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
PipBoundsState pipBoundsState,
PipTaskOrganizer pipTaskOrganizer,
PipMotionHelper pipMotionHelper,
FloatingContentCoordinator floatingContentCoordinator,
PipUiEventLogger pipUiEventLogger,
@ShellMainThread ShellExecutor mainExecutor) {
- return new PipTouchHandler(context, menuPhoneController, pipBoundsAlgorithm,
+ return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm,
pipBoundsState, pipTaskOrganizer, pipMotionHelper,
floatingContentCoordinator, pipUiEventLogger, mainExecutor);
}
@@ -317,15 +445,15 @@ public class WMShellModule {
@WMSingleton
@Provides
static PipTransitionController providePipTransitionController(Context context,
- Transitions transitions, ShellTaskOrganizer shellTaskOrganizer,
+ ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Transitions transitions,
PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm,
PipBoundsState pipBoundsState, PipTransitionState pipTransitionState,
PhonePipMenuController pipMenuController,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
Optional<SplitScreenController> splitScreenOptional) {
- return new PipTransition(context, pipBoundsState, pipTransitionState, pipMenuController,
- pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer,
- pipSurfaceTransactionHelper, splitScreenOptional);
+ return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
+ pipBoundsState, pipTransitionState, pipMenuController, pipBoundsAlgorithm,
+ pipAnimationController, pipSurfaceTransactionHelper, splitScreenOptional);
}
@WMSingleton
@@ -348,6 +476,27 @@ public class WMShellModule {
floatingContentCoordinator);
}
+ @WMSingleton
+ @Provides
+ static PipParamsChangedForwarder providePipParamsChangedForwarder() {
+ return new PipParamsChangedForwarder();
+ }
+
+ //
+ // Transitions
+ //
+
+ @WMSingleton
+ @Provides
+ static DefaultMixedHandler provideDefaultMixedHandler(
+ ShellInit shellInit,
+ Optional<SplitScreenController> splitScreenOptional,
+ Optional<PipTouchHandler> pipTouchHandlerOptional,
+ Transitions transitions) {
+ return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional,
+ pipTouchHandlerOptional);
+ }
+
//
// Unfold transition
//
@@ -355,36 +504,80 @@ public class WMShellModule {
@WMSingleton
@Provides
@DynamicOverride
- static FullscreenUnfoldController provideFullscreenUnfoldController(
- Context context,
+ static UnfoldAnimationController provideUnfoldAnimationController(
Optional<ShellUnfoldProgressProvider> progressProvider,
- Lazy<UnfoldBackgroundController> unfoldBackgroundController,
- DisplayInsetsController displayInsetsController,
+ TransactionPool transactionPool,
+ @UnfoldTransition SplitTaskUnfoldAnimator splitAnimator,
+ FullscreenUnfoldTaskAnimator fullscreenAnimator,
+ Lazy<Optional<UnfoldTransitionHandler>> unfoldTransitionHandler,
+ ShellInit shellInit,
@ShellMainThread ShellExecutor mainExecutor
) {
- return new FullscreenUnfoldController(context, mainExecutor,
- unfoldBackgroundController.get(), progressProvider.get(),
+ final List<UnfoldTaskAnimator> animators = new ArrayList<>();
+ animators.add(splitAnimator);
+ animators.add(fullscreenAnimator);
+
+ return new UnfoldAnimationController(
+ shellInit,
+ transactionPool,
+ progressProvider.get(),
+ animators,
+ unfoldTransitionHandler,
+ mainExecutor
+ );
+ }
+
+ @Provides
+ static FullscreenUnfoldTaskAnimator provideFullscreenUnfoldTaskAnimator(
+ Context context,
+ UnfoldBackgroundController unfoldBackgroundController,
+ DisplayInsetsController displayInsetsController
+ ) {
+ return new FullscreenUnfoldTaskAnimator(context, unfoldBackgroundController,
displayInsetsController);
}
@Provides
- static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController(
- Optional<ShellUnfoldProgressProvider> progressProvider,
+ static SplitTaskUnfoldAnimator provideSplitTaskUnfoldAnimatorBase(
Context context,
- TransactionPool transactionPool,
- Lazy<UnfoldBackgroundController> unfoldBackgroundController,
- DisplayInsetsController displayInsetsController,
- @ShellMainThread ShellExecutor mainExecutor
+ UnfoldBackgroundController backgroundController,
+ @ShellMainThread ShellExecutor executor,
+ Lazy<Optional<SplitScreenController>> splitScreenOptional,
+ DisplayInsetsController displayInsetsController
) {
- return progressProvider.map(shellUnfoldTransitionProgressProvider ->
- new StageTaskUnfoldController(
- context,
- transactionPool,
- shellUnfoldTransitionProgressProvider,
- displayInsetsController,
- unfoldBackgroundController.get(),
- mainExecutor
- ));
+ // TODO(b/238217847): The lazy reference here causes some dependency issues since it
+ // immediately registers a listener on that controller on init. We should reference the
+ // controller directly once we refactor ShellTaskOrganizer to not depend on the unfold
+ // animation controller directly.
+ return new SplitTaskUnfoldAnimator(context, executor, splitScreenOptional,
+ backgroundController, displayInsetsController);
+ }
+
+ @WMSingleton
+ @UnfoldShellTransition
+ @Binds
+ abstract SplitTaskUnfoldAnimator provideShellSplitTaskUnfoldAnimator(
+ SplitTaskUnfoldAnimator splitTaskUnfoldAnimator);
+
+ @WMSingleton
+ @UnfoldTransition
+ @Binds
+ abstract SplitTaskUnfoldAnimator provideSplitTaskUnfoldAnimator(
+ SplitTaskUnfoldAnimator splitTaskUnfoldAnimator);
+
+ @WMSingleton
+ @Provides
+ @DynamicOverride
+ static UnfoldTransitionHandler provideUnfoldTransitionHandler(
+ Optional<ShellUnfoldProgressProvider> progressProvider,
+ FullscreenUnfoldTaskAnimator animator,
+ @UnfoldShellTransition SplitTaskUnfoldAnimator unfoldAnimator,
+ TransactionPool transactionPool,
+ Transitions transitions,
+ @ShellMainThread ShellExecutor executor,
+ ShellInit shellInit) {
+ return new UnfoldTransitionHandler(shellInit, progressProvider.get(), animator,
+ unfoldAnimator, transactionPool, executor, transitions);
}
@WMSingleton
@@ -399,9 +592,45 @@ public class WMShellModule {
);
}
+ //
+ // Desktop mode (optional feature)
+ //
+
@WMSingleton
@Provides
- static PipParamsChangedForwarder providePipParamsChangedForwarder() {
- return new PipParamsChangedForwarder();
+ @DynamicOverride
+ static DesktopModeController provideDesktopModeController(Context context, ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ Transitions transitions,
+ @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+ @ShellMainThread Handler mainHandler,
+ @ShellMainThread ShellExecutor mainExecutor
+ ) {
+ return new DesktopModeController(context, shellInit, shellTaskOrganizer,
+ rootTaskDisplayAreaOrganizer, transitions, desktopModeTaskRepository, mainHandler,
+ mainExecutor);
+ }
+
+ @WMSingleton
+ @Provides
+ @DynamicOverride
+ static DesktopModeTaskRepository provideDesktopModeTaskRepository() {
+ return new DesktopModeTaskRepository();
+ }
+
+ //
+ // Misc
+ //
+
+ // TODO: Temporarily move dependencies to this instead of ShellInit since that is needed to add
+ // the callback. We will be moving to a different explicit startup mechanism in a follow- up CL.
+ @WMSingleton
+ @ShellCreateTriggerOverride
+ @Provides
+ static Object provideIndependentShellComponentsToCreate(
+ DefaultMixedHandler defaultMixedHandler,
+ Optional<DesktopModeController> desktopModeController) {
+ return new Object();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index d7010b174744..ff3be38d09e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,17 +14,18 @@
* limitations under the License.
*/
-package com.android.wm.shell;
+package com.android.wm.shell.desktopmode;
import com.android.wm.shell.common.annotations.ExternalThread;
/**
- * An entry point into the shell for initializing shell internal state.
+ * Interface to interact with desktop mode feature in shell.
*/
@ExternalThread
-public interface ShellInit {
- /**
- * Initializes the shell state.
- */
- void init();
+public interface DesktopMode {
+
+ /** Returns a binder that can be passed to an external process to manipulate DesktopMode. */
+ default IDesktopMode createExternalInterface() {
+ return null;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
new file mode 100644
index 000000000000..99739c457aa6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+
+import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.window.DisplayAreaInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ * Handles windowing changes when desktop mode system setting changes
+ */
+public class DesktopModeController implements RemoteCallable<DesktopModeController> {
+
+ private final Context mContext;
+ private final ShellTaskOrganizer mShellTaskOrganizer;
+ private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+ private final Transitions mTransitions;
+ private final DesktopModeTaskRepository mDesktopModeTaskRepository;
+ private final ShellExecutor mMainExecutor;
+ private final DesktopMode mDesktopModeImpl = new DesktopModeImpl();
+ private final SettingsObserver mSettingsObserver;
+
+ public DesktopModeController(Context context, ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ Transitions transitions,
+ DesktopModeTaskRepository desktopModeTaskRepository,
+ @ShellMainThread Handler mainHandler,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ mContext = context;
+ mShellTaskOrganizer = shellTaskOrganizer;
+ mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+ mTransitions = transitions;
+ mDesktopModeTaskRepository = desktopModeTaskRepository;
+ mMainExecutor = mainExecutor;
+ mSettingsObserver = new SettingsObserver(mContext, mainHandler);
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController");
+ mSettingsObserver.observe();
+ if (DesktopModeStatus.isActive(mContext)) {
+ updateDesktopModeActive(true);
+ }
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public ShellExecutor getRemoteCallExecutor() {
+ return mMainExecutor;
+ }
+
+ /**
+ * Get connection interface between sysui and shell
+ */
+ public DesktopMode asDesktopMode() {
+ return mDesktopModeImpl;
+ }
+
+ @VisibleForTesting
+ void updateDesktopModeActive(boolean active) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
+
+ int displayId = mContext.getDisplayId();
+
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ // Reset freeform windowing mode that is set per task level (tasks should inherit
+ // container value)
+ wct.merge(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(displayId),
+ true /* transfer */);
+ int targetWindowingMode;
+ if (active) {
+ targetWindowingMode = WINDOWING_MODE_FREEFORM;
+ } else {
+ targetWindowingMode = WINDOWING_MODE_FULLSCREEN;
+ // Clear any resized bounds
+ wct.merge(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(displayId),
+ true /* transfer */);
+ }
+ prepareWindowingModeChange(wct, displayId, targetWindowingMode);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitions.startTransition(TRANSIT_CHANGE, wct, null);
+ } else {
+ mRootTaskDisplayAreaOrganizer.applyTransaction(wct);
+ }
+ }
+
+ private void prepareWindowingModeChange(WindowContainerTransaction wct,
+ int displayId, @WindowConfiguration.WindowingMode int windowingMode) {
+ DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer
+ .getDisplayAreaInfo(displayId);
+ if (displayAreaInfo == null) {
+ ProtoLog.e(WM_SHELL_DESKTOP_MODE,
+ "unable to update windowing mode for display %d display not found", displayId);
+ return;
+ }
+
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId,
+ displayAreaInfo.configuration.windowConfiguration.getWindowingMode(),
+ windowingMode);
+
+ wct.setWindowingMode(displayAreaInfo.token, windowingMode);
+ }
+
+ /**
+ * Show apps on desktop
+ */
+ public void showDesktopApps() {
+ ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
+ ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
+ for (Integer taskId : activeTasks) {
+ RunningTaskInfo taskInfo = mShellTaskOrganizer.getRunningTaskInfo(taskId);
+ if (taskInfo != null) {
+ taskInfos.add(taskInfo);
+ }
+ }
+ // Order by lastActiveTime, descending
+ taskInfos.sort(Comparator.comparingLong(task -> -task.lastActiveTime));
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ for (RunningTaskInfo task : taskInfos) {
+ wct.reorder(task.token, true);
+ }
+ mShellTaskOrganizer.applyTransaction(wct);
+ }
+
+ /**
+ * Turn desktop mode on or off
+ * @param active the desired state for desktop mode setting
+ */
+ public void setDesktopModeActive(boolean active) {
+ int value = active ? 1 : 0;
+ Settings.System.putInt(mContext.getContentResolver(), Settings.System.DESKTOP_MODE, value);
+ }
+
+ /**
+ * Returns the windowing mode of the display area with the specified displayId.
+ * @param displayId
+ * @return
+ */
+ public int getDisplayAreaWindowingMode(int displayId) {
+ return mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
+ .configuration.windowConfiguration.getWindowingMode();
+ }
+
+ /**
+ * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
+ */
+ private final class SettingsObserver extends ContentObserver {
+
+ private final Uri mDesktopModeSetting = Settings.System.getUriFor(
+ Settings.System.DESKTOP_MODE);
+
+ private final Context mContext;
+
+ SettingsObserver(Context context, Handler handler) {
+ super(handler);
+ mContext = context;
+ }
+
+ public void observe() {
+ // TODO(b/242867463): listen for setting change for all users
+ mContext.getContentResolver().registerContentObserver(mDesktopModeSetting,
+ false /* notifyForDescendants */, this /* observer */, UserHandle.USER_CURRENT);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, @Nullable Uri uri) {
+ if (mDesktopModeSetting.equals(uri)) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Received update for desktop mode setting");
+ desktopModeSettingChanged();
+ }
+ }
+
+ private void desktopModeSettingChanged() {
+ boolean enabled = DesktopModeStatus.isActive(mContext);
+ updateDesktopModeActive(enabled);
+ }
+ }
+
+ /**
+ * The interface for calls from outside the shell, within the host process.
+ */
+ @ExternalThread
+ private final class DesktopModeImpl implements DesktopMode {
+
+ private IDesktopModeImpl mIDesktopMode;
+
+ @Override
+ public IDesktopMode createExternalInterface() {
+ if (mIDesktopMode != null) {
+ mIDesktopMode.invalidate();
+ }
+ mIDesktopMode = new IDesktopModeImpl(DesktopModeController.this);
+ return mIDesktopMode;
+ }
+ }
+
+ /**
+ * The interface for calls from outside the host process.
+ */
+ @BinderThread
+ private static class IDesktopModeImpl extends IDesktopMode.Stub {
+
+ private DesktopModeController mController;
+
+ IDesktopModeImpl(DesktopModeController controller) {
+ mController = controller;
+ }
+
+ /**
+ * Invalidates this instance, preventing future calls from updating the controller.
+ */
+ void invalidate() {
+ mController = null;
+ }
+
+ public void showDesktopApps() {
+ executeRemoteCallWithTaskPermission(mController, "showDesktopApps",
+ DesktopModeController::showDesktopApps);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
new file mode 100644
index 000000000000..195ff502e7dc
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.content.Context;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import com.android.internal.protolog.common.ProtoLog;
+
+/**
+ * Constants for desktop mode feature
+ */
+public class DesktopModeStatus {
+
+ /**
+ * Flag to indicate whether desktop mode is available on the device
+ */
+ public static final boolean IS_SUPPORTED = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_mode", false);
+
+ /**
+ * Check if desktop mode is active
+ *
+ * @return {@code true} if active
+ */
+ public static boolean isActive(Context context) {
+ if (!IS_SUPPORTED) {
+ return false;
+ }
+ try {
+ int result = Settings.System.getIntForUser(context.getContentResolver(),
+ Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT);
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "isDesktopModeEnabled=%s", result);
+ return result != 0;
+ } catch (Exception e) {
+ ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e);
+ return false;
+ }
+ }
+}
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
new file mode 100644
index 000000000000..988601c0e8a8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.util.ArraySet
+
+/**
+ * Keeps track of task data related to desktop mode.
+ */
+class DesktopModeTaskRepository {
+
+ /**
+ * Set of task ids that are marked as active in desktop mode.
+ * Active tasks in desktop mode are freeform tasks that are visible or have been visible after
+ * desktop mode was activated.
+ * Task gets removed from this list when it vanishes. Or when desktop mode is turned off.
+ */
+ private val activeTasks = ArraySet<Int>()
+ private val listeners = ArraySet<Listener>()
+
+ /**
+ * Add a [Listener] to be notified of updates to the repository.
+ */
+ fun addListener(listener: Listener) {
+ listeners.add(listener)
+ }
+
+ /**
+ * Remove a previously registered [Listener]
+ */
+ fun removeListener(listener: Listener) {
+ listeners.remove(listener)
+ }
+
+ /**
+ * Mark a task with given [taskId] as active.
+ */
+ fun addActiveTask(taskId: Int) {
+ val added = activeTasks.add(taskId)
+ if (added) {
+ listeners.onEach { it.onActiveTasksChanged() }
+ }
+ }
+
+ /**
+ * Remove task with given [taskId] from active tasks.
+ */
+ fun removeActiveTask(taskId: Int) {
+ val removed = activeTasks.remove(taskId)
+ if (removed) {
+ listeners.onEach { it.onActiveTasksChanged() }
+ }
+ }
+
+ /**
+ * Check if a task with the given [taskId] was marked as an active task
+ */
+ fun isActiveTask(taskId: Int): Boolean {
+ return activeTasks.contains(taskId)
+ }
+
+ /**
+ * Get a set of the active tasks
+ */
+ fun getActiveTasks(): ArraySet<Int> {
+ return ArraySet(activeTasks)
+ }
+
+ /**
+ * Defines interface for classes that can listen to changes in repository state.
+ */
+ interface Listener {
+ fun onActiveTasksChanged()
+ }
+}
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
new file mode 100644
index 000000000000..5042bd6f2d65
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode;
+
+/**
+ * Interface that is exposed to remote callers to manipulate desktop mode features.
+ */
+interface IDesktopMode {
+
+ /** Show apps on the desktop */
+ void showDesktopApps();
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md
new file mode 100644
index 000000000000..73a7348d5aca
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md
@@ -0,0 +1,18 @@
+# Window Manager Shell Readme
+
+The following docs present more detail about the implementation of the WMShell library (in no
+particular order):
+
+1) [What is the Shell](overview.md)
+2) [Integration with SystemUI & Launcher](sysui.md)
+3) [Usage of Dagger](dagger.md)
+4) [Threading model in the Shell](threading.md)
+5) [Making changes in the Shell](changes.md)
+6) [Extending the Shell for Products/OEMs](extending.md)
+7) [Debugging in the Shell](debugging.md)
+8) [Testing in the Shell](testing.md)
+
+Todo
+- Per-feature docs
+- Feature flagging
+- Best practices \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
new file mode 100644
index 000000000000..2aa933d641fa
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
@@ -0,0 +1,87 @@
+# Making changes in the Shell
+
+---
+
+## Code reviews
+
+In addition to the individual reviewers who are most familiar with the changes you are making,
+please also add [wm-code-reviewers@google.com](http://g/wm-code-reviewers) to keep other WM folks
+in the loop.
+
+## Adding new code
+
+### Internal Shell utility classes
+If the new component is used only within the WMShell library, then there are no special
+considerations, go ahead and add it (in the `com.android.wm.shell.common` package for example)
+and make sure the appropriate [unit tests](testing.md) are added.
+
+### Internal Shell components
+If the new component is to be used by other components/features within the Shell library, then
+you can create an appropriate package for this component to add your new code. The current
+pattern is to have a single `<Component name>Controller` that handles the initialization of the
+component.
+
+As mentioned in the [Dagger usage](dagger.md) docs, you need to determine whether it should go into:
+- `WMShellBaseModule` for components that other base & product components will depend on
+- or `WMShellModule`, `TvWmShellModule`, etc. for product specific components that no base
+ components depend on
+
+### SysUI accessible components
+In addition to doing the above, you will also need to provide an interface for calling to SysUI
+from the Shell and vice versa. The current pattern is to have a parallel `Optional<Component name>`
+interface that the `<Component name>Controller` implements and handles on the main Shell thread.
+
+In addition, because components accessible to SysUI injection are explicitly listed, you'll have to
+add an appropriate method in `WMComponent` to get the interface and update the `Builder` in
+`SysUIComponent` to take the interface so it can be injected in SysUI code. The binding between
+the two is done in `SystemUIFactory#init()` which will need to be updated as well.
+
+### Launcher accessible components
+Because Launcher is not a part of SystemUI and is a separate process, exposing controllers to
+Launcher requires a new AIDL interface to be created and implemented by the controller. The
+implementation of the stub interface in the controller otherwise behaves similar to the interface
+to SysUI where it posts the work to the main Shell thread.
+
+### Component initialization
+To initialize the component:
+- On the Shell side, you potentially need to do two things to initialize the component:
+ - Inject `ShellInit` into your component and add an init callback
+ - Ensure that your component is a part of the dagger dependency graph, either by:
+ - Making this component a dependency of an existing component already exposed to SystemUI
+ - Explicitly add this component to the WMShellBaseModule @ShellCreateTrigger provider or
+ the @ShellCreateTriggerOverride provider for your product module to expose it explicitly
+ if it is a completely independent component
+- On the SysUI side, update `WMShell` to setup any bindings for the component that depend on
+ SysUI code
+
+To verify that your component is being initialized at startup, you can enable the `WM_SHELL_INIT`
+protolog group and restart the SysUI process:
+```shell
+adb shell wm logging enable-text WM_SHELL_INIT
+adb shell kill `pid com.android.systemui`
+adb logcat *:S WindowManagerShell
+```
+
+### General Do's & Dont's
+Do:
+- Do add unit tests for all new components
+- Do keep controllers simple and break them down as needed
+
+Don't:
+- **Don't** do initialization in the constructor, only do initialization in the init callbacks.
+ Otherwise it complicates the building of the dependency graph.
+- **Don't** create dependencies from base-module components on specific features (the base module
+ is intended for use with all products)
+ - Try adding a mechanism to register and listen for changes from the base module component instead
+- **Don't** add blocking synchronous calls in the SysUI interface between Shell & SysUI
+ - Try adding a push-mechanism to share data, or an async callback to request data
+
+### Exposing shared code for use in Launcher
+Launcher doesn't currently build against the Shell library, but needs to have access to some shared
+AIDL interfaces and constants. Currently, all AIDL files, and classes under the
+`com.android.wm.shell.util` package are automatically built into the `SystemUISharedLib` that
+Launcher uses.
+
+If the new code doesn't fall into those categories, they can be added explicitly in the Shell's
+[Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp) file under the
+`wm_shell_util-sources` filegroup. \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md
new file mode 100644
index 000000000000..6c01d962adc9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md
@@ -0,0 +1,50 @@
+# Usage of Dagger in the Shell library
+
+---
+
+## Dependencies
+
+Dagger is not required to use the Shell library, but it has a lot of obvious benefits:
+
+- Not having to worry about how to instantiate all the dependencies of a class, especially as
+ dependencies evolve (ie. product controller depends on base controller)
+- Can create boundaries within the same app to encourage better code modularity
+
+As such, the Shell also tries to provide some reasonable out-of-the-box modules for use with Dagger.
+
+## Modules
+
+All the Dagger related code in the Shell can be found in the `com.android.wm.shell.dagger` package,
+this is intentional as it keeps the "magic" in a single location. The explicit nature of how
+components in the shell are provided is as a result a bit more verbose, but it makes it easy for
+developers to jump into a few select files and understand how different components are provided
+(especially as products override components).
+
+The module dependency tree looks a bit like:
+- [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java)
+ (provides threading-related components)
+ - [WMShellBaseModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java)
+ (provides components that are likely common to all products, ie. DisplayController,
+ Transactions, etc.)
+ - [WMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java)
+ (phone/tablet specific components only)
+ - [TvPipModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java)
+ (PIP specific components for TV)
+ - [TvWMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java)
+ (TV specific components only)
+ - etc.
+
+Ideally features could be abstracted out into their own modules and included as needed by each
+product.
+
+## Overriding base components
+
+In some rare cases, there are base components that can change behavior depending on which
+product it runs on. If there are hooks that can be added to the component, that is the
+preferable approach.
+
+The alternative is to use the [@DynamicOverride](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java)
+annotation to allow the product module to provide an implementation that the base module can
+reference. This is most useful if the existence of the entire component is controlled by the
+product and the override implementation is optional (there is a default implementation). More
+details can be found in the class's javadoc. \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
new file mode 100644
index 000000000000..99922fbc2d95
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -0,0 +1,69 @@
+# Debugging in the Shell
+
+---
+
+## Logging & ProtoLogs
+
+The interactions in the Shell can be pretty complicated, so having good logging is crucial to
+debugging problems that arise (especially in dogfood). The Shell uses the same efficient Protolog
+mechanism as WM Core, which can be enabled at runtime on debug devices.
+
+**TLDR**&nbsp; Don’t use Logs or Slogs except for error cases, Protologs are much more flexible,
+easy to add and easy to use
+
+### Adding a new ProtoLog
+Update `ShellProtoLogGroup` to include a new log group (ie. NEW_FEATURE) for the content you want to
+log. ProtoLog log calls mirror Log.v/d/e(), and take a format message and arguments:
+```java
+ProtoLog.v(NEW_FEATURE, "Test log w/ params: %d %s", 1, “a”)
+```
+This code itself will not compile by itself, but the `protologtool` will preprocess the file when
+building to check the log state (is enabled) before printing the print format style log.
+
+**Notes**
+- ProtoLogs currently only work from soong builds (ie. via make/mp). We need to reimplement the
+ tool for use with SysUI-studio
+- Non-text ProtoLogs are not currently supported with the Shell library (you can't view them with
+ traces in Winscope)
+
+### Enabling ProtoLog command line logging
+Run these commands to enable protologs for both WM Core and WM Shell to print to logcat.
+```shell
+adb shell wm logging enable-text NEW_FEATURE
+adb shell wm logging disable-text NEW_FEATURE
+```
+
+## Winscope Tracing
+
+The Winscope tool is extremely useful in determining what is happening on-screen in both
+WindowManager and SurfaceFlinger. Follow [go/winscope](http://go/winscope-help) to learn how to
+use the tool.
+
+In addition, there is limited preliminary support for Winscope tracing componetns in the Shell,
+which involves adding trace fields to [wm_shell_trace.proto](frameworks/base/libs/WindowManager/Shell/proto/wm_shell_trace.proto)
+file and ensure it is updated as a part of `WMShell#writeToProto`.
+
+Tracing can be started via the shell command (to be added to the Winscope tool as needed):
+```shell
+adb shell cmd statusbar tracing start
+adb shell cmd statusbar tracing stop
+```
+
+## Dumps
+
+Because the Shell library is built as a part of SystemUI, dumping the state is currently done as a
+part of dumping the SystemUI service. Dumping the Shell specific data can be done by specifying the
+WMShell SysUI service:
+
+```shell
+adb shell dumpsys activity service SystemUIService WMShell
+```
+
+If information should be added to the dump, either:
+- Update `WMShell` if you are dumping SysUI state
+- Inject `ShellCommandHandler` into your Shell class, and add a dump callback
+
+## Debugging in Android Studio
+
+If you are using the [go/sysui-studio](http://go/sysui-studio) project, then you can debug Shell
+code directly from Android Studio like any other app.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md
new file mode 100644
index 000000000000..061ae00e2b25
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md
@@ -0,0 +1,13 @@
+# Extending the Shell for Products/OEMs
+
+---
+
+## General Do's & Dont's
+
+Do:
+- &nbsp;
+
+Don't
+- **Don't** override classes provided by WMShellBaseModule, it makes it difficult to make
+ simple changes to the Shell library base modules which are shared by all products
+ - If possible add mechanisms to modify the base class behavior \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md
new file mode 100644
index 000000000000..a88ef6aea2ec
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md
@@ -0,0 +1,58 @@
+# What is the WindowManager Shell
+
+---
+
+## Motivation
+
+The primary motivation for the WindowManager Shell (WMShell) library is to effectively scale
+WindowManager by making it easy&trade; and safe to create windowing features to fit the needs of
+various Android products and form factors.
+
+To achieve this, WindowManager separates the policy of managing windows (WMCore) from the
+presentation of surfaces (WMShell) and provides a minimal interface boundary for the two to
+communicate.
+
+## Who is using the library?
+
+Currently, the WMShell library is used to drive the windowing experience on handheld
+(phones & tablets), TV, Auto, Arc++, and Wear to varying degrees.
+
+## Where does the code live
+
+The core WMShell library code is currently located in the [frameworks/base/libs/WindowManager/Shell](frameworks/base/libs/WindowManager/Shell)
+directory and is included as a part dependency of the host SystemUI apk.
+
+## How do I build the Shell library
+
+The library can be built directly by running (using [go/makepush](http://go/makepush)):
+```shell
+mp :WindowManager-Shell
+```
+But this is mainly useful for inspecting the contents of the library or verifying it builds. The
+various targets can be found in the Shell library's [Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp)
+file.
+
+Normally, you would build it as a part of the host SystemUI, for example via commandline:
+```shell
+# Phone SystemUI variant
+mp sysuig
+# Building Shell & SysUI changes along w/ framework changes
+mp core services sysuig
+```
+
+Or preferably, if you are making WMShell/SysUI only changes (no other framework changes), then
+building via [go/sysui-studio](http://go/sysui-studio) allows for very quick iteration (one click
+build and push of SysUI in < 30s).
+
+If you are making framework changes and are using `aidegen` to set up your platform IDE, make sure
+to include the appropriate directories to build, for example:
+```shell
+# frameworks/base will include base/libs/WindowManager/Shell and base/packages/SystemUI
+aidegen frameworks/base \
+ vendor/<oem>/packages/SystemUI \
+ ...
+```
+
+## Other useful links
+- [go/o-o-summit-20](go/o-o-summit-20) (Video presentations from the WM team)
+- [go/o-o-summit-21](go/o-o-summit-21) \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
new file mode 100644
index 000000000000..d6302e640ba7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
@@ -0,0 +1,83 @@
+# Shell & SystemUI
+
+---
+
+## Setup
+
+The SystemUI of various products depend on and build against the WM Shell library. To ensure
+that we don't inadvertently build dependencies between the Shell library and one particular
+product (ie. handheld SysUI), we deliberately separate the initialization of the WM Shell
+component from the SysUI component when set up through Dagger.
+
+**TLDR**&nbsp; Initialize everything as needed in the WM component scope and export only well
+defined interfaces to SysUI.
+
+## Initialization
+
+There are more details in the Dagger docs, but the general overview of the SysUI/Shell
+initialization flow is such:
+
+1) SysUI Global scope is initialize (see `GlobalModule` and its included modules)
+2) WM Shell scope is initialized, for example
+ 1) On phones: `WMComponent` includes `WMShellModule` which includes `WMShellBaseModule`
+ (common to all SysUI)
+ 2) On TVs: `TvWMComponent` includes `TvWMShellModule` which includes `WMShellBaseModule`
+ 3) etc.
+3) SysUI explicitly passes interfaces provided from the `WMComponent` to `SysUIComponent` via
+ the `SysUIComponent#Builder`, then builds the SysUI scoped components
+4) `WMShell` is the SystemUI “service” (in the SysUI scope) that initializes with the app after the
+SystemUI part of the dependency graph has been created. It contains the binding code between the
+interfaces provided by the Shell and the rest of SystemUI.
+5) SysUI can inject the interfaces into its own components
+
+More detail can be found in [go/wm-sysui-dagger](http://go/wm-sysui-dagger).
+
+## Interfaces from SysUI to Shell components
+
+Within the same process, the WM Shell components can be running on a different thread than the main
+SysUI thread (disabled on certain products). This introduces challenges where we have to be
+careful about how SysUI calls into the Shell and vice versa.
+
+As a result, we enforce explicit interfaces between SysUI and Shell components, and the
+implementations of the interfaces on each side need to post to the right thread before it calls
+into other code.
+
+For example, you might have:
+1) (Shell) ShellFeature interface to be used from SysUI
+2) (Shell) ShellFeatureController handles logic, implements ShellFeature interface and posts to
+ main Shell thread
+3) SysUI application init injects Optional<ShellFeature> as an interface to SysUI to call
+4) (SysUI) SysUIFeature depends on ShellFeature interface
+5) (SysUI) SysUIFeature injects Optional<ShellFeature>, and sets up a callback for the Shell to
+ call, and the callback posts to the main SysUI thread
+
+Adding an interface to a Shell component may seem like a lot of boiler plate, but is currently
+necessary to maintain proper threading and logic isolation.
+
+## Listening for Configuration changes & other SysUI events
+
+Aside from direct calls into Shell controllers for exposed features, the Shell also receives
+common event callbacks from SysUI via the `ShellController`. This includes things like:
+
+- Configuration changes
+- Keyguard events
+- Shell init
+- Shell dumps & commands
+
+For other events which are specific to the Shell feature, then you can add callback methods on
+the Shell feature interface. Any such calls should <u>**never**</u> be synchronous calls as
+they will need to post to the Shell main thread to run.
+
+## Shell commands & Dumps
+
+Since the Shell library is a part of the SysUI process, it relies on SysUI to trigger commands
+on individual Shell components, or to dump individual shell components.
+
+```shell
+# Dump everything
+adb shell dumpsys activity service SystemUIService WMShell
+
+# Run a specific command
+adb shell dumpsys activity service SystemUIService WMShell help
+adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ...
+``` \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md
new file mode 100644
index 000000000000..8a80333facc4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md
@@ -0,0 +1,49 @@
+# Testing
+
+---
+
+## Unit tests
+
+New WM Shell unit tests can be added to the
+[Shell/tests/unittest](frameworks/base/libs/WindowManager/Shell/tests/unittest) directory, and can
+be run via command line using `atest`:
+```shell
+atest WMShellUnitTests
+```
+
+If you use the SysUI Studio project, you can run and debug tests directly in the source files
+(click on the little arrows next to the test class or test method).
+
+These unit tests are run as a part of WindowManager presubmit, and the dashboards for these unit
+tests tests can be found at [go/wm-tests](http://go/wm-tests).
+
+This [GCL file](http://go/wm-unit-tests-gcl) configures the tests being run on the server.
+
+## Flicker tests
+
+Flicker tests are tests that perform actions and make assertions on the state in Window Manager
+and SurfaceFlinger traces captured during the run.
+
+New WM Shell Flicker tests can be added to the
+[Shell/tests/flicker](frameworks/base/libs/WindowManager/Shell/tests/flicker) directory, and can
+be run via command line using `atest`:
+```shell
+atest WMShellFlickerTests
+```
+
+**Note**: Currently Flicker tests can only be run from the commandline and not via SysUI Studio
+
+A subset of the flicker tests tests are run as a part of WindowManager presubmit, and the
+dashboards for these tests tests can be found at [go/wm-tests-flicker](http://go/wm-tests-flicker).
+
+## CTS tests
+
+Some windowing features also have CTS tests to ensure consistent behavior across OEMs. For example:
+- Picture-in-Picture:
+ [PinnedStackTests](cts/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java)
+- etc.
+
+These can also be run via commandline only using `atest`, for example:
+```shell
+atest PinnedStackTests
+``` \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
new file mode 100644
index 000000000000..eac748894432
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
@@ -0,0 +1,83 @@
+# Threading
+
+---
+
+## Boundaries
+
+```text
+ Thread boundary
+ |
+ WM Shell | SystemUI
+ |
+ |
+FeatureController <-> FeatureInterface <--|--> WMShell <-> SysUI
+ | (^post to shell thread) | (^post to main thread)
+ ... |
+ | |
+ OtherControllers |
+```
+
+## Threads
+
+We currently have multiple threads in use in the Shell library depending on the configuration by
+the product.
+- SysUI main thread (standard main thread)
+- `ShellMainThread` (only used if the resource `config_enableShellMainThread` is set true
+ (ie. phones))
+ - This falls back to the SysUI main thread otherwise
+ - **Note**:
+ - This thread runs with `THREAD_PRIORITY_DISPLAY` priority since so many windowing-critical
+ components depend on it
+ - This is also the UI thread for almost all UI created by the Shell
+ - The Shell main thread Handler (and the Executor that wraps it) is async, so
+ messages/runnables used via this Handler are handled immediately if there is no sync
+ messages prior to it in the queue.
+- `ShellBackgroundThread` (for longer running tasks where we don't want to block the shell main
+ thread)
+ - This is always another thread even if config_enableShellMainThread is not set true
+ - **Note**:
+ - This thread runs with `THREAD_PRIORITY_BACKGROUND` priority
+- `ShellAnimationThread` (currently only used for Transitions and Splitscreen, but potentially all
+ animations could be offloaded here)
+- `ShellSplashScreenThread` (only for use with splashscreens)
+
+## Dagger setup
+
+The threading-related components are provided by the [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java),
+for example, the Executors and Handlers for the various threads that are used. You can request
+an executor of the necessary type by using the appropriate annotation for each of the threads (ie.
+`@ShellMainThread Executor`) when injecting into your Shell component.
+
+To get the SysUI main thread, you can use the `@Main` annotation.
+
+## Best practices
+
+### Components
+- Don't do initialization in the Shell component constructors
+ - If the host SysUI is not careful, it may construct the WMComponent dependencies on the main
+ thread, and this reduces the likelihood that components will intiailize on the wrong thread
+ in such cases
+- Be careful of using CountDownLatch and other blocking synchronization mechanisms in Shell code
+ - If the Shell main thread is not a separate thread, this will cause a deadlock
+- Callbacks, Observers, Listeners to any non-shell component should post onto main Shell thread
+ - This includes Binder calls, SysUI calls, BroadcastReceivers, etc. Basically any API that
+ takes a runnable should either be registered with the right Executor/Handler or posted to
+ the main Shell thread manually
+- Since everything in the Shell runs on the main Shell thread, you do **not** need to explicitly
+ `synchronize` your code (unless you are trying to prevent reentrantcy, but that can also be
+ done in other ways)
+
+### Handlers/Executors
+- You generally **never** need to create Handlers explicitly, instead inject `@ShellMainThread
+ ShellExecutor` instead
+ - This is a common pattern to defer logic in UI code, but the Handler created wraps the Looper
+ that is currently running, which can be wrong (see above for initialization vs construction)
+- That said, sometimes Handlers are necessary because Framework API only takes Handlers or you
+ want to dedupe multiple messages
+ - In such cases inject `@ShellMainThread Handler` or use view.getHandler() which should be OK
+ assuming that the view root was initialized on the main Shell thread
+- **Never use Looper.getMainLooper()**
+ - It's likely going to be wrong, you can inject `@Main ShellExecutor` to get the SysUI main thread
+
+### Testing
+- You can use a `TestShellExecutor` to control the processing of messages \ 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 95de2dc61a43..b59fe1818780 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
@@ -60,29 +60,30 @@ import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import java.util.ArrayList;
-import java.util.Optional;
/**
* Handles the global drag and drop handling for the Shell.
*/
public class DragAndDropController implements DisplayController.OnDisplaysChangedListener,
- View.OnDragListener {
+ View.OnDragListener, ConfigurationChangeListener {
private static final String TAG = DragAndDropController.class.getSimpleName();
private final Context mContext;
+ private final ShellController mShellController;
private final DisplayController mDisplayController;
private final DragAndDropEventLogger mLogger;
private final IconProvider mIconProvider;
private SplitScreenController mSplitScreen;
private ShellExecutor mMainExecutor;
- private DragAndDropImpl mImpl;
private ArrayList<DragAndDropListener> mListeners = new ArrayList<>();
private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>();
- private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
/**
* Listener called during drag events, currently just onDragStarted.
@@ -92,23 +93,40 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
void onDragStarted();
}
- public DragAndDropController(Context context, DisplayController displayController,
- UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor) {
+ public DragAndDropController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ DisplayController displayController,
+ UiEventLogger uiEventLogger,
+ IconProvider iconProvider,
+ ShellExecutor mainExecutor) {
mContext = context;
+ mShellController = shellController;
mDisplayController = displayController;
mLogger = new DragAndDropEventLogger(uiEventLogger);
mIconProvider = iconProvider;
mMainExecutor = mainExecutor;
- mImpl = new DragAndDropImpl();
+ shellInit.addInitCallback(this::onInit, this);
}
- public DragAndDrop asDragAndDrop() {
- return mImpl;
+ /**
+ * Called when the controller is initialized.
+ */
+ public void onInit() {
+ // TODO(b/238217847): The dependency from SplitscreenController on DragAndDropController is
+ // inverted, which leads to SplitscreenController not setting its instance until after
+ // onDisplayAdded. We can remove this post once we fix that dependency.
+ mMainExecutor.executeDelayed(() -> {
+ mDisplayController.addDisplayWindowListener(this);
+ }, 0);
+ mShellController.addConfigurationChangeListener(this);
}
- public void initialize(Optional<SplitScreenController> splitscreen) {
- mSplitScreen = splitscreen.orElse(null);
- mDisplayController.addDisplayWindowListener(this);
+ /**
+ * Sets the splitscreen controller to use if the feature is available.
+ */
+ public void setSplitScreenController(SplitScreenController splitscreen) {
+ mSplitScreen = splitscreen;
}
/** Adds a listener to be notified of drag and drop events. */
@@ -240,12 +258,12 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
break;
case ACTION_DRAG_ENTERED:
pd.dragLayout.show();
- pd.dragLayout.update(event);
break;
case ACTION_DRAG_LOCATION:
pd.dragLayout.update(event);
break;
case ACTION_DROP: {
+ pd.dragLayout.update(event);
return handleDrop(event, pd);
}
case ACTION_DRAG_EXITED: {
@@ -310,13 +328,15 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
return mimeTypes;
}
- private void onThemeChange() {
+ @Override
+ public void onThemeChanged() {
for (int i = 0; i < mDisplayDropTargets.size(); i++) {
mDisplayDropTargets.get(i).dragLayout.onThemeChange();
}
}
- private void onConfigChanged(Configuration newConfig) {
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
for (int i = 0; i < mDisplayDropTargets.size(); i++) {
mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig);
}
@@ -342,21 +362,4 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
dragLayout = dl;
}
}
-
- private class DragAndDropImpl implements DragAndDrop {
-
- @Override
- public void onThemeChanged() {
- mMainExecutor.execute(() -> {
- DragAndDropController.this.onThemeChange();
- });
- }
-
- @Override
- public void onConfigChanged(Configuration newConfig) {
- mMainExecutor.execute(() -> {
- DragAndDropController.this.onConfigChanged(newConfig);
- });
- }
- }
}
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 756831007c35..62bf5172e106 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
@@ -17,6 +17,8 @@
package com.android.wm.shell.draganddrop;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED;
+import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -45,12 +47,10 @@ import android.app.WindowConfiguration;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipDescription;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.LauncherApps;
-import android.content.pm.ResolveInfo;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Bundle;
@@ -64,11 +64,9 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.InstanceId;
-import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
-import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.lang.annotation.Retention;
@@ -267,50 +265,14 @@ public class DragAndDropPolicy {
mStarter.startShortcut(packageName, id, position, opts, user);
} else {
final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT);
- mStarter.startIntent(launchIntent, getStartIntentFillInIntent(launchIntent, position),
- position, opts);
+ // 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(launchIntent, null /* fillIntent */, position, opts);
}
}
/**
- * Returns the fill-in intent to use when starting an app from a drop.
- */
- @VisibleForTesting
- Intent getStartIntentFillInIntent(PendingIntent launchIntent, @SplitPosition int position) {
- // Get the drag app
- final List<ResolveInfo> infos = launchIntent.queryIntentComponents(0 /* flags */);
- final ComponentName dragIntentActivity = !infos.isEmpty()
- ? infos.get(0).activityInfo.getComponentName()
- : null;
-
- // Get the current app (either fullscreen or the remaining app post-drop if in splitscreen)
- final boolean inSplitScreen = mSplitScreen != null
- && mSplitScreen.isSplitScreenVisible();
- final ComponentName currentActivity;
- if (!inSplitScreen) {
- currentActivity = mSession.runningTaskInfo != null
- ? mSession.runningTaskInfo.baseActivity
- : null;
- } else {
- final int nonReplacedSplitPosition = position == SPLIT_POSITION_TOP_OR_LEFT
- ? SPLIT_POSITION_BOTTOM_OR_RIGHT
- : SPLIT_POSITION_TOP_OR_LEFT;
- ActivityManager.RunningTaskInfo nonReplacedTaskInfo =
- mSplitScreen.getTaskInfo(nonReplacedSplitPosition);
- currentActivity = nonReplacedTaskInfo.baseActivity;
- }
-
- if (currentActivity.equals(dragIntentActivity)) {
- // Only apply MULTIPLE_TASK if we are dragging the same activity
- final Intent fillInIntent = new Intent();
- fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Adding MULTIPLE_TASK");
- return fillInIntent;
- }
- return null;
- }
-
- /**
* Per-drag session data.
*/
private static class DragSession {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index ff3c0834cf62..497a6f696df8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -105,6 +105,10 @@ public class DragLayout extends LinearLayout {
MATCH_PARENT));
((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1;
((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1;
+ int orientation = getResources().getConfiguration().orientation;
+ setOrientation(orientation == Configuration.ORIENTATION_LANDSCAPE
+ ? LinearLayout.HORIZONTAL
+ : LinearLayout.VERTICAL);
updateContainerMargins(getResources().getConfiguration().orientation);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java
new file mode 100644
index 000000000000..83a1734dc71a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java
@@ -0,0 +1,259 @@
+/*
+ * 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.floating;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.floating.views.FloatingTaskLayer;
+import com.android.wm.shell.floating.views.FloatingTaskView;
+
+import java.util.Objects;
+
+/**
+ * Controls a floating dismiss circle that has a 'magnetic' field around it, causing views moved
+ * close to the target to be stuck to it unless moved out again.
+ */
+public class FloatingDismissController {
+
+ /** Velocity required to dismiss the view without dragging it into the dismiss target. */
+ private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f;
+ /**
+ * Max velocity that the view can be moving through the target with to stick (i.e. if it's
+ * more than this velocity, it will pass through the target.
+ */
+ private static final float STICK_TO_TARGET_MAX_X_VELOCITY = 2000f;
+ /**
+ * Percentage of the target width to use to determine if an object flung towards the target
+ * should dismiss (e.g. if target is 100px and this is set ot 2f, anything flung within a
+ * 200px-wide area around the target will be considered 'near' enough get dismissed).
+ */
+ private static final float FLING_TO_TARGET_WIDTH_PERCENT = 2f;
+ /** Minimum alpha to apply to the view being dismissed when it is in the target. */
+ private static final float DISMISS_VIEW_MIN_ALPHA = 0.6f;
+ /** Amount to scale down the view being dismissed when it is in the target. */
+ private static final float DISMISS_VIEW_SCALE_DOWN_PERCENT = 0.15f;
+
+ private Context mContext;
+ private FloatingTasksController mController;
+ private FloatingTaskLayer mParent;
+
+ private DismissView mDismissView;
+ private ValueAnimator mDismissAnimator;
+ private View mViewBeingDismissed;
+ private float mDismissSizePercent;
+ private float mDismissSize;
+
+ /**
+ * The currently magnetized object, which is being dragged and will be attracted to the magnetic
+ * dismiss target.
+ */
+ private MagnetizedObject<View> mMagnetizedObject;
+ /**
+ * The MagneticTarget instance for our circular dismiss view. This is added to the
+ * MagnetizedObject instances for the view being dragged.
+ */
+ private MagnetizedObject.MagneticTarget mMagneticTarget;
+ /** Magnet listener that handles animating and dismissing the view. */
+ private MagnetizedObject.MagnetListener mFloatingViewMagnetListener;
+
+ public FloatingDismissController(Context context, FloatingTasksController controller,
+ FloatingTaskLayer parent) {
+ mContext = context;
+ mController = controller;
+ mParent = parent;
+ updateSizes();
+ createAndAddDismissView();
+
+ mDismissAnimator = ValueAnimator.ofFloat(1f, 0f);
+ mDismissAnimator.addUpdateListener(animation -> {
+ final float value = (float) animation.getAnimatedValue();
+ if (mDismissView != null) {
+ mDismissView.setPivotX((mDismissView.getRight() - mDismissView.getLeft()) / 2f);
+ mDismissView.setPivotY((mDismissView.getBottom() - mDismissView.getTop()) / 2f);
+ final float scaleValue = Math.max(value, mDismissSizePercent);
+ mDismissView.getCircle().setScaleX(scaleValue);
+ mDismissView.getCircle().setScaleY(scaleValue);
+ }
+ if (mViewBeingDismissed != null) {
+ // TODO: alpha doesn't actually apply to taskView currently.
+ mViewBeingDismissed.setAlpha(Math.max(value, DISMISS_VIEW_MIN_ALPHA));
+ mViewBeingDismissed.setScaleX(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT));
+ mViewBeingDismissed.setScaleY(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT));
+ }
+ });
+
+ mFloatingViewMagnetListener = new MagnetizedObject.MagnetListener() {
+ @Override
+ public void onStuckToTarget(
+ @NonNull MagnetizedObject.MagneticTarget target) {
+ animateDismissing(/* dismissing= */ true);
+ }
+
+ @Override
+ public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ float velX, float velY, boolean wasFlungOut) {
+ animateDismissing(/* dismissing= */ false);
+ mParent.onUnstuckFromTarget((FloatingTaskView) mViewBeingDismissed, velX, velY,
+ wasFlungOut);
+ }
+
+ @Override
+ public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ doDismiss();
+ }
+ };
+ }
+
+ /** Updates all the sizes used and applies them to the {@link DismissView}. */
+ public void updateSizes() {
+ Resources res = mContext.getResources();
+ mDismissSize = res.getDimensionPixelSize(
+ R.dimen.floating_task_dismiss_circle_size);
+ final float minDismissSize = res.getDimensionPixelSize(
+ R.dimen.floating_dismiss_circle_small);
+ mDismissSizePercent = minDismissSize / mDismissSize;
+
+ if (mDismissView != null) {
+ mDismissView.updateResources();
+ }
+ }
+
+ /** Prepares the view being dragged to be magnetic. */
+ public void setUpMagneticObject(View viewBeingDragged) {
+ mViewBeingDismissed = viewBeingDragged;
+ mMagnetizedObject = getMagnetizedView(viewBeingDragged);
+ mMagnetizedObject.clearAllTargets();
+ mMagnetizedObject.addTarget(mMagneticTarget);
+ mMagnetizedObject.setMagnetListener(mFloatingViewMagnetListener);
+ }
+
+ /** Shows or hides the dismiss target. */
+ public void showDismiss(boolean show) {
+ if (show) {
+ mDismissView.show();
+ } else {
+ mDismissView.hide();
+ }
+ }
+
+ /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
+ public boolean passEventToMagnetizedObject(MotionEvent event) {
+ return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
+ }
+
+ private void createAndAddDismissView() {
+ if (mDismissView != null) {
+ mParent.removeView(mDismissView);
+ }
+ mDismissView = new DismissView(mContext);
+ mDismissView.setTargetSizeResId(R.dimen.floating_task_dismiss_circle_size);
+ mDismissView.updateResources();
+ mParent.addView(mDismissView);
+
+ final float dismissRadius = mDismissSize;
+ // Save the MagneticTarget instance for the newly set up view - we'll add this to the
+ // MagnetizedObjects when the dismiss view gets shown.
+ mMagneticTarget = new MagnetizedObject.MagneticTarget(
+ mDismissView.getCircle(), (int) dismissRadius);
+ }
+
+ private MagnetizedObject<View> getMagnetizedView(View v) {
+ if (mMagnetizedObject != null
+ && Objects.equals(mMagnetizedObject.getUnderlyingObject(), v)) {
+ // Same view being dragged, we can reuse the magnetic object.
+ return mMagnetizedObject;
+ }
+ MagnetizedObject<View> magnetizedView = new MagnetizedObject<View>(
+ mContext,
+ v,
+ DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y
+ ) {
+ @Override
+ public float getWidth(@NonNull View underlyingObject) {
+ return underlyingObject.getWidth();
+ }
+
+ @Override
+ public float getHeight(@NonNull View underlyingObject) {
+ return underlyingObject.getHeight();
+ }
+
+ @Override
+ public void getLocationOnScreen(@NonNull View underlyingObject,
+ @NonNull int[] loc) {
+ loc[0] = (int) underlyingObject.getTranslationX();
+ loc[1] = (int) underlyingObject.getTranslationY();
+ }
+ };
+ magnetizedView.setHapticsEnabled(true);
+ magnetizedView.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ magnetizedView.setStickToTargetMaxXVelocity(STICK_TO_TARGET_MAX_X_VELOCITY);
+ magnetizedView.setFlingToTargetWidthPercent(FLING_TO_TARGET_WIDTH_PERCENT);
+ return magnetizedView;
+ }
+
+ /** Animates the dismiss treatment on the view being dismissed. */
+ private void animateDismissing(boolean shouldDismiss) {
+ if (mViewBeingDismissed == null) {
+ return;
+ }
+ if (shouldDismiss) {
+ mDismissAnimator.removeAllListeners();
+ mDismissAnimator.start();
+ } else {
+ mDismissAnimator.removeAllListeners();
+ mDismissAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ resetDismissAnimator();
+ }
+ });
+ mDismissAnimator.reverse();
+ }
+ }
+
+ /** Actually dismisses the view. */
+ private void doDismiss() {
+ mDismissView.hide();
+ mController.removeTask();
+ resetDismissAnimator();
+ mViewBeingDismissed = null;
+ }
+
+ private void resetDismissAnimator() {
+ mDismissAnimator.removeAllListeners();
+ mDismissAnimator.cancel();
+ if (mDismissView != null) {
+ mDismissView.cancelAnimators();
+ mDismissView.getCircle().setScaleX(1f);
+ mDismissView.getCircle().setScaleY(1f);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java
new file mode 100644
index 000000000000..935666026bf4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java
@@ -0,0 +1,41 @@
+/*
+ * 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.floating;
+
+import android.content.Intent;
+
+import com.android.wm.shell.common.annotations.ExternalThread;
+
+/**
+ * Interface to interact with floating tasks.
+ */
+@ExternalThread
+public interface FloatingTasks {
+
+ /**
+ * Shows, stashes, or un-stashes the floating task depending on state:
+ * - If there is no floating task for this intent, it shows the task for the provided intent.
+ * - If there is a floating task for this intent, but it's stashed, this un-stashes it.
+ * - If there is a floating task for this intent, and it's not stashed, this stashes it.
+ */
+ void showOrSetStashed(Intent intent);
+
+ /** Returns a binder that can be passed to an external process to manipulate FloatingTasks. */
+ default IFloatingTasks createExternalInterface() {
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
new file mode 100644
index 000000000000..67552991869b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
@@ -0,0 +1,455 @@
+/*
+ * 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.floating;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
+import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.os.SystemProperties;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.TaskViewTransitions;
+import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.common.RemoteCallable;
+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.common.annotations.ShellBackgroundThread;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.floating.views.FloatingTaskLayer;
+import com.android.wm.shell.floating.views.FloatingTaskView;
+import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Entry point for creating and managing floating tasks.
+ *
+ * A single window layer is added and the task(s) are displayed using a {@link FloatingTaskView}
+ * within that window.
+ *
+ * Currently optimized for a single task. Multiple tasks are not supported.
+ */
+public class FloatingTasksController implements RemoteCallable<FloatingTasksController>,
+ ConfigurationChangeListener {
+
+ private static final String TAG = FloatingTasksController.class.getSimpleName();
+
+ public static final boolean FLOATING_TASKS_ENABLED =
+ SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
+ public static final boolean SHOW_FLOATING_TASKS_AS_BUBBLES =
+ SystemProperties.getBoolean("persist.wm.debug.floating_tasks_as_bubbles", false);
+
+ @VisibleForTesting
+ static final int SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET = 600;
+
+ // Only used for testing
+ private Configuration mConfig;
+ private boolean mFloatingTasksEnabledForTests;
+
+ private FloatingTaskImpl mImpl = new FloatingTaskImpl();
+ private Context mContext;
+ private ShellController mShellController;
+ private ShellCommandHandler mShellCommandHandler;
+ private @Nullable BubbleController mBubbleController;
+ private WindowManager mWindowManager;
+ private ShellTaskOrganizer mTaskOrganizer;
+ private TaskViewTransitions mTaskViewTransitions;
+ private @ShellMainThread ShellExecutor mMainExecutor;
+ // TODO: mBackgroundThread is not used but we'll probs need it eventually?
+ private @ShellBackgroundThread ShellExecutor mBackgroundThread;
+ private SyncTransactionQueue mSyncQueue;
+
+ private boolean mIsFloatingLayerAdded;
+ private FloatingTaskLayer mFloatingTaskLayer;
+ private final Point mLastPosition = new Point(-1, -1);
+
+ private Task mTask;
+
+ // Simple class to hold onto info for intent or shortcut based tasks.
+ public static class Task {
+ public int taskId = INVALID_TASK_ID;
+ @Nullable
+ public Intent intent;
+ @Nullable
+ public ShortcutInfo info;
+ @Nullable
+ public FloatingTaskView floatingView;
+ }
+
+ public FloatingTasksController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ ShellCommandHandler shellCommandHandler,
+ Optional<BubbleController> bubbleController,
+ WindowManager windowManager,
+ ShellTaskOrganizer organizer,
+ TaskViewTransitions transitions,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellBackgroundThread ShellExecutor bgExceutor,
+ SyncTransactionQueue syncTransactionQueue) {
+ mContext = context;
+ mShellController = shellController;
+ mShellCommandHandler = shellCommandHandler;
+ mBubbleController = bubbleController.get();
+ mWindowManager = windowManager;
+ mTaskOrganizer = organizer;
+ mTaskViewTransitions = transitions;
+ mMainExecutor = mainExecutor;
+ mBackgroundThread = bgExceutor;
+ mSyncQueue = syncTransactionQueue;
+ if (isFloatingTasksEnabled()) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
+ mShellCommandHandler.addDumpCallback(this::dump, this);
+ }
+
+ protected void onInit() {
+ mShellController.addConfigurationChangeListener(this);
+ }
+
+ /** Only used for testing. */
+ @VisibleForTesting
+ void setConfig(Configuration config) {
+ mConfig = config;
+ }
+
+ /** Only used for testing. */
+ @VisibleForTesting
+ void setFloatingTasksEnabled(boolean enabled) {
+ mFloatingTasksEnabledForTests = enabled;
+ }
+
+ /** Whether the floating layer is available. */
+ boolean isFloatingLayerAvailable() {
+ Configuration config = mConfig == null
+ ? mContext.getResources().getConfiguration()
+ : mConfig;
+ return config.smallestScreenWidthDp >= SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET;
+ }
+
+ /** Whether floating tasks are enabled. */
+ boolean isFloatingTasksEnabled() {
+ return FLOATING_TASKS_ENABLED || mFloatingTasksEnabledForTests;
+ }
+
+ @Override
+ public void onThemeChanged() {
+ if (mIsFloatingLayerAdded) {
+ mFloatingTaskLayer.updateSizes();
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // TODO: probably other stuff here to do (e.g. handle rotation)
+ if (mIsFloatingLayerAdded) {
+ mFloatingTaskLayer.updateSizes();
+ }
+ }
+
+ /** Returns false if the task shouldn't be shown. */
+ private boolean canShowTask(Intent intent) {
+ ProtoLog.d(WM_SHELL_FLOATING_APPS, "canShowTask -- %s", intent);
+ if (!isFloatingTasksEnabled() || !isFloatingLayerAvailable()) return false;
+ if (intent == null) {
+ ProtoLog.e(WM_SHELL_FLOATING_APPS, "canShowTask given null intent, doing nothing");
+ return false;
+ }
+ return true;
+ }
+
+ /** Returns true if the task was or should be shown as a bubble. */
+ private boolean maybeShowTaskAsBubble(Intent intent) {
+ if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubbleController != null) {
+ removeFloatingLayer();
+ if (intent.getPackage() != null) {
+ mBubbleController.addAppBubble(intent);
+ ProtoLog.d(WM_SHELL_FLOATING_APPS, "showing floating task as bubble: %s", intent);
+ } else {
+ ProtoLog.d(WM_SHELL_FLOATING_APPS,
+ "failed to show floating task as bubble: %s; unknown package", intent);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Shows, stashes, or un-stashes the floating task depending on state:
+ * - If there is no floating task for this intent, it shows this the provided task.
+ * - If there is a floating task for this intent, but it's stashed, this un-stashes it.
+ * - If there is a floating task for this intent, and it's not stashed, this stashes it.
+ */
+ public void showOrSetStashed(Intent intent) {
+ if (!canShowTask(intent)) return;
+ if (maybeShowTaskAsBubble(intent)) return;
+
+ addFloatingLayer();
+
+ if (isTaskAttached(mTask) && intent.filterEquals(mTask.intent)) {
+ // The task is already added, toggle the stash state.
+ mFloatingTaskLayer.setStashed(mTask, !mTask.floatingView.isStashed());
+ return;
+ }
+
+ // If we're here it's either a new or different task
+ showNewTask(intent);
+ }
+
+ /**
+ * Shows a floating task with the provided intent.
+ * If the same task is present it will un-stash it or do nothing if it is already un-stashed.
+ * Removes any other floating tasks that might exist.
+ */
+ public void showTask(Intent intent) {
+ if (!canShowTask(intent)) return;
+ if (maybeShowTaskAsBubble(intent)) return;
+
+ addFloatingLayer();
+
+ if (isTaskAttached(mTask) && intent.filterEquals(mTask.intent)) {
+ // The task is already added, show it if it's stashed.
+ if (mTask.floatingView.isStashed()) {
+ mFloatingTaskLayer.setStashed(mTask, false);
+ }
+ return;
+ }
+ showNewTask(intent);
+ }
+
+ private void showNewTask(Intent intent) {
+ if (mTask != null && !intent.filterEquals(mTask.intent)) {
+ mFloatingTaskLayer.removeAllTaskViews();
+ mTask.floatingView.cleanUpTaskView();
+ mTask = null;
+ }
+
+ FloatingTaskView ftv = new FloatingTaskView(mContext, this);
+ ftv.createTaskView(mContext, mTaskOrganizer, mTaskViewTransitions, mSyncQueue);
+
+ mTask = new Task();
+ mTask.floatingView = ftv;
+ mTask.intent = intent;
+
+ // Add & start the task.
+ mFloatingTaskLayer.addTask(mTask);
+ ProtoLog.d(WM_SHELL_FLOATING_APPS, "showNewTask, startingIntent: %s", intent);
+ mTask.floatingView.startTask(mMainExecutor, mTask);
+ }
+
+ /**
+ * Removes the task and cleans up the view.
+ */
+ public void removeTask() {
+ if (mTask != null) {
+ ProtoLog.d(WM_SHELL_FLOATING_APPS, "Removing task with id=%d", mTask.taskId);
+
+ if (mTask.floatingView != null) {
+ // TODO: animate it
+ mFloatingTaskLayer.removeView(mTask.floatingView);
+ mTask.floatingView.cleanUpTaskView();
+ }
+ removeFloatingLayer();
+ }
+ }
+
+ /**
+ * Whether there is a floating task and if it is stashed.
+ */
+ public boolean isStashed() {
+ return isTaskAttached(mTask) && mTask.floatingView.isStashed();
+ }
+
+ /**
+ * If a floating task exists, this sets whether it is stashed and animates if needed.
+ */
+ public void setStashed(boolean shouldStash) {
+ if (mTask != null && mTask.floatingView != null && mIsFloatingLayerAdded) {
+ mFloatingTaskLayer.setStashed(mTask, shouldStash);
+ }
+ }
+
+ /**
+ * Saves the last position the floating task was in so that it can be put there again.
+ */
+ public void setLastPosition(int x, int y) {
+ mLastPosition.set(x, y);
+ }
+
+ /**
+ * Returns the last position the floating task was in.
+ */
+ public Point getLastPosition() {
+ return mLastPosition;
+ }
+
+ /**
+ * Whether the provided task has a view that's attached to the floating layer.
+ */
+ private boolean isTaskAttached(Task t) {
+ return t != null && t.floatingView != null
+ && mIsFloatingLayerAdded
+ && mFloatingTaskLayer.getTaskViewCount() > 0
+ && Objects.equals(mFloatingTaskLayer.getFirstTaskView(), t.floatingView);
+ }
+
+ // TODO: when this is added, if there are bubbles, they get hidden? Is only one layer of this
+ // type allowed? Bubbles & floating tasks should probably be in the same layer to reduce
+ // # of windows.
+ private void addFloatingLayer() {
+ if (mIsFloatingLayerAdded) {
+ return;
+ }
+
+ mFloatingTaskLayer = new FloatingTaskLayer(mContext, this, mWindowManager);
+
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ PixelFormat.TRANSLUCENT
+ );
+ params.setTrustedOverlay();
+ params.setFitInsetsTypes(0);
+ params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+ params.setTitle("FloatingTaskLayer");
+ params.packageName = mContext.getPackageName();
+ params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+
+ try {
+ mIsFloatingLayerAdded = true;
+ mWindowManager.addView(mFloatingTaskLayer, params);
+ } catch (IllegalStateException e) {
+ // This means the floating layer has already been added which shouldn't happen.
+ e.printStackTrace();
+ }
+ }
+
+ private void removeFloatingLayer() {
+ if (!mIsFloatingLayerAdded) {
+ return;
+ }
+ try {
+ mIsFloatingLayerAdded = false;
+ if (mFloatingTaskLayer != null) {
+ mWindowManager.removeView(mFloatingTaskLayer);
+ }
+ } catch (IllegalArgumentException e) {
+ // This means the floating layer has already been removed which shouldn't happen.
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Description of current floating task state.
+ */
+ private void dump(PrintWriter pw, String prefix) {
+ pw.println("FloatingTaskController state:");
+ pw.print(" isFloatingLayerAvailable= "); pw.println(isFloatingLayerAvailable());
+ pw.print(" isFloatingTasksEnabled= "); pw.println(isFloatingTasksEnabled());
+ pw.print(" mIsFloatingLayerAdded= "); pw.println(mIsFloatingLayerAdded);
+ pw.print(" mLastPosition= "); pw.println(mLastPosition);
+ pw.println();
+ }
+
+ /** Returns the {@link FloatingTasks} implementation. */
+ public FloatingTasks asFloatingTasks() {
+ return mImpl;
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public ShellExecutor getRemoteCallExecutor() {
+ return mMainExecutor;
+ }
+
+ /**
+ * The interface for calls from outside the shell, within the host process.
+ */
+ @ExternalThread
+ private class FloatingTaskImpl implements FloatingTasks {
+ private IFloatingTasksImpl mIFloatingTasks;
+
+ @Override
+ public void showOrSetStashed(Intent intent) {
+ mMainExecutor.execute(() -> FloatingTasksController.this.showOrSetStashed(intent));
+ }
+
+ @Override
+ public IFloatingTasks createExternalInterface() {
+ if (mIFloatingTasks != null) {
+ mIFloatingTasks.invalidate();
+ }
+ mIFloatingTasks = new IFloatingTasksImpl(FloatingTasksController.this);
+ return mIFloatingTasks;
+ }
+ }
+
+ /**
+ * The interface for calls from outside the host process.
+ */
+ @BinderThread
+ private static class IFloatingTasksImpl extends IFloatingTasks.Stub {
+ private FloatingTasksController mController;
+
+ IFloatingTasksImpl(FloatingTasksController controller) {
+ mController = controller;
+ }
+
+ /**
+ * Invalidates this instance, preventing future calls from updating the controller.
+ */
+ void invalidate() {
+ mController = null;
+ }
+
+ public void showTask(Intent intent) {
+ executeRemoteCallWithTaskPermission(mController, "showTask",
+ (controller) -> controller.showTask(intent));
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl
index af2ab158ab46..f79ca1039865 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,12 +14,15 @@
* limitations under the License.
*/
-package com.android.wm.shell.legacysplitscreen;
+package com.android.wm.shell.floating;
+
+import android.content.Intent;
/**
- * Class to hold state of divider that needs to persist across configuration changes.
+ * Interface that is exposed to remote callers to manipulate floating task features.
*/
-final class DividerState {
- public boolean animateAfterRecentsDrawn;
- public float mRatioPositionBeforeMinimized;
+interface IFloatingTasks {
+
+ void showTask(in Intent intent) = 1;
+
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java
new file mode 100644
index 000000000000..c922109751ba
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java
@@ -0,0 +1,73 @@
+/*
+ * 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.floating.views;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.android.wm.shell.R;
+
+/**
+ * Displays the menu items for a floating task view (e.g. close).
+ */
+public class FloatingMenuView extends LinearLayout {
+
+ private int mItemSize;
+ private int mItemMargin;
+
+ public FloatingMenuView(Context context) {
+ super(context);
+ setOrientation(LinearLayout.HORIZONTAL);
+ setGravity(Gravity.CENTER);
+
+ mItemSize = context.getResources().getDimensionPixelSize(
+ R.dimen.floating_task_menu_item_size);
+ mItemMargin = context.getResources().getDimensionPixelSize(
+ R.dimen.floating_task_menu_item_padding);
+ }
+
+ /** Adds a clickable item to the menu bar. Items are ordered as added. */
+ public void addMenuItem(@Nullable Drawable drawable, View.OnClickListener listener) {
+ ImageView itemView = new ImageView(getContext());
+ itemView.setScaleType(ImageView.ScaleType.CENTER);
+ if (drawable != null) {
+ itemView.setImageDrawable(drawable);
+ }
+ LinearLayout.LayoutParams lp = new LayoutParams(mItemSize,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ lp.setMarginStart(mItemMargin);
+ lp.setMarginEnd(mItemMargin);
+ addView(itemView, lp);
+
+ itemView.setOnClickListener(listener);
+ }
+
+ /**
+ * The menu extends past the top of the TaskView because of the rounded corners. This means
+ * to center content in the menu we must subtract the radius (i.e. the amount of space covered
+ * by TaskView).
+ */
+ public void setCornerRadius(float radius) {
+ setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java
new file mode 100644
index 000000000000..16dab2415bf2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java
@@ -0,0 +1,687 @@
+/*
+ * 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.floating.views;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FlingAnimation;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.floating.FloatingDismissController;
+import com.android.wm.shell.floating.FloatingTasksController;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This is the layout that {@link FloatingTaskView}s are contained in. It handles input and
+ * movement of the task views.
+ */
+public class FloatingTaskLayer extends FrameLayout
+ implements ViewTreeObserver.OnComputeInternalInsetsListener {
+
+ private static final String TAG = FloatingTaskLayer.class.getSimpleName();
+
+ /** How big to make the task view based on screen width of the largest size. */
+ private static final float START_SIZE_WIDTH_PERCENT = 0.33f;
+ /** Min fling velocity required to move the view from one side of the screen to the other. */
+ private static final float ESCAPE_VELOCITY = 750f;
+ /** Amount of friction to apply to fling animations. */
+ private static final float FLING_FRICTION = 1.9f;
+
+ private final FloatingTasksController mController;
+ private final FloatingDismissController mDismissController;
+ private final WindowManager mWindowManager;
+ private final TouchHandlerImpl mTouchHandler;
+
+ private final Region mTouchableRegion = new Region();
+ private final Rect mPositionRect = new Rect();
+ private final Point mDefaultStartPosition = new Point();
+ private final Point mTaskViewSize = new Point();
+ private WindowInsets mWindowInsets;
+ private int mVerticalPadding;
+ private int mOverhangWhenStashed;
+
+ private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
+ private ViewTreeObserver.OnDrawListener mSystemGestureExclusionListener =
+ this::updateSystemGestureExclusion;
+
+ /** Interface allowing something to handle the touch events going to a task. */
+ interface FloatingTaskTouchHandler {
+ void onDown(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float viewInitialX, float viewInitialY);
+
+ void onMove(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float dx, float dy);
+
+ void onUp(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float dx, float dy, float velX, float velY);
+
+ void onClick(@NonNull FloatingTaskView v);
+ }
+
+ public FloatingTaskLayer(Context context,
+ FloatingTasksController controller,
+ WindowManager windowManager) {
+ super(context);
+ // TODO: Why is this necessary? Without it FloatingTaskView does not render correctly.
+ setBackgroundColor(Color.argb(0, 0, 0, 0));
+
+ mController = controller;
+ mWindowManager = windowManager;
+ updateSizes();
+
+ // TODO: Might make sense to put dismiss controller in the touch handler since that's the
+ // main user of dismiss controller.
+ mDismissController = new FloatingDismissController(context, mController, this);
+ mTouchHandler = new TouchHandlerImpl();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ getViewTreeObserver().addOnDrawListener(mSystemGestureExclusionListener);
+ setOnApplyWindowInsetsListener((view, windowInsets) -> {
+ if (!windowInsets.equals(mWindowInsets)) {
+ mWindowInsets = windowInsets;
+ updateSizes();
+ }
+ return windowInsets;
+ });
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+ getViewTreeObserver().removeOnDrawListener(mSystemGestureExclusionListener);
+ }
+
+ @Override
+ public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+ inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ mTouchableRegion.setEmpty();
+ getTouchableRegion(mTouchableRegion);
+ inoutInfo.touchableRegion.set(mTouchableRegion);
+ }
+
+ /** Adds a floating task to the layout. */
+ public void addTask(FloatingTasksController.Task task) {
+ if (task.floatingView == null) return;
+
+ task.floatingView.setTouchHandler(mTouchHandler);
+ addView(task.floatingView, new LayoutParams(mTaskViewSize.x, mTaskViewSize.y));
+ updateTaskViewPosition(task.floatingView);
+ }
+
+ /** Animates the stashed state of the provided task, if it's part of the floating layer. */
+ public void setStashed(FloatingTasksController.Task task, boolean shouldStash) {
+ if (task.floatingView != null && task.floatingView.getParent() == this) {
+ mTouchHandler.stashTaskView(task.floatingView, shouldStash);
+ }
+ }
+
+ /** Removes all {@link FloatingTaskView} from the layout. */
+ public void removeAllTaskViews() {
+ int childCount = getChildCount();
+ ArrayList<View> viewsToRemove = new ArrayList<>();
+ for (int i = 0; i < childCount; i++) {
+ if (getChildAt(i) instanceof FloatingTaskView) {
+ viewsToRemove.add(getChildAt(i));
+ }
+ }
+ for (View v : viewsToRemove) {
+ removeView(v);
+ }
+ }
+
+ /** Returns the number of task views in the layout. */
+ public int getTaskViewCount() {
+ int taskViewCount = 0;
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ if (getChildAt(i) instanceof FloatingTaskView) {
+ taskViewCount++;
+ }
+ }
+ return taskViewCount;
+ }
+
+ /**
+ * Called when the task view is un-stuck from the dismiss target.
+ * @param v the task view being moved.
+ * @param velX the x velocity of the motion event.
+ * @param velY the y velocity of the motion event.
+ * @param wasFlungOut true if the user flung the task view out of the dismiss target (i.e. there
+ * was an 'up' event), otherwise the user is still dragging.
+ */
+ public void onUnstuckFromTarget(FloatingTaskView v, float velX, float velY,
+ boolean wasFlungOut) {
+ mTouchHandler.onUnstuckFromTarget(v, velX, velY, wasFlungOut);
+ }
+
+ /**
+ * Updates dimensions and applies them to any task views.
+ */
+ public void updateSizes() {
+ if (mDismissController != null) {
+ mDismissController.updateSizes();
+ }
+
+ mOverhangWhenStashed = getResources().getDimensionPixelSize(
+ R.dimen.floating_task_stash_offset);
+ mVerticalPadding = getResources().getDimensionPixelSize(
+ R.dimen.floating_task_vertical_padding);
+
+ WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+ WindowInsets windowInsets = windowMetrics.getWindowInsets();
+ Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
+ | WindowInsets.Type.statusBars()
+ | WindowInsets.Type.displayCutout());
+ Rect bounds = windowMetrics.getBounds();
+ mPositionRect.set(bounds.left + insets.left,
+ bounds.top + insets.top + mVerticalPadding,
+ bounds.right - insets.right,
+ bounds.bottom - insets.bottom - mVerticalPadding);
+
+ int taskViewWidth = Math.max(bounds.height(), bounds.width());
+ int taskViewHeight = Math.min(bounds.height(), bounds.width());
+ taskViewHeight = taskViewHeight - (insets.top + insets.bottom + (mVerticalPadding * 2));
+ mTaskViewSize.set((int) (taskViewWidth * START_SIZE_WIDTH_PERCENT), taskViewHeight);
+ mDefaultStartPosition.set(mPositionRect.left, mPositionRect.top);
+
+ // Update existing views
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ if (getChildAt(i) instanceof FloatingTaskView) {
+ FloatingTaskView child = (FloatingTaskView) getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.width = mTaskViewSize.x;
+ lp.height = mTaskViewSize.y;
+ child.setLayoutParams(lp);
+ updateTaskViewPosition(child);
+ }
+ }
+ }
+
+ /** Returns the first floating task view in the layout. (Currently only ever 1 view). */
+ @Nullable
+ public FloatingTaskView getFirstTaskView() {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child instanceof FloatingTaskView) {
+ return (FloatingTaskView) child;
+ }
+ }
+ return null;
+ }
+
+ private void updateTaskViewPosition(FloatingTaskView floatingView) {
+ Point lastPosition = mController.getLastPosition();
+ if (lastPosition.x == -1 && lastPosition.y == -1) {
+ floatingView.setX(mDefaultStartPosition.x);
+ floatingView.setY(mDefaultStartPosition.y);
+ } else {
+ floatingView.setX(lastPosition.x);
+ floatingView.setY(lastPosition.y);
+ }
+ if (mTouchHandler.isStashedPosition(floatingView)) {
+ floatingView.setStashed(true);
+ }
+ floatingView.updateLocation();
+ }
+
+ /**
+ * Updates the area of the screen that shouldn't allow the back gesture due to the placement
+ * of task view (i.e. when task view is stashed on an edge, tapping or swiping that edge would
+ * un-stash the task view instead of performing the back gesture).
+ */
+ private void updateSystemGestureExclusion() {
+ Rect excludeZone = mSystemGestureExclusionRects.get(0);
+ FloatingTaskView floatingTaskView = getFirstTaskView();
+ if (floatingTaskView != null && floatingTaskView.isStashed()) {
+ excludeZone.set(floatingTaskView.getLeft(),
+ floatingTaskView.getTop(),
+ floatingTaskView.getRight(),
+ floatingTaskView.getBottom());
+ excludeZone.offset((int) (floatingTaskView.getTranslationX()),
+ (int) (floatingTaskView.getTranslationY()));
+ setSystemGestureExclusionRects(mSystemGestureExclusionRects);
+ } else {
+ excludeZone.setEmpty();
+ setSystemGestureExclusionRects(Collections.emptyList());
+ }
+ }
+
+ /**
+ * Fills in the touchable region for floating windows. This is used by WindowManager to
+ * decide which touch events go to the floating windows.
+ */
+ private void getTouchableRegion(Region outRegion) {
+ int childCount = getChildCount();
+ Rect temp = new Rect();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child instanceof FloatingTaskView) {
+ child.getBoundsOnScreen(temp);
+ outRegion.op(temp, Region.Op.UNION);
+ }
+ }
+ }
+
+ /**
+ * Implementation of the touch handler. Animates the task view based on touch events.
+ */
+ private class TouchHandlerImpl implements FloatingTaskTouchHandler {
+ /**
+ * The view can be stashed by swiping it towards the current edge or moving it there. If
+ * the view gets moved in a way that is not one of these gestures, this is flipped to false.
+ */
+ private boolean mCanStash = true;
+ /**
+ * This is used to indicate that the view has been un-stuck from the dismiss target and
+ * needs to spring to the current touch location.
+ */
+ // TODO: implement this behavior
+ private boolean mSpringToTouchOnNextMotionEvent = false;
+
+ private ArrayList<FlingAnimation> mFlingAnimations;
+ private ViewPropertyAnimator mViewPropertyAnimation;
+
+ private float mViewInitialX;
+ private float mViewInitialY;
+
+ private float[] mMinMax = new float[2];
+
+ @Override
+ public void onDown(@NonNull FloatingTaskView v, @NonNull MotionEvent ev, float viewInitialX,
+ float viewInitialY) {
+ mCanStash = true;
+ mViewInitialX = viewInitialX;
+ mViewInitialY = viewInitialY;
+ mDismissController.setUpMagneticObject(v);
+ mDismissController.passEventToMagnetizedObject(ev);
+ }
+
+ @Override
+ public void onMove(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float dx, float dy) {
+ // Shows the magnetic dismiss target if needed.
+ mDismissController.showDismiss(/* show= */ true);
+
+ // Send it to magnetic target first.
+ if (mDismissController.passEventToMagnetizedObject(ev)) {
+ v.setStashed(false);
+ mCanStash = true;
+
+ return;
+ }
+
+ // If we're here magnetic target didn't want it so move as per normal.
+
+ v.setTranslationX(capX(v, mViewInitialX + dx, /* isMoving= */ true));
+ v.setTranslationY(capY(v, mViewInitialY + dy));
+ if (v.isStashed()) {
+ // Check if we've moved far enough to be not stashed.
+ final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f);
+ final boolean viewInitiallyOnLeftSide = mViewInitialX < centerX;
+ if (viewInitiallyOnLeftSide) {
+ if (v.getTranslationX() > mPositionRect.left) {
+ v.setStashed(false);
+ mCanStash = true;
+ }
+ } else if (v.getTranslationX() + v.getWidth() < mPositionRect.right) {
+ v.setStashed(false);
+ mCanStash = true;
+ }
+ }
+ }
+
+ // Reference for math / values: StackAnimationController#flingStackThenSpringToEdge.
+ // TODO clean up the code here, pretty hard to comprehend
+ // TODO code here doesn't work the best when in portrait (e.g. can't fling up/down on edges)
+ @Override
+ public void onUp(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float dx, float dy, float velX, float velY) {
+
+ // Send it to magnetic target first.
+ if (mDismissController.passEventToMagnetizedObject(ev)) {
+ v.setStashed(false);
+ return;
+ }
+ mDismissController.showDismiss(/* show= */ false);
+
+ // If we're here magnetic target didn't want it so handle up as per normal.
+
+ final float x = capX(v, mViewInitialX + dx, /* isMoving= */ false);
+ final float centerX = mPositionRect.centerX();
+ final boolean viewInitiallyOnLeftSide = mViewInitialX + v.getWidth() < centerX;
+ final boolean viewOnLeftSide = x + v.getWidth() < centerX;
+ final boolean isFling = Math.abs(velX) > ESCAPE_VELOCITY;
+ final boolean isFlingLeft = isFling && velX < ESCAPE_VELOCITY;
+ // TODO: check velX here sometimes it doesn't stash on move when I think it should
+ final boolean shouldStashFromMove =
+ (velX < 0 && v.getTranslationX() < mPositionRect.left)
+ || (velX > 0
+ && v.getTranslationX() + v.getWidth() > mPositionRect.right);
+ final boolean shouldStashFromFling = viewInitiallyOnLeftSide == viewOnLeftSide
+ && isFling
+ && ((viewOnLeftSide && velX < ESCAPE_VELOCITY)
+ || (!viewOnLeftSide && velX > ESCAPE_VELOCITY));
+ final boolean shouldStash = mCanStash && (shouldStashFromFling || shouldStashFromMove);
+
+ ProtoLog.d(WM_SHELL_FLOATING_APPS,
+ "shouldStash=%s shouldStashFromFling=%s shouldStashFromMove=%s"
+ + " viewInitiallyOnLeftSide=%s viewOnLeftSide=%s isFling=%s velX=%f"
+ + " isStashed=%s", shouldStash, shouldStashFromFling, shouldStashFromMove,
+ viewInitiallyOnLeftSide, viewOnLeftSide, isFling, velX, v.isStashed());
+
+ if (v.isStashed()) {
+ mMinMax[0] = viewOnLeftSide
+ ? mPositionRect.left - v.getWidth() + mOverhangWhenStashed
+ : mPositionRect.right - v.getWidth();
+ mMinMax[1] = viewOnLeftSide
+ ? mPositionRect.left
+ : mPositionRect.right - mOverhangWhenStashed;
+ } else {
+ populateMinMax(v, viewOnLeftSide, shouldStash, mMinMax);
+ }
+
+ boolean movingLeft = isFling ? isFlingLeft : viewOnLeftSide;
+ float destinationRelativeX = movingLeft
+ ? mMinMax[0]
+ : mMinMax[1];
+
+ // TODO: why is this necessary / when does this happen?
+ if (mMinMax[1] < v.getTranslationX()) {
+ mMinMax[1] = v.getTranslationX();
+ }
+ if (v.getTranslationX() < mMinMax[0]) {
+ mMinMax[0] = v.getTranslationX();
+ }
+
+ // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity
+ // so that it'll make it all the way to the side of the screen.
+ final float minimumVelocityToReachEdge =
+ getMinimumVelocityToReachEdge(v, destinationRelativeX);
+ final float startXVelocity = movingLeft
+ ? Math.min(minimumVelocityToReachEdge, velX)
+ : Math.max(minimumVelocityToReachEdge, velX);
+
+ cancelAnyAnimations(v);
+
+ mFlingAnimations = getAnimationForUpEvent(v, shouldStash,
+ startXVelocity, mMinMax[0], mMinMax[1], destinationRelativeX);
+ for (int i = 0; i < mFlingAnimations.size(); i++) {
+ mFlingAnimations.get(i).start();
+ }
+ }
+
+ @Override
+ public void onClick(@NonNull FloatingTaskView v) {
+ if (v.isStashed()) {
+ final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f);
+ final boolean viewOnLeftSide = v.getTranslationX() < centerX;
+ final float destinationRelativeX = viewOnLeftSide
+ ? mPositionRect.left
+ : mPositionRect.right - v.getWidth();
+ final float minimumVelocityToReachEdge =
+ getMinimumVelocityToReachEdge(v, destinationRelativeX);
+ populateMinMax(v, viewOnLeftSide, /* stashed= */ true, mMinMax);
+
+ cancelAnyAnimations(v);
+
+ FlingAnimation flingAnimation = new FlingAnimation(v,
+ DynamicAnimation.TRANSLATION_X);
+ flingAnimation.setFriction(FLING_FRICTION)
+ .setStartVelocity(minimumVelocityToReachEdge)
+ .setMinValue(mMinMax[0])
+ .setMaxValue(mMinMax[1])
+ .addEndListener((animation, canceled, value, velocity) -> {
+ if (canceled) return;
+ mController.setLastPosition((int) v.getTranslationX(),
+ (int) v.getTranslationY());
+ v.setStashed(false);
+ v.updateLocation();
+ });
+ mFlingAnimations = new ArrayList<>();
+ mFlingAnimations.add(flingAnimation);
+ flingAnimation.start();
+ }
+ }
+
+ public void onUnstuckFromTarget(FloatingTaskView v, float velX, float velY,
+ boolean wasFlungOut) {
+ if (wasFlungOut) {
+ snapTaskViewToEdge(v, velX, /* shouldStash= */ false);
+ } else {
+ // TODO: use this for something / to spring the view to the touch location
+ mSpringToTouchOnNextMotionEvent = true;
+ }
+ }
+
+ public void stashTaskView(FloatingTaskView v, boolean shouldStash) {
+ if (v.isStashed() == shouldStash) {
+ return;
+ }
+ final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f);
+ final boolean viewOnLeftSide = v.getTranslationX() < centerX;
+ snapTaskViewToEdge(v, viewOnLeftSide ? -ESCAPE_VELOCITY : ESCAPE_VELOCITY, shouldStash);
+ }
+
+ public boolean isStashedPosition(View v) {
+ return v.getTranslationX() < mPositionRect.left
+ || v.getTranslationX() + v.getWidth() > mPositionRect.right;
+ }
+
+ // TODO: a lot of this is duplicated in onUp -- can it be unified?
+ private void snapTaskViewToEdge(FloatingTaskView v, float velX, boolean shouldStash) {
+ final boolean movingLeft = velX < ESCAPE_VELOCITY;
+ populateMinMax(v, movingLeft, shouldStash, mMinMax);
+ float destinationRelativeX = movingLeft
+ ? mMinMax[0]
+ : mMinMax[1];
+
+ // TODO: why is this necessary / when does this happen?
+ if (mMinMax[1] < v.getTranslationX()) {
+ mMinMax[1] = v.getTranslationX();
+ }
+ if (v.getTranslationX() < mMinMax[0]) {
+ mMinMax[0] = v.getTranslationX();
+ }
+
+ // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity
+ // so that it'll make it all the way to the side of the screen.
+ final float minimumVelocityToReachEdge =
+ getMinimumVelocityToReachEdge(v, destinationRelativeX);
+ final float startXVelocity = movingLeft
+ ? Math.min(minimumVelocityToReachEdge, velX)
+ : Math.max(minimumVelocityToReachEdge, velX);
+
+ cancelAnyAnimations(v);
+
+ mFlingAnimations = getAnimationForUpEvent(v,
+ shouldStash, startXVelocity, mMinMax[0], mMinMax[1],
+ destinationRelativeX);
+ for (int i = 0; i < mFlingAnimations.size(); i++) {
+ mFlingAnimations.get(i).start();
+ }
+ }
+
+ private void cancelAnyAnimations(FloatingTaskView v) {
+ if (mFlingAnimations != null) {
+ for (int i = 0; i < mFlingAnimations.size(); i++) {
+ if (mFlingAnimations.get(i).isRunning()) {
+ mFlingAnimations.get(i).cancel();
+ }
+ }
+ }
+ if (mViewPropertyAnimation != null) {
+ mViewPropertyAnimation.cancel();
+ mViewPropertyAnimation = null;
+ }
+ }
+
+ private ArrayList<FlingAnimation> getAnimationForUpEvent(FloatingTaskView v,
+ boolean shouldStash, float startVelX, float minValue, float maxValue,
+ float destinationRelativeX) {
+ final float ty = v.getTranslationY();
+ final ArrayList<FlingAnimation> animations = new ArrayList<>();
+ if (ty != capY(v, ty)) {
+ // The view was being dismissed so the Y is out of bounds, need to animate that.
+ FlingAnimation yFlingAnimation = new FlingAnimation(v,
+ DynamicAnimation.TRANSLATION_Y);
+ yFlingAnimation.setFriction(FLING_FRICTION)
+ .setStartVelocity(startVelX)
+ .setMinValue(mPositionRect.top)
+ .setMaxValue(mPositionRect.bottom - mTaskViewSize.y);
+ animations.add(yFlingAnimation);
+ }
+ FlingAnimation flingAnimation = new FlingAnimation(v, DynamicAnimation.TRANSLATION_X);
+ flingAnimation.setFriction(FLING_FRICTION)
+ .setStartVelocity(startVelX)
+ .setMinValue(minValue)
+ .setMaxValue(maxValue)
+ .addEndListener((animation, canceled, value, velocity) -> {
+ if (canceled) return;
+ Runnable endAction = () -> {
+ v.setStashed(shouldStash);
+ v.updateLocation();
+ if (!v.isStashed()) {
+ mController.setLastPosition((int) v.getTranslationX(),
+ (int) v.getTranslationY());
+ }
+ };
+ if (!shouldStash) {
+ final int xTranslation = (int) v.getTranslationX();
+ if (xTranslation != destinationRelativeX) {
+ // TODO: this animation doesn't feel great, should figure out
+ // a better way to do this or remove the need for it all together.
+ mViewPropertyAnimation = v.animate()
+ .translationX(destinationRelativeX)
+ .setListener(getAnimationListener(endAction));
+ mViewPropertyAnimation.start();
+ } else {
+ endAction.run();
+ }
+ } else {
+ endAction.run();
+ }
+ });
+ animations.add(flingAnimation);
+ return animations;
+ }
+
+ private AnimatorListenerAdapter getAnimationListener(Runnable endAction) {
+ return new AnimatorListenerAdapter() {
+ boolean translationCanceled = false;
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ translationCanceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (!translationCanceled) {
+ endAction.run();
+ }
+ }
+ };
+ }
+
+ private void populateMinMax(FloatingTaskView v, boolean onLeft, boolean shouldStash,
+ float[] out) {
+ if (shouldStash) {
+ out[0] = onLeft
+ ? mPositionRect.left - v.getWidth() + mOverhangWhenStashed
+ : mPositionRect.right - v.getWidth();
+ out[1] = onLeft
+ ? mPositionRect.left
+ : mPositionRect.right - mOverhangWhenStashed;
+ } else {
+ out[0] = mPositionRect.left;
+ out[1] = mPositionRect.right - mTaskViewSize.x;
+ }
+ }
+
+ private float getMinimumVelocityToReachEdge(FloatingTaskView v,
+ float destinationRelativeX) {
+ // Minimum velocity required for the view to make it to the targeted side of the screen,
+ // taking friction into account (4.2f is the number that friction scalars are multiplied
+ // by in DynamicAnimation.DragForce). This is an estimate and could be slightly off, the
+ // animation at the end will ensure that it reaches the destination X regardless.
+ return (destinationRelativeX - v.getTranslationX()) * (FLING_FRICTION * 4.2f);
+ }
+
+ private float capX(FloatingTaskView v, float x, boolean isMoving) {
+ final int width = v.getWidth();
+ if (v.isStashed() || isMoving) {
+ if (x < mPositionRect.left - v.getWidth() + mOverhangWhenStashed) {
+ return mPositionRect.left - v.getWidth() + mOverhangWhenStashed;
+ }
+ if (x > mPositionRect.right - mOverhangWhenStashed) {
+ return mPositionRect.right - mOverhangWhenStashed;
+ }
+ } else {
+ if (x < mPositionRect.left) {
+ return mPositionRect.left;
+ }
+ if (x > mPositionRect.right - width) {
+ return mPositionRect.right - width;
+ }
+ }
+ return x;
+ }
+
+ private float capY(FloatingTaskView v, float y) {
+ final int height = v.getHeight();
+ if (y < mPositionRect.top) {
+ return mPositionRect.top;
+ }
+ if (y > mPositionRect.bottom - height) {
+ return mPositionRect.bottom - height;
+ }
+ return y;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java
new file mode 100644
index 000000000000..581204a82ec7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java
@@ -0,0 +1,385 @@
+/*
+ * 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.floating.views;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
+
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.TaskView;
+import com.android.wm.shell.TaskViewTransitions;
+import com.android.wm.shell.bubbles.RelativeTouchListener;
+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.floating.FloatingTasksController;
+
+/**
+ * A view that holds a floating task using {@link TaskView} along with additional UI to manage
+ * the task.
+ */
+public class FloatingTaskView extends FrameLayout {
+
+ private static final String TAG = FloatingTaskView.class.getSimpleName();
+
+ private FloatingTasksController mController;
+
+ private FloatingMenuView mMenuView;
+ private int mMenuHeight;
+ private TaskView mTaskView;
+
+ private float mCornerRadius = 0f;
+ private int mBackgroundColor;
+
+ private FloatingTasksController.Task mTask;
+
+ private boolean mIsStashed;
+
+ /**
+ * Creates a floating task view.
+ *
+ * @param context the context to use.
+ * @param controller the controller to notify about changes in the floating task (e.g. removal).
+ */
+ public FloatingTaskView(Context context, FloatingTasksController controller) {
+ super(context);
+ mController = controller;
+ setElevation(getResources().getDimensionPixelSize(R.dimen.floating_task_elevation));
+ mMenuHeight = context.getResources().getDimensionPixelSize(R.dimen.floating_task_menu_size);
+ mMenuView = new FloatingMenuView(context);
+ addView(mMenuView);
+
+ applyThemeAttrs();
+
+ setClipToOutline(true);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
+ }
+ });
+ }
+
+ // TODO: call this when theme/config changes
+ void applyThemeAttrs() {
+ boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
+ mContext.getResources());
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ android.R.attr.dialogCornerRadius,
+ android.R.attr.colorBackgroundFloating});
+ mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
+ mCornerRadius = mCornerRadius / 2f;
+ mBackgroundColor = ta.getColor(1, Color.WHITE);
+
+ ta.recycle();
+
+ mMenuView.setCornerRadius(mCornerRadius);
+ mMenuHeight = getResources().getDimensionPixelSize(
+ R.dimen.floating_task_menu_size);
+
+ if (mTaskView != null) {
+ mTaskView.setCornerRadius(mCornerRadius);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ // Add corner radius here so that the menu extends behind the rounded corners of TaskView.
+ int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height);
+ measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
+ MeasureSpec.getMode(heightMeasureSpec)));
+
+ if (mTaskView != null) {
+ int taskViewHeight = height - menuViewHeight;
+ measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(taskViewHeight,
+ MeasureSpec.getMode(heightMeasureSpec)));
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // Drag handle above
+ final int dragHandleBottom = t + mMenuView.getMeasuredHeight();
+ mMenuView.layout(l, t, r, dragHandleBottom);
+ if (mTaskView != null) {
+ // Subtract radius so that the menu extends behind the rounded corners of TaskView.
+ mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r,
+ dragHandleBottom + mTaskView.getMeasuredHeight());
+ }
+ }
+
+ /**
+ * Constructs the TaskView to display the task. Must be called for {@link #startTask} to work.
+ */
+ public void createTaskView(Context context, ShellTaskOrganizer organizer,
+ TaskViewTransitions transitions, SyncTransactionQueue syncQueue) {
+ mTaskView = new TaskView(context, organizer, transitions, syncQueue);
+ addView(mTaskView);
+ mTaskView.setEnableSurfaceClipping(true);
+ mTaskView.setCornerRadius(mCornerRadius);
+ }
+
+ /**
+ * Starts the provided task in the TaskView, if the TaskView exists. This should be called after
+ * {@link #createTaskView}.
+ */
+ public void startTask(@ShellMainThread ShellExecutor executor,
+ FloatingTasksController.Task task) {
+ if (mTaskView == null) {
+ Log.e(TAG, "starting task before creating the view!");
+ return;
+ }
+ mTask = task;
+ mTaskView.setListener(executor, mTaskViewListener);
+ }
+
+ /**
+ * Sets the touch handler for the view.
+ *
+ * @param handler the touch handler for the view.
+ */
+ public void setTouchHandler(FloatingTaskLayer.FloatingTaskTouchHandler handler) {
+ setOnTouchListener(new RelativeTouchListener() {
+ @Override
+ public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
+ handler.onDown(FloatingTaskView.this, ev, v.getTranslationX(), v.getTranslationY());
+ return true;
+ }
+
+ @Override
+ public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+ float viewInitialY, float dx, float dy) {
+ handler.onMove(FloatingTaskView.this, ev, dx, dy);
+ }
+
+ @Override
+ public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+ float viewInitialY, float dx, float dy, float velX, float velY) {
+ handler.onUp(FloatingTaskView.this, ev, dx, dy, velX, velY);
+ }
+ });
+ setOnClickListener(view -> {
+ handler.onClick(FloatingTaskView.this);
+ });
+
+ mMenuView.addMenuItem(null, view -> {
+ if (mIsStashed) {
+ // If we're stashed all clicks un-stash.
+ handler.onClick(FloatingTaskView.this);
+ }
+ });
+ }
+
+ private void setContentVisibility(boolean visible) {
+ if (mTaskView == null) return;
+ mTaskView.setAlpha(visible ? 1f : 0f);
+ }
+
+ /**
+ * Sets the alpha of both this view and the TaskView.
+ */
+ public void setTaskViewAlpha(float alpha) {
+ if (mTaskView != null) {
+ mTaskView.setAlpha(alpha);
+ }
+ setAlpha(alpha);
+ }
+
+ /**
+ * Call when the location or size of the view has changed to update TaskView.
+ */
+ public void updateLocation() {
+ if (mTaskView == null) return;
+ mTaskView.onLocationChanged();
+ }
+
+ private void updateMenuColor() {
+ ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo();
+ int color = info != null ? info.taskDescription.getBackgroundColor() : -1;
+ if (color != -1) {
+ mMenuView.setBackgroundColor(color);
+ } else {
+ mMenuView.setBackgroundColor(mBackgroundColor);
+ }
+ }
+
+ /**
+ * Sets whether the view is stashed or not.
+ *
+ * Also updates the touchable area based on this. If the view is stashed we don't direct taps
+ * on the activity to the activity, instead a tap will un-stash the view.
+ */
+ public void setStashed(boolean isStashed) {
+ if (mIsStashed != isStashed) {
+ mIsStashed = isStashed;
+ if (mTaskView == null) {
+ return;
+ }
+ updateObscuredTouchRect();
+ }
+ }
+
+ /** Whether the view is stashed at the edge of the screen or not. **/
+ public boolean isStashed() {
+ return mIsStashed;
+ }
+
+ private void updateObscuredTouchRect() {
+ if (mIsStashed) {
+ Rect tmpRect = new Rect();
+ getBoundsOnScreen(tmpRect);
+ mTaskView.setObscuredTouchRect(tmpRect);
+ } else {
+ mTaskView.setObscuredTouchRect(null);
+ }
+ }
+
+ /**
+ * Whether the task needs to be restarted, this can happen when {@link #cleanUpTaskView()} has
+ * been called on this view or if
+ * {@link #startTask(ShellExecutor, FloatingTasksController.Task)} was never called.
+ */
+ public boolean needsTaskStarted() {
+ // If the task needs to be restarted then TaskView would have been cleaned up.
+ return mTaskView == null;
+ }
+
+ /** Call this when the floating task activity is no longer in use. */
+ public void cleanUpTaskView() {
+ if (mTask != null && mTask.taskId != INVALID_TASK_ID) {
+ try {
+ ActivityTaskManager.getService().removeTask(mTask.taskId);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.getMessage());
+ }
+ }
+ if (mTaskView != null) {
+ mTaskView.release();
+ removeView(mTaskView);
+ mTaskView = null;
+ }
+ }
+
+ // TODO: use task background colour / how to get the taskInfo ?
+ private static int getDragBarColor(ActivityManager.RunningTaskInfo taskInfo) {
+ final int taskBgColor = taskInfo.taskDescription.getStatusBarColor();
+ return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
+ }
+
+ private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
+ private boolean mInitialized = false;
+ private boolean mDestroyed = false;
+
+ @Override
+ public void onInitialized() {
+ if (mDestroyed || mInitialized) {
+ return;
+ }
+ // Custom options so there is no activity transition animation
+ ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
+ /* enterResId= */ 0, /* exitResId= */ 0);
+
+ Rect launchBounds = new Rect();
+ mTaskView.getBoundsOnScreen(launchBounds);
+
+ try {
+ options.setTaskAlwaysOnTop(true);
+ if (mTask.intent != null) {
+ Intent fillInIntent = new Intent();
+ // Apply flags to make behaviour match documentLaunchMode=always.
+ fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+
+ PendingIntent pi = PendingIntent.getActivity(mContext, 0, mTask.intent,
+ PendingIntent.FLAG_MUTABLE,
+ null);
+ mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
+ } else {
+ ProtoLog.e(WM_SHELL_FLOATING_APPS, "Tried to start a task with null intent");
+ }
+ } catch (RuntimeException e) {
+ ProtoLog.e(WM_SHELL_FLOATING_APPS, "Exception while starting task: %s",
+ e.getMessage());
+ mController.removeTask();
+ }
+ mInitialized = true;
+ }
+
+ @Override
+ public void onReleased() {
+ mDestroyed = true;
+ }
+
+ @Override
+ public void onTaskCreated(int taskId, ComponentName name) {
+ mTask.taskId = taskId;
+ updateMenuColor();
+ setContentVisibility(true);
+ }
+
+ @Override
+ public void onTaskVisibilityChanged(int taskId, boolean visible) {
+ setContentVisibility(visible);
+ }
+
+ @Override
+ public void onTaskRemovalStarted(int taskId) {
+ // Must post because this is called from a binder thread.
+ post(() -> {
+ mController.removeTask();
+ cleanUpTaskView();
+ });
+ }
+
+ @Override
+ public void onBackPressedOnTaskRoot(int taskId) {
+ if (mTask.taskId == taskId && !mIsStashed) {
+ // TODO: is removing the window the desired behavior?
+ post(() -> mController.removeTask());
+ }
+ }
+ };
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
new file mode 100644
index 000000000000..eee5aaee3ec3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
@@ -0,0 +1,59 @@
+/*
+ * 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.freeform;
+
+import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
+import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.Optional;
+
+/**
+ * Class that holds freeform related classes. It serves as the single injection point of
+ * all freeform classes to avoid leaking implementation details to the base Dagger module.
+ */
+public class FreeformComponents {
+ public final ShellTaskOrganizer.TaskListener mTaskListener;
+ public final Optional<Transitions.TransitionHandler> mTransitionHandler;
+ public final Optional<Transitions.TransitionObserver> mTransitionObserver;
+
+ /**
+ * Creates an instance with the given components.
+ */
+ public FreeformComponents(
+ ShellTaskOrganizer.TaskListener taskListener,
+ Optional<Transitions.TransitionHandler> transitionHandler,
+ Optional<Transitions.TransitionObserver> transitionObserver) {
+ mTaskListener = taskListener;
+ mTransitionHandler = transitionHandler;
+ mTransitionObserver = transitionObserver;
+ }
+
+ /**
+ * Returns if this device supports freeform.
+ */
+ public static boolean isFreeformEnabled(Context context) {
+ return context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
+ || Settings.Global.getInt(context.getContentResolver(),
+ DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
+ }
+}
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 fef9be36a35f..e2d5a499d1e1 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
@@ -16,97 +16,156 @@
package com.android.wm.shell.freeform;
-import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
-import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
+import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM;
import android.app.ActivityManager.RunningTaskInfo;
-import android.content.Context;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.provider.Settings;
-import android.util.Slog;
+import android.util.Log;
import android.util.SparseArray;
import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import androidx.annotation.Nullable;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.SyncTransactionQueue;
+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.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.io.PrintWriter;
+import java.util.Optional;
/**
* {@link ShellTaskOrganizer.TaskListener} for {@link
* ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}.
+ *
+ * @param <T> the type of window decoration instance
*/
-public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
+public class FreeformTaskListener<T extends AutoCloseable>
+ implements ShellTaskOrganizer.TaskListener {
private static final String TAG = "FreeformTaskListener";
- private final SyncTransactionQueue mSyncQueue;
+ private final ShellTaskOrganizer mShellTaskOrganizer;
+ private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
+ private final WindowDecorViewModel<T> mWindowDecorationViewModel;
- private final SparseArray<State> mTasks = new SparseArray<>();
+ private final SparseArray<State<T>> mTasks = new SparseArray<>();
+ private final SparseArray<T> mWindowDecorOfVanishedTasks = new SparseArray<>();
- private static class State {
+ private static class State<T extends AutoCloseable> {
RunningTaskInfo mTaskInfo;
SurfaceControl mLeash;
+ T mWindowDecoration;
+ }
+
+ public FreeformTaskListener(
+ ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
+ Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ WindowDecorViewModel<T> windowDecorationViewModel) {
+ mShellTaskOrganizer = shellTaskOrganizer;
+ mWindowDecorationViewModel = windowDecorationViewModel;
+ mDesktopModeTaskRepository = desktopModeTaskRepository;
+ if (shellInit != null) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
}
- public FreeformTaskListener(SyncTransactionQueue syncQueue) {
- mSyncQueue = syncQueue;
+ private void onInit() {
+ mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM);
}
@Override
public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
- if (mTasks.get(taskInfo.taskId) != null) {
- throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId);
- }
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d",
taskInfo.taskId);
- final State state = new State();
+ final State<T> state = createOrUpdateTaskState(taskInfo, leash);
+ if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ state.mWindowDecoration =
+ mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash, t, t);
+ t.apply();
+ }
+
+ if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isVisible) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "Adding active freeform task: #%d", taskInfo.taskId);
+ mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
+ }
+ }
+
+ private State<T> createOrUpdateTaskState(RunningTaskInfo taskInfo, SurfaceControl leash) {
+ State<T> state = mTasks.get(taskInfo.taskId);
+ if (state != null) {
+ updateTaskInfo(taskInfo);
+ return state;
+ }
+
+ state = new State<>();
state.mTaskInfo = taskInfo;
state.mLeash = leash;
mTasks.put(taskInfo.taskId, state);
- final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
- mSyncQueue.runInSync(t -> {
- Point taskPosition = taskInfo.positionInParent;
- t.setPosition(leash, taskPosition.x, taskPosition.y)
- .setWindowCrop(leash, taskBounds.width(), taskBounds.height())
- .show(leash);
- });
+ return state;
}
@Override
public void onTaskVanished(RunningTaskInfo taskInfo) {
- State state = mTasks.get(taskInfo.taskId);
+ final State<T> state = mTasks.get(taskInfo.taskId);
if (state == null) {
- Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
+ // This is possible if the transition happens before this method.
return;
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d",
taskInfo.taskId);
mTasks.remove(taskInfo.taskId);
+
+ if (DesktopModeStatus.IS_SUPPORTED) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "Removing active freeform task: #%d", taskInfo.taskId);
+ mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId));
+ }
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ // Save window decorations of closing tasks so that we can hand them over to the
+ // transition system if this method happens before the transition. In case where the
+ // transition didn't happen, it'd be cleared when the next transition finished.
+ if (state.mWindowDecoration != null) {
+ mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration);
+ }
+ return;
+ }
+ releaseWindowDecor(state.mWindowDecoration);
}
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
- State state = mTasks.get(taskInfo.taskId);
- if (state == null) {
- throw new RuntimeException(
- "Task info changed before appearing: #" + taskInfo.taskId);
- }
+ final State<T> state = updateTaskInfo(taskInfo);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d",
taskInfo.taskId);
- state.mTaskInfo = taskInfo;
+ if (state.mWindowDecoration != null) {
+ mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo, state.mWindowDecoration);
+ }
- final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
- final SurfaceControl leash = state.mLeash;
- mSyncQueue.runInSync(t -> {
- Point taskPosition = taskInfo.positionInParent;
- t.setPosition(leash, taskPosition.x, taskPosition.y)
- .setWindowCrop(leash, taskBounds.width(), taskBounds.height())
- .show(leash);
- });
+ if (DesktopModeStatus.IS_SUPPORTED) {
+ if (taskInfo.isVisible) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "Adding active freeform task: #%d", taskInfo.taskId);
+ mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
+ }
+ }
+ }
+
+ private State<T> updateTaskInfo(RunningTaskInfo taskInfo) {
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ if (state == null) {
+ throw new RuntimeException("Task info changed before appearing: #" + taskInfo.taskId);
+ }
+ state.mTaskInfo = taskInfo;
+ return state;
}
@Override
@@ -127,6 +186,103 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
return mTasks.get(taskId).mLeash;
}
+ /**
+ * Creates a window decoration for a transition.
+ *
+ * @param change the change of this task transition that needs to have the task layer as the
+ * leash
+ * @return {@code true} if it creates the window decoration; {@code false} otherwise
+ */
+ boolean createWindowDecoration(
+ TransitionInfo.Change change,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
+ if (state.mWindowDecoration != null) {
+ return false;
+ }
+ state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration(
+ state.mTaskInfo, state.mLeash, startT, finishT);
+ return true;
+ }
+
+ /**
+ * Gives out the ownership of the task's window decoration. The given task is leaving (of has
+ * left) this task listener. This is the transition system asking for the ownership.
+ *
+ * @param taskInfo the maximizing task
+ * @return the window decor of the maximizing task if any
+ */
+ T giveWindowDecoration(
+ RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ T windowDecor;
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ if (state != null) {
+ windowDecor = state.mWindowDecoration;
+ state.mWindowDecoration = null;
+ } else {
+ windowDecor =
+ mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId);
+ }
+ if (windowDecor == null) {
+ return null;
+ }
+ mWindowDecorationViewModel.setupWindowDecorationForTransition(
+ taskInfo, startT, finishT, windowDecor);
+ return windowDecor;
+ }
+
+ /**
+ * Adopt the incoming window decoration and lets the window decoration prepare for a transition.
+ *
+ * @param change the change of this task transition that needs to have the task layer as the
+ * leash
+ * @param startT the start transaction of this transition
+ * @param finishT the finish transaction of this transition
+ * @param windowDecor the window decoration to adopt
+ * @return {@code true} if it adopts the window decoration; {@code false} otherwise
+ */
+ boolean adoptWindowDecoration(
+ TransitionInfo.Change change,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT,
+ @Nullable AutoCloseable windowDecor) {
+ final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
+ state.mWindowDecoration = mWindowDecorationViewModel.adoptWindowDecoration(windowDecor);
+ if (state.mWindowDecoration != null) {
+ mWindowDecorationViewModel.setupWindowDecorationForTransition(
+ state.mTaskInfo, startT, finishT, state.mWindowDecoration);
+ return true;
+ } else {
+ state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration(
+ state.mTaskInfo, state.mLeash, startT, finishT);
+ return false;
+ }
+ }
+
+ void onTaskTransitionFinished() {
+ if (mWindowDecorOfVanishedTasks.size() == 0) {
+ return;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Clearing window decors of vanished tasks. There could be visual defects "
+ + "if any of them is used later in transitions.");
+ for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) {
+ releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i));
+ }
+ mWindowDecorOfVanishedTasks.clear();
+ }
+
+ private void releaseWindowDecor(T windowDecor) {
+ try {
+ windowDecor.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to release window decoration.", e);
+ }
+ }
+
@Override
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
@@ -138,16 +294,4 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
public String toString() {
return TAG;
}
-
- /**
- * Checks if freeform support is enabled in system.
- *
- * @param context context used to check settings and package manager.
- * @return {@code true} if freeform is enabled, {@code false} if not.
- */
- public static boolean isFreeformEnabled(Context context) {
- return context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
- || Settings.Global.getInt(context.getContentResolver(),
- DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
new file mode 100644
index 000000000000..fd4c85fad77f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -0,0 +1,174 @@
+/*
+ * 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.freeform;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The {@link Transitions.TransitionHandler} that handles freeform task maximizing and restoring
+ * transitions.
+ */
+public class FreeformTaskTransitionHandler
+ implements Transitions.TransitionHandler, FreeformTaskTransitionStarter {
+
+ private final Transitions mTransitions;
+ private final WindowDecorViewModel<?> mWindowDecorViewModel;
+
+ private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
+
+ public FreeformTaskTransitionHandler(
+ ShellInit shellInit,
+ Transitions transitions,
+ WindowDecorViewModel<?> windowDecorViewModel) {
+ mTransitions = transitions;
+ mWindowDecorViewModel = windowDecorViewModel;
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
+ }
+
+ private void onInit() {
+ mWindowDecorViewModel.setFreeformTaskTransitionStarter(this);
+ }
+
+ @Override
+ public void startWindowingModeTransition(
+ int targetWindowingMode, WindowContainerTransaction wct) {
+ final int type;
+ switch (targetWindowingMode) {
+ case WINDOWING_MODE_FULLSCREEN:
+ type = Transitions.TRANSIT_MAXIMIZE;
+ break;
+ case WINDOWING_MODE_FREEFORM:
+ type = Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE;
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected target windowing mode "
+ + WindowConfiguration.windowingModeToString(targetWindowingMode));
+ }
+ final IBinder token = mTransitions.startTransition(type, wct, this);
+ mPendingTransitionTokens.add(token);
+ }
+
+ @Override
+ public void startMinimizedModeTransition(WindowContainerTransaction wct) {
+ final int type = WindowManager.TRANSIT_TO_BACK;
+ mPendingTransitionTokens.add(mTransitions.startTransition(type, wct, this));
+ }
+
+
+ @Override
+ public void startRemoveTransition(WindowContainerTransaction wct) {
+ final int type = WindowManager.TRANSIT_CLOSE;
+ mPendingTransitionTokens.add(mTransitions.startTransition(type, wct, this));
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ boolean transitionHandled = false;
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
+ continue;
+ }
+
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue;
+ }
+
+ switch (change.getMode()) {
+ case WindowManager.TRANSIT_CHANGE:
+ transitionHandled |= startChangeTransition(
+ transition, info.getType(), change);
+ break;
+ case WindowManager.TRANSIT_TO_BACK:
+ transitionHandled |= startMinimizeTransition(transition);
+ break;
+ }
+ }
+
+ mPendingTransitionTokens.remove(transition);
+
+ if (!transitionHandled) {
+ return false;
+ }
+
+ startT.apply();
+ mTransitions.getMainExecutor().execute(
+ () -> finishCallback.onTransitionFinished(null, null));
+ return true;
+ }
+
+ private boolean startChangeTransition(
+ IBinder transition,
+ int type,
+ TransitionInfo.Change change) {
+ if (!mPendingTransitionTokens.contains(transition)) {
+ return false;
+ }
+
+ boolean handled = false;
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (type == Transitions.TRANSIT_MAXIMIZE
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ // TODO: Add maximize animations
+ handled = true;
+ }
+
+ if (type == Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ // TODO: Add restore animations
+ handled = true;
+ }
+
+ return handled;
+ }
+
+ private boolean startMinimizeTransition(IBinder transition) {
+ return mPendingTransitionTokens.contains(transition);
+ }
+
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
new file mode 100644
index 000000000000..17d60671e964
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -0,0 +1,245 @@
+/*
+ * 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.freeform;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.fullscreen.FullscreenTaskListener;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The {@link Transitions.TransitionHandler} that handles freeform task launches, closes,
+ * maximizing and restoring transitions. It also reports transitions so that window decorations can
+ * be a part of transitions.
+ */
+public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver {
+ private static final String TAG = "FreeformTO";
+
+ private final Transitions mTransitions;
+ private final FreeformTaskListener<?> mFreeformTaskListener;
+ private final FullscreenTaskListener<?> mFullscreenTaskListener;
+
+ private final Map<IBinder, List<AutoCloseable>> mTransitionToWindowDecors = new HashMap<>();
+
+ public FreeformTaskTransitionObserver(
+ Context context,
+ ShellInit shellInit,
+ Transitions transitions,
+ FullscreenTaskListener<?> fullscreenTaskListener,
+ FreeformTaskListener<?> freeformTaskListener) {
+ mTransitions = transitions;
+ mFreeformTaskListener = freeformTaskListener;
+ mFullscreenTaskListener = fullscreenTaskListener;
+ if (Transitions.ENABLE_SHELL_TRANSITIONS && FreeformComponents.isFreeformEnabled(context)) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
+ }
+
+ @VisibleForTesting
+ void onInit() {
+ mTransitions.registerObserver(this);
+ }
+
+ @Override
+ public void onTransitionReady(
+ @NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT) {
+ final ArrayList<AutoCloseable> windowDecors = new ArrayList<>();
+ final ArrayList<WindowContainerToken> taskParents = new ArrayList<>();
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
+ continue;
+ }
+
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue;
+ }
+ // Filter out non-leaf tasks. Freeform/fullscreen don't nest tasks, but split-screen
+ // does, so this prevents adding duplicate captions in that scenario.
+ if (change.getParent() != null
+ && info.getChange(change.getParent()).getTaskInfo() != null) {
+ // This logic relies on 2 assumptions: 1 is that child tasks will be visited before
+ // parents (due to how z-order works). 2 is that no non-tasks are interleaved
+ // between tasks (hierarchically).
+ taskParents.add(change.getContainer());
+ }
+ if (taskParents.contains(change.getContainer())) {
+ continue;
+ }
+
+ switch (change.getMode()) {
+ case WindowManager.TRANSIT_OPEN:
+ case WindowManager.TRANSIT_TO_FRONT:
+ onOpenTransitionReady(change, startT, finishT);
+ break;
+ case WindowManager.TRANSIT_CLOSE: {
+ onCloseTransitionReady(change, windowDecors, startT, finishT);
+ break;
+ }
+ case WindowManager.TRANSIT_CHANGE:
+ onChangeTransitionReady(info.getType(), change, startT, finishT);
+ break;
+ }
+ }
+ if (!windowDecors.isEmpty()) {
+ mTransitionToWindowDecors.put(transition, windowDecors);
+ }
+ }
+
+ private void onOpenTransitionReady(
+ TransitionInfo.Change change,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ switch (change.getTaskInfo().getWindowingMode()){
+ case WINDOWING_MODE_FREEFORM:
+ mFreeformTaskListener.createWindowDecoration(change, startT, finishT);
+ break;
+ case WINDOWING_MODE_FULLSCREEN:
+ mFullscreenTaskListener.createWindowDecoration(change, startT, finishT);
+ break;
+ }
+ }
+
+ private void onCloseTransitionReady(
+ TransitionInfo.Change change,
+ ArrayList<AutoCloseable> windowDecors,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ final AutoCloseable windowDecor;
+ switch (change.getTaskInfo().getWindowingMode()) {
+ case WINDOWING_MODE_FREEFORM:
+ windowDecor = mFreeformTaskListener.giveWindowDecoration(change.getTaskInfo(),
+ startT, finishT);
+ break;
+ case WINDOWING_MODE_FULLSCREEN:
+ windowDecor = mFullscreenTaskListener.giveWindowDecoration(change.getTaskInfo(),
+ startT, finishT);
+ break;
+ default:
+ windowDecor = null;
+ }
+ if (windowDecor != null) {
+ windowDecors.add(windowDecor);
+ }
+ }
+
+ private void onChangeTransitionReady(
+ int type,
+ TransitionInfo.Change change,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ AutoCloseable windowDecor = null;
+
+ boolean adopted = false;
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ windowDecor = mFreeformTaskListener.giveWindowDecoration(
+ change.getTaskInfo(), startT, finishT);
+ if (windowDecor != null) {
+ adopted = mFullscreenTaskListener.adoptWindowDecoration(
+ change, startT, finishT, windowDecor);
+ } else {
+ // will return false if it already has the window decor.
+ adopted = mFullscreenTaskListener.createWindowDecoration(change, startT, finishT);
+ }
+ }
+
+ if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ windowDecor = mFullscreenTaskListener.giveWindowDecoration(
+ change.getTaskInfo(), startT, finishT);
+ if (windowDecor != null) {
+ adopted = mFreeformTaskListener.adoptWindowDecoration(
+ change, startT, finishT, windowDecor);
+ } else {
+ // will return false if it already has the window decor.
+ adopted = mFreeformTaskListener.createWindowDecoration(change, startT, finishT);
+ }
+ }
+
+ if (!adopted) {
+ releaseWindowDecor(windowDecor);
+ }
+ }
+
+ @Override
+ public void onTransitionStarting(@NonNull IBinder transition) {}
+
+ @Override
+ public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
+ final List<AutoCloseable> windowDecorsOfMerged = mTransitionToWindowDecors.get(merged);
+ if (windowDecorsOfMerged == null) {
+ // We are adding window decorations of the merged transition to them of the playing
+ // transition so if there is none of them there is nothing to do.
+ return;
+ }
+ mTransitionToWindowDecors.remove(merged);
+
+ final List<AutoCloseable> windowDecorsOfPlaying = mTransitionToWindowDecors.get(playing);
+ if (windowDecorsOfPlaying != null) {
+ windowDecorsOfPlaying.addAll(windowDecorsOfMerged);
+ } else {
+ mTransitionToWindowDecors.put(playing, windowDecorsOfMerged);
+ }
+ }
+
+ @Override
+ public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {
+ final List<AutoCloseable> windowDecors = mTransitionToWindowDecors.getOrDefault(
+ transition, Collections.emptyList());
+ mTransitionToWindowDecors.remove(transition);
+
+ for (AutoCloseable windowDecor : windowDecors) {
+ releaseWindowDecor(windowDecor);
+ }
+ mFullscreenTaskListener.onTaskTransitionFinished();
+ mFreeformTaskListener.onTaskTransitionFinished();
+ }
+
+ private static void releaseWindowDecor(AutoCloseable windowDecor) {
+ if (windowDecor == null) {
+ return;
+ }
+ try {
+ windowDecor.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to release window decoration.", e);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
new file mode 100644
index 000000000000..8da4c6ab4b36
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
@@ -0,0 +1,51 @@
+/*
+ * 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.freeform;
+
+import android.window.WindowContainerTransaction;
+
+/**
+ * The interface around {@link FreeformTaskTransitionHandler} for task listeners to start freeform
+ * task transitions.
+ */
+public interface FreeformTaskTransitionStarter {
+
+ /**
+ * Starts a windowing mode transition.
+ *
+ * @param targetWindowingMode the target windowing mode
+ * @param wct the {@link WindowContainerTransaction} that changes the windowing mode
+ *
+ */
+ void startWindowingModeTransition(int targetWindowingMode, WindowContainerTransaction wct);
+
+ /**
+ * Starts window minimization transition
+ *
+ * @param wct the {@link WindowContainerTransaction} that changes the windowing mode
+ *
+ */
+ void startMinimizedModeTransition(WindowContainerTransaction wct);
+
+ /**
+ * Starts close window transition
+ *
+ * @param wct the {@link WindowContainerTransaction} that closes the task
+ *
+ */
+ void startRemoveTransition(WindowContainerTransaction wct);
+} \ No newline at end of file
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 73e6cba43ec0..76e296bb8c61 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
@@ -16,110 +16,292 @@
package com.android.wm.shell.fullscreen;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-
import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString;
+import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
-import android.app.TaskInfo;
import android.graphics.Point;
-import android.util.Slog;
+import android.util.Log;
import android.util.SparseArray;
-import android.util.SparseBooleanArray;
import android.view.SurfaceControl;
+import android.window.TransitionInfo;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.io.PrintWriter;
import java.util.Optional;
/**
* Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}.
+ * @param <T> the type of window decoration instance
*/
-public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
+public class FullscreenTaskListener<T extends AutoCloseable>
+ implements ShellTaskOrganizer.TaskListener {
private static final String TAG = "FullscreenTaskListener";
- private final SyncTransactionQueue mSyncQueue;
- private final FullscreenUnfoldController mFullscreenUnfoldController;
- private final Optional<RecentTasksController> mRecentTasksOptional;
+ private final ShellTaskOrganizer mShellTaskOrganizer;
- private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>();
- private final AnimatableTasksListener mAnimatableTasksListener = new AnimatableTasksListener();
+ private final SparseArray<State<T>> mTasks = new SparseArray<>();
+ private final SparseArray<T> mWindowDecorOfVanishedTasks = new SparseArray<>();
- public FullscreenTaskListener(SyncTransactionQueue syncQueue,
- Optional<FullscreenUnfoldController> unfoldController) {
- this(syncQueue, unfoldController, Optional.empty());
+ private static class State<T extends AutoCloseable> {
+ RunningTaskInfo mTaskInfo;
+ SurfaceControl mLeash;
+ T mWindowDecoration;
+ }
+ private final SyncTransactionQueue mSyncQueue;
+ private final Optional<RecentTasksController> mRecentTasksOptional;
+ private final Optional<WindowDecorViewModel<T>> mWindowDecorViewModelOptional;
+ /**
+ * This constructor is used by downstream products.
+ */
+ public FullscreenTaskListener(SyncTransactionQueue syncQueue) {
+ this(null /* shellInit */, null /* shellTaskOrganizer */, syncQueue, Optional.empty(),
+ Optional.empty());
}
- public FullscreenTaskListener(SyncTransactionQueue syncQueue,
- Optional<FullscreenUnfoldController> unfoldController,
- Optional<RecentTasksController> recentTasks) {
+ public FullscreenTaskListener(ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
+ SyncTransactionQueue syncQueue,
+ Optional<RecentTasksController> recentTasksOptional,
+ Optional<WindowDecorViewModel<T>> windowDecorViewModelOptional) {
+ mShellTaskOrganizer = shellTaskOrganizer;
mSyncQueue = syncQueue;
- mFullscreenUnfoldController = unfoldController.orElse(null);
- mRecentTasksOptional = recentTasks;
+ mRecentTasksOptional = recentTasksOptional;
+ mWindowDecorViewModelOptional = windowDecorViewModelOptional;
+ // Note: Some derivative FullscreenTaskListener implementations do not use ShellInit
+ if (shellInit != null) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
+ }
+
+ private void onInit() {
+ mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FULLSCREEN);
}
@Override
public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
- if (mDataByTaskId.get(taskInfo.taskId) != null) {
+ if (mTasks.get(taskInfo.taskId) != null) {
throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId);
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d",
taskInfo.taskId);
final Point positionInParent = taskInfo.positionInParent;
- mDataByTaskId.put(taskInfo.taskId, new TaskData(leash, positionInParent));
- if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
- mSyncQueue.runInSync(t -> {
- // Reset several properties back to fullscreen (PiP, for example, leaves all these
- // properties in a bad state).
- t.setWindowCrop(leash, null);
- t.setPosition(leash, positionInParent.x, positionInParent.y);
- t.setAlpha(leash, 1f);
- t.setMatrix(leash, 1, 0, 0, 1);
- t.show(leash);
- });
+ final State<T> state = new State();
+ state.mLeash = leash;
+ state.mTaskInfo = taskInfo;
+ mTasks.put(taskInfo.taskId, state);
- mAnimatableTasksListener.onTaskAppeared(taskInfo);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
updateRecentsForVisibleFullscreenTask(taskInfo);
+ if (mWindowDecorViewModelOptional.isPresent()) {
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ state.mWindowDecoration =
+ mWindowDecorViewModelOptional.get().createWindowDecoration(taskInfo,
+ leash, t, t);
+ t.apply();
+ }
+ if (state.mWindowDecoration == null) {
+ mSyncQueue.runInSync(t -> {
+ // Reset several properties back to fullscreen (PiP, for example, leaves all these
+ // properties in a bad state).
+ t.setWindowCrop(leash, null);
+ t.setPosition(leash, positionInParent.x, positionInParent.y);
+ t.setAlpha(leash, 1f);
+ t.setMatrix(leash, 1, 0, 0, 1);
+ t.show(leash);
+ });
+ }
}
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ final Point oldPositionInParent = state.mTaskInfo.positionInParent;
+ state.mTaskInfo = taskInfo;
+ if (state.mWindowDecoration != null) {
+ mWindowDecorViewModelOptional.get().onTaskInfoChanged(
+ state.mTaskInfo, state.mWindowDecoration);
+ }
if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
-
- mAnimatableTasksListener.onTaskInfoChanged(taskInfo);
updateRecentsForVisibleFullscreenTask(taskInfo);
- final TaskData data = mDataByTaskId.get(taskInfo.taskId);
- final Point positionInParent = taskInfo.positionInParent;
- if (!positionInParent.equals(data.positionInParent)) {
- data.positionInParent.set(positionInParent.x, positionInParent.y);
+ final Point positionInParent = state.mTaskInfo.positionInParent;
+ if (!oldPositionInParent.equals(state.mTaskInfo.positionInParent)) {
mSyncQueue.runInSync(t -> {
- t.setPosition(data.surface, positionInParent.x, positionInParent.y);
+ t.setPosition(state.mLeash, positionInParent.x, positionInParent.y);
});
}
}
@Override
- public void onTaskVanished(RunningTaskInfo taskInfo) {
- if (mDataByTaskId.get(taskInfo.taskId) == null) {
- Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
+ public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ if (state == null) {
+ // This is possible if the transition happens before this method.
return;
}
-
- mAnimatableTasksListener.onTaskVanished(taskInfo);
- mDataByTaskId.remove(taskInfo.taskId);
-
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
taskInfo.taskId);
+ mTasks.remove(taskInfo.taskId);
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ // Save window decorations of closing tasks so that we can hand them over to the
+ // transition system if this method happens before the transition. In case where the
+ // transition didn't happen, it'd be cleared when the next transition finished.
+ if (state.mWindowDecoration != null) {
+ mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration);
+ }
+ return;
+ }
+ releaseWindowDecor(state.mWindowDecoration);
+ }
+
+ /**
+ * Creates a window decoration for a transition.
+ *
+ * @param change the change of this task transition that needs to have the task layer as the
+ * leash
+ * @return {@code true} if a decoration was actually created.
+ */
+ public boolean createWindowDecoration(TransitionInfo.Change change,
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
+ final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
+ if (!mWindowDecorViewModelOptional.isPresent()) return false;
+ if (state.mWindowDecoration != null) {
+ // Already has a decoration.
+ return false;
+ }
+ T newWindowDecor = mWindowDecorViewModelOptional.get().createWindowDecoration(
+ state.mTaskInfo, state.mLeash, startT, finishT);
+ if (newWindowDecor != null) {
+ state.mWindowDecoration = newWindowDecor;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Adopt the incoming window decoration and lets the window decoration prepare for a transition.
+ *
+ * @param change the change of this task transition that needs to have the task layer as the
+ * leash
+ * @param startT the start transaction of this transition
+ * @param finishT the finish transaction of this transition
+ * @param windowDecor the window decoration to adopt
+ * @return {@code true} if it adopts the window decoration; {@code false} otherwise
+ */
+ public boolean adoptWindowDecoration(
+ TransitionInfo.Change change,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT,
+ @Nullable AutoCloseable windowDecor) {
+ if (!mWindowDecorViewModelOptional.isPresent()) {
+ return false;
+ }
+ final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
+ state.mWindowDecoration = mWindowDecorViewModelOptional.get().adoptWindowDecoration(
+ windowDecor);
+ if (state.mWindowDecoration != null) {
+ mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition(
+ state.mTaskInfo, startT, finishT, state.mWindowDecoration);
+ return true;
+ } else {
+ T newWindowDecor = mWindowDecorViewModelOptional.get().createWindowDecoration(
+ state.mTaskInfo, state.mLeash, startT, finishT);
+ if (newWindowDecor != null) {
+ state.mWindowDecoration = newWindowDecor;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Clear window decors of vanished tasks.
+ */
+ public void onTaskTransitionFinished() {
+ if (mWindowDecorOfVanishedTasks.size() == 0) {
+ return;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Clearing window decors of vanished tasks. There could be visual defects "
+ + "if any of them is used later in transitions.");
+ for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) {
+ releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i));
+ }
+ mWindowDecorOfVanishedTasks.clear();
+ }
+
+ /**
+ * Gives out the ownership of the task's window decoration. The given task is leaving (of has
+ * left) this task listener. This is the transition system asking for the ownership.
+ *
+ * @param taskInfo the maximizing task
+ * @return the window decor of the maximizing task if any
+ */
+ public T giveWindowDecoration(
+ ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ T windowDecor;
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ if (state != null) {
+ windowDecor = state.mWindowDecoration;
+ state.mWindowDecoration = null;
+ } else {
+ windowDecor =
+ mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId);
+ }
+ if (mWindowDecorViewModelOptional.isPresent() && windowDecor != null) {
+ mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition(
+ taskInfo, startT, finishT, windowDecor);
+ }
+
+ return windowDecor;
+ }
+
+ private State<T> createOrUpdateTaskState(ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl leash) {
+ State<T> state = mTasks.get(taskInfo.taskId);
+ if (state != null) {
+ updateTaskInfo(taskInfo);
+ return state;
+ }
+
+ state = new State<T>();
+ state.mTaskInfo = taskInfo;
+ state.mLeash = leash;
+ mTasks.put(taskInfo.taskId, state);
+
+ return state;
+ }
+
+ private State<T> updateTaskInfo(ActivityManager.RunningTaskInfo taskInfo) {
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ state.mTaskInfo = taskInfo;
+ return state;
+ }
+
+ private void releaseWindowDecor(T windowDecor) {
+ if (windowDecor == null) {
+ return;
+ }
+ try {
+ windowDecor.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to release window decoration.", e);
+ }
}
private void updateRecentsForVisibleFullscreenTask(RunningTaskInfo taskInfo) {
@@ -143,17 +325,17 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
}
private SurfaceControl findTaskSurface(int taskId) {
- if (!mDataByTaskId.contains(taskId)) {
+ if (!mTasks.contains(taskId)) {
throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
}
- return mDataByTaskId.get(taskId).surface;
+ return mTasks.get(taskId).mLeash;
}
@Override
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + this);
- pw.println(innerPrefix + mDataByTaskId.size() + " Tasks");
+ pw.println(innerPrefix + mTasks.size() + " Tasks");
}
@Override
@@ -161,77 +343,5 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN);
}
- /**
- * Per-task data for each managed task.
- */
- private static class TaskData {
- public final SurfaceControl surface;
- public final Point positionInParent;
-
- public TaskData(SurfaceControl surface, Point positionInParent) {
- this.surface = surface;
- this.positionInParent = positionInParent;
- }
- }
-
- class AnimatableTasksListener {
- private final SparseBooleanArray mTaskIds = new SparseBooleanArray();
-
- public void onTaskAppeared(RunningTaskInfo taskInfo) {
- final boolean isApplicable = isAnimatable(taskInfo);
- if (isApplicable) {
- mTaskIds.put(taskInfo.taskId, true);
-
- if (mFullscreenUnfoldController != null) {
- SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface;
- mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash);
- }
- }
- }
-
- public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
- final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId);
- final boolean isApplicable = isAnimatable(taskInfo);
-
- if (isCurrentlyApplicable) {
- if (isApplicable) {
- // Still applicable, send update
- if (mFullscreenUnfoldController != null) {
- mFullscreenUnfoldController.onTaskInfoChanged(taskInfo);
- }
- } else {
- // Became inapplicable
- if (mFullscreenUnfoldController != null) {
- mFullscreenUnfoldController.onTaskVanished(taskInfo);
- }
- mTaskIds.put(taskInfo.taskId, false);
- }
- } else {
- if (isApplicable) {
- // Became applicable
- mTaskIds.put(taskInfo.taskId, true);
-
- if (mFullscreenUnfoldController != null) {
- SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface;
- mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash);
- }
- }
- }
- }
- public void onTaskVanished(RunningTaskInfo taskInfo) {
- final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId);
- if (isCurrentlyApplicable && mFullscreenUnfoldController != null) {
- mFullscreenUnfoldController.onTaskVanished(taskInfo);
- }
- mTaskIds.put(taskInfo.taskId, false);
- }
-
- private boolean isAnimatable(TaskInfo taskInfo) {
- // Filter all visible tasks that are not launcher tasks
- // We do not animate launcher as it handles the animation by itself
- return taskInfo != null && taskInfo.isVisible && taskInfo.getConfiguration()
- .windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME;
- }
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java
index 23f76ca5f6ae..32125fa44148 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java
@@ -19,7 +19,6 @@ package com.android.wm.shell.hidedisplaycutout;
import android.content.Context;
import android.content.res.Configuration;
import android.os.SystemProperties;
-import android.util.Slog;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -27,20 +26,23 @@ import androidx.annotation.VisibleForTesting;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import java.io.PrintWriter;
-import java.util.concurrent.TimeUnit;
/**
* Manages the hide display cutout status.
*/
-public class HideDisplayCutoutController {
+public class HideDisplayCutoutController implements ConfigurationChangeListener {
private static final String TAG = "HideDisplayCutoutController";
private final Context mContext;
+ private final ShellCommandHandler mShellCommandHandler;
+ private final ShellController mShellController;
private final HideDisplayCutoutOrganizer mOrganizer;
- private final ShellExecutor mMainExecutor;
- private final HideDisplayCutoutImpl mImpl = new HideDisplayCutoutImpl();
@VisibleForTesting
boolean mEnabled;
@@ -49,8 +51,12 @@ public class HideDisplayCutoutController {
* supported.
*/
@Nullable
- public static HideDisplayCutoutController create(
- Context context, DisplayController displayController, ShellExecutor mainExecutor) {
+ public static HideDisplayCutoutController create(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ DisplayController displayController,
+ ShellExecutor mainExecutor) {
// The SystemProperty is set for devices that support this feature and is used to control
// whether to create the HideDisplayCutout instance.
// It's defined in the device.mk (e.g. device/google/crosshatch/device.mk).
@@ -60,19 +66,26 @@ public class HideDisplayCutoutController {
HideDisplayCutoutOrganizer organizer =
new HideDisplayCutoutOrganizer(context, displayController, mainExecutor);
- return new HideDisplayCutoutController(context, organizer, mainExecutor);
+ return new HideDisplayCutoutController(context, shellInit, shellCommandHandler,
+ shellController, organizer);
}
- HideDisplayCutoutController(Context context, HideDisplayCutoutOrganizer organizer,
- ShellExecutor mainExecutor) {
+ HideDisplayCutoutController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ HideDisplayCutoutOrganizer organizer) {
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
+ mShellController = shellController;
mOrganizer = organizer;
- mMainExecutor = mainExecutor;
- updateStatus();
+ shellInit.addInitCallback(this::onInit, this);
}
- public HideDisplayCutout asHideDisplayCutout() {
- return mImpl;
+ private void onInit() {
+ mShellCommandHandler.addDumpCallback(this::dump, this);
+ updateStatus();
+ mShellController.addConfigurationChangeListener(this);
}
@VisibleForTesting
@@ -94,26 +107,18 @@ public class HideDisplayCutoutController {
}
}
- private void onConfigurationChanged(Configuration newConfig) {
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
updateStatus();
}
- public void dump(@NonNull PrintWriter pw) {
- final String prefix = " ";
+ public void dump(@NonNull PrintWriter pw, String prefix) {
+ final String innerPrefix = " ";
pw.print(TAG);
pw.println(" states: ");
- pw.print(prefix);
+ pw.print(innerPrefix);
pw.print("mEnabled=");
pw.println(mEnabled);
mOrganizer.dump(pw);
}
-
- private class HideDisplayCutoutImpl implements HideDisplayCutout {
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- mMainExecutor.execute(() -> {
- HideDisplayCutoutController.this.onConfigurationChanged(newConfig);
- });
- }
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java
index 3f7d78dda037..f376e1fd6174 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java
@@ -64,8 +64,8 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer {
@VisibleForTesting
final Rect mCurrentDisplayBounds = new Rect();
// The default display cutout in natural orientation.
- private Insets mDefaultCutoutInsets;
- private Insets mCurrentCutoutInsets;
+ private Insets mDefaultCutoutInsets = Insets.NONE;
+ private Insets mCurrentCutoutInsets = Insets.NONE;
private boolean mIsDefaultPortrait;
private int mStatusBarHeight;
@VisibleForTesting
@@ -78,27 +78,35 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer {
private final DisplayController.OnDisplaysChangedListener mListener =
new DisplayController.OnDisplaysChangedListener() {
@Override
+ public void onDisplayAdded(int displayId) {
+ onDisplayChanged(displayId);
+ }
+
+ @Override
public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
- if (displayId != DEFAULT_DISPLAY) {
- return;
- }
- DisplayLayout displayLayout =
- mDisplayController.getDisplayLayout(DEFAULT_DISPLAY);
- if (displayLayout == null) {
- return;
- }
- final boolean rotationChanged = mRotation != displayLayout.rotation();
- mRotation = displayLayout.rotation();
- if (rotationChanged || isDisplayBoundsChanged()) {
- updateBoundsAndOffsets(true /* enabled */);
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
- applyAllBoundsAndOffsets(wct, t);
- applyTransaction(wct, t);
- }
+ onDisplayChanged(displayId);
}
};
+ private void onDisplayChanged(int displayId) {
+ if (displayId != DEFAULT_DISPLAY) {
+ return;
+ }
+ final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(DEFAULT_DISPLAY);
+ if (displayLayout == null) {
+ return;
+ }
+ final boolean rotationChanged = mRotation != displayLayout.rotation();
+ mRotation = displayLayout.rotation();
+ if (rotationChanged || isDisplayBoundsChanged()) {
+ updateBoundsAndOffsets(true /* enabled */);
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ applyAllBoundsAndOffsets(wct, t);
+ applyTransaction(wct, t);
+ }
+ }
+
HideDisplayCutoutOrganizer(Context context, DisplayController displayController,
ShellExecutor mainExecutor) {
super(mainExecutor);
@@ -128,9 +136,10 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer {
final WindowContainerTransaction wct = new WindowContainerTransaction();
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
- applyBoundsAndOffsets(
- displayAreaInfo.token, mDisplayAreaMap.get(displayAreaInfo.token), wct, t);
+ final SurfaceControl leash = mDisplayAreaMap.get(displayAreaInfo.token);
+ applyBoundsAndOffsets(displayAreaInfo.token, leash, wct, t);
applyTransaction(wct, t);
+ leash.release();
mDisplayAreaMap.remove(displayAreaInfo.token);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
index b4c87b6cbf95..e91987dab972 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
@@ -50,7 +50,9 @@ 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.recents.RecentTasksController;
-import com.android.wm.shell.startingsurface.StartingWindowController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
import java.io.PrintWriter;
import java.util.List;
@@ -72,6 +74,7 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer {
private final Handler mMainHandler;
private final Context mContext;
+ private final ShellCommandHandler mShellCommandHandler;
private final SyncTransactionQueue mSyncQueue;
private final DisplayController mDisplayController;
private final DisplayInsetsController mDisplayInsetsController;
@@ -139,45 +142,61 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer {
@VisibleForTesting
KidsModeTaskOrganizer(
- ITaskOrganizerController taskOrganizerController,
- ShellExecutor mainExecutor,
- Handler mainHandler,
Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ITaskOrganizerController taskOrganizerController,
SyncTransactionQueue syncTransactionQueue,
DisplayController displayController,
DisplayInsetsController displayInsetsController,
+ Optional<UnfoldAnimationController> unfoldAnimationController,
Optional<RecentTasksController> recentTasks,
- KidsModeSettingsObserver kidsModeSettingsObserver) {
- super(taskOrganizerController, mainExecutor, context, /* compatUI= */ null, recentTasks);
+ KidsModeSettingsObserver kidsModeSettingsObserver,
+ ShellExecutor mainExecutor,
+ Handler mainHandler) {
+ // Note: we don't call super with the shell init because we will be initializing manually
+ super(/* shellInit= */ null, /* shellCommandHandler= */ null, taskOrganizerController,
+ /* compatUI= */ null, unfoldAnimationController, recentTasks, mainExecutor);
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
mMainHandler = mainHandler;
mSyncQueue = syncTransactionQueue;
mDisplayController = displayController;
mDisplayInsetsController = displayInsetsController;
mKidsModeSettingsObserver = kidsModeSettingsObserver;
+ shellInit.addInitCallback(this::onInit, this);
}
public KidsModeTaskOrganizer(
- ShellExecutor mainExecutor,
- Handler mainHandler,
Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
SyncTransactionQueue syncTransactionQueue,
DisplayController displayController,
DisplayInsetsController displayInsetsController,
- Optional<RecentTasksController> recentTasks) {
- super(mainExecutor, context, /* compatUI= */ null, recentTasks);
+ Optional<UnfoldAnimationController> unfoldAnimationController,
+ Optional<RecentTasksController> recentTasks,
+ ShellExecutor mainExecutor,
+ Handler mainHandler) {
+ // Note: we don't call super with the shell init because we will be initializing manually
+ super(/* shellInit= */ null, /* taskOrganizerController= */ null, /* compatUI= */ null,
+ unfoldAnimationController, recentTasks, mainExecutor);
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
mMainHandler = mainHandler;
mSyncQueue = syncTransactionQueue;
mDisplayController = displayController;
mDisplayInsetsController = displayInsetsController;
+ shellInit.addInitCallback(this::onInit, this);
}
/**
* Initializes kids mode status.
*/
- public void initialize(StartingWindowController startingWindowController) {
- initStartingWindow(startingWindowController);
+ public void onInit() {
+ if (mShellCommandHandler != null) {
+ mShellCommandHandler.addDumpCallback(this::dump, this);
+ }
if (mKidsModeSettingsObserver == null) {
mKidsModeSettingsObserver = new KidsModeSettingsObserver(mMainHandler, mContext);
}
@@ -297,11 +316,13 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer {
true /* onTop */);
wct.reorder(rootToken, mEnabled /* onTop */);
mSyncQueue.queue(wct);
- final SurfaceControl rootLeash = mLaunchRootLeash;
- mSyncQueue.runInSync(t -> {
- t.setPosition(rootLeash, taskBounds.left, taskBounds.top);
- t.setWindowCrop(rootLeash, taskBounds.width(), taskBounds.height());
- });
+ if (mEnabled) {
+ final SurfaceControl rootLeash = mLaunchRootLeash;
+ mSyncQueue.runInSync(t -> {
+ t.setPosition(rootLeash, taskBounds.left, taskBounds.top);
+ t.setWindowCrop(rootLeash, taskBounds.width(), taskBounds.height());
+ });
+ }
}
private Rect calculateBounds() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java
deleted file mode 100644
index aced072c8c71..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java
+++ /dev/null
@@ -1,418 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.legacysplitscreen;
-
-import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
-import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.Nullable;
-import android.graphics.Rect;
-import android.util.Slog;
-import android.view.Choreographer;
-import android.view.SurfaceControl;
-import android.window.TaskOrganizer;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.TransactionPool;
-
-class DividerImeController implements DisplayImeController.ImePositionProcessor {
- private static final String TAG = "DividerImeController";
- private static final boolean DEBUG = LegacySplitScreenController.DEBUG;
-
- private static final float ADJUSTED_NONFOCUS_DIM = 0.3f;
-
- private final LegacySplitScreenTaskListener mSplits;
- private final TransactionPool mTransactionPool;
- private final ShellExecutor mMainExecutor;
- private final TaskOrganizer mTaskOrganizer;
-
- /**
- * These are the y positions of the top of the IME surface when it is hidden and when it is
- * shown respectively. These are NOT necessarily the top of the visible IME itself.
- */
- private int mHiddenTop = 0;
- private int mShownTop = 0;
-
- // The following are target states (what we are curretly animating towards).
- /**
- * {@code true} if, at the end of the animation, the split task positions should be
- * adjusted by height of the IME. This happens when the secondary split is the IME target.
- */
- private boolean mTargetAdjusted = false;
- /**
- * {@code true} if, at the end of the animation, the IME should be shown/visible
- * regardless of what has focus.
- */
- private boolean mTargetShown = false;
- private float mTargetPrimaryDim = 0.f;
- private float mTargetSecondaryDim = 0.f;
-
- // The following are the current (most recent) states set during animation
- /** {@code true} if the secondary split has IME focus. */
- private boolean mSecondaryHasFocus = false;
- /** The dimming currently applied to the primary/secondary splits. */
- private float mLastPrimaryDim = 0.f;
- private float mLastSecondaryDim = 0.f;
- /** The most recent y position of the top of the IME surface */
- private int mLastAdjustTop = -1;
-
- // The following are states reached last time an animation fully completed.
- /** {@code true} if the IME was shown/visible by the last-completed animation. */
- private boolean mImeWasShown = false;
- /** {@code true} if the split positions were adjusted by the last-completed animation. */
- private boolean mAdjusted = false;
-
- /**
- * When some aspect of split-screen needs to animate independent from the IME,
- * this will be non-null and control split animation.
- */
- @Nullable
- private ValueAnimator mAnimation = null;
-
- private boolean mPaused = true;
- private boolean mPausedTargetAdjusted = false;
-
- DividerImeController(LegacySplitScreenTaskListener splits, TransactionPool pool,
- ShellExecutor mainExecutor, TaskOrganizer taskOrganizer) {
- mSplits = splits;
- mTransactionPool = pool;
- mMainExecutor = mainExecutor;
- mTaskOrganizer = taskOrganizer;
- }
-
- private DividerView getView() {
- return mSplits.mSplitScreenController.getDividerView();
- }
-
- private LegacySplitDisplayLayout getLayout() {
- return mSplits.mSplitScreenController.getSplitLayout();
- }
-
- private boolean isDividerHidden() {
- final DividerView view = mSplits.mSplitScreenController.getDividerView();
- return view == null || view.isHidden();
- }
-
- private boolean getSecondaryHasFocus(int displayId) {
- WindowContainerToken imeSplit = mTaskOrganizer.getImeTarget(displayId);
- return imeSplit != null
- && (imeSplit.asBinder() == mSplits.mSecondary.token.asBinder());
- }
-
- void reset() {
- mPaused = true;
- mPausedTargetAdjusted = false;
- mAnimation = null;
- mAdjusted = mTargetAdjusted = false;
- mImeWasShown = mTargetShown = false;
- mTargetPrimaryDim = mTargetSecondaryDim = mLastPrimaryDim = mLastSecondaryDim = 0.f;
- mSecondaryHasFocus = false;
- mLastAdjustTop = -1;
- }
-
- private void updateDimTargets() {
- final boolean splitIsVisible = !getView().isHidden();
- mTargetPrimaryDim = (mSecondaryHasFocus && mTargetShown && splitIsVisible)
- ? ADJUSTED_NONFOCUS_DIM : 0.f;
- mTargetSecondaryDim = (!mSecondaryHasFocus && mTargetShown && splitIsVisible)
- ? ADJUSTED_NONFOCUS_DIM : 0.f;
- }
-
-
- @Override
- public void onImeControlTargetChanged(int displayId, boolean controlling) {
- // Restore the split layout when wm-shell is not controlling IME insets anymore.
- if (!controlling && mTargetShown) {
- mPaused = false;
- mTargetAdjusted = mTargetShown = false;
- mTargetPrimaryDim = mTargetSecondaryDim = 0.f;
- updateImeAdjustState(true /* force */);
- startAsyncAnimation();
- }
- }
-
- @Override
- @ImeAnimationFlags
- public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
- boolean imeShouldShow, boolean imeIsFloating, SurfaceControl.Transaction t) {
- if (isDividerHidden()) {
- return 0;
- }
- mHiddenTop = hiddenTop;
- mShownTop = shownTop;
- mTargetShown = imeShouldShow;
- mSecondaryHasFocus = getSecondaryHasFocus(displayId);
- final boolean targetAdjusted = imeShouldShow && mSecondaryHasFocus
- && !imeIsFloating && !getLayout().mDisplayLayout.isLandscape()
- && !mSplits.mSplitScreenController.isMinimized();
- if (mLastAdjustTop < 0) {
- mLastAdjustTop = imeShouldShow ? hiddenTop : shownTop;
- } else if (mLastAdjustTop != (imeShouldShow ? mShownTop : mHiddenTop)) {
- if (mTargetAdjusted != targetAdjusted && targetAdjusted == mAdjusted) {
- // Check for an "interruption" of an existing animation. In this case, we
- // need to fake-flip the last-known state direction so that the animation
- // completes in the other direction.
- mAdjusted = mTargetAdjusted;
- } else if (targetAdjusted && mTargetAdjusted && mAdjusted) {
- // Already fully adjusted for IME, but IME height has changed; so, force-start
- // an async animation to the new IME height.
- mAdjusted = false;
- }
- }
- if (mPaused) {
- mPausedTargetAdjusted = targetAdjusted;
- if (DEBUG) Slog.d(TAG, " ime starting but paused " + dumpState());
- return (targetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0;
- }
- mTargetAdjusted = targetAdjusted;
- updateDimTargets();
- if (DEBUG) Slog.d(TAG, " ime starting. " + dumpState());
- if (mAnimation != null || (mImeWasShown && imeShouldShow
- && mTargetAdjusted != mAdjusted)) {
- // We need to animate adjustment independently of the IME position, so
- // start our own animation to drive adjustment. This happens when a
- // different split's editor has gained focus while the IME is still visible.
- startAsyncAnimation();
- }
- updateImeAdjustState();
-
- return (mTargetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0;
- }
-
- private void updateImeAdjustState() {
- updateImeAdjustState(false /* force */);
- }
-
- private void updateImeAdjustState(boolean force) {
- if (mAdjusted != mTargetAdjusted || force) {
- // Reposition the server's secondary split position so that it evaluates
- // insets properly.
- WindowContainerTransaction wct = new WindowContainerTransaction();
- final LegacySplitDisplayLayout splitLayout = getLayout();
- if (mTargetAdjusted) {
- splitLayout.updateAdjustedBounds(mShownTop, mHiddenTop, mShownTop);
- wct.setBounds(mSplits.mSecondary.token, splitLayout.mAdjustedSecondary);
- // "Freeze" the configuration size so that the app doesn't get a config
- // or relaunch. This is required because normally nav-bar contributes
- // to configuration bounds (via nondecorframe).
- Rect adjustAppBounds = new Rect(mSplits.mSecondary.configuration
- .windowConfiguration.getAppBounds());
- adjustAppBounds.offset(0, splitLayout.mAdjustedSecondary.top
- - splitLayout.mSecondary.top);
- wct.setAppBounds(mSplits.mSecondary.token, adjustAppBounds);
- wct.setScreenSizeDp(mSplits.mSecondary.token,
- mSplits.mSecondary.configuration.screenWidthDp,
- mSplits.mSecondary.configuration.screenHeightDp);
-
- wct.setBounds(mSplits.mPrimary.token, splitLayout.mAdjustedPrimary);
- adjustAppBounds = new Rect(mSplits.mPrimary.configuration
- .windowConfiguration.getAppBounds());
- adjustAppBounds.offset(0, splitLayout.mAdjustedPrimary.top
- - splitLayout.mPrimary.top);
- wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds);
- wct.setScreenSizeDp(mSplits.mPrimary.token,
- mSplits.mPrimary.configuration.screenWidthDp,
- mSplits.mPrimary.configuration.screenHeightDp);
- } else {
- wct.setBounds(mSplits.mSecondary.token, splitLayout.mSecondary);
- wct.setAppBounds(mSplits.mSecondary.token, null);
- wct.setScreenSizeDp(mSplits.mSecondary.token,
- SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
- wct.setBounds(mSplits.mPrimary.token, splitLayout.mPrimary);
- wct.setAppBounds(mSplits.mPrimary.token, null);
- wct.setScreenSizeDp(mSplits.mPrimary.token,
- SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
- }
-
- if (!mSplits.mSplitScreenController.getWmProxy().queueSyncTransactionIfWaiting(wct)) {
- mTaskOrganizer.applyTransaction(wct);
- }
- }
-
- // Update all the adjusted-for-ime states
- if (!mPaused) {
- final DividerView view = getView();
- if (view != null) {
- view.setAdjustedForIme(mTargetShown, mTargetShown
- ? DisplayImeController.ANIMATION_DURATION_SHOW_MS
- : DisplayImeController.ANIMATION_DURATION_HIDE_MS);
- }
- }
- mSplits.mSplitScreenController.setAdjustedForIme(mTargetShown && !mPaused);
- }
-
- @Override
- public void onImePositionChanged(int displayId, int imeTop,
- SurfaceControl.Transaction t) {
- if (mAnimation != null || isDividerHidden() || mPaused) {
- // Not synchronized with IME anymore, so return.
- return;
- }
- final float fraction = ((float) imeTop - mHiddenTop) / (mShownTop - mHiddenTop);
- final float progress = mTargetShown ? fraction : 1.f - fraction;
- onProgress(progress, t);
- }
-
- @Override
- public void onImeEndPositioning(int displayId, boolean cancelled,
- SurfaceControl.Transaction t) {
- if (mAnimation != null || isDividerHidden() || mPaused) {
- // Not synchronized with IME anymore, so return.
- return;
- }
- onEnd(cancelled, t);
- }
-
- private void onProgress(float progress, SurfaceControl.Transaction t) {
- final DividerView view = getView();
- if (mTargetAdjusted != mAdjusted && !mPaused) {
- final LegacySplitDisplayLayout splitLayout = getLayout();
- final float fraction = mTargetAdjusted ? progress : 1.f - progress;
- mLastAdjustTop = (int) (fraction * mShownTop + (1.f - fraction) * mHiddenTop);
- splitLayout.updateAdjustedBounds(mLastAdjustTop, mHiddenTop, mShownTop);
- view.resizeSplitSurfaces(t, splitLayout.mAdjustedPrimary,
- splitLayout.mAdjustedSecondary);
- }
- final float invProg = 1.f - progress;
- view.setResizeDimLayer(t, true /* primary */,
- mLastPrimaryDim * invProg + progress * mTargetPrimaryDim);
- view.setResizeDimLayer(t, false /* primary */,
- mLastSecondaryDim * invProg + progress * mTargetSecondaryDim);
- }
-
- void setDimsHidden(SurfaceControl.Transaction t, boolean hidden) {
- final DividerView view = getView();
- if (hidden) {
- view.setResizeDimLayer(t, true /* primary */, 0.f /* alpha */);
- view.setResizeDimLayer(t, false /* primary */, 0.f /* alpha */);
- } else {
- updateDimTargets();
- view.setResizeDimLayer(t, true /* primary */, mTargetPrimaryDim);
- view.setResizeDimLayer(t, false /* primary */, mTargetSecondaryDim);
- }
- }
-
- private void onEnd(boolean cancelled, SurfaceControl.Transaction t) {
- if (!cancelled) {
- onProgress(1.f, t);
- mAdjusted = mTargetAdjusted;
- mImeWasShown = mTargetShown;
- mLastAdjustTop = mAdjusted ? mShownTop : mHiddenTop;
- mLastPrimaryDim = mTargetPrimaryDim;
- mLastSecondaryDim = mTargetSecondaryDim;
- }
- }
-
- private void startAsyncAnimation() {
- if (mAnimation != null) {
- mAnimation.cancel();
- }
- mAnimation = ValueAnimator.ofFloat(0.f, 1.f);
- mAnimation.setDuration(DisplayImeController.ANIMATION_DURATION_SHOW_MS);
- if (mTargetAdjusted != mAdjusted) {
- final float fraction =
- ((float) mLastAdjustTop - mHiddenTop) / (mShownTop - mHiddenTop);
- final float progress = mTargetAdjusted ? fraction : 1.f - fraction;
- mAnimation.setCurrentFraction(progress);
- }
-
- mAnimation.addUpdateListener(animation -> {
- SurfaceControl.Transaction t = mTransactionPool.acquire();
- float value = (float) animation.getAnimatedValue();
- onProgress(value, t);
- t.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
- t.apply();
- mTransactionPool.release(t);
- });
- mAnimation.setInterpolator(DisplayImeController.INTERPOLATOR);
- mAnimation.addListener(new AnimatorListenerAdapter() {
- private boolean mCancel = false;
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancel = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- SurfaceControl.Transaction t = mTransactionPool.acquire();
- onEnd(mCancel, t);
- t.apply();
- mTransactionPool.release(t);
- mAnimation = null;
- }
- });
- mAnimation.start();
- }
-
- private String dumpState() {
- return "top:" + mHiddenTop + "->" + mShownTop
- + " adj:" + mAdjusted + "->" + mTargetAdjusted + "(" + mLastAdjustTop + ")"
- + " shw:" + mImeWasShown + "->" + mTargetShown
- + " dims:" + mLastPrimaryDim + "," + mLastSecondaryDim
- + "->" + mTargetPrimaryDim + "," + mTargetSecondaryDim
- + " shf:" + mSecondaryHasFocus + " desync:" + (mAnimation != null)
- + " paus:" + mPaused + "[" + mPausedTargetAdjusted + "]";
- }
-
- /** Completely aborts/resets adjustment state */
- public void pause(int displayId) {
- if (DEBUG) Slog.d(TAG, "ime pause posting " + dumpState());
- mMainExecutor.execute(() -> {
- if (DEBUG) Slog.d(TAG, "ime pause run posted " + dumpState());
- if (mPaused) {
- return;
- }
- mPaused = true;
- mPausedTargetAdjusted = mTargetAdjusted;
- mTargetAdjusted = false;
- mTargetPrimaryDim = mTargetSecondaryDim = 0.f;
- updateImeAdjustState();
- startAsyncAnimation();
- if (mAnimation != null) {
- mAnimation.end();
- }
- });
- }
-
- public void resume(int displayId) {
- if (DEBUG) Slog.d(TAG, "ime resume posting " + dumpState());
- mMainExecutor.execute(() -> {
- if (DEBUG) Slog.d(TAG, "ime resume run posted " + dumpState());
- if (!mPaused) {
- return;
- }
- mPaused = false;
- mTargetAdjusted = mPausedTargetAdjusted;
- updateDimTargets();
- final DividerView view = getView();
- if ((mTargetAdjusted != mAdjusted) && !mSplits.mSplitScreenController.isMinimized()
- && view != null) {
- // End unminimize animations since they conflict with adjustment animations.
- view.finishAnimations();
- }
- updateImeAdjustState();
- startAsyncAnimation();
- });
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java
deleted file mode 100644
index 73be2835d2cd..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java
+++ /dev/null
@@ -1,1314 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.legacysplitscreen;
-
-import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
-import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
-import static android.view.WindowManager.DOCKED_RIGHT;
-
-import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR;
-import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
-import static com.android.wm.shell.common.split.DividerView.TOUCH_ANIMATION_DURATION;
-import static com.android.wm.shell.common.split.DividerView.TOUCH_RELEASE_ANIMATION_DURATION;
-
-import android.animation.AnimationHandler;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.Region.Op;
-import android.hardware.display.DisplayManager;
-import android.os.Bundle;
-import android.util.AttributeSet;
-import android.util.Slog;
-import android.view.Choreographer;
-import android.view.Display;
-import android.view.MotionEvent;
-import android.view.PointerIcon;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.View.OnTouchListener;
-import android.view.ViewConfiguration;
-import android.view.ViewTreeObserver.InternalInsetsInfo;
-import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
-import android.view.animation.Interpolator;
-import android.view.animation.PathInterpolator;
-import android.widget.FrameLayout;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.policy.DividerSnapAlgorithm;
-import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
-import com.android.internal.policy.DockedDividerUtils;
-import com.android.wm.shell.R;
-import com.android.wm.shell.animation.FlingAnimationUtils;
-import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.common.split.DividerHandleView;
-
-import java.util.function.Consumer;
-
-/**
- * Docked stack divider.
- */
-public class DividerView extends FrameLayout implements OnTouchListener,
- OnComputeInternalInsetsListener {
- private static final String TAG = "DividerView";
- private static final boolean DEBUG = LegacySplitScreenController.DEBUG;
-
- interface DividerCallbacks {
- void onDraggingStart();
- void onDraggingEnd();
- }
-
- public static final int INVALID_RECENTS_GROW_TARGET = -1;
-
- private static final int LOG_VALUE_RESIZE_50_50 = 0;
- private static final int LOG_VALUE_RESIZE_DOCKED_SMALLER = 1;
- private static final int LOG_VALUE_RESIZE_DOCKED_LARGER = 2;
-
- private static final int LOG_VALUE_UNDOCK_MAX_DOCKED = 0;
- private static final int LOG_VALUE_UNDOCK_MAX_OTHER = 1;
-
- private static final int TASK_POSITION_SAME = Integer.MAX_VALUE;
-
- /**
- * How much the background gets scaled when we are in the minimized dock state.
- */
- private static final float MINIMIZE_DOCK_SCALE = 0f;
- private static final float ADJUSTED_FOR_IME_SCALE = 0.5f;
-
- private static final Interpolator IME_ADJUST_INTERPOLATOR =
- new PathInterpolator(0.2f, 0f, 0.1f, 1f);
-
- private DividerHandleView mHandle;
- private View mBackground;
- private MinimizedDockShadow mMinimizedShadow;
- private int mStartX;
- private int mStartY;
- private int mStartPosition;
- private int mDockSide;
- private boolean mMoving;
- private int mTouchSlop;
- private boolean mBackgroundLifted;
- private boolean mIsInMinimizeInteraction;
- SnapTarget mSnapTargetBeforeMinimized;
-
- private int mDividerInsets;
- private final Display mDefaultDisplay;
-
- private int mDividerSize;
- private int mTouchElevation;
- private int mLongPressEntraceAnimDuration;
-
- private final Rect mDockedRect = new Rect();
- private final Rect mDockedTaskRect = new Rect();
- private final Rect mOtherTaskRect = new Rect();
- private final Rect mOtherRect = new Rect();
- private final Rect mDockedInsetRect = new Rect();
- private final Rect mOtherInsetRect = new Rect();
- private final Rect mLastResizeRect = new Rect();
- private final Rect mTmpRect = new Rect();
- private LegacySplitScreenController mSplitScreenController;
- private WindowManagerProxy mWindowManagerProxy;
- private DividerWindowManager mWindowManager;
- private VelocityTracker mVelocityTracker;
- private FlingAnimationUtils mFlingAnimationUtils;
- private LegacySplitDisplayLayout mSplitLayout;
- private DividerImeController mImeController;
- private DividerCallbacks mCallback;
-
- private AnimationHandler mSfVsyncAnimationHandler;
- private ValueAnimator mCurrentAnimator;
- private boolean mEntranceAnimationRunning;
- private boolean mExitAnimationRunning;
- private int mExitStartPosition;
- private boolean mDockedStackMinimized;
- private boolean mHomeStackResizable;
- private boolean mAdjustedForIme;
- private DividerState mState;
-
- private LegacySplitScreenTaskListener mTiles;
- boolean mFirstLayout = true;
- int mDividerPositionX;
- int mDividerPositionY;
-
- private final Matrix mTmpMatrix = new Matrix();
- private final float[] mTmpValues = new float[9];
-
- // The view is removed or in the process of been removed from the system.
- private boolean mRemoved;
-
- // Whether the surface for this view has been hidden regardless of actual visibility. This is
- // used interact with keyguard.
- private boolean mSurfaceHidden = false;
-
- private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() {
- @Override
- public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- final DividerSnapAlgorithm snapAlgorithm = getSnapAlgorithm();
- if (isHorizontalDivision()) {
- info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
- mContext.getString(R.string.accessibility_action_divider_top_full)));
- if (snapAlgorithm.isFirstSplitTargetAvailable()) {
- info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
- mContext.getString(R.string.accessibility_action_divider_top_70)));
- }
- if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
- // Only show the middle target if there are more than 1 split target
- info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
- mContext.getString(R.string.accessibility_action_divider_top_50)));
- }
- if (snapAlgorithm.isLastSplitTargetAvailable()) {
- info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
- mContext.getString(R.string.accessibility_action_divider_top_30)));
- }
- info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
- mContext.getString(R.string.accessibility_action_divider_bottom_full)));
- } else {
- info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
- mContext.getString(R.string.accessibility_action_divider_left_full)));
- if (snapAlgorithm.isFirstSplitTargetAvailable()) {
- info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
- mContext.getString(R.string.accessibility_action_divider_left_70)));
- }
- if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
- // Only show the middle target if there are more than 1 split target
- info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
- mContext.getString(R.string.accessibility_action_divider_left_50)));
- }
- if (snapAlgorithm.isLastSplitTargetAvailable()) {
- info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
- mContext.getString(R.string.accessibility_action_divider_left_30)));
- }
- info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
- mContext.getString(R.string.accessibility_action_divider_right_full)));
- }
- }
-
- @Override
- public boolean performAccessibilityAction(View host, int action, Bundle args) {
- int currentPosition = getCurrentPosition();
- SnapTarget nextTarget = null;
- DividerSnapAlgorithm snapAlgorithm = mSplitLayout.getSnapAlgorithm();
- if (action == R.id.action_move_tl_full) {
- nextTarget = snapAlgorithm.getDismissEndTarget();
- } else if (action == R.id.action_move_tl_70) {
- nextTarget = snapAlgorithm.getLastSplitTarget();
- } else if (action == R.id.action_move_tl_50) {
- nextTarget = snapAlgorithm.getMiddleTarget();
- } else if (action == R.id.action_move_tl_30) {
- nextTarget = snapAlgorithm.getFirstSplitTarget();
- } else if (action == R.id.action_move_rb_full) {
- nextTarget = snapAlgorithm.getDismissStartTarget();
- }
- if (nextTarget != null) {
- startDragging(true /* animate */, false /* touching */);
- stopDragging(currentPosition, nextTarget, 250, Interpolators.FAST_OUT_SLOW_IN);
- return true;
- }
- return super.performAccessibilityAction(host, action, args);
- }
- };
-
- private final Runnable mResetBackgroundRunnable = new Runnable() {
- @Override
- public void run() {
- resetBackground();
- }
- };
-
- public DividerView(Context context) {
- this(context, null);
- }
-
- public DividerView(Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- final DisplayManager displayManager =
- (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
- mDefaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
- }
-
- public void setAnimationHandler(AnimationHandler sfVsyncAnimationHandler) {
- mSfVsyncAnimationHandler = sfVsyncAnimationHandler;
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mHandle = findViewById(R.id.docked_divider_handle);
- mBackground = findViewById(R.id.docked_divider_background);
- mMinimizedShadow = findViewById(R.id.minimized_dock_shadow);
- mHandle.setOnTouchListener(this);
- final int dividerWindowWidth = getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.docked_stack_divider_thickness);
- mDividerInsets = getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.docked_stack_divider_insets);
- mDividerSize = dividerWindowWidth - 2 * mDividerInsets;
- mTouchElevation = getResources().getDimensionPixelSize(
- R.dimen.docked_stack_divider_lift_elevation);
- mLongPressEntraceAnimDuration = getResources().getInteger(
- R.integer.long_press_dock_anim_duration);
- mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
- mFlingAnimationUtils = new FlingAnimationUtils(getResources().getDisplayMetrics(), 0.3f);
- boolean landscape = getResources().getConfiguration().orientation
- == Configuration.ORIENTATION_LANDSCAPE;
- mHandle.setPointerIcon(PointerIcon.getSystemIcon(getContext(),
- landscape ? TYPE_HORIZONTAL_DOUBLE_ARROW : TYPE_VERTICAL_DOUBLE_ARROW));
- getViewTreeObserver().addOnComputeInternalInsetsListener(this);
- mHandle.setAccessibilityDelegate(mHandleDelegate);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- // Save the current target if not minimized once attached to window
- if (mDockSide != WindowManager.DOCKED_INVALID && !mIsInMinimizeInteraction) {
- saveSnapTargetBeforeMinimized(mSnapTargetBeforeMinimized);
- }
- mFirstLayout = true;
- }
-
- void onDividerRemoved() {
- mRemoved = true;
- mCallback = null;
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- if (mFirstLayout) {
- // Wait for first layout so that the ViewRootImpl surface has been created.
- initializeSurfaceState();
- mFirstLayout = false;
- }
- int minimizeLeft = 0;
- int minimizeTop = 0;
- if (mDockSide == WindowManager.DOCKED_TOP) {
- minimizeTop = mBackground.getTop();
- } else if (mDockSide == WindowManager.DOCKED_LEFT) {
- minimizeLeft = mBackground.getLeft();
- } else if (mDockSide == WindowManager.DOCKED_RIGHT) {
- minimizeLeft = mBackground.getRight() - mMinimizedShadow.getWidth();
- }
- mMinimizedShadow.layout(minimizeLeft, minimizeTop,
- minimizeLeft + mMinimizedShadow.getMeasuredWidth(),
- minimizeTop + mMinimizedShadow.getMeasuredHeight());
- if (changed) {
- notifySplitScreenBoundsChanged();
- }
- }
-
- void injectDependencies(LegacySplitScreenController splitScreenController,
- DividerWindowManager windowManager, DividerState dividerState,
- DividerCallbacks callback, LegacySplitScreenTaskListener tiles,
- LegacySplitDisplayLayout sdl, DividerImeController imeController,
- WindowManagerProxy wmProxy) {
- mSplitScreenController = splitScreenController;
- mWindowManager = windowManager;
- mState = dividerState;
- mCallback = callback;
- mTiles = tiles;
- mSplitLayout = sdl;
- mImeController = imeController;
- mWindowManagerProxy = wmProxy;
-
- if (mState.mRatioPositionBeforeMinimized == 0) {
- // Set the middle target as the initial state
- mSnapTargetBeforeMinimized = mSplitLayout.getSnapAlgorithm().getMiddleTarget();
- } else {
- repositionSnapTargetBeforeMinimized();
- }
- }
-
- /** Gets non-minimized secondary bounds of split screen. */
- public Rect getNonMinimizedSplitScreenSecondaryBounds() {
- mOtherTaskRect.set(mSplitLayout.mSecondary);
- return mOtherTaskRect;
- }
-
- private boolean inSplitMode() {
- return getVisibility() == VISIBLE;
- }
-
- /** Unlike setVisible, this directly hides the surface without changing view visibility. */
- void setHidden(boolean hidden) {
- if (mSurfaceHidden == hidden) {
- return;
- }
- mSurfaceHidden = hidden;
- post(() -> {
- final SurfaceControl sc = getWindowSurfaceControl();
- if (sc == null) {
- return;
- }
- Transaction t = mTiles.getTransaction();
- if (hidden) {
- t.hide(sc);
- } else {
- t.show(sc);
- }
- mImeController.setDimsHidden(t, hidden);
- t.apply();
- mTiles.releaseTransaction(t);
- });
- }
-
- boolean isHidden() {
- return getVisibility() != View.VISIBLE || mSurfaceHidden;
- }
-
- /** Starts dragging the divider bar. */
- public boolean startDragging(boolean animate, boolean touching) {
- cancelFlingAnimation();
- if (touching) {
- mHandle.setTouching(true, animate);
- }
- mDockSide = mSplitLayout.getPrimarySplitSide();
-
- mWindowManagerProxy.setResizing(true);
- if (touching) {
- mWindowManager.setSlippery(false);
- liftBackground();
- }
- if (mCallback != null) {
- mCallback.onDraggingStart();
- }
- return inSplitMode();
- }
-
- /** Stops dragging the divider bar. */
- public void stopDragging(int position, float velocity, boolean avoidDismissStart,
- boolean logMetrics) {
- mHandle.setTouching(false, true /* animate */);
- fling(position, velocity, avoidDismissStart, logMetrics);
- mWindowManager.setSlippery(true);
- releaseBackground();
- }
-
- private void stopDragging(int position, SnapTarget target, long duration,
- Interpolator interpolator) {
- stopDragging(position, target, duration, 0 /* startDelay*/, 0 /* endDelay */, interpolator);
- }
-
- private void stopDragging(int position, SnapTarget target, long duration,
- Interpolator interpolator, long endDelay) {
- stopDragging(position, target, duration, 0 /* startDelay*/, endDelay, interpolator);
- }
-
- private void stopDragging(int position, SnapTarget target, long duration, long startDelay,
- long endDelay, Interpolator interpolator) {
- mHandle.setTouching(false, true /* animate */);
- flingTo(position, target, duration, startDelay, endDelay, interpolator);
- mWindowManager.setSlippery(true);
- releaseBackground();
- }
-
- private void stopDragging() {
- mHandle.setTouching(false, true /* animate */);
- mWindowManager.setSlippery(true);
- mWindowManagerProxy.setResizing(false);
- releaseBackground();
- }
-
- private void updateDockSide() {
- mDockSide = mSplitLayout.getPrimarySplitSide();
- mMinimizedShadow.setDockSide(mDockSide);
- }
-
- public DividerSnapAlgorithm getSnapAlgorithm() {
- return mDockedStackMinimized ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
- : mSplitLayout.getSnapAlgorithm();
- }
-
- public int getCurrentPosition() {
- return isHorizontalDivision() ? mDividerPositionY : mDividerPositionX;
- }
-
- public boolean isMinimized() {
- return mDockedStackMinimized;
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- convertToScreenCoordinates(event);
- final int action = event.getAction() & MotionEvent.ACTION_MASK;
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- mVelocityTracker = VelocityTracker.obtain();
- mVelocityTracker.addMovement(event);
- mStartX = (int) event.getX();
- mStartY = (int) event.getY();
- boolean result = startDragging(true /* animate */, true /* touching */);
- if (!result) {
-
- // Weren't able to start dragging successfully, so cancel it again.
- stopDragging();
- }
- mStartPosition = getCurrentPosition();
- mMoving = false;
- return result;
- case MotionEvent.ACTION_MOVE:
- mVelocityTracker.addMovement(event);
- int x = (int) event.getX();
- int y = (int) event.getY();
- boolean exceededTouchSlop =
- isHorizontalDivision() && Math.abs(y - mStartY) > mTouchSlop
- || (!isHorizontalDivision() && Math.abs(x - mStartX) > mTouchSlop);
- if (!mMoving && exceededTouchSlop) {
- mStartX = x;
- mStartY = y;
- mMoving = true;
- }
- if (mMoving && mDockSide != WindowManager.DOCKED_INVALID) {
- SnapTarget snapTarget = getSnapAlgorithm().calculateSnapTarget(
- mStartPosition, 0 /* velocity */, false /* hardDismiss */);
- resizeStackSurfaces(calculatePosition(x, y), mStartPosition, snapTarget,
- null /* transaction */);
- }
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- if (!mMoving) {
- stopDragging();
- break;
- }
-
- x = (int) event.getRawX();
- y = (int) event.getRawY();
- mVelocityTracker.addMovement(event);
- mVelocityTracker.computeCurrentVelocity(1000);
- int position = calculatePosition(x, y);
- stopDragging(position, isHorizontalDivision() ? mVelocityTracker.getYVelocity()
- : mVelocityTracker.getXVelocity(), false /* avoidDismissStart */,
- true /* log */);
- mMoving = false;
- break;
- }
- return true;
- }
-
- private void logResizeEvent(SnapTarget snapTarget) {
- if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissStartTarget()) {
- MetricsLogger.action(
- mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideTopLeft(mDockSide)
- ? LOG_VALUE_UNDOCK_MAX_OTHER
- : LOG_VALUE_UNDOCK_MAX_DOCKED);
- } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissEndTarget()) {
- MetricsLogger.action(
- mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideBottomRight(mDockSide)
- ? LOG_VALUE_UNDOCK_MAX_OTHER
- : LOG_VALUE_UNDOCK_MAX_DOCKED);
- } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getMiddleTarget()) {
- MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
- LOG_VALUE_RESIZE_50_50);
- } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getFirstSplitTarget()) {
- MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
- dockSideTopLeft(mDockSide)
- ? LOG_VALUE_RESIZE_DOCKED_SMALLER
- : LOG_VALUE_RESIZE_DOCKED_LARGER);
- } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getLastSplitTarget()) {
- MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
- dockSideTopLeft(mDockSide)
- ? LOG_VALUE_RESIZE_DOCKED_LARGER
- : LOG_VALUE_RESIZE_DOCKED_SMALLER);
- }
- }
-
- private void convertToScreenCoordinates(MotionEvent event) {
- event.setLocation(event.getRawX(), event.getRawY());
- }
-
- private void fling(int position, float velocity, boolean avoidDismissStart,
- boolean logMetrics) {
- DividerSnapAlgorithm currentSnapAlgorithm = getSnapAlgorithm();
- SnapTarget snapTarget = currentSnapAlgorithm.calculateSnapTarget(position, velocity);
- if (avoidDismissStart && snapTarget == currentSnapAlgorithm.getDismissStartTarget()) {
- snapTarget = currentSnapAlgorithm.getFirstSplitTarget();
- }
- if (logMetrics) {
- logResizeEvent(snapTarget);
- }
- ValueAnimator anim = getFlingAnimator(position, snapTarget, 0 /* endDelay */);
- mFlingAnimationUtils.apply(anim, position, snapTarget.position, velocity);
- anim.start();
- }
-
- private void flingTo(int position, SnapTarget target, long duration, long startDelay,
- long endDelay, Interpolator interpolator) {
- ValueAnimator anim = getFlingAnimator(position, target, endDelay);
- anim.setDuration(duration);
- anim.setStartDelay(startDelay);
- anim.setInterpolator(interpolator);
- anim.start();
- }
-
- private ValueAnimator getFlingAnimator(int position, final SnapTarget snapTarget,
- final long endDelay) {
- if (mCurrentAnimator != null) {
- cancelFlingAnimation();
- updateDockSide();
- }
- if (DEBUG) Slog.d(TAG, "Getting fling " + position + "->" + snapTarget.position);
- final boolean taskPositionSameAtEnd = snapTarget.flag == SnapTarget.FLAG_NONE;
- ValueAnimator anim = ValueAnimator.ofInt(position, snapTarget.position);
- anim.addUpdateListener(animation -> resizeStackSurfaces((int) animation.getAnimatedValue(),
- taskPositionSameAtEnd && animation.getAnimatedFraction() == 1f
- ? TASK_POSITION_SAME
- : snapTarget.taskPosition,
- snapTarget, null /* transaction */));
- Consumer<Boolean> endAction = cancelled -> {
- if (DEBUG) Slog.d(TAG, "End Fling " + cancelled + " min:" + mIsInMinimizeInteraction);
- final boolean wasMinimizeInteraction = mIsInMinimizeInteraction;
- // Reset minimized divider position after unminimized state animation finishes.
- if (!cancelled && !mDockedStackMinimized && mIsInMinimizeInteraction) {
- mIsInMinimizeInteraction = false;
- }
- boolean dismissed = commitSnapFlags(snapTarget);
- mWindowManagerProxy.setResizing(false);
- updateDockSide();
- mCurrentAnimator = null;
- mEntranceAnimationRunning = false;
- mExitAnimationRunning = false;
- if (!dismissed && !wasMinimizeInteraction) {
- mWindowManagerProxy.applyResizeSplits(snapTarget.position, mSplitLayout);
- }
- if (mCallback != null) {
- mCallback.onDraggingEnd();
- }
-
- // Record last snap target the divider moved to
- if (!mIsInMinimizeInteraction) {
- // The last snapTarget position can be negative when the last divider position was
- // offscreen. In that case, save the middle (default) SnapTarget so calculating next
- // position isn't negative.
- final SnapTarget saveTarget;
- if (snapTarget.position < 0) {
- saveTarget = mSplitLayout.getSnapAlgorithm().getMiddleTarget();
- } else {
- saveTarget = snapTarget;
- }
- final DividerSnapAlgorithm snapAlgo = mSplitLayout.getSnapAlgorithm();
- if (saveTarget.position != snapAlgo.getDismissEndTarget().position
- && saveTarget.position != snapAlgo.getDismissStartTarget().position) {
- saveSnapTargetBeforeMinimized(saveTarget);
- }
- }
- notifySplitScreenBoundsChanged();
- };
- anim.addListener(new AnimatorListenerAdapter() {
-
- private boolean mCancelled;
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancelled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- long delay = 0;
- if (endDelay != 0) {
- delay = endDelay;
- } else if (mCancelled) {
- delay = 0;
- }
- if (delay == 0) {
- endAction.accept(mCancelled);
- } else {
- final Boolean cancelled = mCancelled;
- if (DEBUG) Slog.d(TAG, "Posting endFling " + cancelled + " d:" + delay + "ms");
- getHandler().postDelayed(() -> endAction.accept(cancelled), delay);
- }
- }
- });
- mCurrentAnimator = anim;
- mCurrentAnimator.setAnimationHandler(mSfVsyncAnimationHandler);
- return anim;
- }
-
- private void notifySplitScreenBoundsChanged() {
- if (mSplitLayout.mPrimary == null || mSplitLayout.mSecondary == null) {
- return;
- }
- mOtherTaskRect.set(mSplitLayout.mSecondary);
-
- mTmpRect.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), mHandle.getBottom());
- if (isHorizontalDivision()) {
- mTmpRect.offsetTo(mHandle.getLeft(), mDividerPositionY);
- } else {
- mTmpRect.offsetTo(mDividerPositionX, mHandle.getTop());
- }
- mWindowManagerProxy.setTouchRegion(mTmpRect);
-
- mTmpRect.set(mSplitLayout.mDisplayLayout.stableInsets());
- switch (mSplitLayout.getPrimarySplitSide()) {
- case WindowManager.DOCKED_LEFT:
- mTmpRect.left = 0;
- break;
- case WindowManager.DOCKED_RIGHT:
- mTmpRect.right = 0;
- break;
- case WindowManager.DOCKED_TOP:
- mTmpRect.top = 0;
- break;
- }
- mSplitScreenController.notifyBoundsChanged(mOtherTaskRect, mTmpRect);
- }
-
- private void cancelFlingAnimation() {
- if (mCurrentAnimator != null) {
- mCurrentAnimator.cancel();
- }
- }
-
- private boolean commitSnapFlags(SnapTarget target) {
- if (target.flag == SnapTarget.FLAG_NONE) {
- return false;
- }
- final boolean dismissOrMaximize;
- if (target.flag == SnapTarget.FLAG_DISMISS_START) {
- dismissOrMaximize = mDockSide == WindowManager.DOCKED_LEFT
- || mDockSide == WindowManager.DOCKED_TOP;
- } else {
- dismissOrMaximize = mDockSide == WindowManager.DOCKED_RIGHT
- || mDockSide == WindowManager.DOCKED_BOTTOM;
- }
- mWindowManagerProxy.dismissOrMaximizeDocked(mTiles, mSplitLayout, dismissOrMaximize);
- Transaction t = mTiles.getTransaction();
- setResizeDimLayer(t, true /* primary */, 0f);
- setResizeDimLayer(t, false /* primary */, 0f);
- t.apply();
- mTiles.releaseTransaction(t);
- return true;
- }
-
- private void liftBackground() {
- if (mBackgroundLifted) {
- return;
- }
- if (isHorizontalDivision()) {
- mBackground.animate().scaleY(1.4f);
- } else {
- mBackground.animate().scaleX(1.4f);
- }
- mBackground.animate()
- .setInterpolator(Interpolators.TOUCH_RESPONSE)
- .setDuration(TOUCH_ANIMATION_DURATION)
- .translationZ(mTouchElevation)
- .start();
-
- // Lift handle as well so it doesn't get behind the background, even though it doesn't
- // cast shadow.
- mHandle.animate()
- .setInterpolator(Interpolators.TOUCH_RESPONSE)
- .setDuration(TOUCH_ANIMATION_DURATION)
- .translationZ(mTouchElevation)
- .start();
- mBackgroundLifted = true;
- }
-
- private void releaseBackground() {
- if (!mBackgroundLifted) {
- return;
- }
- mBackground.animate()
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
- .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
- .translationZ(0)
- .scaleX(1f)
- .scaleY(1f)
- .start();
- mHandle.animate()
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
- .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
- .translationZ(0)
- .start();
- mBackgroundLifted = false;
- }
-
- private void initializeSurfaceState() {
- int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
- // Recalculate the split-layout's internal tile bounds
- mSplitLayout.resizeSplits(midPos);
- Transaction t = mTiles.getTransaction();
- if (mDockedStackMinimized) {
- int position = mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
- .getMiddleTarget().position;
- calculateBoundsForPosition(position, mDockSide, mDockedRect);
- calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
- mOtherRect);
- mDividerPositionX = mDividerPositionY = position;
- resizeSplitSurfaces(t, mDockedRect, mSplitLayout.mPrimary,
- mOtherRect, mSplitLayout.mSecondary);
- } else {
- resizeSplitSurfaces(t, mSplitLayout.mPrimary, null,
- mSplitLayout.mSecondary, null);
- }
- setResizeDimLayer(t, true /* primary */, 0.f /* alpha */);
- setResizeDimLayer(t, false /* secondary */, 0.f /* alpha */);
- t.apply();
- mTiles.releaseTransaction(t);
-
- // Get the actually-visible bar dimensions (relative to full window). This is a thin
- // bar going through the center.
- final Rect dividerBar = isHorizontalDivision()
- ? new Rect(0, mDividerInsets, mSplitLayout.mDisplayLayout.width(),
- mDividerInsets + mDividerSize)
- : new Rect(mDividerInsets, 0, mDividerInsets + mDividerSize,
- mSplitLayout.mDisplayLayout.height());
- final Region touchRegion = new Region(dividerBar);
- // Add in the "draggable" portion. While not visible, this is an expanded area that the
- // user can interact with.
- touchRegion.union(new Rect(mHandle.getLeft(), mHandle.getTop(),
- mHandle.getRight(), mHandle.getBottom()));
- mWindowManager.setTouchRegion(touchRegion);
- }
-
- void setMinimizedDockStack(boolean minimized, boolean isHomeStackResizable,
- Transaction t) {
- mHomeStackResizable = isHomeStackResizable;
- updateDockSide();
- if (!minimized) {
- resetBackground();
- }
- mMinimizedShadow.setAlpha(minimized ? 1f : 0f);
- if (mDockedStackMinimized != minimized) {
- mDockedStackMinimized = minimized;
- if (mSplitLayout.mDisplayLayout.rotation() != mDefaultDisplay.getRotation()) {
- // Splitscreen to minimize is about to starts after rotating landscape to seascape,
- // update display info and snap algorithm targets
- repositionSnapTargetBeforeMinimized();
- }
- if (mIsInMinimizeInteraction != minimized || mCurrentAnimator != null) {
- cancelFlingAnimation();
- if (minimized) {
- // Relayout to recalculate the divider shadow when minimizing
- requestLayout();
- mIsInMinimizeInteraction = true;
- resizeStackSurfaces(mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
- .getMiddleTarget(), t);
- } else {
- resizeStackSurfaces(mSnapTargetBeforeMinimized, t);
- mIsInMinimizeInteraction = false;
- }
- }
- }
- }
-
- void enterSplitMode(boolean isHomeStackResizable) {
- setHidden(false);
-
- SnapTarget miniMid =
- mSplitLayout.getMinimizedSnapAlgorithm(isHomeStackResizable).getMiddleTarget();
- if (mDockedStackMinimized) {
- mDividerPositionY = mDividerPositionX = miniMid.position;
- }
- }
-
- /**
- * Tries to grab a surface control from ViewRootImpl. If this isn't available for some reason
- * (ie. the window isn't ready yet), it will get the surfacecontrol that the WindowlessWM has
- * assigned to it.
- */
- private SurfaceControl getWindowSurfaceControl() {
- return mWindowManager.mSystemWindows.getViewSurface(this);
- }
-
- void exitSplitMode() {
- // The view is going to be removed right after this function involved, updates the surface
- // in the current thread instead of posting it to the view's UI thread.
- final SurfaceControl sc = getWindowSurfaceControl();
- if (sc == null) {
- return;
- }
- Transaction t = mTiles.getTransaction();
- t.hide(sc);
- mImeController.setDimsHidden(t, true);
- t.apply();
- mTiles.releaseTransaction(t);
-
- // Reset tile bounds
- int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
- mWindowManagerProxy.applyResizeSplits(midPos, mSplitLayout);
- }
-
- void setMinimizedDockStack(boolean minimized, long animDuration,
- boolean isHomeStackResizable) {
- if (DEBUG) Slog.d(TAG, "setMinDock: " + mDockedStackMinimized + "->" + minimized);
- mHomeStackResizable = isHomeStackResizable;
- updateDockSide();
- if (mDockedStackMinimized != minimized) {
- mIsInMinimizeInteraction = true;
- mDockedStackMinimized = minimized;
- stopDragging(minimized
- ? mSnapTargetBeforeMinimized.position
- : getCurrentPosition(),
- minimized
- ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
- .getMiddleTarget()
- : mSnapTargetBeforeMinimized,
- animDuration, Interpolators.FAST_OUT_SLOW_IN, 0);
- setAdjustedForIme(false, animDuration);
- }
- if (!minimized) {
- mBackground.animate().withEndAction(mResetBackgroundRunnable);
- }
- mBackground.animate()
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
- .setDuration(animDuration)
- .start();
- }
-
- // Needed to end any currently playing animations when they might compete with other anims
- // (specifically, IME adjust animation immediately after leaving minimized). Someday maybe
- // these can be unified, but not today.
- void finishAnimations() {
- if (mCurrentAnimator != null) {
- mCurrentAnimator.end();
- }
- }
-
- void setAdjustedForIme(boolean adjustedForIme, long animDuration) {
- if (mAdjustedForIme == adjustedForIme) {
- return;
- }
- updateDockSide();
- mHandle.animate()
- .setInterpolator(IME_ADJUST_INTERPOLATOR)
- .setDuration(animDuration)
- .alpha(adjustedForIme ? 0f : 1f)
- .start();
- if (mDockSide == WindowManager.DOCKED_TOP) {
- mBackground.setPivotY(0);
- mBackground.animate()
- .scaleY(adjustedForIme ? ADJUSTED_FOR_IME_SCALE : 1f);
- }
- if (!adjustedForIme) {
- mBackground.animate().withEndAction(mResetBackgroundRunnable);
- }
- mBackground.animate()
- .setInterpolator(IME_ADJUST_INTERPOLATOR)
- .setDuration(animDuration)
- .start();
- mAdjustedForIme = adjustedForIme;
- }
-
- private void saveSnapTargetBeforeMinimized(SnapTarget target) {
- mSnapTargetBeforeMinimized = target;
- mState.mRatioPositionBeforeMinimized = (float) target.position
- / (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height()
- : mSplitLayout.mDisplayLayout.width());
- }
-
- private void resetBackground() {
- mBackground.setPivotX(mBackground.getWidth() / 2);
- mBackground.setPivotY(mBackground.getHeight() / 2);
- mBackground.setScaleX(1f);
- mBackground.setScaleY(1f);
- mMinimizedShadow.setAlpha(0f);
- }
-
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- }
-
- private void repositionSnapTargetBeforeMinimized() {
- int position = (int) (mState.mRatioPositionBeforeMinimized
- * (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height()
- : mSplitLayout.mDisplayLayout.width()));
-
- // Set the snap target before minimized but do not save until divider is attached and not
- // minimized because it does not know its minimized state yet.
- mSnapTargetBeforeMinimized =
- mSplitLayout.getSnapAlgorithm().calculateNonDismissingSnapTarget(position);
- }
-
- private int calculatePosition(int touchX, int touchY) {
- return isHorizontalDivision() ? calculateYPosition(touchY) : calculateXPosition(touchX);
- }
-
- public boolean isHorizontalDivision() {
- return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
- }
-
- private int calculateXPosition(int touchX) {
- return mStartPosition + touchX - mStartX;
- }
-
- private int calculateYPosition(int touchY) {
- return mStartPosition + touchY - mStartY;
- }
-
- private void alignTopLeft(Rect containingRect, Rect rect) {
- int width = rect.width();
- int height = rect.height();
- rect.set(containingRect.left, containingRect.top,
- containingRect.left + width, containingRect.top + height);
- }
-
- private void alignBottomRight(Rect containingRect, Rect rect) {
- int width = rect.width();
- int height = rect.height();
- rect.set(containingRect.right - width, containingRect.bottom - height,
- containingRect.right, containingRect.bottom);
- }
-
- private void calculateBoundsForPosition(int position, int dockSide, Rect outRect) {
- DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outRect,
- mSplitLayout.mDisplayLayout.width(), mSplitLayout.mDisplayLayout.height(),
- mDividerSize);
- }
-
- private void resizeStackSurfaces(SnapTarget taskSnapTarget, Transaction t) {
- resizeStackSurfaces(taskSnapTarget.position, taskSnapTarget.position, taskSnapTarget, t);
- }
-
- void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect otherRect) {
- resizeSplitSurfaces(t, dockedRect, null, otherRect, null);
- }
-
- private void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect dockedTaskRect,
- Rect otherRect, Rect otherTaskRect) {
- dockedTaskRect = dockedTaskRect == null ? dockedRect : dockedTaskRect;
- otherTaskRect = otherTaskRect == null ? otherRect : otherTaskRect;
-
- mDividerPositionX = mSplitLayout.getPrimarySplitSide() == DOCKED_RIGHT
- ? otherRect.right : dockedRect.right;
- mDividerPositionY = dockedRect.bottom;
-
- if (DEBUG) {
- Slog.d(TAG, "Resizing split surfaces: " + dockedRect + " " + dockedTaskRect
- + " " + otherRect + " " + otherTaskRect);
- }
-
- t.setPosition(mTiles.mPrimarySurface, dockedTaskRect.left, dockedTaskRect.top);
- Rect crop = new Rect(dockedRect);
- crop.offsetTo(-Math.min(dockedTaskRect.left - dockedRect.left, 0),
- -Math.min(dockedTaskRect.top - dockedRect.top, 0));
- t.setWindowCrop(mTiles.mPrimarySurface, crop);
- t.setPosition(mTiles.mSecondarySurface, otherTaskRect.left, otherTaskRect.top);
- crop.set(otherRect);
- crop.offsetTo(-(otherTaskRect.left - otherRect.left),
- -(otherTaskRect.top - otherRect.top));
- t.setWindowCrop(mTiles.mSecondarySurface, crop);
- final SurfaceControl dividerCtrl = getWindowSurfaceControl();
- if (dividerCtrl != null) {
- if (isHorizontalDivision()) {
- t.setPosition(dividerCtrl, 0, mDividerPositionY - mDividerInsets);
- } else {
- t.setPosition(dividerCtrl, mDividerPositionX - mDividerInsets, 0);
- }
- }
- }
-
- void setResizeDimLayer(Transaction t, boolean primary, float alpha) {
- SurfaceControl dim = primary ? mTiles.mPrimaryDim : mTiles.mSecondaryDim;
- if (alpha <= 0.001f) {
- t.hide(dim);
- } else {
- t.setAlpha(dim, alpha);
- t.show(dim);
- }
- }
-
- void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarget,
- Transaction transaction) {
- if (mRemoved) {
- // This divider view has been removed so shouldn't have any additional influence.
- return;
- }
- calculateBoundsForPosition(position, mDockSide, mDockedRect);
- calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
- mOtherRect);
-
- if (mDockedRect.equals(mLastResizeRect) && !mEntranceAnimationRunning) {
- return;
- }
-
- // Make sure shadows are updated
- if (mBackground.getZ() > 0f) {
- mBackground.invalidate();
- }
-
- final boolean ownTransaction = transaction == null;
- final Transaction t = ownTransaction ? mTiles.getTransaction() : transaction;
- mLastResizeRect.set(mDockedRect);
- if (mIsInMinimizeInteraction) {
- calculateBoundsForPosition(mSnapTargetBeforeMinimized.position, mDockSide,
- mDockedTaskRect);
- calculateBoundsForPosition(mSnapTargetBeforeMinimized.position,
- DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect);
-
- // Move a right-docked-app to line up with the divider while dragging it
- if (mDockSide == DOCKED_RIGHT) {
- mDockedTaskRect.offset(Math.max(position, -mDividerSize)
- - mDockedTaskRect.left + mDividerSize, 0);
- }
- resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
- if (ownTransaction) {
- t.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
- t.apply();
- mTiles.releaseTransaction(t);
- }
- return;
- }
-
- if (mEntranceAnimationRunning && taskPosition != TASK_POSITION_SAME) {
- calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect);
-
- // Move a docked app if from the right in position with the divider up to insets
- if (mDockSide == DOCKED_RIGHT) {
- mDockedTaskRect.offset(Math.max(position, -mDividerSize)
- - mDockedTaskRect.left + mDividerSize, 0);
- }
- calculateBoundsForPosition(taskPosition, DockedDividerUtils.invertDockSide(mDockSide),
- mOtherTaskRect);
- resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
- } else if (mExitAnimationRunning && taskPosition != TASK_POSITION_SAME) {
- calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect);
- mDockedInsetRect.set(mDockedTaskRect);
- calculateBoundsForPosition(mExitStartPosition,
- DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect);
- mOtherInsetRect.set(mOtherTaskRect);
- applyExitAnimationParallax(mOtherTaskRect, position);
-
- // Move a right-docked-app to line up with the divider while dragging it
- if (mDockSide == DOCKED_RIGHT) {
- mDockedTaskRect.offset(position + mDividerSize, 0);
- }
- resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
- } else if (taskPosition != TASK_POSITION_SAME) {
- calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
- mOtherRect);
- int dockSideInverted = DockedDividerUtils.invertDockSide(mDockSide);
- int taskPositionDocked =
- restrictDismissingTaskPosition(taskPosition, mDockSide, taskSnapTarget);
- int taskPositionOther =
- restrictDismissingTaskPosition(taskPosition, dockSideInverted, taskSnapTarget);
- calculateBoundsForPosition(taskPositionDocked, mDockSide, mDockedTaskRect);
- calculateBoundsForPosition(taskPositionOther, dockSideInverted, mOtherTaskRect);
- mTmpRect.set(0, 0, mSplitLayout.mDisplayLayout.width(),
- mSplitLayout.mDisplayLayout.height());
- alignTopLeft(mDockedRect, mDockedTaskRect);
- alignTopLeft(mOtherRect, mOtherTaskRect);
- mDockedInsetRect.set(mDockedTaskRect);
- mOtherInsetRect.set(mOtherTaskRect);
- if (dockSideTopLeft(mDockSide)) {
- alignTopLeft(mTmpRect, mDockedInsetRect);
- alignBottomRight(mTmpRect, mOtherInsetRect);
- } else {
- alignBottomRight(mTmpRect, mDockedInsetRect);
- alignTopLeft(mTmpRect, mOtherInsetRect);
- }
- applyDismissingParallax(mDockedTaskRect, mDockSide, taskSnapTarget, position,
- taskPositionDocked);
- applyDismissingParallax(mOtherTaskRect, dockSideInverted, taskSnapTarget, position,
- taskPositionOther);
- resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
- } else {
- resizeSplitSurfaces(t, mDockedRect, null, mOtherRect, null);
- }
- SnapTarget closestDismissTarget = getSnapAlgorithm().getClosestDismissTarget(position);
- float dimFraction = getDimFraction(position, closestDismissTarget);
- setResizeDimLayer(t, isDismissTargetPrimary(closestDismissTarget), dimFraction);
- if (ownTransaction) {
- t.apply();
- mTiles.releaseTransaction(t);
- }
- }
-
- private void applyExitAnimationParallax(Rect taskRect, int position) {
- if (mDockSide == WindowManager.DOCKED_TOP) {
- taskRect.offset(0, (int) ((position - mExitStartPosition) * 0.25f));
- } else if (mDockSide == WindowManager.DOCKED_LEFT) {
- taskRect.offset((int) ((position - mExitStartPosition) * 0.25f), 0);
- } else if (mDockSide == WindowManager.DOCKED_RIGHT) {
- taskRect.offset((int) ((mExitStartPosition - position) * 0.25f), 0);
- }
- }
-
- private float getDimFraction(int position, SnapTarget dismissTarget) {
- if (mEntranceAnimationRunning) {
- return 0f;
- }
- float fraction = getSnapAlgorithm().calculateDismissingFraction(position);
- fraction = Math.max(0, Math.min(fraction, 1f));
- fraction = DIM_INTERPOLATOR.getInterpolation(fraction);
- return fraction;
- }
-
- /**
- * When the snap target is dismissing one side, make sure that the dismissing side doesn't get
- * 0 size.
- */
- private int restrictDismissingTaskPosition(int taskPosition, int dockSide,
- SnapTarget snapTarget) {
- if (snapTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(dockSide)) {
- return Math.max(mSplitLayout.getSnapAlgorithm().getFirstSplitTarget().position,
- mStartPosition);
- } else if (snapTarget.flag == SnapTarget.FLAG_DISMISS_END
- && dockSideBottomRight(dockSide)) {
- return Math.min(mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position,
- mStartPosition);
- } else {
- return taskPosition;
- }
- }
-
- /**
- * Applies a parallax to the task when dismissing.
- */
- private void applyDismissingParallax(Rect taskRect, int dockSide, SnapTarget snapTarget,
- int position, int taskPosition) {
- float fraction = Math.min(1, Math.max(0,
- mSplitLayout.getSnapAlgorithm().calculateDismissingFraction(position)));
- SnapTarget dismissTarget = null;
- SnapTarget splitTarget = null;
- int start = 0;
- if (position <= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position
- && dockSideTopLeft(dockSide)) {
- dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissStartTarget();
- splitTarget = mSplitLayout.getSnapAlgorithm().getFirstSplitTarget();
- start = taskPosition;
- } else if (position >= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position
- && dockSideBottomRight(dockSide)) {
- dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissEndTarget();
- splitTarget = mSplitLayout.getSnapAlgorithm().getLastSplitTarget();
- start = splitTarget.position;
- }
- if (dismissTarget != null && fraction > 0f
- && isDismissing(splitTarget, position, dockSide)) {
- fraction = calculateParallaxDismissingFraction(fraction, dockSide);
- int offsetPosition = (int) (start + fraction
- * (dismissTarget.position - splitTarget.position));
- int width = taskRect.width();
- int height = taskRect.height();
- switch (dockSide) {
- case WindowManager.DOCKED_LEFT:
- taskRect.left = offsetPosition - width;
- taskRect.right = offsetPosition;
- break;
- case WindowManager.DOCKED_RIGHT:
- taskRect.left = offsetPosition + mDividerSize;
- taskRect.right = offsetPosition + width + mDividerSize;
- break;
- case WindowManager.DOCKED_TOP:
- taskRect.top = offsetPosition - height;
- taskRect.bottom = offsetPosition;
- break;
- case WindowManager.DOCKED_BOTTOM:
- taskRect.top = offsetPosition + mDividerSize;
- taskRect.bottom = offsetPosition + height + mDividerSize;
- break;
- }
- }
- }
-
- /**
- * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
- * slowing down parallax effect
- */
- private static float calculateParallaxDismissingFraction(float fraction, int dockSide) {
- float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
-
- // Less parallax at the top, just because.
- if (dockSide == WindowManager.DOCKED_TOP) {
- result /= 2f;
- }
- return result;
- }
-
- private static boolean isDismissing(SnapTarget snapTarget, int position, int dockSide) {
- if (dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT) {
- return position < snapTarget.position;
- } else {
- return position > snapTarget.position;
- }
- }
-
- private boolean isDismissTargetPrimary(SnapTarget dismissTarget) {
- return (dismissTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(mDockSide))
- || (dismissTarget.flag == SnapTarget.FLAG_DISMISS_END
- && dockSideBottomRight(mDockSide));
- }
-
- /**
- * @return true if and only if {@code dockSide} is top or left
- */
- private static boolean dockSideTopLeft(int dockSide) {
- return dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT;
- }
-
- /**
- * @return true if and only if {@code dockSide} is bottom or right
- */
- private static boolean dockSideBottomRight(int dockSide) {
- return dockSide == WindowManager.DOCKED_BOTTOM || dockSide == WindowManager.DOCKED_RIGHT;
- }
-
- @Override
- public void onComputeInternalInsets(InternalInsetsInfo inoutInfo) {
- inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
- inoutInfo.touchableRegion.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(),
- mHandle.getBottom());
- inoutInfo.touchableRegion.op(mBackground.getLeft(), mBackground.getTop(),
- mBackground.getRight(), mBackground.getBottom(), Op.UNION);
- }
-
- void onUndockingTask() {
- int dockSide = mSplitLayout.getPrimarySplitSide();
- if (inSplitMode()) {
- startDragging(false /* animate */, false /* touching */);
- SnapTarget target = dockSideTopLeft(dockSide)
- ? mSplitLayout.getSnapAlgorithm().getDismissEndTarget()
- : mSplitLayout.getSnapAlgorithm().getDismissStartTarget();
-
- // Don't start immediately - give a little bit time to settle the drag resize change.
- mExitAnimationRunning = true;
- mExitStartPosition = getCurrentPosition();
- stopDragging(mExitStartPosition, target, 336 /* duration */, 100 /* startDelay */,
- 0 /* endDelay */, Interpolators.FAST_OUT_SLOW_IN);
- }
- }
-
- private int calculatePositionForInsetBounds() {
- mSplitLayout.mDisplayLayout.getStableBounds(mTmpRect);
- return DockedDividerUtils.calculatePositionForBounds(mTmpRect, mDockSide, mDividerSize);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerWindowManager.java
deleted file mode 100644
index 2c3ae68e4749..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerWindowManager.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.legacysplitscreen;
-
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
-import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
-import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
-import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-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_DOCK_DIVIDER;
-import static android.view.WindowManager.SHELL_ROOT_LAYER_DIVIDER;
-
-import android.graphics.PixelFormat;
-import android.graphics.Region;
-import android.os.Binder;
-import android.view.View;
-import android.view.WindowManager;
-
-import com.android.wm.shell.common.SystemWindows;
-
-/**
- * Manages the window parameters of the docked stack divider.
- */
-final class DividerWindowManager {
-
- private static final String WINDOW_TITLE = "DockedStackDivider";
-
- final SystemWindows mSystemWindows;
- private WindowManager.LayoutParams mLp;
- private View mView;
-
- DividerWindowManager(SystemWindows systemWindows) {
- mSystemWindows = systemWindows;
- }
-
- /** Add a divider view */
- void add(View view, int width, int height, int displayId) {
- mLp = new WindowManager.LayoutParams(
- width, height, TYPE_DOCK_DIVIDER,
- FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL
- | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY,
- PixelFormat.TRANSLUCENT);
- mLp.token = new Binder();
- mLp.setTitle(WINDOW_TITLE);
- mLp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
- mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
- view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
- mSystemWindows.addView(view, mLp, displayId, SHELL_ROOT_LAYER_DIVIDER);
- mView = view;
- }
-
- void remove() {
- if (mView != null) {
- mSystemWindows.removeView(mView);
- }
- mView = null;
- }
-
- void setSlippery(boolean slippery) {
- boolean changed = false;
- if (slippery && (mLp.flags & FLAG_SLIPPERY) == 0) {
- mLp.flags |= FLAG_SLIPPERY;
- changed = true;
- } else if (!slippery && (mLp.flags & FLAG_SLIPPERY) != 0) {
- mLp.flags &= ~FLAG_SLIPPERY;
- changed = true;
- }
- if (changed) {
- mSystemWindows.updateViewLayout(mView, mLp);
- }
- }
-
- void setTouchable(boolean touchable) {
- if (mView == null) {
- return;
- }
- boolean changed = false;
- if (!touchable && (mLp.flags & FLAG_NOT_TOUCHABLE) == 0) {
- mLp.flags |= FLAG_NOT_TOUCHABLE;
- changed = true;
- } else if (touchable && (mLp.flags & FLAG_NOT_TOUCHABLE) != 0) {
- mLp.flags &= ~FLAG_NOT_TOUCHABLE;
- changed = true;
- }
- if (changed) {
- mSystemWindows.updateViewLayout(mView, mLp);
- }
- }
-
- /** Sets the touch region to `touchRegion`. Use null to unset.*/
- void setTouchRegion(Region touchRegion) {
- if (mView == null) {
- return;
- }
- mSystemWindows.setTouchableRegion(mView, touchRegion);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivity.java
deleted file mode 100644
index 4fe28e630114..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivity.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.legacysplitscreen;
-
-import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY;
-import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
-
-import android.annotation.Nullable;
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.os.Bundle;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.View.OnTouchListener;
-import android.widget.TextView;
-
-import com.android.wm.shell.R;
-
-/**
- * Translucent activity that gets started on top of a task in multi-window to inform the user that
- * we forced the activity below to be resizable.
- *
- * Note: This activity runs on the main thread of the process hosting the Shell lib.
- */
-public class ForcedResizableInfoActivity extends Activity implements OnTouchListener {
-
- public static final String EXTRA_FORCED_RESIZEABLE_REASON = "extra_forced_resizeable_reason";
-
- private static final long DISMISS_DELAY = 2500;
-
- private final Runnable mFinishRunnable = new Runnable() {
- @Override
- public void run() {
- finish();
- }
- };
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.forced_resizable_activity);
- TextView tv = findViewById(com.android.internal.R.id.message);
- int reason = getIntent().getIntExtra(EXTRA_FORCED_RESIZEABLE_REASON, -1);
- String text;
- switch (reason) {
- case FORCED_RESIZEABLE_REASON_SPLIT_SCREEN:
- text = getString(R.string.dock_forced_resizable);
- break;
- case FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY:
- text = getString(R.string.forced_resizable_secondary_display);
- break;
- default:
- throw new IllegalArgumentException("Unexpected forced resizeable reason: "
- + reason);
- }
- tv.setText(text);
- getWindow().setTitle(text);
- getWindow().getDecorView().setOnTouchListener(this);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- getWindow().getDecorView().postDelayed(mFinishRunnable, DISMISS_DELAY);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- finish();
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- finish();
- return true;
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- finish();
- return true;
- }
-
- @Override
- public void finish() {
- super.finish();
- overridePendingTransition(0, R.anim.forced_resizable_exit);
- }
-
- @Override
- public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
- // Do nothing
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivityController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivityController.java
deleted file mode 100644
index 139544f951ce..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivityController.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.legacysplitscreen;
-
-
-import static com.android.wm.shell.legacysplitscreen.ForcedResizableInfoActivity.EXTRA_FORCED_RESIZEABLE_REASON;
-
-import android.app.ActivityOptions;
-import android.content.Context;
-import android.content.Intent;
-import android.os.UserHandle;
-import android.util.ArraySet;
-import android.widget.Toast;
-
-import com.android.wm.shell.R;
-import com.android.wm.shell.common.ShellExecutor;
-
-import java.util.function.Consumer;
-
-/**
- * Controller that decides when to show the {@link ForcedResizableInfoActivity}.
- */
-final class ForcedResizableInfoActivityController implements DividerView.DividerCallbacks {
-
- private static final String SELF_PACKAGE_NAME = "com.android.systemui";
-
- private static final int TIMEOUT = 1000;
- private final Context mContext;
- private final ShellExecutor mMainExecutor;
- private final ArraySet<PendingTaskRecord> mPendingTasks = new ArraySet<>();
- private final ArraySet<String> mPackagesShownInSession = new ArraySet<>();
- private boolean mDividerDragging;
-
- private final Runnable mTimeoutRunnable = this::showPending;
-
- private final Consumer<Boolean> mDockedStackExistsListener = exists -> {
- if (!exists) {
- mPackagesShownInSession.clear();
- }
- };
-
- /** Record of force resized task that's pending to be handled. */
- private class PendingTaskRecord {
- int mTaskId;
- /**
- * {@link android.app.ITaskStackListener#FORCED_RESIZEABLE_REASON_SPLIT_SCREEN} or
- * {@link android.app.ITaskStackListener#FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY}
- */
- int mReason;
-
- PendingTaskRecord(int taskId, int reason) {
- this.mTaskId = taskId;
- this.mReason = reason;
- }
- }
-
- ForcedResizableInfoActivityController(Context context,
- LegacySplitScreenController splitScreenController,
- ShellExecutor mainExecutor) {
- mContext = context;
- mMainExecutor = mainExecutor;
- splitScreenController.registerInSplitScreenListener(mDockedStackExistsListener);
- }
-
- @Override
- public void onDraggingStart() {
- mDividerDragging = true;
- mMainExecutor.removeCallbacks(mTimeoutRunnable);
- }
-
- @Override
- public void onDraggingEnd() {
- mDividerDragging = false;
- showPending();
- }
-
- void onAppTransitionFinished() {
- if (!mDividerDragging) {
- showPending();
- }
- }
-
- void activityForcedResizable(String packageName, int taskId, int reason) {
- if (debounce(packageName)) {
- return;
- }
- mPendingTasks.add(new PendingTaskRecord(taskId, reason));
- postTimeout();
- }
-
- void activityDismissingSplitScreen() {
- Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text,
- Toast.LENGTH_SHORT).show();
- }
-
- void activityLaunchOnSecondaryDisplayFailed() {
- Toast.makeText(mContext, R.string.activity_launch_on_secondary_display_failed_text,
- Toast.LENGTH_SHORT).show();
- }
-
- private void showPending() {
- mMainExecutor.removeCallbacks(mTimeoutRunnable);
- for (int i = mPendingTasks.size() - 1; i >= 0; i--) {
- PendingTaskRecord pendingRecord = mPendingTasks.valueAt(i);
- Intent intent = new Intent(mContext, ForcedResizableInfoActivity.class);
- ActivityOptions options = ActivityOptions.makeBasic();
- options.setLaunchTaskId(pendingRecord.mTaskId);
- // Set as task overlay and allow to resume, so that when an app enters split-screen and
- // becomes paused, the overlay will still be shown.
- options.setTaskOverlay(true, true /* canResume */);
- intent.putExtra(EXTRA_FORCED_RESIZEABLE_REASON, pendingRecord.mReason);
- mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT);
- }
- mPendingTasks.clear();
- }
-
- private void postTimeout() {
- mMainExecutor.removeCallbacks(mTimeoutRunnable);
- mMainExecutor.executeDelayed(mTimeoutRunnable, TIMEOUT);
- }
-
- private boolean debounce(String packageName) {
- if (packageName == null) {
- return false;
- }
-
- // We launch ForcedResizableInfoActivity into a task that was forced resizable, so that
- // triggers another notification. So ignore our own activity.
- if (SELF_PACKAGE_NAME.equals(packageName)) {
- return true;
- }
- boolean debounce = mPackagesShownInSession.contains(packageName);
- mPackagesShownInSession.add(packageName);
- return debounce;
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java
deleted file mode 100644
index f201634d3d4a..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.legacysplitscreen;
-
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.util.RotationUtils.rotateBounds;
-import static android.view.WindowManager.DOCKED_BOTTOM;
-import static android.view.WindowManager.DOCKED_INVALID;
-import static android.view.WindowManager.DOCKED_LEFT;
-import static android.view.WindowManager.DOCKED_RIGHT;
-import static android.view.WindowManager.DOCKED_TOP;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.util.TypedValue;
-import android.window.WindowContainerTransaction;
-
-import com.android.internal.policy.DividerSnapAlgorithm;
-import com.android.internal.policy.DockedDividerUtils;
-import com.android.wm.shell.common.DisplayLayout;
-
-/**
- * Handles split-screen related internal display layout. In general, this represents the
- * WM-facing understanding of the splits.
- */
-public class LegacySplitDisplayLayout {
- /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to
- * restrict IME adjustment so that a min portion of top stack remains visible.*/
- private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f;
-
- private static final int DIVIDER_WIDTH_INACTIVE_DP = 4;
-
- LegacySplitScreenTaskListener mTiles;
- DisplayLayout mDisplayLayout;
- Context mContext;
-
- // Lazy stuff
- boolean mResourcesValid = false;
- int mDividerSize;
- int mDividerSizeInactive;
- private DividerSnapAlgorithm mSnapAlgorithm = null;
- private DividerSnapAlgorithm mMinimizedSnapAlgorithm = null;
- Rect mPrimary = null;
- Rect mSecondary = null;
- Rect mAdjustedPrimary = null;
- Rect mAdjustedSecondary = null;
- final Rect mTmpBounds = new Rect();
-
- public LegacySplitDisplayLayout(Context ctx, DisplayLayout dl,
- LegacySplitScreenTaskListener taskTiles) {
- mTiles = taskTiles;
- mDisplayLayout = dl;
- mContext = ctx;
- }
-
- void rotateTo(int newRotation) {
- mDisplayLayout.rotateTo(mContext.getResources(), newRotation);
- final Configuration config = new Configuration();
- config.unset();
- config.orientation = mDisplayLayout.getOrientation();
- Rect tmpRect = new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
- tmpRect.inset(mDisplayLayout.nonDecorInsets());
- config.windowConfiguration.setAppBounds(tmpRect);
- tmpRect.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
- tmpRect.inset(mDisplayLayout.stableInsets());
- config.screenWidthDp = (int) (tmpRect.width() / mDisplayLayout.density());
- config.screenHeightDp = (int) (tmpRect.height() / mDisplayLayout.density());
- mContext = mContext.createConfigurationContext(config);
- mSnapAlgorithm = null;
- mMinimizedSnapAlgorithm = null;
- mResourcesValid = false;
- }
-
- private void updateResources() {
- if (mResourcesValid) {
- return;
- }
- mResourcesValid = true;
- Resources res = mContext.getResources();
- mDividerSize = DockedDividerUtils.getDividerSize(res,
- DockedDividerUtils.getDividerInsets(res));
- mDividerSizeInactive = (int) TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP, DIVIDER_WIDTH_INACTIVE_DP, res.getDisplayMetrics());
- }
-
- int getPrimarySplitSide() {
- switch (mDisplayLayout.getNavigationBarPosition(mContext.getResources())) {
- case DisplayLayout.NAV_BAR_BOTTOM:
- return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP;
- case DisplayLayout.NAV_BAR_LEFT:
- return DOCKED_RIGHT;
- case DisplayLayout.NAV_BAR_RIGHT:
- return DOCKED_LEFT;
- default:
- return DOCKED_INVALID;
- }
- }
-
- DividerSnapAlgorithm getSnapAlgorithm() {
- if (mSnapAlgorithm == null) {
- updateResources();
- boolean isHorizontalDivision = !mDisplayLayout.isLandscape();
- mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(),
- mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize,
- isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide());
- }
- return mSnapAlgorithm;
- }
-
- DividerSnapAlgorithm getMinimizedSnapAlgorithm(boolean homeStackResizable) {
- if (mMinimizedSnapAlgorithm == null) {
- updateResources();
- boolean isHorizontalDivision = !mDisplayLayout.isLandscape();
- mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(),
- mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize,
- isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(),
- true /* isMinimized */, homeStackResizable);
- }
- return mMinimizedSnapAlgorithm;
- }
-
- /**
- * Resize primary bounds and secondary bounds by divider position.
- *
- * @param position divider position.
- * @return true if calculated bounds changed.
- */
- boolean resizeSplits(int position) {
- mPrimary = mPrimary == null ? new Rect() : mPrimary;
- mSecondary = mSecondary == null ? new Rect() : mSecondary;
- int dockSide = getPrimarySplitSide();
- boolean boundsChanged;
-
- mTmpBounds.set(mPrimary);
- DockedDividerUtils.calculateBoundsForPosition(position, dockSide, mPrimary,
- mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
- boundsChanged = !mPrimary.equals(mTmpBounds);
-
- mTmpBounds.set(mSecondary);
- DockedDividerUtils.calculateBoundsForPosition(position,
- DockedDividerUtils.invertDockSide(dockSide), mSecondary, mDisplayLayout.width(),
- mDisplayLayout.height(), mDividerSize);
- boundsChanged |= !mSecondary.equals(mTmpBounds);
- return boundsChanged;
- }
-
- void resizeSplits(int position, WindowContainerTransaction t) {
- if (resizeSplits(position)) {
- t.setBounds(mTiles.mPrimary.token, mPrimary);
- t.setBounds(mTiles.mSecondary.token, mSecondary);
-
- t.setSmallestScreenWidthDp(mTiles.mPrimary.token,
- getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary));
- t.setSmallestScreenWidthDp(mTiles.mSecondary.token,
- getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary));
- }
- }
-
- Rect calcResizableMinimizedHomeStackBounds() {
- DividerSnapAlgorithm.SnapTarget miniMid =
- getMinimizedSnapAlgorithm(true /* resizable */).getMiddleTarget();
- Rect homeBounds = new Rect();
- DockedDividerUtils.calculateBoundsForPosition(miniMid.position,
- DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds,
- mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
- return homeBounds;
- }
-
- /**
- * Updates the adjustment depending on it's current state.
- */
- void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) {
- adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize,
- mDividerSizeInactive, mPrimary, mSecondary);
- }
-
- /** Assumes top/bottom split. Splits are not adjusted for left/right splits. */
- private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop,
- int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) {
- if (mAdjustedPrimary == null) {
- mAdjustedPrimary = new Rect();
- mAdjustedSecondary = new Rect();
- }
-
- final Rect displayStableRect = new Rect();
- dl.getStableBounds(displayStableRect);
-
- final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop);
- final int currDividerWidth =
- (int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction));
-
- // Calculate the highest we can move the bottom of the top stack to keep 30% visible.
- final int minTopStackBottom = displayStableRect.top
- + (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN);
- // Based on that, calculate the maximum amount we'll allow the ime to shift things.
- final int maxOffset = mPrimary.bottom - minTopStackBottom;
- // Calculate how much we would shift things without limits (basically the height of ime).
- final int desiredOffset = hiddenTop - shownTop;
- // Calculate an "adjustedTop" which is the currImeTop but restricted by our constraints.
- // We want an effect where the adjustment only occurs during the "highest" portion of the
- // ime animation. This is done by shifting the adjustment values by the difference in
- // offsets (effectively playing the whole adjustment animation some fixed amount of pixels
- // below the ime top).
- final int topCorrection = Math.max(0, desiredOffset - maxOffset);
- final int adjustedTop = currImeTop + topCorrection;
- // The actual yOffset is the distance between adjustedTop and the bottom of the display.
- // Since our adjustedTop values are playing "below" the ime, we clamp at 0 so we only
- // see adjustment upward.
- final int yOffset = Math.max(0, dl.height() - adjustedTop);
-
- // TOP
- // Reduce the offset by an additional small amount to squish the divider bar.
- mAdjustedPrimary.set(primaryBounds);
- mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth));
-
- // BOTTOM
- mAdjustedSecondary.set(secondaryBounds);
- mAdjustedSecondary.offset(0, -yOffset);
- }
-
- static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl,
- Rect bounds) {
- int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(),
- DockedDividerUtils.getDividerInsets(context.getResources()));
-
- int minWidth = Integer.MAX_VALUE;
-
- // Go through all screen orientations and find the orientation in which the task has the
- // smallest width.
- Rect tmpRect = new Rect();
- Rect rotatedDisplayRect = new Rect();
- Rect displayRect = new Rect(0, 0, dl.width(), dl.height());
-
- DisplayLayout tmpDL = new DisplayLayout();
- for (int rotation = 0; rotation < 4; rotation++) {
- tmpDL.set(dl);
- tmpDL.rotateTo(context.getResources(), rotation);
- DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize);
-
- tmpRect.set(bounds);
- rotateBounds(tmpRect, displayRect, dl.rotation(), rotation);
- rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height());
- final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect,
- tmpDL.getOrientation());
- final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide,
- dividerSize);
-
- final int snappedPosition =
- snap.calculateNonDismissingSnapTarget(position).position;
- DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect,
- tmpDL.width(), tmpDL.height(), dividerSize);
- Rect insettedDisplay = new Rect(rotatedDisplayRect);
- insettedDisplay.inset(tmpDL.stableInsets());
- tmpRect.intersect(insettedDisplay);
- minWidth = Math.min(tmpRect.width(), minWidth);
- }
- return (int) (minWidth / dl.density());
- }
-
- static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl,
- int dividerSize) {
- final Configuration config = new Configuration();
- config.unset();
- config.orientation = dl.getOrientation();
- Rect tmpRect = new Rect(0, 0, dl.width(), dl.height());
- tmpRect.inset(dl.nonDecorInsets());
- config.windowConfiguration.setAppBounds(tmpRect);
- tmpRect.set(0, 0, dl.width(), dl.height());
- tmpRect.inset(dl.stableInsets());
- config.screenWidthDp = (int) (tmpRect.width() / dl.density());
- config.screenHeightDp = (int) (tmpRect.height() / dl.density());
- final Context rotationContext = context.createConfigurationContext(config);
- return new DividerSnapAlgorithm(
- rotationContext.getResources(), dl.width(), dl.height(), dividerSize,
- config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets());
- }
-
- /**
- * Get the current primary-split side. Determined by its location of {@param bounds} within
- * {@param displayRect} but if both are the same, it will try to dock to each side and determine
- * if allowed in its respected {@param orientation}.
- *
- * @param bounds bounds of the primary split task to get which side is docked
- * @param displayRect bounds of the display that contains the primary split task
- * @param orientation the origination of device
- * @return current primary-split side
- */
- static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) {
- if (orientation == ORIENTATION_PORTRAIT) {
- // Portrait mode, docked either at the top or the bottom.
- final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top);
- if (diff < 0) {
- return DOCKED_BOTTOM;
- } else {
- // Top is default
- return DOCKED_TOP;
- }
- } else if (orientation == ORIENTATION_LANDSCAPE) {
- // Landscape mode, docked either on the left or on the right.
- final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left);
- if (diff < 0) {
- return DOCKED_RIGHT;
- }
- return DOCKED_LEFT;
- }
- return DOCKED_INVALID;
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreen.java
deleted file mode 100644
index 499a9c5fa631..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreen.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.legacysplitscreen;
-
-import android.graphics.Rect;
-import android.window.WindowContainerToken;
-
-import com.android.wm.shell.common.annotations.ExternalThread;
-
-import java.io.PrintWriter;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-
-/**
- * Interface to engage split screen feature.
- */
-@ExternalThread
-public interface LegacySplitScreen {
- /** Called when keyguard showing state changed. */
- void onKeyguardVisibilityChanged(boolean isShowing);
-
- /** Returns {@link DividerView}. */
- DividerView getDividerView();
-
- /** Returns {@code true} if one of the split screen is in minimized mode. */
- boolean isMinimized();
-
- /** Returns {@code true} if the home stack is resizable. */
- boolean isHomeStackResizable();
-
- /** Returns {@code true} if the divider is visible. */
- boolean isDividerVisible();
-
- /** Switch to minimized state if appropriate. */
- void setMinimized(boolean minimized);
-
- /** Called when there's a task undocking. */
- void onUndockingTask();
-
- /** Called when app transition finished. */
- void onAppTransitionFinished();
-
- /** Dumps current status of Split Screen. */
- void dump(PrintWriter pw);
-
- /** Registers listener that gets called whenever the existence of the divider changes. */
- void registerInSplitScreenListener(Consumer<Boolean> listener);
-
- /** Unregisters listener that gets called whenever the existence of the divider changes. */
- void unregisterInSplitScreenListener(Consumer<Boolean> listener);
-
- /** Registers listener that gets called whenever the split screen bounds changes. */
- void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener);
-
- /** @return the container token for the secondary split root task. */
- WindowContainerToken getSecondaryRoot();
-
- /**
- * Splits the primary task if feasible, this is to preserve legacy way to toggle split screen.
- * Like triggering split screen through long pressing recents app button or through
- * {@link android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN}.
- *
- * @return {@code true} if it successes to split the primary task.
- */
- boolean splitPrimaryTask();
-
- /**
- * Exits the split to make the primary task fullscreen.
- */
- void dismissSplitToPrimaryTask();
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java
deleted file mode 100644
index 67e487de0993..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java
+++ /dev/null
@@ -1,762 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.legacysplitscreen;
-
-import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import android.animation.AnimationHandler;
-import android.app.ActivityManager;
-import android.app.ActivityManager.RunningTaskInfo;
-import android.app.ActivityTaskManager;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.os.RemoteException;
-import android.provider.Settings;
-import android.util.Slog;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Toast;
-import android.window.TaskOrganizer;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import com.android.internal.policy.DividerSnapAlgorithm;
-import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayChangeController;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayImeController;
-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.SystemWindows;
-import com.android.wm.shell.common.TaskStackListenerCallback;
-import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.transition.Transitions;
-
-import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-
-/**
- * Controls split screen feature.
- */
-public class LegacySplitScreenController implements DisplayController.OnDisplaysChangedListener {
- static final boolean DEBUG = false;
-
- private static final String TAG = "SplitScreenCtrl";
- private static final int DEFAULT_APP_TRANSITION_DURATION = 336;
-
- private final Context mContext;
- private final DisplayChangeController.OnDisplayChangingListener mRotationController;
- private final DisplayController mDisplayController;
- private final DisplayImeController mImeController;
- private final DividerImeController mImePositionProcessor;
- private final DividerState mDividerState = new DividerState();
- private final ForcedResizableInfoActivityController mForcedResizableController;
- private final ShellExecutor mMainExecutor;
- private final AnimationHandler mSfVsyncAnimationHandler;
- private final LegacySplitScreenTaskListener mSplits;
- private final SystemWindows mSystemWindows;
- final TransactionPool mTransactionPool;
- private final WindowManagerProxy mWindowManagerProxy;
- private final TaskOrganizer mTaskOrganizer;
- private final SplitScreenImpl mImpl = new SplitScreenImpl();
-
- private final CopyOnWriteArrayList<WeakReference<Consumer<Boolean>>> mDockedStackExistsListeners
- = new CopyOnWriteArrayList<>();
- private final ArrayList<WeakReference<BiConsumer<Rect, Rect>>> mBoundsChangedListeners =
- new ArrayList<>();
-
-
- private DividerWindowManager mWindowManager;
- private DividerView mView;
-
- // Keeps track of real-time split geometry including snap positions and ime adjustments
- private LegacySplitDisplayLayout mSplitLayout;
-
- // Transient: this contains the layout calculated for a new rotation requested by WM. This is
- // kept around so that we can wait for a matching configuration change and then use the exact
- // layout that we sent back to WM.
- private LegacySplitDisplayLayout mRotateSplitLayout;
-
- private boolean mIsKeyguardShowing;
- private boolean mVisible = false;
- private volatile boolean mMinimized = false;
- private volatile boolean mAdjustedForIme = false;
- private boolean mHomeStackResizable = false;
-
- public LegacySplitScreenController(Context context,
- DisplayController displayController, SystemWindows systemWindows,
- DisplayImeController imeController, TransactionPool transactionPool,
- ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue,
- TaskStackListenerImpl taskStackListener, Transitions transitions,
- ShellExecutor mainExecutor, AnimationHandler sfVsyncAnimationHandler) {
- mContext = context;
- mDisplayController = displayController;
- mSystemWindows = systemWindows;
- mImeController = imeController;
- mMainExecutor = mainExecutor;
- mSfVsyncAnimationHandler = sfVsyncAnimationHandler;
- mForcedResizableController = new ForcedResizableInfoActivityController(context, this,
- mainExecutor);
- mTransactionPool = transactionPool;
- mWindowManagerProxy = new WindowManagerProxy(syncQueue, shellTaskOrganizer);
- mTaskOrganizer = shellTaskOrganizer;
- mSplits = new LegacySplitScreenTaskListener(this, shellTaskOrganizer, transitions,
- syncQueue);
- mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mMainExecutor,
- shellTaskOrganizer);
- mRotationController =
- (display, fromRotation, toRotation, wct) -> {
- if (!mSplits.isSplitScreenSupported() || mWindowManagerProxy == null) {
- return;
- }
- WindowContainerTransaction t = new WindowContainerTransaction();
- DisplayLayout displayLayout =
- new DisplayLayout(mDisplayController.getDisplayLayout(display));
- LegacySplitDisplayLayout sdl =
- new LegacySplitDisplayLayout(mContext, displayLayout, mSplits);
- sdl.rotateTo(toRotation);
- mRotateSplitLayout = sdl;
- // snap resets to middle target when not minimized and rotation changed.
- final int position = mMinimized ? mView.mSnapTargetBeforeMinimized.position
- : sdl.getSnapAlgorithm().getMiddleTarget().position;
- DividerSnapAlgorithm snap = sdl.getSnapAlgorithm();
- final DividerSnapAlgorithm.SnapTarget target =
- snap.calculateNonDismissingSnapTarget(position);
- sdl.resizeSplits(target.position, t);
-
- if (isSplitActive() && mHomeStackResizable) {
- mWindowManagerProxy
- .applyHomeTasksMinimized(sdl, mSplits.mSecondary.token, t);
- }
- if (mWindowManagerProxy.queueSyncTransactionIfWaiting(t)) {
- // Because sync transactions are serialized, its possible for an "older"
- // bounds-change to get applied after a screen rotation. In that case, we
- // want to actually defer on that rather than apply immediately. Of course,
- // this means that the bounds may not change until after the rotation so
- // the user might see some artifacts. This should be rare.
- Slog.w(TAG, "Screen rotated while other operations were pending, this may"
- + " result in some graphical artifacts.");
- } else {
- wct.merge(t, true /* transfer */);
- }
- };
-
- mWindowManager = new DividerWindowManager(mSystemWindows);
-
- // No need to listen to display window container or create root tasks if the device is not
- // using legacy split screen.
- if (!context.getResources().getBoolean(com.android.internal.R.bool.config_useLegacySplit)) {
- return;
- }
-
-
- mDisplayController.addDisplayWindowListener(this);
- // Don't initialize the divider or anything until we get the default display.
-
- taskStackListener.addListener(
- new TaskStackListenerCallback() {
- @Override
- public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
- boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
- if (!wasVisible || task.getWindowingMode()
- != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
- || !mSplits.isSplitScreenSupported()) {
- return;
- }
-
- if (isMinimized()) {
- onUndockingTask();
- }
- }
-
- @Override
- public void onActivityForcedResizable(String packageName, int taskId,
- int reason) {
- mForcedResizableController.activityForcedResizable(packageName, taskId,
- reason);
- }
-
- @Override
- public void onActivityDismissingDockedStack() {
- mForcedResizableController.activityDismissingSplitScreen();
- }
-
- @Override
- public void onActivityLaunchOnSecondaryDisplayFailed() {
- mForcedResizableController.activityLaunchOnSecondaryDisplayFailed();
- }
- });
- }
-
- public LegacySplitScreen asLegacySplitScreen() {
- return mImpl;
- }
-
- public void onSplitScreenSupported() {
- // Set starting tile bounds based on middle target
- final WindowContainerTransaction tct = new WindowContainerTransaction();
- int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
- mSplitLayout.resizeSplits(midPos, tct);
- mTaskOrganizer.applyTransaction(tct);
- }
-
- public void onKeyguardVisibilityChanged(boolean showing) {
- if (!isSplitActive() || mView == null) {
- return;
- }
- mView.setHidden(showing);
- mIsKeyguardShowing = showing;
- }
-
- @Override
- public void onDisplayAdded(int displayId) {
- if (displayId != DEFAULT_DISPLAY) {
- return;
- }
- mSplitLayout = new LegacySplitDisplayLayout(mDisplayController.getDisplayContext(displayId),
- mDisplayController.getDisplayLayout(displayId), mSplits);
- mImeController.addPositionProcessor(mImePositionProcessor);
- mDisplayController.addDisplayChangingController(mRotationController);
- if (!ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)) {
- removeDivider();
- return;
- }
- try {
- mSplits.init();
- } catch (Exception e) {
- Slog.e(TAG, "Failed to register docked stack listener", e);
- removeDivider();
- return;
- }
- }
-
- @Override
- public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
- if (displayId != DEFAULT_DISPLAY || !mSplits.isSplitScreenSupported()) {
- return;
- }
- mSplitLayout = new LegacySplitDisplayLayout(mDisplayController.getDisplayContext(displayId),
- mDisplayController.getDisplayLayout(displayId), mSplits);
- if (mRotateSplitLayout == null) {
- int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
- final WindowContainerTransaction tct = new WindowContainerTransaction();
- mSplitLayout.resizeSplits(midPos, tct);
- mTaskOrganizer.applyTransaction(tct);
- } else if (mSplitLayout.mDisplayLayout.rotation()
- == mRotateSplitLayout.mDisplayLayout.rotation()) {
- mSplitLayout.mPrimary = new Rect(mRotateSplitLayout.mPrimary);
- mSplitLayout.mSecondary = new Rect(mRotateSplitLayout.mSecondary);
- mRotateSplitLayout = null;
- }
- if (isSplitActive()) {
- update(newConfig);
- }
- }
-
- public boolean isMinimized() {
- return mMinimized;
- }
-
- public boolean isHomeStackResizable() {
- return mHomeStackResizable;
- }
-
- public DividerView getDividerView() {
- return mView;
- }
-
- public boolean isDividerVisible() {
- return mView != null && mView.getVisibility() == View.VISIBLE;
- }
-
- /**
- * This indicates that at-least one of the splits has content. This differs from
- * isDividerVisible because the divider is only visible once *everything* is in split mode
- * while this only cares if some things are (eg. while entering/exiting as well).
- */
- public boolean isSplitActive() {
- return mSplits.mPrimary != null && mSplits.mSecondary != null
- && (mSplits.mPrimary.topActivityType != ACTIVITY_TYPE_UNDEFINED
- || mSplits.mSecondary.topActivityType != ACTIVITY_TYPE_UNDEFINED);
- }
-
- public void addDivider(Configuration configuration) {
- Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId());
- mView = (DividerView)
- LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null);
- mView.setAnimationHandler(mSfVsyncAnimationHandler);
- DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId());
- mView.injectDependencies(this, mWindowManager, mDividerState, mForcedResizableController,
- mSplits, mSplitLayout, mImePositionProcessor, mWindowManagerProxy);
- mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE);
- mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, null /* transaction */);
- final int size = dctx.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.docked_stack_divider_thickness);
- final boolean landscape = configuration.orientation == ORIENTATION_LANDSCAPE;
- final int width = landscape ? size : displayLayout.width();
- final int height = landscape ? displayLayout.height() : size;
- mWindowManager.add(mView, width, height, mContext.getDisplayId());
- }
-
- public void removeDivider() {
- if (mView != null) {
- mView.onDividerRemoved();
- }
- mWindowManager.remove();
- }
-
- public void update(Configuration configuration) {
- final boolean isDividerHidden = mView != null && mIsKeyguardShowing;
-
- removeDivider();
- addDivider(configuration);
-
- if (mMinimized) {
- mView.setMinimizedDockStack(true, mHomeStackResizable, null /* transaction */);
- updateTouchable();
- }
- mView.setHidden(isDividerHidden);
- }
-
- public void onTaskVanished() {
- removeDivider();
- }
-
- public void updateVisibility(final boolean visible) {
- if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible);
- if (mVisible != visible) {
- mVisible = visible;
- mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
-
- if (visible) {
- mView.enterSplitMode(mHomeStackResizable);
- // Update state because animations won't finish.
- mWindowManagerProxy.runInSync(
- t -> mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, t));
-
- } else {
- mView.exitSplitMode();
- mWindowManagerProxy.runInSync(
- t -> mView.setMinimizedDockStack(false, mHomeStackResizable, t));
- }
- // Notify existence listeners
- synchronized (mDockedStackExistsListeners) {
- mDockedStackExistsListeners.removeIf(wf -> {
- Consumer<Boolean> l = wf.get();
- if (l != null) l.accept(visible);
- return l == null;
- });
- }
- }
- }
-
- public void setMinimized(final boolean minimized) {
- if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible);
- mMainExecutor.execute(() -> {
- if (DEBUG) Slog.d(TAG, "run posted ext setMinimized " + minimized + " vis:" + mVisible);
- if (!mVisible) {
- return;
- }
- setHomeMinimized(minimized);
- });
- }
-
- public void setHomeMinimized(final boolean minimized) {
- if (DEBUG) {
- Slog.d(TAG, "setHomeMinimized min:" + mMinimized + "->" + minimized + " hrsz:"
- + mHomeStackResizable + " split:" + isDividerVisible());
- }
- WindowContainerTransaction wct = new WindowContainerTransaction();
- final boolean minimizedChanged = mMinimized != minimized;
- // Update minimized state
- if (minimizedChanged) {
- mMinimized = minimized;
- }
- // Always set this because we could be entering split when mMinimized is already true
- wct.setFocusable(mSplits.mPrimary.token, !mMinimized);
-
- // Sync state to DividerView if it exists.
- if (mView != null) {
- final int displayId = mView.getDisplay() != null
- ? mView.getDisplay().getDisplayId() : DEFAULT_DISPLAY;
- // pause ime here (before updateMinimizedDockedStack)
- if (mMinimized) {
- mImePositionProcessor.pause(displayId);
- }
- if (minimizedChanged) {
- // This conflicts with IME adjustment, so only call it when things change.
- mView.setMinimizedDockStack(minimized, getAnimDuration(), mHomeStackResizable);
- }
- if (!mMinimized) {
- // afterwards so it can end any animations started in view
- mImePositionProcessor.resume(displayId);
- }
- }
- updateTouchable();
-
- // If we are only setting focusability, a sync transaction isn't necessary (in fact it
- // can interrupt other animations), so see if it can be submitted on pending instead.
- if (!mWindowManagerProxy.queueSyncTransactionIfWaiting(wct)) {
- mTaskOrganizer.applyTransaction(wct);
- }
- }
-
- public void setAdjustedForIme(boolean adjustedForIme) {
- if (mAdjustedForIme == adjustedForIme) {
- return;
- }
- mAdjustedForIme = adjustedForIme;
- updateTouchable();
- }
-
- public void updateTouchable() {
- mWindowManager.setTouchable(!mAdjustedForIme);
- }
-
- public void onUndockingTask() {
- if (mView != null) {
- mView.onUndockingTask();
- }
- }
-
- public void onAppTransitionFinished() {
- if (mView == null) {
- return;
- }
- mForcedResizableController.onAppTransitionFinished();
- }
-
- public void dump(PrintWriter pw) {
- pw.print(" mVisible="); pw.println(mVisible);
- pw.print(" mMinimized="); pw.println(mMinimized);
- pw.print(" mAdjustedForIme="); pw.println(mAdjustedForIme);
- }
-
- public long getAnimDuration() {
- float transitionScale = Settings.Global.getFloat(mContext.getContentResolver(),
- Settings.Global.TRANSITION_ANIMATION_SCALE,
- mContext.getResources().getFloat(
- com.android.internal.R.dimen
- .config_appTransitionAnimationDurationScaleDefault));
- final long transitionDuration = DEFAULT_APP_TRANSITION_DURATION;
- return (long) (transitionDuration * transitionScale);
- }
-
- public void registerInSplitScreenListener(Consumer<Boolean> listener) {
- listener.accept(isDividerVisible());
- synchronized (mDockedStackExistsListeners) {
- mDockedStackExistsListeners.add(new WeakReference<>(listener));
- }
- }
-
- public void unregisterInSplitScreenListener(Consumer<Boolean> listener) {
- synchronized (mDockedStackExistsListeners) {
- for (int i = mDockedStackExistsListeners.size() - 1; i >= 0; i--) {
- if (mDockedStackExistsListeners.get(i) == listener) {
- mDockedStackExistsListeners.remove(i);
- }
- }
- }
- }
-
- public void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) {
- synchronized (mBoundsChangedListeners) {
- mBoundsChangedListeners.add(new WeakReference<>(listener));
- }
- }
-
- public boolean splitPrimaryTask() {
- try {
- if (ActivityTaskManager.getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED) {
- return false;
- }
- } catch (RemoteException e) {
- return false;
- }
- if (isSplitActive() || mSplits.mPrimary == null) {
- return false;
- }
-
- // Try fetching the top running task.
- final List<RunningTaskInfo> runningTasks =
- ActivityTaskManager.getInstance().getTasks(1 /* maxNum */);
- if (runningTasks == null || runningTasks.isEmpty()) {
- return false;
- }
- // Note: The set of running tasks from the system is ordered by recency.
- final RunningTaskInfo topRunningTask = runningTasks.get(0);
- final int activityType = topRunningTask.getActivityType();
- if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) {
- return false;
- }
-
- if (!topRunningTask.supportsSplitScreenMultiWindow) {
- Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text,
- Toast.LENGTH_SHORT).show();
- return false;
- }
-
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- // Clear out current windowing mode before reparenting to split task.
- wct.setWindowingMode(topRunningTask.token, WINDOWING_MODE_UNDEFINED);
- wct.reparent(topRunningTask.token, mSplits.mPrimary.token, true /* onTop */);
- mWindowManagerProxy.applySyncTransaction(wct);
- return true;
- }
-
- public void dismissSplitToPrimaryTask() {
- startDismissSplit(true /* toPrimaryTask */);
- }
-
- /** Notifies the bounds of split screen changed. */
- public void notifyBoundsChanged(Rect secondaryWindowBounds, Rect secondaryWindowInsets) {
- synchronized (mBoundsChangedListeners) {
- mBoundsChangedListeners.removeIf(wf -> {
- BiConsumer<Rect, Rect> l = wf.get();
- if (l != null) l.accept(secondaryWindowBounds, secondaryWindowInsets);
- return l == null;
- });
- }
- }
-
- public void startEnterSplit() {
- update(mDisplayController.getDisplayContext(
- mContext.getDisplayId()).getResources().getConfiguration());
- // Set resizable directly here because applyEnterSplit already resizes home stack.
- mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits,
- mRotateSplitLayout != null ? mRotateSplitLayout : mSplitLayout);
- }
-
- public void prepareEnterSplitTransition(WindowContainerTransaction outWct) {
- // Set resizable directly here because buildEnterSplit already resizes home stack.
- mHomeStackResizable = mWindowManagerProxy.buildEnterSplit(outWct, mSplits,
- mRotateSplitLayout != null ? mRotateSplitLayout : mSplitLayout);
- }
-
- public void finishEnterSplitTransition(boolean minimized) {
- update(mDisplayController.getDisplayContext(
- mContext.getDisplayId()).getResources().getConfiguration());
- if (minimized) {
- ensureMinimizedSplit();
- } else {
- ensureNormalSplit();
- }
- }
-
- public void startDismissSplit(boolean toPrimaryTask) {
- startDismissSplit(toPrimaryTask, false /* snapped */);
- }
-
- public void startDismissSplit(boolean toPrimaryTask, boolean snapped) {
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- mSplits.getSplitTransitions().dismissSplit(
- mSplits, mSplitLayout, !toPrimaryTask, snapped);
- } else {
- mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, !toPrimaryTask);
- onDismissSplit();
- }
- }
-
- public void onDismissSplit() {
- updateVisibility(false /* visible */);
- mMinimized = false;
- // Resets divider bar position to undefined, so new divider bar will apply default position
- // next time entering split mode.
- mDividerState.mRatioPositionBeforeMinimized = 0;
- removeDivider();
- mImePositionProcessor.reset();
- }
-
- public void ensureMinimizedSplit() {
- setHomeMinimized(true /* minimized */);
- if (mView != null && !isDividerVisible()) {
- // Wasn't in split-mode yet, so enter now.
- if (DEBUG) {
- Slog.d(TAG, " entering split mode with minimized=true");
- }
- updateVisibility(true /* visible */);
- }
- }
-
- public void ensureNormalSplit() {
- setHomeMinimized(false /* minimized */);
- if (mView != null && !isDividerVisible()) {
- // Wasn't in split-mode, so enter now.
- if (DEBUG) {
- Slog.d(TAG, " enter split mode unminimized ");
- }
- updateVisibility(true /* visible */);
- }
- }
-
- public LegacySplitDisplayLayout getSplitLayout() {
- return mSplitLayout;
- }
-
- public WindowManagerProxy getWmProxy() {
- return mWindowManagerProxy;
- }
-
- public WindowContainerToken getSecondaryRoot() {
- if (mSplits == null || mSplits.mSecondary == null) {
- return null;
- }
- return mSplits.mSecondary.token;
- }
-
- private class SplitScreenImpl implements LegacySplitScreen {
- @Override
- public boolean isMinimized() {
- return mMinimized;
- }
-
- @Override
- public boolean isHomeStackResizable() {
- return mHomeStackResizable;
- }
-
- /**
- * TODO: Remove usage from outside the shell.
- */
- @Override
- public DividerView getDividerView() {
- return LegacySplitScreenController.this.getDividerView();
- }
-
- @Override
- public boolean isDividerVisible() {
- boolean[] result = new boolean[1];
- try {
- mMainExecutor.executeBlocking(() -> {
- result[0] = LegacySplitScreenController.this.isDividerVisible();
- });
- } catch (InterruptedException e) {
- Slog.e(TAG, "Failed to get divider visible");
- }
- return result[0];
- }
-
- @Override
- public void onKeyguardVisibilityChanged(boolean isShowing) {
- mMainExecutor.execute(() -> {
- LegacySplitScreenController.this.onKeyguardVisibilityChanged(isShowing);
- });
- }
-
- @Override
- public void setMinimized(boolean minimized) {
- mMainExecutor.execute(() -> {
- LegacySplitScreenController.this.setMinimized(minimized);
- });
- }
-
- @Override
- public void onUndockingTask() {
- mMainExecutor.execute(() -> {
- LegacySplitScreenController.this.onUndockingTask();
- });
- }
-
- @Override
- public void onAppTransitionFinished() {
- mMainExecutor.execute(() -> {
- LegacySplitScreenController.this.onAppTransitionFinished();
- });
- }
-
- @Override
- public void registerInSplitScreenListener(Consumer<Boolean> listener) {
- mMainExecutor.execute(() -> {
- LegacySplitScreenController.this.registerInSplitScreenListener(listener);
- });
- }
-
- @Override
- public void unregisterInSplitScreenListener(Consumer<Boolean> listener) {
- mMainExecutor.execute(() -> {
- LegacySplitScreenController.this.unregisterInSplitScreenListener(listener);
- });
- }
-
- @Override
- public void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) {
- mMainExecutor.execute(() -> {
- LegacySplitScreenController.this.registerBoundsChangeListener(listener);
- });
- }
-
- @Override
- public WindowContainerToken getSecondaryRoot() {
- WindowContainerToken[] result = new WindowContainerToken[1];
- try {
- mMainExecutor.executeBlocking(() -> {
- result[0] = LegacySplitScreenController.this.getSecondaryRoot();
- });
- } catch (InterruptedException e) {
- Slog.e(TAG, "Failed to get secondary root");
- }
- return result[0];
- }
-
- @Override
- public boolean splitPrimaryTask() {
- boolean[] result = new boolean[1];
- try {
- mMainExecutor.executeBlocking(() -> {
- result[0] = LegacySplitScreenController.this.splitPrimaryTask();
- });
- } catch (InterruptedException e) {
- Slog.e(TAG, "Failed to split primary task");
- }
- return result[0];
- }
-
- @Override
- public void dismissSplitToPrimaryTask() {
- mMainExecutor.execute(() -> {
- LegacySplitScreenController.this.dismissSplitToPrimaryTask();
- });
- }
-
- @Override
- public void dump(PrintWriter pw) {
- try {
- mMainExecutor.executeBlocking(() -> {
- LegacySplitScreenController.this.dump(pw);
- });
- } catch (InterruptedException e) {
- Slog.e(TAG, "Failed to dump LegacySplitScreenController in 2s");
- }
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java
deleted file mode 100644
index d2f42c39acd5..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java
+++ /dev/null
@@ -1,376 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.legacysplitscreen;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
-
-import android.app.ActivityManager.RunningTaskInfo;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.SurfaceControl;
-import android.view.SurfaceSession;
-import android.window.TaskOrganizer;
-
-import androidx.annotation.NonNull;
-
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.SurfaceUtils;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.transition.Transitions;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-
-class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener {
- private static final String TAG = LegacySplitScreenTaskListener.class.getSimpleName();
- private static final boolean DEBUG = LegacySplitScreenController.DEBUG;
-
- private final ShellTaskOrganizer mTaskOrganizer;
- private final SyncTransactionQueue mSyncQueue;
- private final SparseArray<SurfaceControl> mLeashByTaskId = new SparseArray<>();
-
- // TODO(shell-transitions): Remove when switched to shell-transitions.
- private final SparseArray<Point> mPositionByTaskId = new SparseArray<>();
-
- RunningTaskInfo mPrimary;
- RunningTaskInfo mSecondary;
- SurfaceControl mPrimarySurface;
- SurfaceControl mSecondarySurface;
- SurfaceControl mPrimaryDim;
- SurfaceControl mSecondaryDim;
- Rect mHomeBounds = new Rect();
- final LegacySplitScreenController mSplitScreenController;
- private boolean mSplitScreenSupported = false;
-
- final SurfaceSession mSurfaceSession = new SurfaceSession();
-
- private final LegacySplitScreenTransitions mSplitTransitions;
-
- LegacySplitScreenTaskListener(LegacySplitScreenController splitScreenController,
- ShellTaskOrganizer shellTaskOrganizer,
- Transitions transitions,
- SyncTransactionQueue syncQueue) {
- mSplitScreenController = splitScreenController;
- mTaskOrganizer = shellTaskOrganizer;
- mSplitTransitions = new LegacySplitScreenTransitions(splitScreenController.mTransactionPool,
- transitions, mSplitScreenController, this);
- transitions.addHandler(mSplitTransitions);
- mSyncQueue = syncQueue;
- }
-
- void init() {
- synchronized (this) {
- try {
- mTaskOrganizer.createRootTask(
- DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, this);
- mTaskOrganizer.createRootTask(
- DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, this);
- } catch (Exception e) {
- // teardown to prevent callbacks
- mTaskOrganizer.removeListener(this);
- throw e;
- }
- }
- }
-
- boolean isSplitScreenSupported() {
- return mSplitScreenSupported;
- }
-
- SurfaceControl.Transaction getTransaction() {
- return mSplitScreenController.mTransactionPool.acquire();
- }
-
- void releaseTransaction(SurfaceControl.Transaction t) {
- mSplitScreenController.mTransactionPool.release(t);
- }
-
- TaskOrganizer getTaskOrganizer() {
- return mTaskOrganizer;
- }
-
- LegacySplitScreenTransitions getSplitTransitions() {
- return mSplitTransitions;
- }
-
- @Override
- public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
- synchronized (this) {
- if (taskInfo.hasParentTask()) {
- handleChildTaskAppeared(taskInfo, leash);
- return;
- }
-
- final int winMode = taskInfo.getWindowingMode();
- if (winMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
- ProtoLog.v(WM_SHELL_TASK_ORG,
- "%s onTaskAppeared Primary taskId=%d", TAG, taskInfo.taskId);
- mPrimary = taskInfo;
- mPrimarySurface = leash;
- } else if (winMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
- ProtoLog.v(WM_SHELL_TASK_ORG,
- "%s onTaskAppeared Secondary taskId=%d", TAG, taskInfo.taskId);
- mSecondary = taskInfo;
- mSecondarySurface = leash;
- } else {
- ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared unknown taskId=%d winMode=%d",
- TAG, taskInfo.taskId, winMode);
- }
-
- if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) {
- mSplitScreenSupported = true;
- mSplitScreenController.onSplitScreenSupported();
- ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared Supported", TAG);
-
- // Initialize dim surfaces:
- SurfaceControl.Transaction t = getTransaction();
- mPrimaryDim = SurfaceUtils.makeDimLayer(
- t, mPrimarySurface, "Primary Divider Dim", mSurfaceSession);
- mSecondaryDim = SurfaceUtils.makeDimLayer(
- t, mSecondarySurface, "Secondary Divider Dim", mSurfaceSession);
- t.apply();
- releaseTransaction(t);
- }
- }
- }
-
- @Override
- public void onTaskVanished(RunningTaskInfo taskInfo) {
- synchronized (this) {
- mPositionByTaskId.remove(taskInfo.taskId);
- if (taskInfo.hasParentTask()) {
- mLeashByTaskId.remove(taskInfo.taskId);
- return;
- }
-
- final boolean isPrimaryTask = mPrimary != null
- && taskInfo.token.equals(mPrimary.token);
- final boolean isSecondaryTask = mSecondary != null
- && taskInfo.token.equals(mSecondary.token);
-
- if (mSplitScreenSupported && (isPrimaryTask || isSecondaryTask)) {
- mSplitScreenSupported = false;
-
- SurfaceControl.Transaction t = getTransaction();
- t.remove(mPrimaryDim);
- t.remove(mSecondaryDim);
- t.remove(mPrimarySurface);
- t.remove(mSecondarySurface);
- t.apply();
- releaseTransaction(t);
-
- mSplitScreenController.onTaskVanished();
- }
- }
- }
-
- @Override
- public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
- if (taskInfo.displayId != DEFAULT_DISPLAY) {
- return;
- }
- synchronized (this) {
- if (!taskInfo.supportsMultiWindow) {
- if (mSplitScreenController.isDividerVisible()) {
- // Dismiss the split screen if the task no longer supports multi window.
- if (taskInfo.taskId == mPrimary.taskId
- || taskInfo.parentTaskId == mPrimary.taskId) {
- // If the primary is focused, dismiss to primary.
- mSplitScreenController
- .startDismissSplit(taskInfo.isFocused /* toPrimaryTask */);
- } else {
- // If the secondary is not focused, dismiss to primary.
- mSplitScreenController
- .startDismissSplit(!taskInfo.isFocused /* toPrimaryTask */);
- }
- }
- return;
- }
- if (taskInfo.hasParentTask()) {
- // changed messages are noisy since it reports on every ensureVisibility. This
- // conflicts with legacy app-transitions which "swaps" the position to a
- // leash. For now, only update when position actually changes to avoid
- // poorly-timed duplicate calls.
- if (taskInfo.positionInParent.equals(mPositionByTaskId.get(taskInfo.taskId))) {
- return;
- }
- handleChildTaskChanged(taskInfo);
- } else {
- handleTaskInfoChanged(taskInfo);
- }
- mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent));
- }
- }
-
- private void handleChildTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
- mLeashByTaskId.put(taskInfo.taskId, leash);
- mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent));
- if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
- updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
- }
-
- private void handleChildTaskChanged(RunningTaskInfo taskInfo) {
- if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
- final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId);
- updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */);
- }
-
- private void updateChildTaskSurface(
- RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared) {
- final Point taskPositionInParent = taskInfo.positionInParent;
- mSyncQueue.runInSync(t -> {
- t.setWindowCrop(leash, null);
- t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
- if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) {
- t.setAlpha(leash, 1f);
- t.setMatrix(leash, 1, 0, 0, 1);
- t.show(leash);
- }
- });
- }
-
- /**
- * This is effectively a finite state machine which moves between the various split-screen
- * presentations based on the contents of the split regions.
- */
- private void handleTaskInfoChanged(RunningTaskInfo info) {
- if (!mSplitScreenSupported) {
- // This shouldn't happen; but apparently there is a chance that SysUI crashes without
- // system server receiving binder-death (or maybe it receives binder-death too late?).
- // In this situation, when sys-ui restarts, the split root-tasks will still exist so
- // there is a small window of time during init() where WM might send messages here
- // before init() fails. So, avoid a cycle of crashes by returning early.
- Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info);
- return;
- }
- final boolean secondaryImpliedMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
- || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
- && mSplitScreenController.isHomeStackResizable());
- final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
- final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
- if (info.token.asBinder() == mPrimary.token.asBinder()) {
- mPrimary = info;
- } else if (info.token.asBinder() == mSecondary.token.asBinder()) {
- mSecondary = info;
- }
- if (DEBUG) {
- Log.d(TAG, "onTaskInfoChanged " + mPrimary + " " + mSecondary);
- }
- if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
- final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
- final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
- final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
- || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
- && mSplitScreenController.isHomeStackResizable());
- if (primaryIsEmpty == primaryWasEmpty && secondaryWasEmpty == secondaryIsEmpty
- && secondaryImpliedMinimize == secondaryImpliesMinimize) {
- // No relevant changes
- return;
- }
- if (primaryIsEmpty || secondaryIsEmpty) {
- // At-least one of the splits is empty which means we are currently transitioning
- // into or out-of split-screen mode.
- if (DEBUG) {
- Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType
- + " " + mSecondary.topActivityType);
- }
- if (mSplitScreenController.isDividerVisible()) {
- // Was in split-mode, which means we are leaving split, so continue that.
- // This happens when the stack in the primary-split is dismissed.
- if (DEBUG) {
- Log.d(TAG, " was in split, so this means leave it "
- + mPrimary.topActivityType + " " + mSecondary.topActivityType);
- }
- mSplitScreenController.startDismissSplit(false /* toPrimaryTask */);
- } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) {
- // Wasn't in split-mode (both were empty), but now that the primary split is
- // populated, we should fully enter split by moving everything else into secondary.
- // This just tells window-manager to reparent things, the UI will respond
- // when it gets new task info for the secondary split.
- if (DEBUG) {
- Log.d(TAG, " was not in split, but primary is populated, so enter it");
- }
- mSplitScreenController.startEnterSplit();
- }
- } else if (secondaryImpliesMinimize) {
- // Workaround for b/172686383, we can't rely on the sync bounds change transaction for
- // the home task to finish before the last updateChildTaskSurface() call even if it's
- // queued on the sync transaction queue, so ensure that the home task surface is updated
- // again before we minimize
- final ArrayList<RunningTaskInfo> tasks = new ArrayList<>();
- mSplitScreenController.getWmProxy().getHomeAndRecentsTasks(tasks,
- mSplitScreenController.getSecondaryRoot());
- for (int i = 0; i < tasks.size(); i++) {
- final RunningTaskInfo taskInfo = tasks.get(i);
- final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId);
- if (leash != null) {
- updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */);
- }
- }
-
- // Both splits are populated but the secondary split has a home/recents stack on top,
- // so enter minimized mode.
- mSplitScreenController.ensureMinimizedSplit();
- } else {
- // Both splits are populated by normal activities, so make sure we aren't minimized.
- mSplitScreenController.ensureNormalSplit();
- }
- }
-
- @Override
- public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
- b.setParent(findTaskSurface(taskId));
- }
-
- @Override
- public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc,
- SurfaceControl.Transaction t) {
- t.reparent(sc, findTaskSurface(taskId));
- }
-
- private SurfaceControl findTaskSurface(int taskId) {
- if (!mLeashByTaskId.contains(taskId)) {
- throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
- }
- return mLeashByTaskId.get(taskId);
- }
-
- @Override
- public void dump(@NonNull PrintWriter pw, String prefix) {
- final String innerPrefix = prefix + " ";
- final String childPrefix = innerPrefix + " ";
- pw.println(prefix + this);
- pw.println(innerPrefix + "mSplitScreenSupported=" + mSplitScreenSupported);
- if (mPrimary != null) pw.println(innerPrefix + "mPrimary.taskId=" + mPrimary.taskId);
- if (mSecondary != null) pw.println(innerPrefix + "mSecondary.taskId=" + mSecondary.taskId);
- }
-
- @Override
- public String toString() {
- return TAG;
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java
deleted file mode 100644
index b1fa2ac25fe7..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.legacysplitscreen;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
-import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.WindowConfiguration;
-import android.graphics.Rect;
-import android.os.IBinder;
-import android.view.SurfaceControl;
-import android.view.WindowManager;
-import android.window.TransitionInfo;
-import android.window.TransitionRequestInfo;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.transition.Transitions;
-
-import java.util.ArrayList;
-
-/** Plays transition animations for split-screen */
-public class LegacySplitScreenTransitions implements Transitions.TransitionHandler {
- private static final String TAG = "SplitScreenTransitions";
-
- public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 10;
-
- private final TransactionPool mTransactionPool;
- private final Transitions mTransitions;
- private final LegacySplitScreenController mSplitScreen;
- private final LegacySplitScreenTaskListener mListener;
-
- private IBinder mPendingDismiss = null;
- private boolean mDismissFromSnap = false;
- private IBinder mPendingEnter = null;
- private IBinder mAnimatingTransition = null;
-
- /** Keeps track of currently running animations */
- private final ArrayList<Animator> mAnimations = new ArrayList<>();
-
- private Transitions.TransitionFinishCallback mFinishCallback = null;
- private SurfaceControl.Transaction mFinishTransaction;
-
- LegacySplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions,
- @NonNull LegacySplitScreenController splitScreen,
- @NonNull LegacySplitScreenTaskListener listener) {
- mTransactionPool = pool;
- mTransitions = transitions;
- mSplitScreen = splitScreen;
- mListener = listener;
- }
-
- @Override
- public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
- @Nullable TransitionRequestInfo request) {
- WindowContainerTransaction out = null;
- final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
- final @WindowManager.TransitionType int type = request.getType();
- if (mSplitScreen.isDividerVisible()) {
- // try to handle everything while in split-screen
- out = new WindowContainerTransaction();
- if (triggerTask != null) {
- final boolean shouldDismiss =
- // if we close the primary-docked task, then leave split-screen since there
- // is nothing behind it.
- ((type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK)
- && triggerTask.parentTaskId == mListener.mPrimary.taskId)
- // if an activity that is not supported in multi window mode is launched,
- // we also need to leave split-screen.
- || ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)
- && !triggerTask.supportsMultiWindow);
- // In both cases, dismiss the primary
- if (shouldDismiss) {
- WindowManagerProxy.buildDismissSplit(out, mListener,
- mSplitScreen.getSplitLayout(), true /* dismiss */);
- if (type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) {
- out.reorder(triggerTask.token, true /* onTop */);
- }
- mPendingDismiss = transition;
- }
- }
- } else if (triggerTask != null) {
- // Not in split mode, so look for an open with a trigger task.
- if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)
- && triggerTask.configuration.windowConfiguration.getWindowingMode()
- == WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
- out = new WindowContainerTransaction();
- mSplitScreen.prepareEnterSplitTransition(out);
- mPendingEnter = transition;
- }
- }
- return out;
- }
-
- // TODO(shell-transitions): real animations
- private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) {
- final float end = show ? 1.f : 0.f;
- final float start = 1.f - end;
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- final ValueAnimator va = ValueAnimator.ofFloat(start, end);
- va.setDuration(500);
- va.addUpdateListener(animation -> {
- float fraction = animation.getAnimatedFraction();
- transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
- transaction.apply();
- });
- final Runnable finisher = () -> {
- transaction.setAlpha(leash, end);
- transaction.apply();
- mTransactionPool.release(transaction);
- mTransitions.getMainExecutor().execute(() -> {
- mAnimations.remove(va);
- onFinish();
- });
- };
- va.addListener(new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) { }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- finisher.run();
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- finisher.run();
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) { }
- });
- mAnimations.add(va);
- mTransitions.getAnimExecutor().execute(va::start);
- }
-
- // TODO(shell-transitions): real animations
- private void startExampleResizeAnimation(@NonNull SurfaceControl leash,
- @NonNull Rect startBounds, @NonNull Rect endBounds) {
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f);
- va.setDuration(500);
- va.addUpdateListener(animation -> {
- float fraction = animation.getAnimatedFraction();
- transaction.setWindowCrop(leash,
- (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction),
- (int) (startBounds.height() * (1.f - fraction)
- + endBounds.height() * fraction));
- transaction.setPosition(leash,
- startBounds.left * (1.f - fraction) + endBounds.left * fraction,
- startBounds.top * (1.f - fraction) + endBounds.top * fraction);
- transaction.apply();
- });
- final Runnable finisher = () -> {
- transaction.setWindowCrop(leash, 0, 0);
- transaction.setPosition(leash, endBounds.left, endBounds.top);
- transaction.apply();
- mTransactionPool.release(transaction);
- mTransitions.getMainExecutor().execute(() -> {
- mAnimations.remove(va);
- onFinish();
- });
- };
- va.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- finisher.run();
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- finisher.run();
- }
- });
- mAnimations.add(va);
- mTransitions.getAnimExecutor().execute(va::start);
- }
-
- @Override
- public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (transition != mPendingDismiss && transition != mPendingEnter) {
- // If we're not in split-mode, just abort
- if (!mSplitScreen.isDividerVisible()) return false;
- // Check to see if HOME is involved
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- if (change.getTaskInfo() == null
- || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) continue;
- if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) {
- mSplitScreen.ensureMinimizedSplit();
- } else if (change.getMode() == TRANSIT_CLOSE
- || change.getMode() == TRANSIT_TO_BACK) {
- mSplitScreen.ensureNormalSplit();
- }
- }
- // Use normal animations.
- return false;
- }
-
- mFinishCallback = finishCallback;
- mFinishTransaction = mTransactionPool.acquire();
- mAnimatingTransition = transition;
-
- // Play fade animations
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- final SurfaceControl leash = change.getLeash();
- final int mode = info.getChanges().get(i).getMode();
-
- if (mode == TRANSIT_CHANGE) {
- if (change.getParent() != null) {
- // This is probably reparented, so we want the parent to be immediately visible
- final TransitionInfo.Change parentChange = info.getChange(change.getParent());
- startTransaction.show(parentChange.getLeash());
- startTransaction.setAlpha(parentChange.getLeash(), 1.f);
- // and then animate this layer outside the parent (since, for example, this is
- // the home task animating from fullscreen to part-screen).
- startTransaction.reparent(leash, info.getRootLeash());
- startTransaction.setLayer(leash, info.getChanges().size() - i);
- // build the finish reparent/reposition
- mFinishTransaction.reparent(leash, parentChange.getLeash());
- mFinishTransaction.setPosition(leash,
- change.getEndRelOffset().x, change.getEndRelOffset().y);
- }
- // TODO(shell-transitions): screenshot here
- final Rect startBounds = new Rect(change.getStartAbsBounds());
- final boolean isHome = change.getTaskInfo() != null
- && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
- if (mPendingDismiss == transition && mDismissFromSnap && !isHome) {
- // Home is special since it doesn't move during fling. Everything else, though,
- // when dismissing from snap, the top/left is at 0,0.
- startBounds.offsetTo(0, 0);
- }
- final Rect endBounds = new Rect(change.getEndAbsBounds());
- startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
- endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
- startExampleResizeAnimation(leash, startBounds, endBounds);
- }
- if (change.getParent() != null) {
- continue;
- }
-
- if (transition == mPendingEnter
- && mListener.mPrimary.token.equals(change.getContainer())
- || mListener.mSecondary.token.equals(change.getContainer())) {
- startTransaction.setWindowCrop(leash, change.getStartAbsBounds().width(),
- change.getStartAbsBounds().height());
- if (mListener.mPrimary.token.equals(change.getContainer())) {
- // Move layer to top since we want it above the oversized home task during
- // animation even though home task is on top in hierarchy.
- startTransaction.setLayer(leash, info.getChanges().size() + 1);
- }
- }
- boolean isOpening = Transitions.isOpeningType(info.getType());
- if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
- // fade in
- startExampleAnimation(leash, true /* show */);
- } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
- // fade out
- if (transition == mPendingDismiss && mDismissFromSnap) {
- // Dismissing via snap-to-top/bottom means that the dismissed task is already
- // not-visible (usually cropped to oblivion) so immediately set its alpha to 0
- // and don't animate it so it doesn't pop-in when reparented.
- startTransaction.setAlpha(leash, 0.f);
- } else {
- startExampleAnimation(leash, false /* show */);
- }
- }
- }
- if (transition == mPendingEnter) {
- // If entering, check if we should enter into minimized or normal split
- boolean homeIsVisible = false;
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- if (change.getTaskInfo() == null
- || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) {
- continue;
- }
- homeIsVisible = change.getMode() == TRANSIT_OPEN
- || change.getMode() == TRANSIT_TO_FRONT
- || change.getMode() == TRANSIT_CHANGE;
- break;
- }
- mSplitScreen.finishEnterSplitTransition(homeIsVisible);
- }
- startTransaction.apply();
- onFinish();
- return true;
- }
-
- @ExternalThread
- void dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout,
- boolean dismissOrMaximize, boolean snapped) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- WindowManagerProxy.buildDismissSplit(wct, tiles, layout, dismissOrMaximize);
- mTransitions.getMainExecutor().execute(() -> {
- mDismissFromSnap = snapped;
- mPendingDismiss = mTransitions.startTransition(TRANSIT_SPLIT_DISMISS_SNAP, wct, this);
- });
- }
-
- private void onFinish() {
- if (!mAnimations.isEmpty()) return;
- mFinishTransaction.apply();
- mTransactionPool.release(mFinishTransaction);
- mFinishTransaction = null;
- mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
- mFinishCallback = null;
- if (mAnimatingTransition == mPendingEnter) {
- mPendingEnter = null;
- }
- if (mAnimatingTransition == mPendingDismiss) {
- mSplitScreen.onDismissSplit();
- mPendingDismiss = null;
- }
- mDismissFromSnap = false;
- mAnimatingTransition = null;
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/MinimizedDockShadow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/MinimizedDockShadow.java
deleted file mode 100644
index 1e9223cbe3e2..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/MinimizedDockShadow.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.legacysplitscreen;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.Shader;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.WindowManager;
-
-import com.android.wm.shell.R;
-
-/**
- * Shadow for the minimized dock state on homescreen.
- */
-public class MinimizedDockShadow extends View {
-
- private final Paint mShadowPaint = new Paint();
-
- private int mDockSide = WindowManager.DOCKED_INVALID;
-
- public MinimizedDockShadow(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- void setDockSide(int dockSide) {
- if (dockSide != mDockSide) {
- mDockSide = dockSide;
- updatePaint(getLeft(), getTop(), getRight(), getBottom());
- invalidate();
- }
- }
-
- private void updatePaint(int left, int top, int right, int bottom) {
- int startColor = mContext.getResources().getColor(
- R.color.minimize_dock_shadow_start, null);
- int endColor = mContext.getResources().getColor(
- R.color.minimize_dock_shadow_end, null);
- final int middleColor = Color.argb(
- (Color.alpha(startColor) + Color.alpha(endColor)) / 2, 0, 0, 0);
- final int quarter = Color.argb(
- (int) (Color.alpha(startColor) * 0.25f + Color.alpha(endColor) * 0.75f),
- 0, 0, 0);
- if (mDockSide == WindowManager.DOCKED_TOP) {
- mShadowPaint.setShader(new LinearGradient(
- 0, 0, 0, bottom - top,
- new int[] { startColor, middleColor, quarter, endColor },
- new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP));
- } else if (mDockSide == WindowManager.DOCKED_LEFT) {
- mShadowPaint.setShader(new LinearGradient(
- 0, 0, right - left, 0,
- new int[] { startColor, middleColor, quarter, endColor },
- new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP));
- } else if (mDockSide == WindowManager.DOCKED_RIGHT) {
- mShadowPaint.setShader(new LinearGradient(
- right - left, 0, 0, 0,
- new int[] { startColor, middleColor, quarter, endColor },
- new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP));
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- if (changed) {
- updatePaint(left, top, right, bottom);
- invalidate();
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawRect(0, 0, getWidth(), getHeight(), mShadowPaint);
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java
deleted file mode 100644
index e42e43bbc2be..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java
+++ /dev/null
@@ -1,386 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.legacysplitscreen;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import android.annotation.NonNull;
-import android.app.ActivityManager;
-import android.app.ActivityTaskManager;
-import android.graphics.Rect;
-import android.os.RemoteException;
-import android.util.Log;
-import android.view.Display;
-import android.view.SurfaceControl;
-import android.view.WindowManagerGlobal;
-import android.window.TaskOrganizer;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.ArrayUtils;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.transition.Transitions;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Proxy to simplify calls into window manager/activity manager
- */
-class WindowManagerProxy {
-
- private static final String TAG = "WindowManagerProxy";
- private static final int[] HOME_AND_RECENTS = {ACTIVITY_TYPE_HOME, ACTIVITY_TYPE_RECENTS};
- private static final int[] CONTROLLED_ACTIVITY_TYPES = {
- ACTIVITY_TYPE_STANDARD,
- ACTIVITY_TYPE_HOME,
- ACTIVITY_TYPE_RECENTS,
- ACTIVITY_TYPE_UNDEFINED
- };
- private static final int[] CONTROLLED_WINDOWING_MODES = {
- WINDOWING_MODE_FULLSCREEN,
- WINDOWING_MODE_SPLIT_SCREEN_SECONDARY,
- WINDOWING_MODE_UNDEFINED
- };
-
- @GuardedBy("mDockedRect")
- private final Rect mDockedRect = new Rect();
-
- private final Rect mTmpRect1 = new Rect();
-
- @GuardedBy("mDockedRect")
- private final Rect mTouchableRegion = new Rect();
-
- private final SyncTransactionQueue mSyncTransactionQueue;
- private final TaskOrganizer mTaskOrganizer;
-
- WindowManagerProxy(SyncTransactionQueue syncQueue, TaskOrganizer taskOrganizer) {
- mSyncTransactionQueue = syncQueue;
- mTaskOrganizer = taskOrganizer;
- }
-
- void dismissOrMaximizeDocked(final LegacySplitScreenTaskListener tiles,
- LegacySplitDisplayLayout layout, final boolean dismissOrMaximize) {
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- tiles.mSplitScreenController.startDismissSplit(!dismissOrMaximize, true /* snapped */);
- } else {
- applyDismissSplit(tiles, layout, dismissOrMaximize);
- }
- }
-
- public void setResizing(final boolean resizing) {
- try {
- ActivityTaskManager.getService().setSplitScreenResizing(resizing);
- } catch (RemoteException e) {
- Log.w(TAG, "Error calling setDockedStackResizing: " + e);
- }
- }
-
- /** Sets a touch region */
- public void setTouchRegion(Rect region) {
- try {
- synchronized (mDockedRect) {
- mTouchableRegion.set(region);
- }
- WindowManagerGlobal.getWindowManagerService().setDockedTaskDividerTouchRegion(
- mTouchableRegion);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to set touchable region: " + e);
- }
- }
-
- void applyResizeSplits(int position, LegacySplitDisplayLayout splitLayout) {
- WindowContainerTransaction t = new WindowContainerTransaction();
- splitLayout.resizeSplits(position, t);
- applySyncTransaction(t);
- }
-
- boolean getHomeAndRecentsTasks(List<ActivityManager.RunningTaskInfo> out,
- WindowContainerToken parent) {
- boolean resizable = false;
- List<ActivityManager.RunningTaskInfo> rootTasks = parent == null
- ? mTaskOrganizer.getRootTasks(Display.DEFAULT_DISPLAY, HOME_AND_RECENTS)
- : mTaskOrganizer.getChildTasks(parent, HOME_AND_RECENTS);
- for (int i = 0, n = rootTasks.size(); i < n; ++i) {
- final ActivityManager.RunningTaskInfo ti = rootTasks.get(i);
- out.add(ti);
- if (ti.topActivityType == ACTIVITY_TYPE_HOME) {
- resizable = ti.isResizeable;
- }
- }
- return resizable;
- }
-
- /**
- * Assign a fixed override-bounds to home tasks that reflect their geometry while the primary
- * split is minimized. This actually "sticks out" of the secondary split area, but when in
- * minimized mode, the secondary split gets a 'negative' crop to expose it.
- */
- boolean applyHomeTasksMinimized(LegacySplitDisplayLayout layout, WindowContainerToken parent,
- @NonNull WindowContainerTransaction wct) {
- // Resize the home/recents stacks to the larger minimized-state size
- final Rect homeBounds;
- final ArrayList<ActivityManager.RunningTaskInfo> homeStacks = new ArrayList<>();
- boolean isHomeResizable = getHomeAndRecentsTasks(homeStacks, parent);
- if (isHomeResizable) {
- homeBounds = layout.calcResizableMinimizedHomeStackBounds();
- } else {
- // home is not resizable, so lock it to its inherent orientation size.
- homeBounds = new Rect(0, 0, 0, 0);
- for (int i = homeStacks.size() - 1; i >= 0; --i) {
- if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_HOME) {
- final int orient = homeStacks.get(i).configuration.orientation;
- final boolean displayLandscape = layout.mDisplayLayout.isLandscape();
- final boolean isLandscape = orient == ORIENTATION_LANDSCAPE
- || (orient == ORIENTATION_UNDEFINED && displayLandscape);
- homeBounds.right = isLandscape == displayLandscape
- ? layout.mDisplayLayout.width() : layout.mDisplayLayout.height();
- homeBounds.bottom = isLandscape == displayLandscape
- ? layout.mDisplayLayout.height() : layout.mDisplayLayout.width();
- break;
- }
- }
- }
- for (int i = homeStacks.size() - 1; i >= 0; --i) {
- // For non-resizable homes, the minimized size is actually the fullscreen-size. As a
- // result, we don't minimize for recents since it only shows half-size screenshots.
- if (!isHomeResizable) {
- if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_RECENTS) {
- continue;
- }
- wct.setWindowingMode(homeStacks.get(i).token, WINDOWING_MODE_FULLSCREEN);
- }
- wct.setBounds(homeStacks.get(i).token, homeBounds);
- }
- layout.mTiles.mHomeBounds.set(homeBounds);
- return isHomeResizable;
- }
-
- /** @see #buildEnterSplit */
- boolean applyEnterSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout) {
- // Set launchtile first so that any stack created after
- // getAllRootTaskInfos and before reparent (even if unlikely) are placed
- // correctly.
- WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setLaunchRoot(tiles.mSecondary.token, CONTROLLED_WINDOWING_MODES,
- CONTROLLED_ACTIVITY_TYPES);
- final boolean isHomeResizable = buildEnterSplit(wct, tiles, layout);
- applySyncTransaction(wct);
- return isHomeResizable;
- }
-
- /**
- * Finishes entering split-screen by reparenting all FULLSCREEN tasks into the secondary split.
- * This assumes there is already something in the primary split since that is usually what
- * triggers a call to this. In the same transaction, this overrides the home task bounds via
- * {@link #applyHomeTasksMinimized}.
- *
- * @return whether the home stack is resizable
- */
- boolean buildEnterSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles,
- LegacySplitDisplayLayout layout) {
- List<ActivityManager.RunningTaskInfo> rootTasks =
- mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, null /* activityTypes */);
- if (rootTasks.isEmpty()) {
- return false;
- }
- ActivityManager.RunningTaskInfo topHomeTask = null;
- for (int i = rootTasks.size() - 1; i >= 0; --i) {
- final ActivityManager.RunningTaskInfo rootTask = rootTasks.get(i);
- // Check whether the task can be moved to split secondary.
- if (!rootTask.supportsMultiWindow && rootTask.topActivityType != ACTIVITY_TYPE_HOME) {
- continue;
- }
- // Only move split controlling tasks to split secondary.
- final int windowingMode = rootTask.getWindowingMode();
- if (!ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, windowingMode)
- || !ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, rootTask.getActivityType())
- // Excludes split screen secondary due to it's the root we're reparenting to.
- || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
- continue;
- }
- // Since this iterates from bottom to top, update topHomeTask for every fullscreen task
- // so it will be left with the status of the top one.
- topHomeTask = isHomeOrRecentTask(rootTask) ? rootTask : null;
- outWct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */);
- }
- // Move the secondary split-forward.
- outWct.reorder(tiles.mSecondary.token, true /* onTop */);
- boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */,
- outWct);
- if (topHomeTask != null && !Transitions.ENABLE_SHELL_TRANSITIONS) {
- // Translate/update-crop of secondary out-of-band with sync transaction -- Until BALST
- // is enabled, this temporarily syncs the home surface position with offset until
- // sync transaction finishes.
- outWct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds);
- }
- return isHomeResizable;
- }
-
- static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) {
- final int atype = ti.getActivityType();
- return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS;
- }
-
- /** @see #buildDismissSplit */
- void applyDismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout,
- boolean dismissOrMaximize) {
- // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished
- // plus specific APIs to clean this up.
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- // Set launch root first so that any task created after getChildContainers and
- // before reparent (pretty unlikely) are put into fullscreen.
- wct.setLaunchRoot(tiles.mSecondary.token, null, null);
- buildDismissSplit(wct, tiles, layout, dismissOrMaximize);
- applySyncTransaction(wct);
- }
-
- /**
- * Reparents all tile members back to their display and resets home task override bounds.
- * @param dismissOrMaximize When {@code true} this resolves the split by closing the primary
- * split (thus resulting in the top of the secondary split becoming
- * fullscreen. {@code false} resolves the other way.
- */
- static void buildDismissSplit(WindowContainerTransaction outWct,
- LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout,
- boolean dismissOrMaximize) {
- // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished
- // plus specific APIs to clean this up.
- final TaskOrganizer taskOrg = tiles.getTaskOrganizer();
- List<ActivityManager.RunningTaskInfo> primaryChildren =
- taskOrg.getChildTasks(tiles.mPrimary.token, null /* activityTypes */);
- List<ActivityManager.RunningTaskInfo> secondaryChildren =
- taskOrg.getChildTasks(tiles.mSecondary.token, null /* activityTypes */);
- // In some cases (eg. non-resizable is launched), system-server will leave split-screen.
- // as a result, the above will not capture any tasks; yet, we need to clean-up the
- // home task bounds.
- List<ActivityManager.RunningTaskInfo> freeHomeAndRecents =
- taskOrg.getRootTasks(DEFAULT_DISPLAY, HOME_AND_RECENTS);
- // Filter out the root split tasks
- freeHomeAndRecents.removeIf(p -> p.token.equals(tiles.mSecondary.token)
- || p.token.equals(tiles.mPrimary.token));
-
- if (primaryChildren.isEmpty() && secondaryChildren.isEmpty()
- && freeHomeAndRecents.isEmpty()) {
- return;
- }
- if (dismissOrMaximize) {
- // Dismissing, so move all primary split tasks first
- for (int i = primaryChildren.size() - 1; i >= 0; --i) {
- outWct.reparent(primaryChildren.get(i).token, null /* parent */,
- true /* onTop */);
- }
- boolean homeOnTop = false;
- // Don't need to worry about home tasks because they are already in the "proper"
- // order within the secondary split.
- for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
- final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i);
- outWct.reparent(ti.token, null /* parent */, true /* onTop */);
- if (isHomeOrRecentTask(ti)) {
- outWct.setBounds(ti.token, null);
- outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED);
- if (i == 0) {
- homeOnTop = true;
- }
- }
- }
- if (homeOnTop && !Transitions.ENABLE_SHELL_TRANSITIONS) {
- // Translate/update-crop of secondary out-of-band with sync transaction -- instead
- // play this in sync with new home-app frame because until BALST is enabled this
- // shows up on screen before the syncTransaction returns.
- // We only have access to the secondary root surface, though, so in order to
- // position things properly, we have to take into account the existing negative
- // offset/crop of the minimized-home task.
- final boolean landscape = layout.mDisplayLayout.isLandscape();
- final int posX = landscape ? layout.mSecondary.left - tiles.mHomeBounds.left
- : layout.mSecondary.left;
- final int posY = landscape ? layout.mSecondary.top
- : layout.mSecondary.top - tiles.mHomeBounds.top;
- final SurfaceControl.Transaction sft = new SurfaceControl.Transaction();
- sft.setPosition(tiles.mSecondarySurface, posX, posY);
- final Rect crop = new Rect(0, 0, layout.mDisplayLayout.width(),
- layout.mDisplayLayout.height());
- crop.offset(-posX, -posY);
- sft.setWindowCrop(tiles.mSecondarySurface, crop);
- outWct.setBoundsChangeTransaction(tiles.mSecondary.token, sft);
- }
- } else {
- // Maximize, so move non-home secondary split first
- for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
- if (isHomeOrRecentTask(secondaryChildren.get(i))) {
- continue;
- }
- outWct.reparent(secondaryChildren.get(i).token, null /* parent */,
- true /* onTop */);
- }
- // Find and place home tasks in-between. This simulates the fact that there was
- // nothing behind the primary split's tasks.
- for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
- final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i);
- if (isHomeOrRecentTask(ti)) {
- outWct.reparent(ti.token, null /* parent */, true /* onTop */);
- // reset bounds and mode too
- outWct.setBounds(ti.token, null);
- outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED);
- }
- }
- for (int i = primaryChildren.size() - 1; i >= 0; --i) {
- outWct.reparent(primaryChildren.get(i).token, null /* parent */,
- true /* onTop */);
- }
- }
- for (int i = freeHomeAndRecents.size() - 1; i >= 0; --i) {
- outWct.setBounds(freeHomeAndRecents.get(i).token, null);
- outWct.setWindowingMode(freeHomeAndRecents.get(i).token, WINDOWING_MODE_UNDEFINED);
- }
- // Reset focusable to true
- outWct.setFocusable(tiles.mPrimary.token, true /* focusable */);
- }
-
- /**
- * Utility to apply a sync transaction serially with other sync transactions.
- *
- * @see SyncTransactionQueue#queue
- */
- void applySyncTransaction(WindowContainerTransaction wct) {
- mSyncTransactionQueue.queue(wct);
- }
-
- /**
- * @see SyncTransactionQueue#queueIfWaiting
- */
- boolean queueSyncTransactionIfWaiting(WindowContainerTransaction wct) {
- return mSyncTransactionQueue.queueIfWaiting(wct);
- }
-
- /**
- * @see SyncTransactionQueue#runInSync
- */
- void runInSync(SyncTransactionQueue.TransactionRunnable runnable) {
- mSyncTransactionQueue.runInSync(runnable);
- }
-}
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 b00182f36cc8..7129165a78dc 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
@@ -16,7 +16,6 @@
package com.android.wm.shell.onehanded;
-import android.content.res.Configuration;
import android.os.SystemProperties;
import com.android.wm.shell.common.annotations.ExternalThread;
@@ -38,16 +37,6 @@ public interface OneHanded {
}
/**
- * Return one handed settings enabled or not.
- */
- boolean isOneHandedEnabled();
-
- /**
- * Return swipe to notification settings enabled or not.
- */
- boolean isSwipeToNotificationEnabled();
-
- /**
* Enters one handed mode.
*/
void startOneHanded();
@@ -81,19 +70,4 @@ public interface OneHanded {
* transition start or finish
*/
void registerTransitionCallback(OneHandedTransitionCallback callback);
-
- /**
- * Receive onConfigurationChanged() events
- */
- void onConfigChanged(Configuration newConfig);
-
- /**
- * Notifies when user switch complete
- */
- void onUserSwitch(int userId);
-
- /**
- * Notifies when keyguard visibility changed
- */
- void onKeyguardVisibilityChanged(boolean showing);
}
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 179b725ab210..e0c4fe8c4fba 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
@@ -28,17 +28,16 @@ import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE;
import android.annotation.BinderThread;
import android.content.ComponentName;
import android.content.Context;
-import android.content.om.IOverlayManager;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.graphics.Rect;
import android.os.Handler;
-import android.os.ServiceManager;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Slog;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
+import android.window.DisplayAreaInfo;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
@@ -55,6 +54,12 @@ 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.sysui.ConfigurationChangeListener;
+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.sysui.UserChangeListener;
import java.io.PrintWriter;
@@ -62,7 +67,8 @@ import java.io.PrintWriter;
* Manages and manipulates the one handed states, transitions, and gesture for phones.
*/
public class OneHandedController implements RemoteCallable<OneHandedController>,
- DisplayChangeController.OnDisplayChangingListener {
+ DisplayChangeController.OnDisplayChangingListener, ConfigurationChangeListener,
+ KeyguardChangeListener, UserChangeListener {
private static final String TAG = "OneHandedController";
private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
@@ -71,8 +77,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
public static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
- private volatile boolean mIsOneHandedEnabled;
- private volatile boolean mIsSwipeToNotificationEnabled;
+ private boolean mIsOneHandedEnabled;
+ private boolean mIsSwipeToNotificationEnabled;
private boolean mIsShortcutEnabled;
private boolean mTaskChangeToExit;
private boolean mLockedDisabled;
@@ -82,6 +88,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
private Context mContext;
+ private final ShellCommandHandler mShellCommandHandler;
+ private final ShellController mShellController;
private final AccessibilityManager mAccessibilityManager;
private final DisplayController mDisplayController;
private final OneHandedSettingsUtil mOneHandedSettingsUtil;
@@ -91,7 +99,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
private final OneHandedState mState;
private final OneHandedTutorialHandler mTutorialHandler;
private final TaskStackListenerImpl mTaskStackListener;
- private final IOverlayManager mOverlayManager;
private final ShellExecutor mMainExecutor;
private final Handler mMainHandler;
private final OneHandedImpl mImpl = new OneHandedImpl();
@@ -189,9 +196,11 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
/**
* Creates {@link OneHandedController}, returns {@code null} if the feature is not supported.
*/
- public static OneHandedController create(
- Context context, WindowManager windowManager, DisplayController displayController,
- DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener,
+ public static OneHandedController create(Context context,
+ ShellInit shellInit, ShellCommandHandler shellCommandHandler,
+ ShellController shellController, WindowManager windowManager,
+ DisplayController displayController, DisplayLayout displayLayout,
+ TaskStackListenerImpl taskStackListener,
InteractionJankMonitor jankMonitor, UiEventLogger uiEventLogger,
ShellExecutor mainExecutor, Handler mainHandler) {
OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil();
@@ -209,16 +218,17 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
context, displayLayout, settingsUtil, animationController, tutorialHandler,
jankMonitor, mainExecutor);
OneHandedUiEventLogger oneHandedUiEventsLogger = new OneHandedUiEventLogger(uiEventLogger);
- IOverlayManager overlayManager = IOverlayManager.Stub.asInterface(
- ServiceManager.getService(Context.OVERLAY_SERVICE));
- return new OneHandedController(context, displayController, organizer, touchHandler,
- tutorialHandler, settingsUtil, accessibilityUtil, timeoutHandler, oneHandedState,
- jankMonitor, oneHandedUiEventsLogger, overlayManager, taskStackListener,
- mainExecutor, mainHandler);
+ return new OneHandedController(context, shellInit, shellCommandHandler, shellController,
+ displayController, organizer, touchHandler, tutorialHandler, settingsUtil,
+ accessibilityUtil, timeoutHandler, oneHandedState, oneHandedUiEventsLogger,
+ taskStackListener, mainExecutor, mainHandler);
}
@VisibleForTesting
OneHandedController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
DisplayController displayController,
OneHandedDisplayAreaOrganizer displayAreaOrganizer,
OneHandedTouchHandler touchHandler,
@@ -227,13 +237,13 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
OneHandedAccessibilityUtil oneHandedAccessibilityUtil,
OneHandedTimeoutHandler timeoutHandler,
OneHandedState state,
- InteractionJankMonitor jankMonitor,
OneHandedUiEventLogger uiEventsLogger,
- IOverlayManager overlayManager,
TaskStackListenerImpl taskStackListener,
ShellExecutor mainExecutor,
Handler mainHandler) {
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
+ mShellController = shellController;
mOneHandedSettingsUtil = settingsUtil;
mOneHandedAccessibilityUtil = oneHandedAccessibilityUtil;
mDisplayAreaOrganizer = displayAreaOrganizer;
@@ -241,13 +251,12 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
mTouchHandler = touchHandler;
mState = state;
mTutorialHandler = tutorialHandler;
- mOverlayManager = overlayManager;
mMainExecutor = mainExecutor;
mMainHandler = mainHandler;
mOneHandedUiEventLogger = uiEventsLogger;
mTaskStackListener = taskStackListener;
+ mAccessibilityManager = AccessibilityManager.getInstance(mContext);
- mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
final float offsetPercentageConfig = context.getResources().getFraction(
R.fraction.config_one_handed_offset, 1, 1);
final int sysPropPercentageConfig = SystemProperties.getInt(
@@ -267,6 +276,12 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
getObserver(this::onSwipeToNotificationEnabledChanged);
mShortcutEnabledObserver = getObserver(this::onShortcutEnabledChanged);
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ mShellCommandHandler.addDumpCallback(this::dump, this);
+ mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
mDisplayController.addDisplayChangingController(this);
setupCallback();
registerSettingObservers(mUserId);
@@ -274,11 +289,13 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
updateSettings();
updateDisplayLayout(mContext.getDisplayId());
- mAccessibilityManager = AccessibilityManager.getInstance(context);
mAccessibilityManager.addAccessibilityStateChangeListener(
mAccessibilityStateChangeListener);
mState.addSListeners(mTutorialHandler);
+ mShellController.addConfigurationChangeListener(this);
+ mShellController.addKeyguardChangeListener(this);
+ mShellController.addUserChangeListener(this);
}
public OneHanded asOneHanded() {
@@ -594,7 +611,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
mLockedDisabled = locked && !enabled;
}
- private void onConfigChanged(Configuration newConfig) {
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
if (mTutorialHandler == null) {
return;
}
@@ -604,11 +622,15 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
mTutorialHandler.onConfigurationChanged();
}
- private void onKeyguardVisibilityChanged(boolean showing) {
- mKeyguardShowing = showing;
+ @Override
+ public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
+ boolean animatingDismiss) {
+ mKeyguardShowing = visible;
+ stopOneHanded();
}
- private void onUserSwitch(int newUserId) {
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
unregisterSettingObservers();
mUserId = newUserId;
registerSettingObservers(newUserId);
@@ -616,7 +638,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
updateOneHandedEnabled();
}
- public void dump(@NonNull PrintWriter pw) {
+ public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = " ";
pw.println();
pw.println(TAG);
@@ -659,11 +681,11 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
}
/**
- * Handles rotation based on OnDisplayChangingListener callback
+ * Handles display change based on OnDisplayChangingListener callback
*/
@Override
- public void onRotateDisplay(int displayId, int fromRotation, int toRotation,
- WindowContainerTransaction wct) {
+ public void onDisplayChange(int displayId, int fromRotation, int toRotation,
+ DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
if (!isInitialized()) {
return;
}
@@ -674,9 +696,12 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
return;
}
+ if (mState.getState() == STATE_ACTIVE) {
+ mOneHandedUiEventLogger.writeEvent(
+ OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT);
+ }
+
mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation, wct);
- mOneHandedUiEventLogger.writeEvent(
- OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT);
}
/**
@@ -696,18 +721,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
}
@Override
- public boolean isOneHandedEnabled() {
- // This is volatile so return directly
- return mIsOneHandedEnabled;
- }
-
- @Override
- public boolean isSwipeToNotificationEnabled() {
- // This is volatile so return directly
- return mIsSwipeToNotificationEnabled;
- }
-
- @Override
public void startOneHanded() {
mMainExecutor.execute(() -> {
OneHandedController.this.startOneHanded();
@@ -748,27 +761,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
OneHandedController.this.registerTransitionCallback(callback);
});
}
-
- @Override
- public void onConfigChanged(Configuration newConfig) {
- mMainExecutor.execute(() -> {
- OneHandedController.this.onConfigChanged(newConfig);
- });
- }
-
- @Override
- public void onUserSwitch(int userId) {
- mMainExecutor.execute(() -> {
- OneHandedController.this.onUserSwitch(userId);
- });
- }
-
- @Override
- public void onKeyguardVisibilityChanged(boolean showing) {
- mMainExecutor.execute(() -> {
- OneHandedController.this.onKeyguardVisibilityChanged(showing);
- });
- }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
index f61d1b95bd85..451afa08040c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
@@ -159,6 +159,10 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer {
@Override
public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) {
+ final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token);
+ if (leash != null) {
+ leash.release();
+ }
mDisplayAreaTokenMap.remove(displayAreaInfo.token);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
index e03421dd58ac..4def15db2f52 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
@@ -37,12 +37,13 @@ interface IPip {
* @param activityInfo ActivityInfo tied to the Activity
* @param pictureInPictureParams PictureInPictureParams tied to the Activity
* @param launcherRotation Launcher rotation to calculate the PiP destination bounds
- * @param shelfHeight Shelf height of launcher to calculate the PiP destination bounds
+ * @param hotseatKeepClearArea Bounds of Hotseat to avoid used to calculate PiP destination
+ bounds
* @return destination bounds the PiP window should land into
*/
Rect startSwipePipToHome(in ComponentName componentName, in ActivityInfo activityInfo,
in PictureInPictureParams pictureInPictureParams,
- int launcherRotation, int shelfHeight) = 1;
+ int launcherRotation, in Rect hotseatKeepClearArea) = 1;
/**
* Notifies the swiping Activity to PiP onto home transition is finished
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 3b3091a9caf3..c06881ae6ad7 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
@@ -16,12 +16,10 @@
package com.android.wm.shell.pip;
-import android.content.res.Configuration;
import android.graphics.Rect;
import com.android.wm.shell.common.annotations.ExternalThread;
-import java.io.PrintWriter;
import java.util.function.Consumer;
/**
@@ -44,24 +42,6 @@ public interface Pip {
}
/**
- * Called when configuration is changed.
- */
- default void onConfigurationChanged(Configuration newConfig) {
- }
-
- /**
- * Called when display size or font size of settings changed
- */
- default void onDensityOrFontScaleChanged() {
- }
-
- /**
- * Called when overlay package change invoked.
- */
- default void onOverlayChanged() {
- }
-
- /**
* Called when SysUI state changed.
*
* @param isSysUiStateValid Is SysUI state valid or not.
@@ -71,12 +51,6 @@ public interface Pip {
}
/**
- * Registers the session listener for the current user.
- */
- default void registerSessionListenerForCurrentUser() {
- }
-
- /**
* Sets both shelf visibility and its height.
*
* @param visible visibility of shelf.
@@ -86,12 +60,12 @@ public interface Pip {
}
/**
- * Registers the pinned stack animation listener.
+ * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed.
*
- * @param callback The callback of pinned stack animation.
+ * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()}
+ * when it's changed.
*/
- default void setPinnedStackAnimationListener(Consumer<Boolean> callback) {
- }
+ default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {}
/**
* Set the pinned stack with {@link PipAnimationController.AnimationType}
@@ -118,29 +92,4 @@ public interface Pip {
* view hierarchy or destroyed.
*/
default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { }
-
- /**
- * Called when the visibility of keyguard is changed.
- * @param showing {@code true} if keyguard is now showing, {@code false} otherwise.
- * @param animating {@code true} if system is animating between keyguard and surface behind,
- * this only makes sense when showing is {@code false}.
- */
- default void onKeyguardVisibilityChanged(boolean showing, boolean animating) { }
-
- /**
- * Called when the dismissing animation keyguard and surfaces behind is finished.
- * See also {@link #onKeyguardVisibilityChanged(boolean, boolean)}.
- *
- * TODO(b/206741900) deprecate this path once we're able to animate the PiP window as part of
- * keyguard dismiss animation.
- */
- default void onKeyguardDismissAnimationFinished() { }
-
- /**
- * Dump the current state and information if need.
- *
- * @param pw The stream to dump information to.
- */
- default void dump(PrintWriter pw) {
- }
}
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 4eba1697b595..6728c00af51b 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
@@ -29,7 +29,6 @@ import android.annotation.NonNull;
import android.app.TaskInfo;
import android.content.Context;
import android.graphics.Rect;
-import android.view.Choreographer;
import android.view.Surface;
import android.view.SurfaceControl;
import android.window.TaskSnapshot;
@@ -183,7 +182,7 @@ public class PipAnimationController {
return mCurrentAnimator;
}
- PipTransitionAnimator getCurrentAnimator() {
+ public PipTransitionAnimator getCurrentAnimator() {
return mCurrentAnimator;
}
@@ -196,6 +195,17 @@ public class PipAnimationController {
}
/**
+ * Returns true if the PiP window is currently being animated.
+ */
+ public boolean isAnimating() {
+ PipAnimationController.PipTransitionAnimator animator = getCurrentAnimator();
+ if (animator != null && animator.isRunning()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
* Quietly cancel the animator by removing the listeners first.
*/
static void quietCancel(@NonNull ValueAnimator animator) {
@@ -279,14 +289,15 @@ public class PipAnimationController {
mEndValue = endValue;
addListener(this);
addUpdateListener(this);
- mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+ mSurfaceControlTransactionFactory =
+ new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
mTransitionDirection = TRANSITION_DIRECTION_NONE;
}
@Override
public void onAnimationStart(Animator animation) {
mCurrentValue = mStartValue;
- onStartTransaction(mLeash, newSurfaceControlTransaction());
+ onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction());
if (mPipAnimationCallback != null) {
mPipAnimationCallback.onPipAnimationStart(mTaskInfo, this);
}
@@ -294,14 +305,16 @@ public class PipAnimationController {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
- applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(),
+ applySurfaceControlTransaction(mLeash,
+ mSurfaceControlTransactionFactory.getTransaction(),
animation.getAnimatedFraction());
}
@Override
public void onAnimationEnd(Animator animation) {
mCurrentValue = mEndValue;
- final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
onEndTransaction(mLeash, tx, mTransitionDirection);
if (mPipAnimationCallback != null) {
mPipAnimationCallback.onPipAnimationEnd(mTaskInfo, tx, this);
@@ -348,7 +361,8 @@ public class PipAnimationController {
}
void setColorContentOverlay(Context context) {
- final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
if (mContentOverlay != null) {
mContentOverlay.detach(tx);
}
@@ -357,7 +371,8 @@ public class PipAnimationController {
}
void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
- final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
if (mContentOverlay != null) {
mContentOverlay.detach(tx);
}
@@ -406,7 +421,7 @@ public class PipAnimationController {
void setDestinationBounds(Rect destinationBounds) {
mDestinationBounds.set(destinationBounds);
if (mAnimationType == ANIM_TYPE_ALPHA) {
- onStartTransaction(mLeash, newSurfaceControlTransaction());
+ onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction());
}
}
@@ -441,16 +456,6 @@ public class PipAnimationController {
mEndValue = endValue;
}
- /**
- * @return {@link SurfaceControl.Transaction} instance with vsync-id.
- */
- protected SurfaceControl.Transaction newSurfaceControlTransaction() {
- final SurfaceControl.Transaction tx =
- mSurfaceControlTransactionFactory.getTransaction();
- tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
- return tx;
- }
-
@VisibleForTesting
public void setSurfaceControlTransactionFactory(
PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) {
@@ -591,7 +596,7 @@ public class PipAnimationController {
final Rect insets = computeInsets(fraction);
getSurfaceTransactionHelper().scaleAndCrop(tx, leash,
sourceHintRect, initialSourceValue, bounds, insets,
- isInPipDirection);
+ isInPipDirection, fraction);
if (shouldApplyCornerRadius()) {
final Rect sourceBounds = new Rect(initialContainerRect);
sourceBounds.inset(insets);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
index 7397e5273753..cd61dbb5b7d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
@@ -47,6 +47,7 @@ public class PipBoundsAlgorithm {
private final @NonNull PipBoundsState mPipBoundsState;
private final PipSnapAlgorithm mSnapAlgorithm;
+ private final PipKeepClearAlgorithm mPipKeepClearAlgorithm;
private float mDefaultSizePercent;
private float mMinAspectRatioForMinSize;
@@ -60,9 +61,11 @@ public class PipBoundsAlgorithm {
protected Point mScreenEdgeInsets;
public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState,
- @NonNull PipSnapAlgorithm pipSnapAlgorithm) {
+ @NonNull PipSnapAlgorithm pipSnapAlgorithm,
+ @NonNull PipKeepClearAlgorithm pipKeepClearAlgorithm) {
mPipBoundsState = pipBoundsState;
mSnapAlgorithm = pipSnapAlgorithm;
+ mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
reloadResources(context);
// Initialize the aspect ratio to the default aspect ratio. Don't do this in reload
// resources as it would clobber mAspectRatio when entering PiP from fullscreen which
@@ -129,8 +132,21 @@ public class PipBoundsAlgorithm {
return getDefaultBounds(INVALID_SNAP_FRACTION, null /* size */);
}
- /** Returns the destination bounds to place the PIP window on entry. */
+ /**
+ * Returns the destination bounds to place the PIP window on entry.
+ * If there are any keep clear areas registered, the position will try to avoid occluding them.
+ */
public Rect getEntryDestinationBounds() {
+ Rect entryBounds = getEntryDestinationBoundsIgnoringKeepClearAreas();
+ Rect insets = new Rect();
+ getInsetBounds(insets);
+ return mPipKeepClearAlgorithm.findUnoccludedPosition(entryBounds,
+ mPipBoundsState.getRestrictedKeepClearAreas(),
+ mPipBoundsState.getUnrestrictedKeepClearAreas(), insets);
+ }
+
+ /** Returns the destination bounds to place the PIP window on entry. */
+ public Rect getEntryDestinationBoundsIgnoringKeepClearAreas() {
final PipBoundsState.PipReentryState reentryState = mPipBoundsState.getReentryState();
final Rect destinationBounds = reentryState != null
@@ -138,9 +154,10 @@ public class PipBoundsAlgorithm {
: getDefaultBounds();
final boolean useCurrentSize = reentryState != null && reentryState.getSize() != null;
- return transformBoundsToAspectRatioIfValid(destinationBounds,
+ Rect aspectRatioBounds = transformBoundsToAspectRatioIfValid(destinationBounds,
mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,
useCurrentSize);
+ return aspectRatioBounds;
}
/** Returns the current bounds adjusted to the new aspect ratio, if valid. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
index 0e32663955d3..7096a645ef85 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
@@ -111,9 +111,6 @@ public abstract class PipContentOverlay {
private final TaskSnapshot mSnapshot;
private final Rect mSourceRectHint;
- private float mTaskSnapshotScaleX;
- private float mTaskSnapshotScaleY;
-
public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
mSnapshot = snapshot;
mSourceRectHint = new Rect(sourceRectHint);
@@ -125,16 +122,16 @@ public abstract class PipContentOverlay {
@Override
public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
- mTaskSnapshotScaleX = (float) mSnapshot.getTaskSize().x
+ final float taskSnapshotScaleX = (float) mSnapshot.getTaskSize().x
/ mSnapshot.getHardwareBuffer().getWidth();
- mTaskSnapshotScaleY = (float) mSnapshot.getTaskSize().y
+ final float taskSnapshotScaleY = (float) mSnapshot.getTaskSize().y
/ mSnapshot.getHardwareBuffer().getHeight();
tx.show(mLeash);
tx.setLayer(mLeash, Integer.MAX_VALUE);
tx.setBuffer(mLeash, mSnapshot.getHardwareBuffer());
// Relocate the content to parentLeash's coordinates.
tx.setPosition(mLeash, -mSourceRectHint.left, -mSourceRectHint.top);
- tx.setScale(mLeash, mTaskSnapshotScaleX, mTaskSnapshotScaleY);
+ tx.setScale(mLeash, taskSnapshotScaleX, taskSnapshotScaleY);
tx.reparent(mLeash, parentLeash);
tx.apply();
}
@@ -146,20 +143,6 @@ public abstract class PipContentOverlay {
@Override
public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) {
- // Work around to make sure the snapshot overlay is aligned with PiP window before
- // the atomicTx is committed along with the final WindowContainerTransaction.
- final SurfaceControl.Transaction nonAtomicTx = new SurfaceControl.Transaction();
- final float scaleX = (float) destinationBounds.width()
- / mSourceRectHint.width();
- final float scaleY = (float) destinationBounds.height()
- / mSourceRectHint.height();
- final float scale = Math.max(
- scaleX * mTaskSnapshotScaleX, scaleY * mTaskSnapshotScaleY);
- nonAtomicTx.setScale(mLeash, scale, scale);
- nonAtomicTx.setPosition(mLeash,
- -scale * mSourceRectHint.left / mTaskSnapshotScaleX,
- -scale * mSourceRectHint.top / mTaskSnapshotScaleY);
- nonAtomicTx.apply();
atomicTx.remove(mLeash);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
new file mode 100644
index 000000000000..e3495e100c62
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
@@ -0,0 +1,53 @@
+/*
+ * 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.pip;
+
+import android.graphics.Rect;
+
+import java.util.Set;
+
+/**
+ * Interface for interacting with keep clear algorithm used to move PiP window out of the way of
+ * keep clear areas.
+ */
+public interface PipKeepClearAlgorithm {
+
+ /**
+ * Adjust the position of picture in picture window based on the registered keep clear areas.
+ * @param pipBoundsState state of the PiP to use for the calculations
+ * @param pipBoundsAlgorithm algorithm implementation used to get the entry destination bounds
+ * @return
+ */
+ default Rect adjust(PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm) {
+ return pipBoundsState.getBounds();
+ }
+
+ /**
+ * Calculate the bounds so that none of the keep clear areas are occluded, while the bounds stay
+ * within the allowed bounds. If such position is not feasible, return original bounds.
+ * @param defaultBounds initial bounds used in the calculation
+ * @param restrictedKeepClearAreas registered restricted keep clear areas
+ * @param unrestrictedKeepClearAreas registered unrestricted keep clear areas
+ * @param allowedBounds bounds that define the allowed space for the output, result will always
+ * be inside those bounds
+ * @return bounds that don't cover any of the keep clear areas and are within allowed bounds
+ */
+ default Rect findUnoccludedPosition(Rect defaultBounds, Set<Rect> restrictedKeepClearAreas,
+ Set<Rect> unrestrictedKeepClearAreas, Rect allowedBounds) {
+ return defaultBounds;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index a017a2674359..b9746e338ced 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.view.Choreographer;
import android.view.SurfaceControl;
import com.android.wm.shell.R;
@@ -39,6 +40,10 @@ public class PipSurfaceTransactionHelper {
private int mCornerRadius;
private int mShadowRadius;
+ public PipSurfaceTransactionHelper(Context context) {
+ onDensityOrFontScaleChanged(context);
+ }
+
/**
* Called when display size or font size of settings changed
*
@@ -104,7 +109,7 @@ public class PipSurfaceTransactionHelper {
public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx,
SurfaceControl leash, Rect sourceRectHint,
Rect sourceBounds, Rect destinationBounds, Rect insets,
- boolean isInPipDirection) {
+ boolean isInPipDirection, float fraction) {
mTmpDestinationRect.set(sourceBounds);
// Similar to {@link #scale}, we want to position the surface relative to the screen
// coordinates so offset the bounds to 0,0
@@ -116,9 +121,13 @@ public class PipSurfaceTransactionHelper {
if (isInPipDirection
&& sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) {
// scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only.
- scale = sourceBounds.width() <= sourceBounds.height()
+ final float endScale = sourceBounds.width() <= sourceBounds.height()
? (float) destinationBounds.width() / sourceRectHint.width()
: (float) destinationBounds.height() / sourceRectHint.height();
+ final float startScale = sourceBounds.width() <= sourceBounds.height()
+ ? (float) destinationBounds.width() / sourceBounds.width()
+ : (float) destinationBounds.height() / sourceBounds.height();
+ scale = (1 - fraction) * startScale + fraction * endScale;
} else {
scale = sourceBounds.width() <= sourceBounds.height()
? (float) destinationBounds.width() / sourceBounds.width()
@@ -226,4 +235,18 @@ public class PipSurfaceTransactionHelper {
public interface SurfaceControlTransactionFactory {
SurfaceControl.Transaction getTransaction();
}
+
+ /**
+ * Implementation of {@link SurfaceControlTransactionFactory} that returns
+ * {@link SurfaceControl.Transaction} with VsyncId being set.
+ */
+ public static class VsyncSurfaceControlTransactionFactory
+ implements SurfaceControlTransactionFactory {
+ @Override
+ public SurfaceControl.Transaction getTransaction() {
+ final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+ return tx;
+ }
+ }
}
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 e624de661737..f170e774739f 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
@@ -62,6 +62,7 @@ import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -126,7 +127,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private final PipBoundsAlgorithm mPipBoundsAlgorithm;
private final @NonNull PipMenuController mPipMenuController;
private final PipAnimationController mPipAnimationController;
- private final PipTransitionController mPipTransitionController;
+ protected final PipTransitionController mPipTransitionController;
protected final PipParamsChangedForwarder mPipParamsChangedForwarder;
private final PipUiEventLogger mPipUiEventLoggerLogger;
private final int mEnterAnimationDuration;
@@ -195,6 +196,26 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
};
+ @VisibleForTesting
+ final PipTransitionController.PipTransitionCallback mPipTransitionCallback =
+ new PipTransitionController.PipTransitionCallback() {
+ @Override
+ public void onPipTransitionStarted(int direction, Rect pipBounds) {}
+
+ @Override
+ public void onPipTransitionFinished(int direction) {
+ // Apply the deferred RunningTaskInfo if applicable after all proper callbacks
+ // are sent.
+ if (direction == TRANSITION_DIRECTION_TO_PIP && mDeferredTaskInfo != null) {
+ onTaskInfoChanged(mDeferredTaskInfo);
+ mDeferredTaskInfo = null;
+ }
+ }
+
+ @Override
+ public void onPipTransitionCanceled(int direction) {}
+ };
+
private final PipAnimationController.PipTransactionHandler mPipTransactionHandler =
new PipAnimationController.PipTransactionHandler() {
@Override
@@ -215,7 +236,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private ActivityManager.RunningTaskInfo mDeferredTaskInfo;
private WindowContainerToken mToken;
private SurfaceControl mLeash;
- private PipTransitionState mPipTransitionState;
+ protected PipTransitionState mPipTransitionState;
private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
private long mLastOneShotAlphaAnimationTime;
private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
@@ -283,7 +304,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mSurfaceTransactionHelper = surfaceTransactionHelper;
mPipAnimationController = pipAnimationController;
mPipUiEventLoggerLogger = pipUiEventLogger;
- mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+ mSurfaceControlTransactionFactory =
+ new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
mSplitScreenOptional = splitScreenOptional;
mTaskOrganizer = shellTaskOrganizer;
mMainExecutor = mainExecutor;
@@ -295,6 +317,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mTaskOrganizer.addFocusListener(this);
mPipTransitionController.setPipOrganizer(this);
displayController.addDisplayWindowListener(this);
+ pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback);
+ }
+
+ public PipTransitionController getTransitionController() {
+ return mPipTransitionController;
}
public Rect getCurrentOrAnimatingBounds() {
@@ -440,7 +467,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// When exit to fullscreen with Shell transition enabled, we update the Task windowing
// mode directly so that it can also trigger display rotation and visibility update in
// the same transition if there will be any.
- wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+ wct.setWindowingMode(mToken, getOutPipWindowingMode());
// We can inherit the parent bounds as it is going to be fullscreen. The
// destinationBounds calculated above will be incorrect if this is with rotation.
wct.setBounds(mToken, null);
@@ -458,7 +485,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
// Cancel the existing animator if there is any.
- cancelCurrentAnimator();
+ // TODO(b/232439933): this is disabled temporarily to unblock b/234502692.
+ // cancelCurrentAnimator();
// Set the exiting state first so if there is fixed rotation later, the running animation
// won't be interrupted by alpha animation for existing PiP.
@@ -538,7 +566,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setBounds(mToken, null);
- wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+ wct.setWindowingMode(mToken, getOutPipWindowingMode());
wct.reorder(mToken, false);
mPipTransitionController.startExitTransition(TRANSIT_REMOVE_PIP, wct,
null /* destinationBounds */);
@@ -767,11 +795,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP);
}
mPipTransitionController.sendOnPipTransitionFinished(direction);
- // Apply the deferred RunningTaskInfo if applicable after all proper callbacks are sent.
- if (direction == TRANSITION_DIRECTION_TO_PIP && mDeferredTaskInfo != null) {
- onTaskInfoChanged(mDeferredTaskInfo);
- mDeferredTaskInfo = null;
- }
}
private void sendOnPipTransitionCancelled(
@@ -925,6 +948,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
/** Called when exiting PIP transition is finished to do the state cleanup. */
void onExitPipFinished(TaskInfo info) {
+ if (mLeash == null) {
+ // TODO(239461594): Remove once the double call to onExitPipFinished() is fixed
+ Log.w(TAG, "Warning, onExitPipFinished() called multiple times in the same sessino");
+ return;
+ }
+
clearWaitForFixedRotation();
if (mSwipePipToHomeOverlay != null) {
removeContentOverlay(mSwipePipToHomeOverlay, null /* callback */);
@@ -938,6 +967,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mPipBoundsState.setBounds(new Rect());
mPipUiEventLoggerLogger.setTaskInfo(null);
mPipMenuController.detach();
+ mLeash = null;
if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) {
mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY);
@@ -1276,6 +1306,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
return;
}
+ if (mLeash == null || !mLeash.isValid()) {
+ Log.e(TAG, String.format("scheduleFinishResizePip with null leash! mState=%d",
+ mPipTransitionState.getTransitionState()));
+ return;
+ }
+
finishResize(createFinishResizeSurfaceTransaction(destinationBounds), destinationBounds,
direction, -1);
if (updateBoundsCallback != null) {
@@ -1467,6 +1503,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
"%s: Abort animation, invalid leash", TAG);
return null;
}
+ if (isInPipDirection(direction)
+ && !isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) {
+ // The given source rect hint is too small for enter PiP animation, reset it to null.
+ sourceHintRect = null;
+ }
final int rotationDelta = mWaitForFixedRotation
? deltaRotation(mCurrentRotation, mNextRotation)
: Surface.ROTATION_0;
@@ -1541,6 +1582,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
/**
+ * This is a situation in which the source rect hint on at least one axis is smaller
+ * than the destination bounds, which represents a problem because we would have to scale
+ * up that axis to fit the bounds. So instead, just fallback to the non-source hint
+ * animation in this case.
+ *
+ * @return {@code false} if the given source is too small to use for the entering animation.
+ */
+ private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) {
+ return sourceRectHint != null
+ && sourceRectHint.width() > destinationBounds.width()
+ && sourceRectHint.height() > destinationBounds.height();
+ }
+
+ /**
* Sync with {@link SplitScreenController} on destination bounds if PiP is going to
* split screen.
*
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 36e712459863..33761d23379d 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
@@ -28,7 +28,6 @@ import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.transitTypeToString;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
-import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS;
@@ -43,6 +42,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SP
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
import static com.android.wm.shell.transition.Transitions.isOpeningType;
+import android.animation.Animator;
import android.app.ActivityManager;
import android.app.TaskInfo;
import android.content.Context;
@@ -52,6 +52,7 @@ import android.graphics.Rect;
import android.os.IBinder;
import android.view.Surface;
import android.view.SurfaceControl;
+import android.view.WindowManager;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerToken;
@@ -65,6 +66,7 @@ import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.CounterRotatorHelper;
import com.android.wm.shell.transition.Transitions;
@@ -106,17 +108,18 @@ public class PipTransition extends PipTransitionController {
private boolean mHasFadeOut;
public PipTransition(Context context,
+ @NonNull ShellInit shellInit,
+ @NonNull ShellTaskOrganizer shellTaskOrganizer,
+ @NonNull Transitions transitions,
PipBoundsState pipBoundsState,
PipTransitionState pipTransitionState,
PipMenuController pipMenuController,
PipBoundsAlgorithm pipBoundsAlgorithm,
PipAnimationController pipAnimationController,
- Transitions transitions,
- @NonNull ShellTaskOrganizer shellTaskOrganizer,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
Optional<SplitScreenController> splitScreenOptional) {
- super(pipBoundsState, pipMenuController, pipBoundsAlgorithm,
- pipAnimationController, transitions, shellTaskOrganizer);
+ super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
+ pipBoundsAlgorithm, pipAnimationController);
mContext = context;
mPipTransitionState = pipTransitionState;
mEnterExitAnimationDuration = context.getResources()
@@ -145,6 +148,11 @@ public class PipTransition extends PipTransitionController {
if (destinationBounds != null) {
mExitDestinationBounds.set(destinationBounds);
}
+ final PipAnimationController.PipTransitionAnimator animator =
+ mPipAnimationController.getCurrentAnimator();
+ if (animator != null && animator.isRunning()) {
+ animator.cancel();
+ }
mExitTransition = mTransitions.startTransition(type, out, this);
}
@@ -217,13 +225,20 @@ public class PipTransition extends PipTransitionController {
}
// Entering PIP.
- if (isEnteringPip(info, mCurrentPipTaskToken)) {
- return startEnterAnimation(info, startTransaction, finishTransaction, finishCallback);
+ if (isEnteringPip(info)) {
+ startEnterAnimation(info, startTransaction, finishTransaction, finishCallback);
+ return true;
}
// For transition that we don't animate, but contains the PIP leash, we need to update the
// PIP surface, otherwise it will be reset after the transition.
if (currentPipTaskChange != null) {
+ // Set the "end" bounds of pip. The default setup uses the start bounds. Since this is
+ // changing the *finish*Transaction, we need to use the end bounds. This will also
+ // make sure that the fade-in animation (below) uses the end bounds as well.
+ if (!currentPipTaskChange.getEndAbsBounds().isEmpty()) {
+ mPipBoundsState.setBounds(currentPipTaskChange.getEndAbsBounds());
+ }
updatePipForUnhandledTransition(currentPipTaskChange, startTransaction,
finishTransaction);
}
@@ -236,6 +251,13 @@ public class PipTransition extends PipTransitionController {
return false;
}
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ end();
+ }
+
/** Helper to identify whether this handler is currently the one playing an animation */
private boolean isAnimatingLocally() {
return mFinishTransaction != null;
@@ -245,16 +267,9 @@ public class PipTransition extends PipTransitionController {
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
- if (request.getType() == TRANSIT_PIP) {
+ if (requestHasPipEnter(request)) {
WindowContainerTransaction wct = new WindowContainerTransaction();
- if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
- mRequestedEnterTransition = transition;
- mRequestedEnterTask = request.getTriggerTask().token;
- wct.setActivityWindowingMode(request.getTriggerTask().token,
- WINDOWING_MODE_UNDEFINED);
- final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
- wct.setBounds(request.getTriggerTask().token, destinationBounds);
- }
+ augmentRequest(transition, request, wct);
return wct;
} else {
return null;
@@ -262,6 +277,29 @@ public class PipTransition extends PipTransitionController {
}
@Override
+ public void augmentRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request, @NonNull WindowContainerTransaction outWCT) {
+ if (!requestHasPipEnter(request)) {
+ throw new IllegalStateException("Called PiP augmentRequest when request has no PiP");
+ }
+ if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ mRequestedEnterTransition = transition;
+ mRequestedEnterTask = request.getTriggerTask().token;
+ outWCT.setActivityWindowingMode(request.getTriggerTask().token,
+ WINDOWING_MODE_UNDEFINED);
+ final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
+ outWCT.setBounds(request.getTriggerTask().token, destinationBounds);
+ }
+ }
+
+ @Override
+ public void end() {
+ Animator animator = mPipAnimationController.getCurrentAnimator();
+ if (animator == null) return;
+ animator.end();
+ }
+
+ @Override
public boolean handleRotateDisplay(int startRotation, int endRotation,
WindowContainerTransaction wct) {
if (mRequestedEnterTransition != null && mOneShotAnimationType == ANIM_TYPE_ALPHA) {
@@ -279,7 +317,8 @@ public class PipTransition extends PipTransitionController {
}
@Override
- public void onTransitionMerged(@NonNull IBinder transition) {
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishT) {
if (transition != mExitTransition) {
return;
}
@@ -292,7 +331,7 @@ public class PipTransition extends PipTransitionController {
}
// Unset exitTransition AFTER cancel so that finishResize knows we are merging.
mExitTransition = null;
- if (!cancelled) return;
+ if (!cancelled || aborted) return;
final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo();
if (taskInfo != null) {
startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(),
@@ -315,11 +354,27 @@ public class PipTransition extends PipTransitionController {
// (likely a remote like launcher), so don't fire the finish-callback here -- wait until
// the exit transition is merged.
if ((mExitTransition == null || isAnimatingLocally()) && mFinishCallback != null) {
- WindowContainerTransaction wct = new WindowContainerTransaction();
- prepareFinishResizeTransaction(taskInfo, destinationBounds,
- direction, wct);
- if (tx != null) {
- wct.setBoundsChangeTransaction(taskInfo.token, tx);
+ WindowContainerTransaction wct = null;
+ if (isOutPipDirection(direction)) {
+ // Only need to reset surface properties. The server-side operations were already
+ // done at the start.
+ if (tx != null) {
+ mFinishTransaction.merge(tx);
+ }
+ } else {
+ wct = new WindowContainerTransaction();
+ if (isInPipDirection(direction)) {
+ // If we are animating from fullscreen using a bounds animation, then reset the
+ // activity windowing mode, and set the task bounds to the final bounds
+ wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ wct.scheduleFinishEnterPip(taskInfo.token, destinationBounds);
+ wct.setBounds(taskInfo.token, destinationBounds);
+ } else {
+ wct.setBounds(taskInfo.token, null /* bounds */);
+ }
+ if (tx != null) {
+ wct.setBoundsChangeTransaction(taskInfo.token, tx);
+ }
}
final SurfaceControl leash = mPipOrganizer.getSurfaceControl();
final int displayRotation = taskInfo.getConfiguration().windowConfiguration
@@ -559,92 +614,94 @@ public class PipTransition extends PipTransitionController {
}
/** Whether we should handle the given {@link TransitionInfo} animation as entering PIP. */
- private static boolean isEnteringPip(@NonNull TransitionInfo info,
- @Nullable WindowContainerToken currentPipTaskToken) {
+ private boolean isEnteringPip(@NonNull TransitionInfo info) {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
- if (change.getTaskInfo() != null
- && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED
- && !change.getContainer().equals(currentPipTaskToken)) {
- // We support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps
- // that enter PiP instantly on opening, mostly from CTS/Flicker tests)
- if (info.getType() == TRANSIT_PIP || info.getType() == TRANSIT_OPEN) {
- return true;
- }
- // This can happen if the request to enter PIP happens when we are collecting for
- // another transition, such as TRANSIT_CHANGE (display rotation).
- if (info.getType() == TRANSIT_CHANGE) {
- return true;
- }
+ if (isEnteringPip(change, info.getType())) return true;
+ }
+ return false;
+ }
- // Please file a bug to handle the unexpected transition type.
- throw new IllegalStateException("Entering PIP with unexpected transition type="
- + transitTypeToString(info.getType()));
+ /** Whether a particular change is a window that is entering pip. */
+ @Override
+ public boolean isEnteringPip(@NonNull TransitionInfo.Change change,
+ @WindowManager.TransitionType int transitType) {
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED
+ && !change.getContainer().equals(mCurrentPipTaskToken)) {
+ // We support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps
+ // that enter PiP instantly on opening, mostly from CTS/Flicker tests)
+ if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN) {
+ return true;
}
+ // This can happen if the request to enter PIP happens when we are collecting for
+ // another transition, such as TRANSIT_CHANGE (display rotation).
+ if (transitType == TRANSIT_CHANGE) {
+ return true;
+ }
+
+ // Please file a bug to handle the unexpected transition type.
+ throw new IllegalStateException("Entering PIP with unexpected transition type="
+ + transitTypeToString(transitType));
}
return false;
}
- private boolean startEnterAnimation(@NonNull TransitionInfo info,
+ private void startEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- // Search for an Enter PiP transition (along with a show wallpaper one)
+ // Search for an Enter PiP transition
TransitionInfo.Change enterPip = null;
- TransitionInfo.Change wallpaper = null;
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if (change.getTaskInfo() != null
&& change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
enterPip = change;
- } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
- wallpaper = change;
}
}
if (enterPip == null) {
- return false;
+ throw new IllegalStateException("Trying to start PiP animation without a pip"
+ + "participant");
}
- // Keep track of the PIP task.
- mCurrentPipTaskToken = enterPip.getContainer();
- mHasFadeOut = false;
- if (mFinishCallback != null) {
- callFinishCallback(null /* wct */);
- mFinishTransaction = null;
- throw new RuntimeException("Previous callback not called, aborting entering PIP.");
- }
-
- // Show the wallpaper if there is a wallpaper change.
- if (wallpaper != null) {
- startTransaction.show(wallpaper.getLeash());
- startTransaction.setAlpha(wallpaper.getLeash(), 1.f);
- }
// Make sure other open changes are visible as entering PIP. Some may be hidden in
// Transitions#setupStartState because the transition type is OPEN (such as auto-enter).
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
- if (change == enterPip || change == wallpaper) {
- continue;
- }
+ if (change == enterPip) continue;
if (isOpeningType(change.getMode())) {
final SurfaceControl leash = change.getLeash();
startTransaction.show(leash).setAlpha(leash, 1.f);
}
}
+ startEnterAnimation(enterPip, startTransaction, finishTransaction, finishCallback);
+ }
+
+ @Override
+ public void startEnterAnimation(@NonNull final TransitionInfo.Change pipChange,
+ @NonNull final SurfaceControl.Transaction startTransaction,
+ @NonNull final SurfaceControl.Transaction finishTransaction,
+ @NonNull final Transitions.TransitionFinishCallback finishCallback) {
+ if (mFinishCallback != null) {
+ callFinishCallback(null /* wct */);
+ mFinishTransaction = null;
+ throw new RuntimeException("Previous callback not called, aborting entering PIP.");
+ }
+
+ // Keep track of the PIP task and animation.
+ mCurrentPipTaskToken = pipChange.getContainer();
+ mHasFadeOut = false;
mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
mFinishCallback = finishCallback;
mFinishTransaction = finishTransaction;
- final int endRotation = mInFixedRotation ? mEndFixedRotation : enterPip.getEndRotation();
- return startEnterAnimation(enterPip.getTaskInfo(), enterPip.getLeash(),
- startTransaction, finishTransaction, enterPip.getStartRotation(),
- endRotation);
- }
- private boolean startEnterAnimation(final TaskInfo taskInfo, final SurfaceControl leash,
- final SurfaceControl.Transaction startTransaction,
- final SurfaceControl.Transaction finishTransaction,
- final int startRotation, final int endRotation) {
+ final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo();
+ final SurfaceControl leash = pipChange.getLeash();
+ final int startRotation = pipChange.getStartRotation();
+ final int endRotation = mInFixedRotation ? mEndFixedRotation : pipChange.getEndRotation();
+
setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams,
taskInfo.topActivityInfo);
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
@@ -657,12 +714,11 @@ public class PipTransition extends PipTransitionController {
computeEnterPipRotatedBounds(rotationDelta, startRotation, endRotation, taskInfo,
destinationBounds, sourceHintRect);
}
- PipAnimationController.PipTransitionAnimator animator;
// Set corner radius for entering pip.
mSurfaceTransactionHelper
.crop(finishTransaction, leash, destinationBounds)
.round(finishTransaction, leash, true /* applyCornerRadius */);
- mPipMenuController.attach(leash);
+ mTransitions.getMainExecutor().executeDelayed(() -> mPipMenuController.attach(leash), 0);
if (taskInfo.pictureInPictureParams != null
&& taskInfo.pictureInPictureParams.isAutoEnterEnabled()
@@ -694,7 +750,7 @@ public class PipTransition extends PipTransitionController {
null /* callback */, false /* withStartDelay */);
}
mPipTransitionState.setInSwipePipToHomeTransition(false);
- return true;
+ return;
}
if (rotationDelta != Surface.ROTATION_0) {
@@ -702,6 +758,12 @@ public class PipTransition extends PipTransitionController {
tmpTransform.postRotate(rotationDelta);
startTransaction.setMatrix(leash, tmpTransform, new float[9]);
}
+ if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ startTransaction.setAlpha(leash, 0f);
+ }
+ startTransaction.apply();
+
+ PipAnimationController.PipTransitionAnimator animator;
if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds,
currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
@@ -712,7 +774,6 @@ public class PipTransition extends PipTransitionController {
animator.setColorContentOverlay(mContext);
}
} else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
- startTransaction.setAlpha(leash, 0f);
animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,
0f, 1f);
mOneShotAnimationType = ANIM_TYPE_BOUNDS;
@@ -720,7 +781,6 @@ public class PipTransition extends PipTransitionController {
throw new RuntimeException("Unrecognized animation type: "
+ mOneShotAnimationType);
}
- startTransaction.apply();
animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP)
.setPipAnimationCallback(mPipAnimationCallback)
.setDuration(mEnterExitAnimationDuration);
@@ -731,8 +791,6 @@ public class PipTransition extends PipTransitionController {
animator.setDestinationBounds(mPipBoundsAlgorithm.getEntryDestinationBounds());
}
animator.start();
-
- return true;
}
/** Computes destination bounds in old rotation and updates source hint rect if available. */
@@ -852,27 +910,4 @@ public class PipTransition extends PipTransitionController {
mPipMenuController.movePipMenu(null, null, destinationBounds);
mPipMenuController.updateMenuBounds(destinationBounds);
}
-
- private void prepareFinishResizeTransaction(TaskInfo taskInfo, Rect destinationBounds,
- @PipAnimationController.TransitionDirection int direction,
- WindowContainerTransaction wct) {
- Rect taskBounds = null;
- if (isInPipDirection(direction)) {
- // If we are animating from fullscreen using a bounds animation, then reset the
- // activity windowing mode set by WM, and set the task bounds to the final bounds
- taskBounds = destinationBounds;
- wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
- wct.scheduleFinishEnterPip(taskInfo.token, destinationBounds);
- } else if (isOutPipDirection(direction)) {
- // If we are animating to fullscreen, then we need to reset the override bounds
- // on the task to ensure that the task "matches" the parent's bounds.
- taskBounds = (direction == TRANSITION_DIRECTION_LEAVE_PIP)
- ? null : destinationBounds;
- wct.setWindowingMode(taskInfo.token, getOutPipWindowingMode());
- // Simply reset the activity mode set prior to the animation running.
- wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
- }
-
- wct.setBounds(taskInfo.token, taskBounds);
- }
}
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 54f46e0c9938..f51e247fe112 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
@@ -17,6 +17,7 @@
package com.android.wm.shell.pip;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.WindowManager.TRANSIT_PIP;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection;
@@ -27,12 +28,17 @@ import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
+import android.os.IBinder;
import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
+import androidx.annotation.NonNull;
+
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import java.util.ArrayList;
@@ -49,7 +55,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH
protected final ShellTaskOrganizer mShellTaskOrganizer;
protected final PipMenuController mPipMenuController;
protected final Transitions mTransitions;
- private final Handler mMainHandler;
private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
protected PipTaskOrganizer mPipOrganizer;
@@ -127,22 +132,28 @@ public abstract class PipTransitionController implements Transitions.TransitionH
public void onFixedRotationStarted() {
}
- public PipTransitionController(PipBoundsState pipBoundsState,
+ public PipTransitionController(
+ @NonNull ShellInit shellInit,
+ @NonNull ShellTaskOrganizer shellTaskOrganizer,
+ @NonNull Transitions transitions,
+ PipBoundsState pipBoundsState,
PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm,
- PipAnimationController pipAnimationController, Transitions transitions,
- @android.annotation.NonNull ShellTaskOrganizer shellTaskOrganizer) {
+ PipAnimationController pipAnimationController) {
mPipBoundsState = pipBoundsState;
mPipMenuController = pipMenuController;
mShellTaskOrganizer = shellTaskOrganizer;
mPipBoundsAlgorithm = pipBoundsAlgorithm;
mPipAnimationController = pipAnimationController;
mTransitions = transitions;
- mMainHandler = new Handler(Looper.getMainLooper());
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- transitions.addHandler(this);
+ shellInit.addInitCallback(this::onInit, this);
}
}
+ private void onInit() {
+ mTransitions.addHandler(this);
+ }
+
void setPipOrganizer(PipTaskOrganizer pto) {
mPipOrganizer = pto;
}
@@ -206,6 +217,34 @@ public abstract class PipTransitionController implements Transitions.TransitionH
return false;
}
+ /** @return whether the transition-request represents a pip-entry. */
+ public boolean requestHasPipEnter(@NonNull TransitionRequestInfo request) {
+ return request.getType() == TRANSIT_PIP;
+ }
+
+ /** Whether a particular change is a window that is entering pip. */
+ public boolean isEnteringPip(@NonNull TransitionInfo.Change change,
+ @WindowManager.TransitionType int transitType) {
+ return false;
+ }
+
+ /** Add PiP-related changes to `outWCT` for the given request. */
+ public void augmentRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request, @NonNull WindowContainerTransaction outWCT) {
+ throw new IllegalStateException("Request isn't entering PiP");
+ }
+
+ /** Play a transition animation for entering PiP on a specific PiP change. */
+ public void startEnterAnimation(@NonNull final TransitionInfo.Change pipChange,
+ @NonNull final SurfaceControl.Transaction startTransaction,
+ @NonNull final SurfaceControl.Transaction finishTransaction,
+ @NonNull final Transitions.TransitionFinishCallback finishCallback) {
+ }
+
+ /** End the currently-playing PiP animation. */
+ public void end() {
+ }
+
/**
* Callback interface for PiP transitions (both from and to PiP mode)
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
index 85e56b7dd99f..c6b5ce93fd35 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
@@ -17,12 +17,15 @@
package com.android.wm.shell.pip;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.app.PictureInPictureParams;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
/**
* Used to keep track of PiP leash state as it appears and animates by {@link PipTaskOrganizer} and
@@ -37,6 +40,9 @@ public class PipTransitionState {
public static final int ENTERED_PIP = 4;
public static final int EXITING_PIP = 5;
+ private final List<OnPipTransitionStateChangedListener> mOnPipTransitionStateChangedListeners =
+ new ArrayList<>();
+
/**
* If set to {@code true}, no entering PiP transition would be kicked off and most likely
* it's due to the fact that Launcher is handling the transition directly when swiping
@@ -65,7 +71,13 @@ public class PipTransitionState {
}
public void setTransitionState(@TransitionState int state) {
- mState = state;
+ if (mState != state) {
+ for (int i = 0; i < mOnPipTransitionStateChangedListeners.size(); i++) {
+ mOnPipTransitionStateChangedListeners.get(i).onPipTransitionStateChanged(
+ mState, state);
+ }
+ mState = state;
+ }
}
public @TransitionState int getTransitionState() {
@@ -73,8 +85,12 @@ public class PipTransitionState {
}
public boolean isInPip() {
- return mState >= TASK_APPEARED
- && mState != EXITING_PIP;
+ return isInPip(mState);
+ }
+
+ /** Returns true if activity has fully entered PiP mode. */
+ public boolean hasEnteredPip() {
+ return hasEnteredPip(mState);
}
public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) {
@@ -94,4 +110,28 @@ public class PipTransitionState {
return mState < ENTERING_PIP
|| mState == EXITING_PIP;
}
+
+ public void addOnPipTransitionStateChangedListener(
+ @NonNull OnPipTransitionStateChangedListener listener) {
+ mOnPipTransitionStateChangedListeners.add(listener);
+ }
+
+ public void removeOnPipTransitionStateChangedListener(
+ @NonNull OnPipTransitionStateChangedListener listener) {
+ mOnPipTransitionStateChangedListeners.remove(listener);
+ }
+
+ public static boolean isInPip(@TransitionState int state) {
+ return state >= TASK_APPEARED && state != EXITING_PIP;
+ }
+
+ /** Returns true if activity has fully entered PiP mode. */
+ public static boolean hasEnteredPip(@TransitionState int state) {
+ return state == ENTERED_PIP;
+ }
+
+ public interface OnPipTransitionStateChangedListener {
+ void onPipTransitionStateChanged(@TransitionState int oldState,
+ @TransitionState int newState);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
index dc60bcf742ce..fa0061982c45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
@@ -83,7 +83,9 @@ public class PipUtils {
public static boolean remoteActionsMatch(RemoteAction action1, RemoteAction action2) {
if (action1 == action2) return true;
if (action1 == null || action2 == null) return false;
- return Objects.equals(action1.getTitle(), action2.getTitle())
+ return action1.isEnabled() == action2.isEnabled()
+ && action1.shouldShowIcon() == action2.shouldShowIcon()
+ && Objects.equals(action1.getTitle(), action2.getTitle())
&& Objects.equals(action1.getContentDescription(), action2.getContentDescription())
&& Objects.equals(action1.getActionIntent(), action2.getActionIntent());
}
@@ -116,7 +118,7 @@ public class PipUtils {
if (taskId <= 0) return null;
try {
return ActivityTaskManager.getService().getTaskSnapshot(
- taskId, isLowResolution);
+ taskId, isLowResolution, false /* takeSnapshotIfNeeded */);
} catch (RemoteException e) {
Log.e(TAG, "Failed to get task snapshot, taskId=" + taskId, e);
return null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
new file mode 100644
index 000000000000..84071e08d472
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
@@ -0,0 +1,142 @@
+/*
+ * 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.pip.phone;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.util.ArraySet;
+import android.view.Gravity;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+
+import java.util.Set;
+
+/**
+ * Calculates the adjusted position that does not occlude keep clear areas.
+ */
+public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithm {
+
+ protected int mKeepClearAreasPadding;
+
+ public PhonePipKeepClearAlgorithm(Context context) {
+ reloadResources(context);
+ }
+
+ private void reloadResources(Context context) {
+ final Resources res = context.getResources();
+ mKeepClearAreasPadding = res.getDimensionPixelSize(R.dimen.pip_keep_clear_areas_padding);
+ }
+
+ /**
+ * Adjusts the current position of PiP to avoid occluding keep clear areas. This will push PiP
+ * towards the closest edge and then apply calculations to avoid occluding keep clear areas.
+ */
+ public Rect adjust(PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm) {
+ Rect startingBounds = pipBoundsState.getBounds().isEmpty()
+ ? pipBoundsAlgorithm.getEntryDestinationBoundsIgnoringKeepClearAreas()
+ : pipBoundsState.getBounds();
+ float snapFraction = pipBoundsAlgorithm.getSnapFraction(startingBounds);
+ int verticalGravity = Gravity.BOTTOM;
+ int horizontalGravity;
+ if (snapFraction >= 0.5f && snapFraction < 2.5f) {
+ horizontalGravity = Gravity.RIGHT;
+ } else {
+ horizontalGravity = Gravity.LEFT;
+ }
+ // push the bounds based on the gravity
+ Rect insets = new Rect();
+ pipBoundsAlgorithm.getInsetBounds(insets);
+ if (pipBoundsState.isImeShowing()) {
+ insets.bottom -= pipBoundsState.getImeHeight();
+ }
+ Rect pushedBounds = new Rect(startingBounds);
+ if (verticalGravity == Gravity.BOTTOM) {
+ pushedBounds.offsetTo(pushedBounds.left,
+ insets.bottom - pushedBounds.height());
+ }
+ if (horizontalGravity == Gravity.RIGHT) {
+ pushedBounds.offsetTo(insets.right - pushedBounds.width(), pushedBounds.top);
+ } else {
+ pushedBounds.offsetTo(insets.left, pushedBounds.top);
+ }
+ return findUnoccludedPosition(pushedBounds, pipBoundsState.getRestrictedKeepClearAreas(),
+ pipBoundsState.getUnrestrictedKeepClearAreas(), insets);
+ }
+
+ /** Returns a new {@code Rect} that does not occlude the provided keep clear areas. */
+ public Rect findUnoccludedPosition(Rect defaultBounds, Set<Rect> restrictedKeepClearAreas,
+ Set<Rect> unrestrictedKeepClearAreas, Rect allowedBounds) {
+ if (restrictedKeepClearAreas.isEmpty() && unrestrictedKeepClearAreas.isEmpty()) {
+ return defaultBounds;
+ }
+ Set<Rect> keepClearAreas = new ArraySet<>();
+ if (!restrictedKeepClearAreas.isEmpty()) {
+ keepClearAreas.addAll(restrictedKeepClearAreas);
+ }
+ if (!unrestrictedKeepClearAreas.isEmpty()) {
+ keepClearAreas.addAll(unrestrictedKeepClearAreas);
+ }
+ Rect outBounds = new Rect(defaultBounds);
+ for (Rect r : keepClearAreas) {
+ Rect tmpRect = new Rect(r);
+ // add extra padding to the keep clear area
+ tmpRect.inset(-mKeepClearAreasPadding, -mKeepClearAreasPadding);
+ if (Rect.intersects(r, outBounds)) {
+ if (tryOffsetUp(outBounds, tmpRect, allowedBounds)) continue;
+ if (tryOffsetLeft(outBounds, tmpRect, allowedBounds)) continue;
+ if (tryOffsetDown(outBounds, tmpRect, allowedBounds)) continue;
+ if (tryOffsetRight(outBounds, tmpRect, allowedBounds)) continue;
+ }
+ }
+ return outBounds;
+ }
+
+ private static boolean tryOffsetLeft(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds) {
+ return tryOffset(rectToMove, rectToAvoid, allowedBounds,
+ rectToAvoid.left - rectToMove.right, 0);
+ }
+
+ private static boolean tryOffsetRight(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds) {
+ return tryOffset(rectToMove, rectToAvoid, allowedBounds,
+ rectToAvoid.right - rectToMove.left, 0);
+ }
+
+ private static boolean tryOffsetUp(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds) {
+ return tryOffset(rectToMove, rectToAvoid, allowedBounds,
+ 0, rectToAvoid.top - rectToMove.bottom);
+ }
+
+ private static boolean tryOffsetDown(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds) {
+ return tryOffset(rectToMove, rectToAvoid, allowedBounds,
+ 0, rectToAvoid.bottom - rectToMove.top);
+ }
+
+ private static boolean tryOffset(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds,
+ int dx, int dy) {
+ Rect tmp = new Rect(rectToMove);
+ tmp.offset(dx, dy);
+ if (!Rect.intersects(rectToAvoid, tmp) && allowedBounds.contains(tmp)) {
+ rectToMove.offsetTo(tmp.left, tmp.top);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 4942987742a0..281ea530e9e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -31,8 +31,6 @@ import android.os.RemoteException;
import android.util.Size;
import android.view.MotionEvent;
import android.view.SurfaceControl;
-import android.view.SyncRtSurfaceTransactionApplier;
-import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
import android.view.WindowManagerGlobal;
import com.android.internal.protolog.common.ProtoLog;
@@ -42,6 +40,7 @@ import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipMediaController.ActionListener;
import com.android.wm.shell.pip.PipMenuController;
+import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip.PipUiEventLogger;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -115,6 +114,10 @@ public class PhonePipMenuController implements PipMenuController {
private final ShellExecutor mMainExecutor;
private final Handler mMainHandler;
+ private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+ private final float[] mTmpTransform = new float[9];
+
private final ArrayList<Listener> mListeners = new ArrayList<>();
private final SystemWindows mSystemWindows;
private final Optional<SplitScreenController> mSplitScreenController;
@@ -124,7 +127,6 @@ public class PhonePipMenuController implements PipMenuController {
private RemoteAction mCloseAction;
private List<RemoteAction> mMediaActions;
- private SyncRtSurfaceTransactionApplier mApplier;
private int mMenuState;
private PipMenuView mPipMenuView;
@@ -150,6 +152,9 @@ public class PhonePipMenuController implements PipMenuController {
mMainHandler = mainHandler;
mSplitScreenController = splitScreenOptional;
mPipUiEventLogger = pipUiEventLogger;
+
+ mSurfaceControlTransactionFactory =
+ new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
}
public boolean isMenuVisible() {
@@ -194,7 +199,6 @@ public class PhonePipMenuController implements PipMenuController {
return;
}
- mApplier = null;
mSystemWindows.removeView(mPipMenuView);
mPipMenuView = null;
}
@@ -289,7 +293,7 @@ public class PhonePipMenuController implements PipMenuController {
willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, " "));
}
- if (!maybeCreateSyncApplier()) {
+ if (!checkPipMenuState()) {
return;
}
@@ -312,7 +316,7 @@ public class PhonePipMenuController implements PipMenuController {
return;
}
- if (!maybeCreateSyncApplier()) {
+ if (!checkPipMenuState()) {
return;
}
@@ -328,18 +332,15 @@ public class PhonePipMenuController implements PipMenuController {
mTmpSourceRectF.set(mTmpSourceBounds);
mTmpDestinationRectF.set(destinationBounds);
mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
- SurfaceControl surfaceControl = getSurfaceControl();
- SurfaceParams params = new SurfaceParams.Builder(surfaceControl)
- .withMatrix(mMoveTransform)
- .build();
+ final SurfaceControl surfaceControl = getSurfaceControl();
+ final SurfaceControl.Transaction menuTx =
+ mSurfaceControlTransactionFactory.getTransaction();
+ menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform);
if (pipLeash != null && t != null) {
- SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash)
- .withMergeTransaction(t)
- .build();
- mApplier.scheduleApply(params, pipParams);
- } else {
- mApplier.scheduleApply(params);
+ // Merge the two transactions, vsyncId has been set on menuTx.
+ menuTx.merge(t);
}
+ menuTx.apply();
}
/**
@@ -353,36 +354,29 @@ public class PhonePipMenuController implements PipMenuController {
return;
}
- if (!maybeCreateSyncApplier()) {
+ if (!checkPipMenuState()) {
return;
}
- SurfaceControl surfaceControl = getSurfaceControl();
- SurfaceParams params = new SurfaceParams.Builder(surfaceControl)
- .withWindowCrop(destinationBounds)
- .build();
+ final SurfaceControl surfaceControl = getSurfaceControl();
+ final SurfaceControl.Transaction menuTx =
+ mSurfaceControlTransactionFactory.getTransaction();
+ menuTx.setCrop(surfaceControl, destinationBounds);
if (pipLeash != null && t != null) {
- SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash)
- .withMergeTransaction(t)
- .build();
- mApplier.scheduleApply(params, pipParams);
- } else {
- mApplier.scheduleApply(params);
+ // Merge the two transactions, vsyncId has been set on menuTx.
+ menuTx.merge(t);
}
+ menuTx.apply();
}
- private boolean maybeCreateSyncApplier() {
+ private boolean checkPipMenuState() {
if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Not going to move PiP, either menu or its parent is not created.", TAG);
return false;
}
- if (mApplier == null) {
- mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView);
- }
-
- return mApplier != null;
+ return true;
}
/**
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 dad261ad9580..af47666efa5a 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
@@ -43,11 +43,13 @@ import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.RemoteException;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Pair;
import android.util.Size;
import android.view.DisplayInfo;
+import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.WindowManagerGlobal;
import android.window.WindowContainerTransaction;
@@ -63,6 +65,7 @@ import com.android.wm.shell.R;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.common.DisplayChangeController;
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.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
@@ -79,13 +82,21 @@ import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipAppOpsListener;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipKeepClearAlgorithm;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ConfigurationChangeListener;
+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.sysui.UserChangeListener;
import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
@@ -99,38 +110,96 @@ import java.util.function.Consumer;
* Manages the picture-in-picture (PIP) UI and states for Phones.
*/
public class PipController implements PipTransitionController.PipTransitionCallback,
- RemoteCallable<PipController> {
+ RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener,
+ UserChangeListener {
private static final String TAG = "PipController";
+ private static final long PIP_KEEP_CLEAR_AREAS_DELAY =
+ SystemProperties.getLong("persist.wm.debug.pip_keep_clear_areas_delay", 200);
+
+ private boolean mEnablePipKeepClearAlgorithm =
+ SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", false);
+
+ @VisibleForTesting
+ void setEnablePipKeepClearAlgorithm(boolean value) {
+ mEnablePipKeepClearAlgorithm = value;
+ }
+
private Context mContext;
protected ShellExecutor mMainExecutor;
private DisplayController mDisplayController;
private PipInputConsumer mPipInputConsumer;
private WindowManagerShellWrapper mWindowManagerShellWrapper;
+ private PipAnimationController mPipAnimationController;
private PipAppOpsListener mAppOpsListener;
private PipMediaController mMediaController;
private PipBoundsAlgorithm mPipBoundsAlgorithm;
+ private PipKeepClearAlgorithm mPipKeepClearAlgorithm;
private PipBoundsState mPipBoundsState;
+ private PipMotionHelper mPipMotionHelper;
private PipTouchHandler mTouchHandler;
private PipTransitionController mPipTransitionController;
private TaskStackListenerImpl mTaskStackListener;
private PipParamsChangedForwarder mPipParamsChangedForwarder;
+ private DisplayInsetsController mDisplayInsetsController;
private Optional<OneHandedController> mOneHandedController;
+ private final ShellCommandHandler mShellCommandHandler;
+ private final ShellController mShellController;
protected final PipImpl mImpl;
private final Rect mTmpInsetBounds = new Rect();
private final int mEnterAnimationDuration;
+ private final Runnable mMovePipInResponseToKeepClearAreasChangeCallback =
+ this::onKeepClearAreasChangedCallback;
+
+ private void onKeepClearAreasChangedCallback() {
+ if (!mEnablePipKeepClearAlgorithm) {
+ // early bail out if the keep clear areas feature is disabled
+ return;
+ }
+ // if there is another animation ongoing, wait for it to finish and try again
+ if (mPipAnimationController.isAnimating()) {
+ mMainExecutor.removeCallbacks(
+ mMovePipInResponseToKeepClearAreasChangeCallback);
+ mMainExecutor.executeDelayed(
+ mMovePipInResponseToKeepClearAreasChangeCallback,
+ PIP_KEEP_CLEAR_AREAS_DELAY);
+ return;
+ }
+ updatePipPositionForKeepClearAreas();
+ }
+
+ private void updatePipPositionForKeepClearAreas() {
+ if (!mEnablePipKeepClearAlgorithm) {
+ // early bail out if the keep clear areas feature is disabled
+ return;
+ }
+ // only move if already in pip, other transitions account for keep clear areas
+ if (mPipTransitionState.hasEnteredPip()) {
+ Rect destBounds = mPipKeepClearAlgorithm.adjust(mPipBoundsState,
+ mPipBoundsAlgorithm);
+ // only move if the bounds are actually different
+ if (destBounds != mPipBoundsState.getBounds()) {
+ mPipTaskOrganizer.scheduleAnimateResizePip(destBounds,
+ mEnterAnimationDuration, null);
+ }
+ }
+ }
+
private boolean mIsInFixedRotation;
private PipAnimationListener mPinnedStackAnimationRecentsCallback;
protected PhonePipMenuController mMenuController;
protected PipTaskOrganizer mPipTaskOrganizer;
+ private PipTransitionState mPipTransitionState;
protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener =
new PipControllerPinnedTaskListener();
private boolean mIsKeyguardShowingOrAnimating;
+ private Consumer<Boolean> mOnIsInPipStateChangedListener;
+
private interface PipAnimationListener {
/**
* Notifies the listener that the Pip animation is started.
@@ -156,7 +225,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
* Handler for display rotation changes.
*/
private final DisplayChangeController.OnDisplayChangingListener mRotationController = (
- int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> {
+ displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> {
if (mPipTransitionController.handleRotateDisplay(fromRotation, toRotation, t)) {
return;
}
@@ -247,7 +316,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb
public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
Set<Rect> unrestricted) {
if (mPipBoundsState.getDisplayId() == displayId) {
- mPipBoundsState.setKeepClearAreas(restricted, unrestricted);
+ if (mEnablePipKeepClearAlgorithm) {
+ mPipBoundsState.setKeepClearAreas(restricted, unrestricted);
+
+ mMainExecutor.removeCallbacks(
+ mMovePipInResponseToKeepClearAreasChangeCallback);
+ mMainExecutor.executeDelayed(
+ mMovePipInResponseToKeepClearAreasChangeCallback,
+ PIP_KEEP_CLEAR_AREAS_DELAY);
+ }
}
}
};
@@ -261,6 +338,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
mPipBoundsState.setImeVisibility(imeVisible, imeHeight);
mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight);
+ if (imeVisible) {
+ updatePipPositionForKeepClearAreas();
+ }
}
@Override
@@ -284,14 +364,27 @@ public class PipController implements PipTransitionController.PipTransitionCallb
* Instantiates {@link PipController}, returns {@code null} if the feature not supported.
*/
@Nullable
- public static Pip create(Context context, DisplayController displayController,
- PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm,
- PipBoundsState pipBoundsState, PipMediaController pipMediaController,
- PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer,
- PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController,
+ public static Pip create(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ DisplayController displayController,
+ PipAnimationController pipAnimationController,
+ PipAppOpsListener pipAppOpsListener,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ PipKeepClearAlgorithm pipKeepClearAlgorithm,
+ PipBoundsState pipBoundsState,
+ PipMotionHelper pipMotionHelper,
+ PipMediaController pipMediaController,
+ PhonePipMenuController phonePipMenuController,
+ PipTaskOrganizer pipTaskOrganizer,
+ PipTransitionState pipTransitionState,
+ PipTouchHandler pipTouchHandler,
+ PipTransitionController pipTransitionController,
WindowManagerShellWrapper windowManagerShellWrapper,
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
+ DisplayInsetsController displayInsetsController,
Optional<OneHandedController> oneHandedController,
ShellExecutor mainExecutor) {
if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
@@ -300,27 +393,37 @@ public class PipController implements PipTransitionController.PipTransitionCallb
return null;
}
- return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm,
- pipBoundsState, pipMediaController,
- phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController,
- windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
+ return new PipController(context, shellInit, shellCommandHandler, shellController,
+ displayController, pipAnimationController, pipAppOpsListener,
+ pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper,
+ pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState,
+ pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
+ taskStackListener, pipParamsChangedForwarder, displayInsetsController,
oneHandedController, mainExecutor)
.mImpl;
}
protected PipController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
DisplayController displayController,
+ PipAnimationController pipAnimationController,
PipAppOpsListener pipAppOpsListener,
PipBoundsAlgorithm pipBoundsAlgorithm,
+ PipKeepClearAlgorithm pipKeepClearAlgorithm,
@NonNull PipBoundsState pipBoundsState,
+ PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
PhonePipMenuController phonePipMenuController,
PipTaskOrganizer pipTaskOrganizer,
+ PipTransitionState pipTransitionState,
PipTouchHandler pipTouchHandler,
PipTransitionController pipTransitionController,
WindowManagerShellWrapper windowManagerShellWrapper,
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
+ DisplayInsetsController displayInsetsController,
Optional<OneHandedController> oneHandedController,
ShellExecutor mainExecutor
) {
@@ -331,16 +434,22 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
+ mShellController = shellController;
mImpl = new PipImpl();
mWindowManagerShellWrapper = windowManagerShellWrapper;
mDisplayController = displayController;
mPipBoundsAlgorithm = pipBoundsAlgorithm;
+ mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
mPipBoundsState = pipBoundsState;
+ mPipMotionHelper = pipMotionHelper;
mPipTaskOrganizer = pipTaskOrganizer;
+ mPipTransitionState = pipTransitionState;
mMainExecutor = mainExecutor;
mMediaController = pipMediaController;
mMenuController = phonePipMenuController;
mTouchHandler = pipTouchHandler;
+ mPipAnimationController = pipAnimationController;
mAppOpsListener = pipAppOpsListener;
mOneHandedController = oneHandedController;
mPipTransitionController = pipTransitionController;
@@ -349,12 +458,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mEnterAnimationDuration = mContext.getResources()
.getInteger(R.integer.config_pipEnterAnimationDuration);
mPipParamsChangedForwarder = pipParamsChangedForwarder;
+ mDisplayInsetsController = displayInsetsController;
- //TODO: move this to ShellInit when PipController can be injected
- mMainExecutor.execute(this::init);
+ shellInit.addInitCallback(this::onInit, this);
}
- public void init() {
+ private void onInit() {
+ mShellCommandHandler.addDumpCallback(this::dump, this);
mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(),
INPUT_CONSUMER_PIP, mMainExecutor);
mPipTransitionController.registerPipTransitionCallback(this);
@@ -363,6 +473,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb
onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
false /* saveRestoreSnapFraction */);
});
+ mPipTransitionState.addOnPipTransitionStateChangedListener((oldState, newState) -> {
+ if (mOnIsInPipStateChangedListener != null) {
+ final boolean wasInPip = PipTransitionState.isInPip(oldState);
+ final boolean nowInPip = PipTransitionState.isInPip(newState);
+ if (nowInPip != wasInPip) {
+ mOnIsInPipStateChangedListener.accept(nowInPip);
+ }
+ }
+ });
mPipBoundsState.setOnMinimalSizeChangeCallback(
() -> {
// The minimal size drives the normal bounds, so they need to be recalculated.
@@ -458,14 +577,21 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mPipBoundsState.getBounds(),
mPipBoundsState.getAspectRatio());
Objects.requireNonNull(destinationBounds, "Missing destination bounds");
- mPipTaskOrganizer.scheduleAnimateResizePip(destinationBounds,
- mEnterAnimationDuration,
- null /* updateBoundsCallback */);
-
- mTouchHandler.onAspectRatioChanged();
- updateMovementBounds(null /* toBounds */, false /* fromRotation */,
- false /* fromImeAdjustment */, false /* fromShelfAdjustment */,
- null /* windowContainerTransaction */);
+ if (!destinationBounds.equals(mPipBoundsState.getBounds())) {
+ mPipTaskOrganizer.scheduleAnimateResizePip(destinationBounds,
+ mEnterAnimationDuration,
+ null /* updateBoundsCallback */);
+ mTouchHandler.onAspectRatioChanged();
+ updateMovementBounds(null /* toBounds */, false /* fromRotation */,
+ false /* fromImeAdjustment */, false /* fromShelfAdjustment */,
+ null /* windowContainerTransaction */);
+ } else {
+ // when we enter pip for the first time, the destination bounds and pip
+ // bounds will already match, since they are calculated prior to
+ // starting the animation, so we only need to update the min/max size
+ // that is used for e.g. double tap to maximized state
+ mTouchHandler.updateMinMaxSize(ratio);
+ }
}
@Override
@@ -475,8 +601,18 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
});
+ mDisplayInsetsController.addInsetsChangedListener(mPipBoundsState.getDisplayId(),
+ new DisplayInsetsController.OnInsetsChangedListener() {
+ @Override
+ public void insetsChanged(InsetsState insetsState) {
+ onDisplayChanged(
+ mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()),
+ false /* saveRestoreSnapFraction */);
+ }
+ });
+
mOneHandedController.ifPresent(controller -> {
- controller.asOneHanded().registerTransitionCallback(
+ controller.registerTransitionCallback(
new OneHandedTransitionCallback() {
@Override
public void onStartFinished(Rect bounds) {
@@ -489,6 +625,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
});
});
+
+ mMediaController.registerSessionListenerForCurrentUser();
+
+ mShellController.addConfigurationChangeListener(this);
+ mShellController.addKeyguardChangeListener(this);
+ mShellController.addUserChangeListener(this);
}
@Override
@@ -501,18 +643,27 @@ public class PipController implements PipTransitionController.PipTransitionCallb
return mMainExecutor;
}
- private void onConfigurationChanged(Configuration newConfig) {
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ // Re-register the media session listener when switching users
+ mMediaController.registerSessionListenerForCurrentUser();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
mPipBoundsAlgorithm.onConfigurationChanged(mContext);
mTouchHandler.onConfigurationChanged();
mPipBoundsState.onConfigurationChanged();
}
- private void onDensityOrFontScaleChanged() {
+ @Override
+ public void onDensityOrFontScaleChanged() {
mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext);
onPipResourceDimensionsChanged();
}
- private void onOverlayChanged() {
+ @Override
+ public void onThemeChanged() {
mTouchHandler.onOverlayChanged();
onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()),
false /* saveRestoreSnapFraction */);
@@ -542,33 +693,50 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mMenuController.attachPipMenuView();
// Calculate the snap fraction of the current stack along the old movement bounds
final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm();
- final Rect postChangeStackBounds = new Rect(mPipBoundsState.getBounds());
- final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeStackBounds,
- mPipBoundsAlgorithm.getMovementBounds(postChangeStackBounds),
+ final Rect postChangeBounds = new Rect(mPipBoundsState.getBounds());
+ final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeBounds,
+ mPipBoundsAlgorithm.getMovementBounds(postChangeBounds),
mPipBoundsState.getStashedState());
+ // Scale PiP on density dpi change, so it appears to be the same size physically.
+ final boolean densityDpiChanged = mPipBoundsState.getDisplayLayout().densityDpi() != 0
+ && (mPipBoundsState.getDisplayLayout().densityDpi() != layout.densityDpi());
+ if (densityDpiChanged) {
+ final float scale = (float) layout.densityDpi()
+ / mPipBoundsState.getDisplayLayout().densityDpi();
+ postChangeBounds.set(0, 0,
+ (int) (postChangeBounds.width() * scale),
+ (int) (postChangeBounds.height() * scale));
+ }
+
updateDisplayLayout.run();
- // Calculate the stack bounds in the new orientation based on same fraction along the
+ // Calculate the PiP bounds in the new orientation based on same fraction along the
// rotated movement bounds.
final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds(
- postChangeStackBounds, false /* adjustForIme */);
- pipSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
+ postChangeBounds, false /* adjustForIme */);
+ pipSnapAlgorithm.applySnapFraction(postChangeBounds, postChangeMovementBounds,
snapFraction, mPipBoundsState.getStashedState(),
mPipBoundsState.getStashOffset(),
mPipBoundsState.getDisplayBounds(),
mPipBoundsState.getDisplayLayout().stableInsets());
- mTouchHandler.getMotionHelper().movePip(postChangeStackBounds);
+ if (densityDpiChanged) {
+ // Using PipMotionHelper#movePip directly here may cause race condition since
+ // the app content in PiP mode may or may not be updated for the new density dpi.
+ final int duration = mContext.getResources().getInteger(
+ R.integer.config_pipEnterAnimationDuration);
+ mPipTaskOrganizer.scheduleAnimateResizePip(
+ postChangeBounds, duration, null /* updateBoundsCallback */);
+ } else {
+ // Directly move PiP to its final destination bounds without animation.
+ mPipTaskOrganizer.scheduleFinishResizePip(postChangeBounds);
+ }
} else {
updateDisplayLayout.run();
}
}
- private void registerSessionListenerForCurrentUser() {
- mMediaController.registerSessionListenerForCurrentUser();
- }
-
private void onSystemUiStateChanged(boolean isValidState, int flag) {
mTouchHandler.onSystemUiStateChanged(isValidState);
}
@@ -600,21 +768,24 @@ public class PipController implements PipTransitionController.PipTransitionCallb
* finished first to reset the visibility of PiP window.
* See also {@link #onKeyguardDismissAnimationFinished()}
*/
- private void onKeyguardVisibilityChanged(boolean keyguardShowing, boolean animating) {
+ @Override
+ public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
+ boolean animatingDismiss) {
if (!mPipTaskOrganizer.isInPip()) {
return;
}
- if (keyguardShowing) {
+ if (visible) {
mIsKeyguardShowingOrAnimating = true;
hidePipMenu(null /* onStartCallback */, null /* onEndCallback */);
mPipTaskOrganizer.setPipVisibility(false);
- } else if (!animating) {
+ } else if (!animatingDismiss) {
mIsKeyguardShowingOrAnimating = false;
mPipTaskOrganizer.setPipVisibility(true);
}
}
- private void onKeyguardDismissAnimationFinished() {
+ @Override
+ public void onKeyguardDismissAnimationFinished() {
if (mPipTaskOrganizer.isInPip()) {
mIsKeyguardShowingOrAnimating = false;
mPipTaskOrganizer.setPipVisibility(true);
@@ -637,6 +808,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
}
+ private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
+ mOnIsInPipStateChangedListener = callback;
+ if (mOnIsInPipStateChangedListener != null) {
+ callback.accept(mPipTransitionState.isInPip());
+ }
+ }
+
private void setShelfHeightLocked(boolean visible, int height) {
final int shelfHeight = visible ? height : 0;
mPipBoundsState.setShelfVisibility(visible, shelfHeight);
@@ -663,8 +841,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb
private Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
PictureInPictureParams pictureInPictureParams,
- int launcherRotation, int shelfHeight) {
- setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight);
+ int launcherRotation, Rect hotseatKeepClearArea) {
+
+ if (mEnablePipKeepClearAlgorithm) {
+ // pre-emptively add the keep clear area for Hotseat, so that it is taken into account
+ // when calculating the entry destination bounds of PiP window
+ mPipBoundsState.getRestrictedKeepClearAreas().add(hotseatKeepClearArea);
+ } else {
+ int shelfHeight = hotseatKeepClearArea.height();
+ setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight);
+ }
onDisplayRotationChangedNotInPip(mContext, launcherRotation);
final Rect entryBounds = mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo,
pictureInPictureParams);
@@ -838,7 +1024,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
return true;
}
- private void dump(PrintWriter pw) {
+ private void dump(PrintWriter pw, String prefix) {
final String innerPrefix = " ";
pw.println(TAG);
mMenuController.dump(pw, innerPrefix);
@@ -872,27 +1058,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
@Override
- public void onConfigurationChanged(Configuration newConfig) {
- mMainExecutor.execute(() -> {
- PipController.this.onConfigurationChanged(newConfig);
- });
- }
-
- @Override
- public void onDensityOrFontScaleChanged() {
- mMainExecutor.execute(() -> {
- PipController.this.onDensityOrFontScaleChanged();
- });
- }
-
- @Override
- public void onOverlayChanged() {
- mMainExecutor.execute(() -> {
- PipController.this.onOverlayChanged();
- });
- }
-
- @Override
public void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) {
mMainExecutor.execute(() -> {
PipController.this.onSystemUiStateChanged(isSysUiStateValid, flag);
@@ -900,16 +1065,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
@Override
- public void registerSessionListenerForCurrentUser() {
+ public void setShelfHeight(boolean visible, int height) {
mMainExecutor.execute(() -> {
- PipController.this.registerSessionListenerForCurrentUser();
+ PipController.this.setShelfHeight(visible, height);
});
}
@Override
- public void setShelfHeight(boolean visible, int height) {
+ public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
mMainExecutor.execute(() -> {
- PipController.this.setShelfHeight(visible, height);
+ PipController.this.setOnIsInPipStateChangedListener(callback);
});
}
@@ -940,30 +1105,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb
PipController.this.showPictureInPictureMenu();
});
}
-
- @Override
- public void onKeyguardVisibilityChanged(boolean showing, boolean animating) {
- mMainExecutor.execute(() -> {
- PipController.this.onKeyguardVisibilityChanged(showing, animating);
- });
- }
-
- @Override
- public void onKeyguardDismissAnimationFinished() {
- mMainExecutor.execute(PipController.this::onKeyguardDismissAnimationFinished);
- }
-
- @Override
- public void dump(PrintWriter pw) {
- try {
- mMainExecutor.executeBlocking(() -> {
- PipController.this.dump(pw);
- });
- } catch (InterruptedException e) {
- ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Failed to dump PipController in 2s", TAG);
- }
- }
}
/**
@@ -1008,12 +1149,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb
@Override
public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
PictureInPictureParams pictureInPictureParams, int launcherRotation,
- int shelfHeight) {
+ Rect keepClearArea) {
Rect[] result = new Rect[1];
executeRemoteCallWithTaskPermission(mController, "startSwipePipToHome",
(controller) -> {
result[0] = controller.startSwipePipToHome(componentName, activityInfo,
- pictureInPictureParams, launcherRotation, shelfHeight);
+ pictureInPictureParams, launcherRotation, keepClearArea);
}, true /* blocking */);
return result[0];
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
index a0e22011b5d0..7619646804ad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
@@ -288,8 +288,10 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen
if (mTargetViewContainer.getVisibility() != View.VISIBLE) {
mTargetViewContainer.getViewTreeObserver().addOnPreDrawListener(this);
- mTargetViewContainer.show();
}
+ // 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. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
new file mode 100644
index 000000000000..acc0caf95e35
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
@@ -0,0 +1,123 @@
+/*
+ * 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.pip.phone;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.graphics.Rect;
+
+import com.android.wm.shell.pip.PipBoundsState;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Static utilities to get appropriate {@link PipDoubleTapHelper.PipSizeSpec} on a double tap.
+ */
+public class PipDoubleTapHelper {
+
+ /**
+ * Should not be instantiated as a stateless class.
+ */
+ private PipDoubleTapHelper() {}
+
+ /**
+ * A constant that represents a pip screen size.
+ *
+ * <p>CUSTOM - user resized screen size (by pinching in/out)</p>
+ * <p>DEFAULT - normal screen size used as default when entering pip mode</p>
+ * <p>MAX - maximum allowed screen size</p>
+ */
+ @IntDef(value = {
+ SIZE_SPEC_CUSTOM,
+ SIZE_SPEC_DEFAULT,
+ SIZE_SPEC_MAX
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface PipSizeSpec {}
+
+ static final int SIZE_SPEC_CUSTOM = 2;
+ static final int SIZE_SPEC_DEFAULT = 0;
+ static final int SIZE_SPEC_MAX = 1;
+
+ /**
+ * Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from.
+ *
+ * <p>Each double tap toggles back and forth between {@code PipSizeSpec.CUSTOM} and
+ * either {@code PipSizeSpec.MAX} or {@code PipSizeSpec.DEFAULT}. The choice between
+ * the latter two sizes is determined based on the current state of the pip screen.</p>
+ *
+ * @param mPipBoundsState current state of the pip screen
+ */
+ @PipSizeSpec
+ private static int getMaxOrDefaultPipSizeSpec(@NonNull PipBoundsState mPipBoundsState) {
+ // determine the average pip screen width
+ int averageWidth = (mPipBoundsState.getMaxSize().x
+ + mPipBoundsState.getMinSize().x) / 2;
+
+ // If pip screen width is above average, DEFAULT is the size spec we need to
+ // toggle to. Otherwise, we choose MAX.
+ return (mPipBoundsState.getBounds().width() > averageWidth)
+ ? SIZE_SPEC_DEFAULT
+ : SIZE_SPEC_MAX;
+ }
+
+ /**
+ * Determines the {@link PipSizeSpec} to toggle to on double tap.
+ *
+ * @param mPipBoundsState current state of the pip screen
+ * @param userResizeBounds latest user resized bounds (by pinching in/out)
+ * @return pip screen size to switch to
+ */
+ @PipSizeSpec
+ static int nextSizeSpec(@NonNull PipBoundsState mPipBoundsState,
+ @NonNull Rect userResizeBounds) {
+ // is pip screen at its maximum
+ boolean isScreenMax = mPipBoundsState.getBounds().width()
+ == mPipBoundsState.getMaxSize().x;
+
+ // is pip screen at its normal default size
+ boolean isScreenDefault = (mPipBoundsState.getBounds().width()
+ == mPipBoundsState.getNormalBounds().width())
+ && (mPipBoundsState.getBounds().height()
+ == mPipBoundsState.getNormalBounds().height());
+
+ // edge case 1
+ // if user hasn't resized screen yet, i.e. CUSTOM size does not exist yet
+ // or if user has resized exactly to DEFAULT, then we just want to maximize
+ if (isScreenDefault
+ && userResizeBounds.width() == mPipBoundsState.getNormalBounds().width()) {
+ return SIZE_SPEC_MAX;
+ }
+
+ // edge case 2
+ // if user has maximized, then we want to toggle to DEFAULT
+ if (isScreenMax
+ && userResizeBounds.width() == mPipBoundsState.getMaxSize().x) {
+ return SIZE_SPEC_DEFAULT;
+ }
+
+ // otherwise in general we want to toggle back to user's CUSTOM size
+ if (isScreenDefault || isScreenMax) {
+ return SIZE_SPEC_CUSTOM;
+ }
+
+ // if we are currently in user resized CUSTOM size state
+ // then we toggle either to MAX or DEFAULT depending on the current pip screen state
+ return getMaxOrDefaultPipSizeSpec(mPipBoundsState);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
index 0f3ff36601fb..8e3376f163c1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
@@ -146,11 +146,8 @@ public class PipInputConsumer {
"%s: Failed to create input consumer, %s", TAG, e);
}
mMainExecutor.execute(() -> {
- // Choreographer.getSfInstance() must be called on the thread that the input event
- // receiver should be receiving events
- // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions
mInputEventReceiver = new InputEventReceiver(inputChannel,
- Looper.myLooper(), Choreographer.getSfInstance());
+ Looper.myLooper(), Choreographer.getInstance());
if (mRegistrationListener != null) {
mRegistrationListener.onRegistrationChanged(true /* isRegistered */);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
index 6390c8984dac..979b7c7dc31f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
@@ -207,6 +207,9 @@ public class PipMenuView extends FrameLayout {
}
});
+ // this disables the ripples
+ mEnterSplitButton.setEnabled(false);
+
findViewById(R.id.resize_handle).setAlpha(0);
mActionsGroup = findViewById(R.id.actions_group);
@@ -282,7 +285,7 @@ public class PipMenuView extends FrameLayout {
&& mSplitScreenControllerOptional.get().isTaskInSplitScreen(taskInfo.taskId);
mFocusedTaskAllowSplitScreen = isSplitScreen
|| (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
- && taskInfo.supportsSplitScreenMultiWindow
+ && taskInfo.supportsMultiWindow
&& taskInfo.topActivityType != WindowConfiguration.ACTIVITY_TYPE_HOME);
}
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 5a21e0734277..afb64c9eec41 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
@@ -33,10 +33,7 @@ import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Debug;
-import android.os.Looper;
-import android.view.Choreographer;
-
-import androidx.dynamicanimation.animation.FrameCallbackScheduler;
+import android.os.SystemProperties;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
@@ -62,6 +59,8 @@ import kotlin.jvm.functions.Function0;
public class PipMotionHelper implements PipAppOpsListener.Callback,
FloatingContentCoordinator.FloatingContent {
+ public static final boolean ENABLE_FLING_TO_DISMISS_PIP =
+ SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", true);
private static final String TAG = "PipMotionHelper";
private static final boolean DEBUG = false;
@@ -89,25 +88,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
/** Coordinator instance for resolving conflicts with other floating content. */
private FloatingContentCoordinator mFloatingContentCoordinator;
- private ThreadLocal<FrameCallbackScheduler> mSfSchedulerThreadLocal =
- ThreadLocal.withInitial(() -> {
- final Looper initialLooper = Looper.myLooper();
- final FrameCallbackScheduler scheduler = new FrameCallbackScheduler() {
- @Override
- public void postFrameCallback(@androidx.annotation.NonNull Runnable runnable) {
- // TODO(b/222697646): remove getSfInstance usage and use vsyncId for
- // transactions
- Choreographer.getSfInstance().postFrameCallback(t -> runnable.run());
- }
-
- @Override
- public boolean isCurrentThread() {
- return Looper.myLooper() == initialLooper;
- }
- };
- return scheduler;
- });
-
/**
* PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()}
* using physics animations.
@@ -210,10 +190,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
}
public void init() {
- // Note: Needs to get the shell main thread sf vsync animation handler
mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance(
mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
- mTemporaryBoundsPhysicsAnimator.setCustomScheduler(mSfSchedulerThreadLocal.get());
}
@NonNull
@@ -729,6 +707,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
loc[1] = animatedPipBounds.top;
}
};
+ mMagnetizedPip.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_PIP);
}
return mMagnetizedPip;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
index abf1a9500e6d..89d85e4b292d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -625,8 +625,7 @@ public class PipResizeGestureHandler {
class PipResizeInputEventReceiver extends BatchedInputEventReceiver {
PipResizeInputEventReceiver(InputChannel channel, Looper looper) {
- // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions
- super(channel, looper, Choreographer.getSfInstance());
+ super(channel, looper, Choreographer.getInstance());
}
public void onInputEvent(InputEvent event) {
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 ac7b9033b2b9..1f3f31e025a0 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
@@ -34,6 +34,7 @@ import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.os.SystemProperties;
import android.provider.DeviceConfig;
import android.util.Size;
import android.view.DisplayCutout;
@@ -54,8 +55,10 @@ import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipTaskOrganizer;
+import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipUiEventLogger;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellInit;
import java.io.PrintWriter;
@@ -68,6 +71,9 @@ public class PipTouchHandler {
private static final String TAG = "PipTouchHandler";
private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f;
+ private static final boolean ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
+ SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", false);
+
// Allow PIP to resize to a slightly bigger state upon touch
private boolean mEnableResize;
private final Context mContext;
@@ -164,6 +170,7 @@ public class PipTouchHandler {
@SuppressLint("InflateParams")
public PipTouchHandler(Context context,
+ ShellInit shellInit,
PhonePipMenuController menuController,
PipBoundsAlgorithm pipBoundsAlgorithm,
@NonNull PipBoundsState pipBoundsState,
@@ -172,7 +179,6 @@ public class PipTouchHandler {
FloatingContentCoordinator floatingContentCoordinator,
PipUiEventLogger pipUiEventLogger,
ShellExecutor mainExecutor) {
- // Initialize the Pip input consumer
mContext = context;
mMainExecutor = mainExecutor;
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
@@ -212,9 +218,17 @@ public class PipTouchHandler {
mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(),
this::onAccessibilityShowMenu, this::updateMovementBounds,
this::animateToUnStashedState, mainExecutor);
+
+ // 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
+ // if it is needed
+ shellInit.addInitCallback(this::onInit, this);
}
- public void init() {
+ /**
+ * Called when the touch handler is initialized.
+ */
+ public void onInit() {
Resources res = mContext.getResources();
mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu);
reloadResources();
@@ -250,6 +264,10 @@ public class PipTouchHandler {
});
}
+ public PipTransitionController getTransitionHandler() {
+ return mPipTaskOrganizer.getTransitionController();
+ }
+
private void reloadResources() {
final Resources res = mContext.getResources();
mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer);
@@ -398,13 +416,7 @@ public class PipTouchHandler {
mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds,
bottomOffset);
- if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
- updatePinchResizeSizeConstraints(insetBounds, normalBounds, aspectRatio);
- } else {
- mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height());
- mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(),
- mPipBoundsState.getExpandedBounds().height());
- }
+ updatePipSizeConstraints(insetBounds, 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
@@ -418,6 +430,9 @@ public class PipTouchHandler {
if (mTouchState.isUserInteracting()) {
// Defer the update of the current movement bounds until after the user finishes
// touching the screen
+ } else if (ENABLE_PIP_KEEP_CLEAR_ALGORITHM) {
+ // Ignore moving PiP if keep clear algorithm is enabled, since IME and shelf height
+ // now are accounted for in the keep clear algorithm calculations
} else {
final boolean isExpanded = mMenuState == MENU_STATE_FULL && willResizeMenu();
final Rect toMovementBounds = new Rect();
@@ -473,6 +488,27 @@ public class PipTouchHandler {
}
}
+ /**
+ * 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(mInsetBounds, mPipBoundsState.getNormalBounds(),
+ aspectRatio);
+ }
+
+ private void updatePipSizeConstraints(Rect insetBounds, Rect normalBounds,
+ float aspectRatio) {
+ if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
+ updatePinchResizeSizeConstraints(insetBounds, normalBounds, aspectRatio);
+ } else {
+ mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height());
+ mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(),
+ mPipBoundsState.getExpandedBounds().height());
+ }
+ }
+
private void updatePinchResizeSizeConstraints(Rect insetBounds, Rect normalBounds,
float aspectRatio) {
final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(),
@@ -900,9 +936,18 @@ public class PipTouchHandler {
if (mMenuController.isMenuVisible()) {
mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */);
}
- if (toExpand) {
+
+ // 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());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index a2eadcdf6210..ce34d2f9547d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -39,6 +39,7 @@ import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithm;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -63,7 +64,8 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
public TvPipBoundsAlgorithm(Context context,
@NonNull TvPipBoundsState tvPipBoundsState,
@NonNull PipSnapAlgorithm pipSnapAlgorithm) {
- super(context, tvPipBoundsState, pipSnapAlgorithm);
+ super(context, tvPipBoundsState, pipSnapAlgorithm,
+ new PipKeepClearAlgorithm() {});
this.mTvPipBoundsState = tvPipBoundsState;
this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
reloadResources(context);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index fa48def9c7d7..4e1b0469eb96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -32,6 +32,8 @@ import android.graphics.Rect;
import android.os.RemoteException;
import android.view.Gravity;
+import androidx.annotation.NonNull;
+
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.WindowManagerShellWrapper;
@@ -49,6 +51,10 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder;
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.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.UserChangeListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -61,7 +67,8 @@ import java.util.Set;
*/
public class TvPipController implements PipTransitionController.PipTransitionCallback,
TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
- TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener {
+ TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener,
+ ConfigurationChangeListener, UserChangeListener {
private static final String TAG = "TvPipController";
static final boolean DEBUG = false;
@@ -93,6 +100,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private final Context mContext;
+ private final ShellController mShellController;
private final TvPipBoundsState mTvPipBoundsState;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
private final TvPipBoundsController mTvPipBoundsController;
@@ -101,6 +109,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private final PipMediaController mPipMediaController;
private final TvPipNotificationController mPipNotificationController;
private final TvPipMenuController mTvPipMenuController;
+ private final PipTransitionController mPipTransitionController;
+ private final TaskStackListenerImpl mTaskStackListener;
+ private final PipParamsChangedForwarder mPipParamsChangedForwarder;
+ private final DisplayController mDisplayController;
+ private final WindowManagerShellWrapper mWmShellWrapper;
private final ShellExecutor mMainExecutor;
private final TvPipImpl mImpl = new TvPipImpl();
@@ -117,6 +130,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
public static Pip create(
Context context,
+ ShellInit shellInit,
+ ShellController shellController,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
@@ -133,6 +148,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
ShellExecutor mainExecutor) {
return new TvPipController(
context,
+ shellInit,
+ shellController,
tvPipBoundsState,
tvPipBoundsAlgorithm,
tvPipBoundsController,
@@ -151,6 +168,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private TvPipController(
Context context,
+ ShellInit shellInit,
+ ShellController shellController,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
@@ -163,10 +182,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
- WindowManagerShellWrapper wmShell,
+ WindowManagerShellWrapper wmShellWrapper,
ShellExecutor mainExecutor) {
mContext = context;
mMainExecutor = mainExecutor;
+ mShellController = shellController;
+ mDisplayController = displayController;
mTvPipBoundsState = tvPipBoundsState;
mTvPipBoundsState.setDisplayId(context.getDisplayId());
@@ -185,17 +206,36 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
mAppOpsListener = pipAppOpsListener;
mPipTaskOrganizer = pipTaskOrganizer;
- pipTransitionController.registerPipTransitionCallback(this);
+ mPipTransitionController = pipTransitionController;
+ mPipParamsChangedForwarder = pipParamsChangedForwarder;
+ mTaskStackListener = taskStackListener;
+ mWmShellWrapper = wmShellWrapper;
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ mPipTransitionController.registerPipTransitionCallback(this);
loadConfigurations();
- registerPipParamsChangedListener(pipParamsChangedForwarder);
- registerTaskStackListenerCallback(taskStackListener);
- registerWmShellPinnedStackListener(wmShell);
- displayController.addDisplayWindowListener(this);
+ registerPipParamsChangedListener(mPipParamsChangedForwarder);
+ registerTaskStackListenerCallback(mTaskStackListener);
+ registerWmShellPinnedStackListener(mWmShellWrapper);
+ registerSessionListenerForCurrentUser();
+ mDisplayController.addDisplayWindowListener(this);
+
+ mShellController.addConfigurationChangeListener(this);
+ mShellController.addUserChangeListener(this);
+ }
+
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ // Re-register the media session listener when switching users
+ registerSessionListenerForCurrentUser();
}
- private void onConfigurationChanged(Configuration newConfig) {
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
@@ -668,18 +708,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
private class TvPipImpl implements Pip {
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- mMainExecutor.execute(() -> {
- TvPipController.this.onConfigurationChanged(newConfig);
- });
- }
-
- @Override
- public void registerSessionListenerForCurrentUser() {
- mMainExecutor.execute(() -> {
- TvPipController.this.registerSessionListenerForCurrentUser();
- });
- }
+ // Not used
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
index 5062cc436461..8ebcf63f36e9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
@@ -32,6 +32,7 @@ import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMenuController;
import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
/**
@@ -39,14 +40,16 @@ import com.android.wm.shell.transition.Transitions;
* TODO: Implement animation once TV is using Transitions.
*/
public class TvPipTransition extends PipTransitionController {
- public TvPipTransition(PipBoundsState pipBoundsState,
+ public TvPipTransition(
+ @NonNull ShellInit shellInit,
+ @NonNull ShellTaskOrganizer shellTaskOrganizer,
+ @NonNull Transitions transitions,
+ PipBoundsState pipBoundsState,
PipMenuController pipMenuController,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
- PipAnimationController pipAnimationController,
- Transitions transitions,
- @NonNull ShellTaskOrganizer shellTaskOrganizer) {
- super(pipBoundsState, pipMenuController, tvPipBoundsAlgorithm, pipAnimationController,
- transitions, shellTaskOrganizer);
+ PipAnimationController pipAnimationController) {
+ super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
+ tvPipBoundsAlgorithm, pipAnimationController);
}
@Override
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 d04c34916256..c52ed249c2ca 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
@@ -26,11 +26,13 @@ import com.android.internal.protolog.common.IProtoLogGroup;
public enum ShellProtoLogGroup implements IProtoLogGroup {
// NOTE: Since we enable these from the same WM ShellCommand, these names should not conflict
// with those in the framework ProtoLogGroup
+ WM_SHELL_INIT(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
+ Consts.TAG_WM_SHELL),
WM_SHELL_TASK_ORG(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_SHELL),
WM_SHELL_TRANSITIONS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
Consts.TAG_WM_SHELL),
- WM_SHELL_DRAG_AND_DROP(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+ WM_SHELL_DRAG_AND_DROP(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
Consts.TAG_WM_SHELL),
WM_SHELL_STARTING_WINDOW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_STARTING_WINDOW),
@@ -38,10 +40,16 @@ public enum ShellProtoLogGroup implements IProtoLogGroup {
"ShellBackPreview"),
WM_SHELL_RECENT_TASKS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_SHELL),
- WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG,
- false, Consts.TAG_WM_SHELL),
+ WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+ Consts.TAG_WM_SHELL),
WM_SHELL_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_SHELL),
+ 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, false,
+ Consts.TAG_WM_SHELL),
+ WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+ Consts.TAG_WM_SHELL),
TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
private final boolean mEnabled;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
index 6e78fcba4a00..b71cc32a0347 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
@@ -16,6 +16,8 @@
package com.android.wm.shell.recents;
+import android.app.ActivityManager;
+
import com.android.wm.shell.recents.IRecentTasksListener;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
@@ -38,4 +40,9 @@ interface IRecentTasks {
* Gets the set of recent tasks.
*/
GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId) = 3;
+
+ /**
+ * Gets the set of running tasks.
+ */
+ ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) = 4;
}
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 8efa42830d80..59f72335678e 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
@@ -16,6 +16,8 @@
package com.android.wm.shell.recents;
+import android.app.ActivityManager;
+
/**
* Listener interface that Launcher attaches to SystemUI to get split-screen callbacks.
*/
@@ -25,4 +27,14 @@ oneway interface IRecentTasksListener {
* Called when the set of recent tasks change.
*/
void onRecentTasksChanged();
+
+ /**
+ * Called when a running task appears.
+ */
+ void onRunningTaskAppeared(in ActivityManager.RunningTaskInfo taskInfo);
+
+ /**
+ * Called when a running task vanishes.
+ */
+ void onRunningTaskVanished(in ActivityManager.RunningTaskInfo taskInfo);
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
index a5748f69388f..2a625524b48b 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
@@ -17,6 +17,11 @@
package com.android.wm.shell.recents;
import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.util.GroupedRecentTaskInfo;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Interface for interacting with the recent tasks.
@@ -29,4 +34,11 @@ public interface RecentTasks {
default IRecentTasks createExternalInterface() {
return null;
}
+
+ /**
+ * Gets the set of recent tasks.
+ */
+ default void getRecentTasks(int maxNum, int flags, int userId, Executor callbackExecutor,
+ Consumer<List<GroupedRecentTaskInfo>> callback) {
+ }
}
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 c166178e9bbd..02b5a35f653b 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
@@ -17,12 +17,14 @@
package com.android.wm.shell.recents;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.content.pm.PackageManager.FEATURE_PC;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.TaskInfo;
+import android.content.ComponentName;
import android.content.Context;
import android.os.RemoteException;
import android.util.Slog;
@@ -42,39 +44,50 @@ 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.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
-import com.android.wm.shell.util.StagedSplitBounds;
+import com.android.wm.shell.util.SplitBounds;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Manages the recent task list from the system, caching it as necessary.
*/
public class RecentTasksController implements TaskStackListenerCallback,
- RemoteCallable<RecentTasksController> {
+ RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.Listener {
private static final String TAG = RecentTasksController.class.getSimpleName();
private final Context mContext;
+ private final ShellCommandHandler mShellCommandHandler;
+ private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
private final ShellExecutor mMainExecutor;
private final TaskStackListenerImpl mTaskStackListener;
private final RecentTasks mImpl = new RecentTasksImpl();
+ private final ActivityTaskManager mActivityTaskManager;
+ private IRecentTasksListener mListener;
+ private final boolean mIsDesktopMode;
- private final ArrayList<Runnable> mCallbacks = new ArrayList<>();
// 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)
private final SparseIntArray mSplitTasks = new SparseIntArray();
/**
- * Maps taskId to {@link StagedSplitBounds} for both taskIDs.
+ * Maps taskId to {@link SplitBounds} for both taskIDs.
* Meaning there will be two taskId integers mapping to the same object.
* If there's any ordering to the pairing than we can probably just get away with only one
* taskID mapping to it, leaving both for consistency with {@link #mSplitTasks} for now.
*/
- private final Map<Integer, StagedSplitBounds> mTaskSplitBoundsMap = new HashMap<>();
+ private final Map<Integer, SplitBounds> mTaskSplitBoundsMap = new HashMap<>();
/**
* Creates {@link RecentTasksController}, returns {@code null} if the feature is not
@@ -83,34 +96,51 @@ public class RecentTasksController implements TaskStackListenerCallback,
@Nullable
public static RecentTasksController create(
Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
TaskStackListenerImpl taskStackListener,
+ ActivityTaskManager activityTaskManager,
+ Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
@ShellMainThread ShellExecutor mainExecutor
) {
if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
return null;
}
- return new RecentTasksController(context, taskStackListener, mainExecutor);
+ return new RecentTasksController(context, shellInit, shellCommandHandler, taskStackListener,
+ activityTaskManager, desktopModeTaskRepository, mainExecutor);
}
- RecentTasksController(Context context, TaskStackListenerImpl taskStackListener,
+ RecentTasksController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ TaskStackListenerImpl taskStackListener,
+ ActivityTaskManager activityTaskManager,
+ Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
ShellExecutor mainExecutor) {
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
+ mActivityTaskManager = activityTaskManager;
+ mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
mTaskStackListener = taskStackListener;
+ mDesktopModeTaskRepository = desktopModeTaskRepository;
mMainExecutor = mainExecutor;
+ shellInit.addInitCallback(this::onInit, this);
}
public RecentTasks asRecentTasks() {
return mImpl;
}
- public void init() {
+ private void onInit() {
+ mShellCommandHandler.addDumpCallback(this::dump, this);
mTaskStackListener.addListener(this);
+ mDesktopModeTaskRepository.ifPresent(it -> it.addListener(this));
}
/**
* Adds a split pair. This call does not validate the taskIds, only that they are not the same.
*/
- public void addSplitPair(int taskId1, int taskId2, StagedSplitBounds splitBounds) {
+ public void addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds) {
if (taskId1 == taskId2) {
return;
}
@@ -176,44 +206,80 @@ public class RecentTasksController implements TaskStackListenerCallback,
notifyRecentTasksChanged();
}
- public void onTaskRemoved(TaskInfo taskInfo) {
+ public void onTaskAdded(ActivityManager.RunningTaskInfo taskInfo) {
+ notifyRunningTaskAppeared(taskInfo);
+ }
+
+ public void onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo) {
// Remove any split pairs associated with this task
removeSplitPair(taskInfo.taskId);
notifyRecentTasksChanged();
+ notifyRunningTaskVanished(taskInfo);
}
public void onTaskWindowingModeChanged(TaskInfo taskInfo) {
notifyRecentTasksChanged();
}
+ @Override
+ public void onActiveTasksChanged() {
+ notifyRecentTasksChanged();
+ }
+
@VisibleForTesting
void notifyRecentTasksChanged() {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed");
- for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).run();
+ if (mListener == null) {
+ return;
+ }
+ try {
+ mListener.onRecentTasksChanged();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed call notifyRecentTasksChanged", e);
+ }
+ }
+
+ /**
+ * 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) {
+ return;
+ }
+ try {
+ mListener.onRunningTaskAppeared(taskInfo);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed call onRunningTaskAppeared", e);
}
}
- private void registerRecentTasksListener(Runnable listener) {
- if (!mCallbacks.contains(listener)) {
- mCallbacks.add(listener);
+ /**
+ * 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) {
+ return;
+ }
+ try {
+ mListener.onRunningTaskVanished(taskInfo);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed call onRunningTaskVanished", e);
}
}
- private void unregisterRecentTasksListener(Runnable listener) {
- mCallbacks.remove(listener);
+ private void registerRecentTasksListener(IRecentTasksListener listener) {
+ mListener = listener;
}
- @VisibleForTesting
- List<ActivityManager.RecentTaskInfo> getRawRecentTasks(int maxNum, int flags, int userId) {
- return ActivityTaskManager.getInstance().getRecentTasks(maxNum, flags, userId);
+ private void unregisterRecentTasksListener() {
+ mListener = null;
}
@VisibleForTesting
ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) {
// Note: the returned task list is from the most-recent to least-recent order
- final List<ActivityManager.RecentTaskInfo> rawList = getRawRecentTasks(maxNum, flags,
- userId);
+ final List<ActivityManager.RecentTaskInfo> rawList = mActivityTaskManager.getRecentTasks(
+ maxNum, flags, userId);
// Make a mapping of task id -> task info
final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>();
@@ -222,6 +288,9 @@ public class RecentTasksController implements TaskStackListenerCallback,
rawMapping.put(taskInfo.taskId, taskInfo);
}
+ boolean desktopModeActive = DesktopModeStatus.isActive(mContext);
+ ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>();
+
// Pull out the pairs as we iterate back in the list
ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>();
for (int i = 0; i < rawList.size(); i++) {
@@ -231,19 +300,57 @@ public class RecentTasksController implements TaskStackListenerCallback,
continue;
}
+ if (desktopModeActive && mDesktopModeTaskRepository.isPresent()
+ && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
+ // Freeform tasks will be added as a separate entry
+ freeformTasks.add(taskInfo);
+ continue;
+ }
+
final int pairedTaskId = mSplitTasks.get(taskInfo.taskId);
- if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(pairedTaskId)) {
+ if (!desktopModeActive && pairedTaskId != INVALID_TASK_ID && rawMapping.contains(
+ pairedTaskId)) {
final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId);
rawMapping.remove(pairedTaskId);
- recentTasks.add(new GroupedRecentTaskInfo(taskInfo, pairedTaskInfo,
+ recentTasks.add(GroupedRecentTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo,
mTaskSplitBoundsMap.get(pairedTaskId)));
} else {
- recentTasks.add(new GroupedRecentTaskInfo(taskInfo));
+ recentTasks.add(GroupedRecentTaskInfo.forSingleTask(taskInfo));
}
}
+
+ // Add a special entry for freeform tasks
+ if (!freeformTasks.isEmpty()) {
+ recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks(
+ freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0])));
+ }
+
return recentTasks;
}
+ /**
+ * Find the background task that match the given component.
+ */
+ @Nullable
+ public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName) {
+ if (componentName == null) {
+ return null;
+ }
+ List<ActivityManager.RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks(
+ Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE,
+ ActivityManager.getCurrentUser());
+ for (int i = 0; i < tasks.size(); i++) {
+ final ActivityManager.RecentTaskInfo task = tasks.get(i);
+ if (task.isVisible) {
+ continue;
+ }
+ if (componentName.equals(task.baseIntent.getComponent())) {
+ return task;
+ }
+ }
+ return null;
+ }
+
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
@@ -269,6 +376,16 @@ public class RecentTasksController implements TaskStackListenerCallback,
mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this);
return mIRecentTasks;
}
+
+ @Override
+ public void getRecentTasks(int maxNum, int flags, int userId, Executor executor,
+ Consumer<List<GroupedRecentTaskInfo>> callback) {
+ mMainExecutor.execute(() -> {
+ List<GroupedRecentTaskInfo> tasks =
+ RecentTasksController.this.getRecentTasks(maxNum, flags, userId);
+ executor.execute(() -> callback.accept(tasks));
+ });
+ }
}
@@ -280,19 +397,28 @@ public class RecentTasksController implements TaskStackListenerCallback,
private RecentTasksController mController;
private final SingleInstanceRemoteListener<RecentTasksController,
IRecentTasksListener> mListener;
- private final Runnable mRecentTasksListener =
- new Runnable() {
- @Override
- public void run() {
- mListener.call(l -> l.onRecentTasksChanged());
- }
- };
+ private final IRecentTasksListener mRecentTasksListener = new IRecentTasksListener.Stub() {
+ @Override
+ public void onRecentTasksChanged() throws RemoteException {
+ mListener.call(l -> l.onRecentTasksChanged());
+ }
+
+ @Override
+ public void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
+ mListener.call(l -> l.onRunningTaskAppeared(taskInfo));
+ }
+
+ @Override
+ public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+ mListener.call(l -> l.onRunningTaskVanished(taskInfo));
+ }
+ };
public IRecentTasksImpl(RecentTasksController controller) {
mController = controller;
mListener = new SingleInstanceRemoteListener<>(controller,
c -> c.registerRecentTasksListener(mRecentTasksListener),
- c -> c.unregisterRecentTasksListener(mRecentTasksListener));
+ c -> c.unregisterRecentTasksListener());
}
/**
@@ -331,5 +457,16 @@ public class RecentTasksController implements TaskStackListenerCallback,
true /* blocking */);
return out[0];
}
+
+ @Override
+ public ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) {
+ final ActivityManager.RunningTaskInfo[][] tasks =
+ new ActivityManager.RunningTaskInfo[][] {null};
+ executeRemoteCallWithTaskPermission(mController, "getRunningTasks",
+ (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum)
+ .toArray(new ActivityManager.RunningTaskInfo[0]),
+ true /* blocking */);
+ return tasks[0];
+ }
}
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index 51921e747f1a..ecdafa9a63f4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -18,8 +18,10 @@ package com.android.wm.shell.splitscreen;
import android.app.PendingIntent;
import android.content.Intent;
+import android.content.pm.ShortcutInfo;
import android.os.Bundle;
import android.os.UserHandle;
+import com.android.internal.logging.InstanceId;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.window.RemoteTransition;
@@ -66,34 +68,35 @@ interface ISplitScreen {
* Starts a shortcut in a stage.
*/
oneway void startShortcut(String packageName, String shortcutId, int position,
- in Bundle options, in UserHandle user) = 8;
+ in Bundle options, in UserHandle user, in InstanceId instanceId) = 8;
/**
* Starts an activity in a stage.
*/
oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int position,
- in Bundle options) = 9;
+ in Bundle options, in InstanceId instanceId) = 9;
/**
* Starts tasks simultaneously in one transition.
*/
oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
in Bundle sideOptions, int sidePosition, float splitRatio,
- in RemoteTransition remoteTransition) = 10;
+ in RemoteTransition remoteTransition, in InstanceId instanceId) = 10;
/**
* Version of startTasks using legacy transition system.
*/
oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions,
int sideTaskId, in Bundle sideOptions, int sidePosition,
- float splitRatio, in RemoteAnimationAdapter adapter) = 11;
+ float splitRatio, in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 11;
/**
- * Start a pair of intent and task using legacy transition system.
+ * Starts a pair of intent and task using legacy transition system.
*/
oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent,
in Intent fillInIntent, int taskId, in Bundle mainOptions,in Bundle sideOptions,
- int sidePosition, float splitRatio, in RemoteAnimationAdapter adapter) = 12;
+ int sidePosition, float splitRatio, in RemoteAnimationAdapter adapter,
+ in InstanceId instanceId) = 12;
/**
* Blocking call that notifies and gets additional split-screen targets when entering
@@ -108,4 +111,11 @@ interface ISplitScreen {
* does not expect split to currently be running.
*/
RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14;
+
+ /**
+ * Starts a pair of shortcut and task using legacy transition system.
+ */
+ oneway void startShortcutAndTaskWithLegacyTransition(in ShortcutInfo shortcutInfo, int taskId,
+ in Bundle mainOptions, in Bundle sideOptions, int sidePosition, float splitRatio,
+ in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
index ae5e075c4d3f..e7ec15e70c11 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
@@ -16,7 +16,9 @@
package com.android.wm.shell.splitscreen;
-import android.annotation.Nullable;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
+
import android.content.Context;
import android.view.SurfaceSession;
import android.window.WindowContainerToken;
@@ -38,10 +40,9 @@ class MainStage extends StageTaskListener {
MainStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
- SurfaceSession surfaceSession, IconProvider iconProvider,
- @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
- super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, iconProvider,
- stageTaskUnfoldController);
+ SurfaceSession surfaceSession, IconProvider iconProvider) {
+ super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+ iconProvider);
}
boolean isActive() {
@@ -76,10 +77,10 @@ class MainStage extends StageTaskListener {
if (mRootTaskInfo == null) return;
final WindowContainerToken rootToken = mRootTaskInfo.token;
wct.reparentTasks(
- rootToken,
- null /* newParent */,
- CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
- CONTROLLED_ACTIVITY_TYPES,
- toTop);
+ rootToken,
+ null /* newParent */,
+ null /* windowingModes */,
+ null /* activityTypes */,
+ toTop);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
index d55619f5e5ed..8639b36faf4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
@@ -16,7 +16,6 @@
package com.android.wm.shell.splitscreen;
-import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.view.SurfaceSession;
@@ -38,10 +37,9 @@ class SideStage extends StageTaskListener {
SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
- SurfaceSession surfaceSession, IconProvider iconProvider,
- @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
- super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, iconProvider,
- stageTaskUnfoldController);
+ SurfaceSession surfaceSession, IconProvider iconProvider) {
+ super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+ iconProvider);
}
boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
@@ -49,8 +47,8 @@ class SideStage extends StageTaskListener {
wct.reparentTasks(
mRootTaskInfo.token,
null /* newParent */,
- CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
- CONTROLLED_ACTIVITY_TYPES,
+ null /* windowingModes */,
+ null /* activityTypes */,
toTop);
return true;
}
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 448773ae9ea2..e73b799b7a3d 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,6 +18,7 @@ package com.android.wm.shell.splitscreen;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.graphics.Rect;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
@@ -58,6 +59,7 @@ public interface SplitScreen {
interface SplitScreenListener {
default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {}
default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {}
+ default void onSplitBoundsChanged(Rect rootBounds, Rect mainBounds, Rect sideBounds) {}
default void onSplitVisibilityChanged(boolean visible) {}
}
@@ -75,12 +77,6 @@ public interface SplitScreen {
return null;
}
- /**
- * Called when the visibility of the keyguard changes.
- * @param showing Indicates if the keyguard is now visible.
- */
- void onKeyguardVisibilityChanged(boolean showing);
-
/** Called when device waking up finished. */
void onFinishedWakingUp();
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 31b510c38457..07a6895e2720 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
@@ -18,6 +18,7 @@ package com.android.wm.shell.splitscreen;
import static android.app.ActivityManager.START_SUCCESS;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
@@ -31,6 +32,7 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
@@ -38,6 +40,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.RemoteException;
@@ -45,6 +48,7 @@ import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
@@ -58,7 +62,9 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -73,21 +79,24 @@ import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
+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.splitscreen.SplitScreen.StageType;
-import com.android.wm.shell.transition.LegacyTransitions;
+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 java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
+import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executor;
-import javax.inject.Provider;
-
/**
* Class manages split-screen multitasking mode and implements the main interface
* {@link SplitScreen}.
@@ -96,19 +105,19 @@ import javax.inject.Provider;
*/
// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen.
public class SplitScreenController implements DragAndDropPolicy.Starter,
- RemoteCallable<SplitScreenController> {
+ RemoteCallable<SplitScreenController>, KeyguardChangeListener {
private static final String TAG = SplitScreenController.class.getSimpleName();
- static final int EXIT_REASON_UNKNOWN = 0;
- static final int EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW = 1;
- static final int EXIT_REASON_APP_FINISHED = 2;
- static final int EXIT_REASON_DEVICE_FOLDED = 3;
- static final int EXIT_REASON_DRAG_DIVIDER = 4;
- static final int EXIT_REASON_RETURN_HOME = 5;
- static final int EXIT_REASON_ROOT_TASK_VANISHED = 6;
- static final int EXIT_REASON_SCREEN_LOCKED = 7;
- static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8;
- static final int EXIT_REASON_CHILD_TASK_ENTER_PIP = 9;
+ public static final int EXIT_REASON_UNKNOWN = 0;
+ public static final int EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW = 1;
+ public static final int EXIT_REASON_APP_FINISHED = 2;
+ public static final int EXIT_REASON_DEVICE_FOLDED = 3;
+ public static final int EXIT_REASON_DRAG_DIVIDER = 4;
+ public static final int EXIT_REASON_RETURN_HOME = 5;
+ public static final int EXIT_REASON_ROOT_TASK_VANISHED = 6;
+ public static final int EXIT_REASON_SCREEN_LOCKED = 7;
+ public static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8;
+ public static final int EXIT_REASON_CHILD_TASK_ENTER_PIP = 9;
@IntDef(value = {
EXIT_REASON_UNKNOWN,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW,
@@ -124,6 +133,22 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
@Retention(RetentionPolicy.SOURCE)
@interface ExitReason{}
+ public static final int ENTER_REASON_UNKNOWN = 0;
+ public static final int ENTER_REASON_MULTI_INSTANCE = 1;
+ public static final int ENTER_REASON_DRAG = 2;
+ public static final int ENTER_REASON_LAUNCHER = 3;
+ /** Acts as a mapping to the actual EnterReasons as defined in the logging proto */
+ @IntDef(value = {
+ ENTER_REASON_MULTI_INSTANCE,
+ ENTER_REASON_DRAG,
+ ENTER_REASON_LAUNCHER,
+ ENTER_REASON_UNKNOWN
+ })
+ public @interface SplitEnterReason {
+ }
+
+ private final ShellCommandHandler mShellCommandHandler;
+ private final ShellController mShellController;
private final ShellTaskOrganizer mTaskOrganizer;
private final SyncTransactionQueue mSyncQueue;
private final Context mContext;
@@ -133,27 +158,37 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
private final DisplayController mDisplayController;
private final DisplayImeController mDisplayImeController;
private final DisplayInsetsController mDisplayInsetsController;
+ private final DragAndDropController mDragAndDropController;
private final Transitions mTransitions;
private final TransactionPool mTransactionPool;
- private final SplitscreenEventLogger mLogger;
private final IconProvider mIconProvider;
private final Optional<RecentTasksController> mRecentTasksOptional;
- private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
+ private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
private StageCoordinator mStageCoordinator;
// Only used for the legacy recents animation from splitscreen to allow the tasks to be animated
// outside the bounds of the roots by being reparented into a higher level fullscreen container
- private SurfaceControl mSplitTasksContainerLayer;
-
- public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
- SyncTransactionQueue syncQueue, Context context,
+ private SurfaceControl mGoingToRecentsTasksLayer;
+ private SurfaceControl mStartingSplitTasksLayer;
+
+ public SplitScreenController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ ShellTaskOrganizer shellTaskOrganizer,
+ SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTDAOrganizer,
- ShellExecutor mainExecutor, DisplayController displayController,
+ DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
- Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider,
+ DragAndDropController dragAndDropController,
+ Transitions transitions,
+ TransactionPool transactionPool,
+ IconProvider iconProvider,
Optional<RecentTasksController> recentTasks,
- Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+ ShellExecutor mainExecutor) {
+ mShellCommandHandler = shellCommandHandler;
+ mShellController = shellController;
mTaskOrganizer = shellTaskOrganizer;
mSyncQueue = syncQueue;
mContext = context;
@@ -162,18 +197,47 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
mDisplayController = displayController;
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
+ mDragAndDropController = dragAndDropController;
mTransitions = transitions;
mTransactionPool = transactionPool;
- mUnfoldControllerProvider = unfoldControllerProvider;
- mLogger = new SplitscreenEventLogger();
mIconProvider = iconProvider;
mRecentTasksOptional = recentTasks;
+ mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
+ // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
+ // override for this controller from the base module
+ if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
}
public SplitScreen asSplitScreen() {
return mImpl;
}
+ /**
+ * This will be called after ShellTaskOrganizer has initialized/registered because of the
+ * dependency order.
+ */
+ @VisibleForTesting
+ void onInit() {
+ mShellCommandHandler.addDumpCallback(this::dump, this);
+ mShellCommandHandler.addCommandCallback("splitscreen", mSplitScreenShellCommandHandler,
+ this);
+ mShellController.addKeyguardChangeListener(this);
+ if (mStageCoordinator == null) {
+ // TODO: Multi-display
+ mStageCoordinator = createStageCoordinator();
+ }
+ mDragAndDropController.setSplitScreenController(this);
+ }
+
+ protected StageCoordinator createStageCoordinator() {
+ return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
+ mTaskOrganizer, mDisplayController, mDisplayImeController,
+ mDisplayInsetsController, mTransitions, mTransactionPool,
+ mIconProvider, mMainExecutor, mRecentTasksOptional);
+ }
+
@Override
public Context getContext() {
return mContext;
@@ -184,20 +248,22 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
return mMainExecutor;
}
- public void onOrganizerRegistered() {
- if (mStageCoordinator == null) {
- // TODO: Multi-display
- mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
- mTaskOrganizer, mDisplayController, mDisplayImeController,
- mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
- mIconProvider, mMainExecutor, mRecentTasksOptional, mUnfoldControllerProvider);
- }
- }
-
public boolean isSplitScreenVisible() {
return mStageCoordinator.isSplitScreenVisible();
}
+ public StageCoordinator getTransitionHandler() {
+ return mStageCoordinator;
+ }
+
+ public ActivityManager.RunningTaskInfo getFocusingTaskInfo() {
+ return mStageCoordinator.getFocusingTaskInfo();
+ }
+
+ public boolean isValidToEnterSplitScreen(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
+ return mStageCoordinator.isValidToEnterSplitScreen(taskInfo);
+ }
+
@Nullable
public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) {
if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) {
@@ -222,6 +288,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
new WindowContainerTransaction());
}
+ /**
+ * Update surfaces of the split screen layout based on the current state
+ * @param transaction to write the updates to
+ */
+ public void updateSplitScreenSurfaces(SurfaceControl.Transaction transaction) {
+ mStageCoordinator.updateSurfaces(transaction);
+ }
+
private boolean moveToStage(int taskId, @StageType int stageType,
@SplitPosition int stagePosition, WindowContainerTransaction wct) {
final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId);
@@ -263,8 +337,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason);
}
- public void onKeyguardVisibilityChanged(boolean showing) {
- mStageCoordinator.onKeyguardVisibilityChanged(showing);
+ @Override
+ public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
+ boolean animatingDismiss) {
+ mStageCoordinator.onKeyguardVisibilityChanged(visible);
}
public void onFinishedWakingUp() {
@@ -288,179 +364,250 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
+ final int[] result = new int[1];
+ IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
+ @Override
+ public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+ RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps,
+ final IRemoteAnimationFinishedCallback finishedCallback) {
+ try {
+ finishedCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to invoke onAnimationFinished", e);
+ }
+ if (result[0] == START_SUCCESS || result[0] == START_TASK_TO_FRONT) {
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+ mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct);
+ mSyncQueue.queue(evictWct);
+ }
+ }
+ @Override
+ public void onAnimationCancelled(boolean isKeyguardOccluded) {
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+ mStageCoordinator.prepareEvictInvisibleChildTasks(evictWct);
+ mSyncQueue.queue(evictWct);
+ }
+ };
options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
null /* wct */);
+ RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
+ 0 /* duration */, 0 /* statusBarTransitionDelay */);
+ ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+ activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
try {
- final WindowContainerTransaction evictWct = new WindowContainerTransaction();
- mStageCoordinator.prepareEvictChildTasks(position, evictWct);
- final int result =
- ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
- if (result == START_SUCCESS || result == START_TASK_TO_FRONT) {
- mSyncQueue.queue(evictWct);
- }
+ result[0] = ActivityTaskManager.getService().startActivityFromRecents(taskId,
+ activityOptions.toBundle());
} catch (RemoteException e) {
Slog.e(TAG, "Failed to launch task", e);
}
}
+ /**
+ * See {@link #startShortcut(String, String, int, Bundle, UserHandle)}
+ * @param instanceId to be used by {@link SplitscreenEventLogger}
+ */
+ public void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
+ @Nullable Bundle options, UserHandle user, @NonNull InstanceId instanceId) {
+ mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER);
+ startShortcut(packageName, shortcutId, position, options, user);
+ }
+
+ @Override
public void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
@Nullable Bundle options, UserHandle user) {
+ IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
+ @Override
+ public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+ RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps,
+ final IRemoteAnimationFinishedCallback finishedCallback) {
+ try {
+ finishedCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to invoke onAnimationFinished", e);
+ }
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+ mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct);
+ mSyncQueue.queue(evictWct);
+ }
+ @Override
+ public void onAnimationCancelled(boolean isKeyguardOccluded) {
+ }
+ };
options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
null /* wct */);
- final WindowContainerTransaction evictWct = new WindowContainerTransaction();
- mStageCoordinator.prepareEvictChildTasks(position, evictWct);
-
+ RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
+ 0 /* duration */, 0 /* statusBarTransitionDelay */);
+ ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+ activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
try {
- LauncherApps launcherApps =
- mContext.getSystemService(LauncherApps.class);
+ LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
- options, user);
- mSyncQueue.queue(evictWct);
+ activityOptions.toBundle(), user);
} catch (ActivityNotFoundException e) {
Slog.e(TAG, "Failed to launch shortcut", e);
}
}
+ /**
+ * See {@link #startIntent(PendingIntent, Intent, int, Bundle)}
+ * @param instanceId to be used by {@link SplitscreenEventLogger}
+ */
+ public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
+ @SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) {
+ mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER);
+ startIntent(intent, fillInIntent, position, options);
+ }
+
+ @Override
public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
@SplitPosition int position, @Nullable Bundle options) {
- if (!ENABLE_SHELL_TRANSITIONS) {
- startIntentLegacy(intent, fillInIntent, position, options);
- return;
+ if (fillInIntent == null) {
+ fillInIntent = new Intent();
}
+ // Flag this as a no-user-action launch to prevent sending user leaving event to the
+ // current top activity since it's going to be put into another side of the split. This
+ // prevents the current top activity from going into pip mode due to user leaving event.
+ fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
- try {
- options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
- null /* wct */);
-
- // Flag this as a no-user-action launch to prevent sending user leaving event to the
- // current top activity since it's going to be put into another side of the split. This
- // prevents the current top activity from going into pip mode due to user leaving event.
- if (fillInIntent == null) {
- fillInIntent = new Intent();
+ // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
+ // split and there is no reusable background task.
+ if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) {
+ final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional.isPresent()
+ ? mRecentTasksOptional.get().findTaskInBackground(
+ intent.getIntent().getComponent())
+ : null;
+ if (taskInfo != null) {
+ startTask(taskInfo.taskId, position, options);
+ return;
}
- fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ }
- intent.send(mContext, 0, fillInIntent, null /* onFinished */, null /* handler */,
- null /* requiredPermission */, options);
- } catch (PendingIntent.CanceledException e) {
- Slog.e(TAG, "Failed to launch task", e);
+ if (!ENABLE_SHELL_TRANSITIONS) {
+ mStageCoordinator.startIntentLegacy(intent, fillInIntent, position, options);
+ return;
}
+ mStageCoordinator.startIntent(intent, fillInIntent, position, options);
}
- private void startIntentLegacy(PendingIntent intent, @Nullable Intent fillInIntent,
- @SplitPosition int position, @Nullable Bundle options) {
- final WindowContainerTransaction evictWct = new WindowContainerTransaction();
- mStageCoordinator.prepareEvictChildTasks(position, evictWct);
-
- LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
- @Override
- public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback,
- SurfaceControl.Transaction t) {
- if (apps == null || apps.length == 0) {
- final ActivityManager.RunningTaskInfo pairedTaskInfo =
- getTaskInfo(SplitLayout.reversePosition(position));
- final ComponentName pairedActivity =
- pairedTaskInfo != null ? pairedTaskInfo.baseActivity : null;
- final ComponentName intentActivity =
- intent.getIntent() != null ? intent.getIntent().getComponent() : null;
- if (pairedActivity != null && pairedActivity.equals(intentActivity)) {
- // Switch split position if dragging the same activity to another side.
- setSideStagePosition(SplitLayout.reversePosition(
- mStageCoordinator.getSideStagePosition()));
- }
-
- // Do nothing when the animation was cancelled.
- t.apply();
- return;
- }
-
- mStageCoordinator.updateSurfaceBounds(null /* layout */, t,
- false /* applyResizingOffset */);
- for (int i = 0; i < apps.length; ++i) {
- if (apps[i].mode == MODE_OPENING) {
- t.show(apps[i].leash);
- }
- }
- t.apply();
+ /** Returns {@code true} if it's launching the same component on both sides of the split. */
+ @VisibleForTesting
+ boolean shouldAddMultipleTaskFlag(@Nullable Intent startIntent, @SplitPosition int position) {
+ if (startIntent == null) {
+ return false;
+ }
- if (finishedCallback != null) {
- try {
- finishedCallback.onAnimationFinished();
- } catch (RemoteException e) {
- Slog.e(TAG, "Error finishing legacy transition: ", e);
- }
- }
+ final ComponentName launchingActivity = startIntent.getComponent();
+ if (launchingActivity == null) {
+ return false;
+ }
- mSyncQueue.queue(evictWct);
+ if (isSplitScreenVisible()) {
+ // To prevent users from constantly dropping the same app to the same side resulting in
+ // a large number of instances in the background.
+ final ActivityManager.RunningTaskInfo targetTaskInfo = getTaskInfo(position);
+ final ComponentName targetActivity = targetTaskInfo != null
+ ? targetTaskInfo.baseIntent.getComponent() : null;
+ if (Objects.equals(launchingActivity, targetActivity)) {
+ return false;
}
- };
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct);
+ // Allow users to start a new instance the same to adjacent side.
+ final ActivityManager.RunningTaskInfo pairedTaskInfo =
+ getTaskInfo(SplitLayout.reversePosition(position));
+ final ComponentName pairedActivity = pairedTaskInfo != null
+ ? pairedTaskInfo.baseIntent.getComponent() : null;
+ return Objects.equals(launchingActivity, pairedActivity);
+ }
- // Flag this as a no-user-action launch to prevent sending user leaving event to the current
- // top activity since it's going to be put into another side of the split. This prevents the
- // current top activity from going into pip mode due to user leaving event.
- if (fillInIntent == null) {
- fillInIntent = new Intent();
+ final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo();
+ if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) {
+ return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
}
- fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
- wct.sendPendingIntent(intent, fillInIntent, options);
- mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
+ return false;
}
RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) {
+ if (ENABLE_SHELL_TRANSITIONS) return null;
+
if (isSplitScreenVisible()) {
// Evict child tasks except the top visible one under split root to ensure it could be
// launched as full screen when switching to it on recents.
final WindowContainerTransaction wct = new WindowContainerTransaction();
mStageCoordinator.prepareEvictInvisibleChildTasks(wct);
mSyncQueue.queue(wct);
+ } else {
+ return null;
}
- return reparentSplitTasksForAnimation(apps, true /*splitExpectedToBeVisible*/);
- }
- RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) {
- return reparentSplitTasksForAnimation(apps, false /*splitExpectedToBeVisible*/);
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ if (mGoingToRecentsTasksLayer != null) {
+ t.remove(mGoingToRecentsTasksLayer);
+ }
+ mGoingToRecentsTasksLayer = reparentSplitTasksForAnimation(apps, t,
+ "SplitScreenController#onGoingToRecentsLegacy" /* callsite */);
+ t.apply();
+ mTransactionPool.release(t);
+
+ return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()};
}
- private RemoteAnimationTarget[] reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps,
- boolean splitExpectedToBeVisible) {
+ RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) {
if (ENABLE_SHELL_TRANSITIONS) return null;
- // TODO(b/206487881): Integrate this with shell transition.
- if (splitExpectedToBeVisible && !isSplitScreenVisible()) return null;
- // Split not visible, but not enough apps to have split, also return null
- if (!splitExpectedToBeVisible && apps.length < 2) return null;
- SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
- if (mSplitTasksContainerLayer != null) {
- // Remove the previous layer before recreating
- transaction.remove(mSplitTasksContainerLayer);
+ int openingApps = 0;
+ for (int i = 0; i < apps.length; ++i) {
+ if (apps[i].mode == MODE_OPENING) openingApps++;
+ }
+ if (openingApps < 2) {
+ // Not having enough apps to enter split screen
+ return null;
+ }
+
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ if (mStartingSplitTasksLayer != null) {
+ t.remove(mStartingSplitTasksLayer);
+ }
+ mStartingSplitTasksLayer = reparentSplitTasksForAnimation(apps, t,
+ "SplitScreenController#onStartingSplitLegacy" /* callsite */);
+ t.apply();
+ mTransactionPool.release(t);
+
+ try {
+ return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()};
+ } finally {
+ for (RemoteAnimationTarget appTarget : apps) {
+ if (appTarget.leash != null) {
+ appTarget.leash.release();
+ }
+ }
}
+ }
+
+ private SurfaceControl reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps,
+ SurfaceControl.Transaction t, String callsite) {
final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
.setContainerLayer()
.setName("RecentsAnimationSplitTasks")
.setHidden(false)
- .setCallsite("SplitScreenController#onGoingtoRecentsLegacy");
+ .setCallsite(callsite);
mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder);
- mSplitTasksContainerLayer = builder.build();
-
- // Ensure that we order these in the parent in the right z-order as their previous order
- Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex);
- int layer = 1;
- for (RemoteAnimationTarget appTarget : apps) {
- transaction.reparent(appTarget.leash, mSplitTasksContainerLayer);
- transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left,
+ final SurfaceControl splitTasksLayer = builder.build();
+
+ for (int i = 0; i < apps.length; ++i) {
+ final RemoteAnimationTarget appTarget = apps[i];
+ t.reparent(appTarget.leash, splitTasksLayer);
+ t.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left,
appTarget.screenSpaceBounds.top);
- transaction.setLayer(appTarget.leash, layer++);
}
- transaction.apply();
- transaction.close();
- return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()};
+ return splitTasksLayer;
}
/**
* Sets drag info to be logged when splitscreen is entered.
@@ -535,6 +682,17 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
@Override
+ public void onSplitBoundsChanged(Rect rootBounds, Rect mainBounds, Rect sideBounds) {
+ for (int i = 0; i < mExecutors.size(); i++) {
+ final int index = i;
+ mExecutors.valueAt(index).execute(() -> {
+ mExecutors.keyAt(index).onSplitBoundsChanged(rootBounds, mainBounds,
+ sideBounds);
+ });
+ }
+ }
+
+ @Override
public void onSplitVisibilityChanged(boolean visible) {
for (int i = 0; i < mExecutors.size(); i++) {
final int index = i;
@@ -583,13 +741,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
@Override
- public void onKeyguardVisibilityChanged(boolean showing) {
- mMainExecutor.execute(() -> {
- SplitScreenController.this.onKeyguardVisibilityChanged(showing);
- });
- }
-
- @Override
public void onFinishedWakingUp() {
mMainExecutor.execute(() -> {
SplitScreenController.this.onFinishedWakingUp();
@@ -679,49 +830,64 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
@Override
public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
- float splitRatio, RemoteAnimationAdapter adapter) {
+ float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startTasks",
(controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition(
mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition,
- splitRatio, adapter));
+ splitRatio, adapter, instanceId));
}
@Override
public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
Intent fillInIntent, int taskId, Bundle mainOptions, Bundle sideOptions,
- int sidePosition, float splitRatio, RemoteAnimationAdapter adapter) {
+ int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+ InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController,
"startIntentAndTaskWithLegacyTransition", (controller) ->
controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition(
pendingIntent, fillInIntent, taskId, mainOptions, sideOptions,
- sidePosition, splitRatio, adapter));
+ sidePosition, splitRatio, adapter, instanceId));
+ }
+
+ @Override
+ public void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
+ int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions,
+ @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+ InstanceId instanceId) {
+ executeRemoteCallWithTaskPermission(mController,
+ "startShortcutAndTaskWithLegacyTransition", (controller) ->
+ controller.mStageCoordinator.startShortcutAndTaskWithLegacyTransition(
+ shortcutInfo, taskId, mainOptions, sideOptions, sidePosition,
+ splitRatio, adapter, instanceId));
}
@Override
public void startTasks(int mainTaskId, @Nullable Bundle mainOptions,
int sideTaskId, @Nullable Bundle sideOptions,
@SplitPosition int sidePosition, float splitRatio,
- @Nullable RemoteTransition remoteTransition) {
+ @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startTasks",
(controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions,
- sideTaskId, sideOptions, sidePosition, splitRatio, remoteTransition));
+ sideTaskId, sideOptions, sidePosition, splitRatio, remoteTransition,
+ instanceId));
}
@Override
public void startShortcut(String packageName, String shortcutId, int position,
- @Nullable Bundle options, UserHandle user) {
+ @Nullable Bundle options, UserHandle user, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startShortcut",
(controller) -> {
- controller.startShortcut(packageName, shortcutId, position, options, user);
+ controller.startShortcut(packageName, shortcutId, position, options, user,
+ instanceId);
});
}
@Override
public void startIntent(PendingIntent intent, Intent fillInIntent, int position,
- @Nullable Bundle options) {
+ @Nullable Bundle options, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startIntent",
(controller) -> {
- controller.startIntent(intent, fillInIntent, position, options);
+ controller.startIntent(intent, fillInIntent, position, options, instanceId);
});
}
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
new file mode 100644
index 000000000000..7fd03a9a306b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java
@@ -0,0 +1,96 @@
+/*
+ * 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.splitscreen;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+
+import com.android.wm.shell.sysui.ShellCommandHandler;
+
+import java.io.PrintWriter;
+
+/**
+ * Handles the shell commands for the SplitscreenController.
+ */
+public class SplitScreenShellCommandHandler implements
+ ShellCommandHandler.ShellCommandActionHandler {
+
+ private final SplitScreenController mController;
+
+ public SplitScreenShellCommandHandler(SplitScreenController controller) {
+ mController = controller;
+ }
+
+ @Override
+ public boolean onShellCommand(String[] args, PrintWriter pw) {
+ switch (args[0]) {
+ case "moveToSideStage":
+ return runMoveToSideStage(args, pw);
+ case "removeFromSideStage":
+ return runRemoveFromSideStage(args, pw);
+ case "setSideStagePosition":
+ return runSetSideStagePosition(args, pw);
+ default:
+ pw.println("Invalid command: " + args[0]);
+ return false;
+ }
+ }
+
+ private boolean runMoveToSideStage(String[] args, PrintWriter pw) {
+ if (args.length < 3) {
+ // First argument is the action name.
+ pw.println("Error: task id should be provided as arguments");
+ return false;
+ }
+ final int taskId = new Integer(args[1]);
+ final int sideStagePosition = args.length > 2
+ ? new Integer(args[2]) : SPLIT_POSITION_BOTTOM_OR_RIGHT;
+ mController.moveToSideStage(taskId, sideStagePosition);
+ return true;
+ }
+
+ private boolean runRemoveFromSideStage(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 = new Integer(args[1]);
+ mController.removeFromSideStage(taskId);
+ return true;
+ }
+
+ private boolean runSetSideStagePosition(String[] args, PrintWriter pw) {
+ if (args.length < 2) {
+ // First argument is the action name.
+ pw.println("Error: side stage position should be provided as arguments");
+ return false;
+ }
+ final int position = new Integer(args[1]);
+ mController.setSideStagePosition(position);
+ return true;
+ }
+
+ @Override
+ public void printShellCommandHelp(PrintWriter pw, String prefix) {
+ pw.println(prefix + "moveToSideStage <taskId> <SideStagePosition>");
+ pw.println(prefix + " Move a task with given id in split-screen mode.");
+ pw.println(prefix + "removeFromSideStage <taskId>");
+ pw.println(prefix + " Remove a task with given id in split-screen mode.");
+ pw.println(prefix + "setSideStagePosition <SideStagePosition>");
+ pw.println(prefix + " Sets the position of the side-stage.");
+ }
+}
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 cd121ed41fdd..d7ca791e3863 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
@@ -21,7 +21,6 @@ import static android.view.WindowManager.TRANSIT_CLOSE;
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 android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
@@ -58,19 +57,16 @@ import java.util.ArrayList;
class SplitScreenTransitions {
private static final String TAG = "SplitScreenTransitions";
- /** Flag applied to a transition change to identify it as a divider bar for animation. */
- public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
-
private final TransactionPool mTransactionPool;
private final Transitions mTransitions;
private final Runnable mOnFinish;
DismissTransition mPendingDismiss = null;
- IBinder mPendingEnter = null;
- IBinder mPendingRecent = null;
+ TransitSession mPendingEnter = null;
+ TransitSession mPendingRecent = null;
private IBinder mAnimatingTransition = null;
- private OneShotRemoteHandler mPendingRemoteHandler = null;
+ OneShotRemoteHandler mPendingRemoteHandler = null;
private OneShotRemoteHandler mActiveRemoteHandler = null;
private final Transitions.TransitionFinishCallback mRemoteFinishCB = this::onFinish;
@@ -94,9 +90,11 @@ class SplitScreenTransitions {
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback,
- @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) {
+ @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
+ @NonNull WindowContainerToken topRoot) {
mFinishCallback = finishCallback;
mAnimatingTransition = transition;
+ mFinishTransaction = finishTransaction;
if (mPendingRemoteHandler != null) {
mPendingRemoteHandler.startAnimation(transition, info, startTransaction,
finishTransaction, mRemoteFinishCB);
@@ -104,14 +102,12 @@ class SplitScreenTransitions {
mPendingRemoteHandler = null;
return;
}
- playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot);
+ playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot, topRoot);
}
private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot,
- @NonNull WindowContainerToken sideRoot) {
- mFinishTransaction = mTransactionPool.acquire();
-
+ @NonNull WindowContainerToken sideRoot, @NonNull WindowContainerToken topRoot) {
// Play some place-holder fade animations
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
@@ -140,11 +136,14 @@ class SplitScreenTransitions {
endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
startExampleResizeAnimation(leash, startBounds, endBounds);
}
- if (change.getParent() != null) {
+ boolean isRootOrSplitSideRoot = change.getParent() == null
+ || topRoot.equals(change.getParent());
+ // For enter or exit, we only want to animate the side roots but not the top-root.
+ if (!isRootOrSplitSideRoot || topRoot.equals(change.getContainer())) {
continue;
}
- if (transition == mPendingEnter && (mainRoot.equals(change.getContainer())
+ if (isPendingEnter(transition) && (mainRoot.equals(change.getContainer())
|| sideRoot.equals(change.getContainer()))) {
t.setPosition(leash, change.getEndAbsBounds().left, change.getEndAbsBounds().top);
t.setWindowCrop(leash, change.getEndAbsBounds().width(),
@@ -170,12 +169,40 @@ class SplitScreenTransitions {
onFinish(null /* wct */, null /* wctCB */);
}
+ boolean isPendingTransition(IBinder transition) {
+ return isPendingEnter(transition)
+ || isPendingDismiss(transition)
+ || isPendingRecent(transition);
+ }
+
+ boolean isPendingEnter(IBinder transition) {
+ return mPendingEnter != null && mPendingEnter.mTransition == transition;
+ }
+
+ boolean isPendingRecent(IBinder transition) {
+ return mPendingRecent != null && mPendingRecent.mTransition == transition;
+ }
+
+ boolean isPendingDismiss(IBinder transition) {
+ return mPendingDismiss != null && mPendingDismiss.mTransition == transition;
+ }
+
/** Starts a transition to enter split with a remote transition animator. */
- IBinder startEnterTransition(@WindowManager.TransitionType int transitType,
- @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition,
- @NonNull Transitions.TransitionHandler handler) {
+ IBinder startEnterTransition(
+ @WindowManager.TransitionType int transitType,
+ WindowContainerTransaction wct,
+ @Nullable RemoteTransition remoteTransition,
+ Transitions.TransitionHandler handler,
+ @Nullable TransitionCallback callback) {
final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
- mPendingEnter = transition;
+ setEnterTransition(transition, remoteTransition, callback);
+ return transition;
+ }
+
+ /** Sets a transition to enter split. */
+ void setEnterTransition(@NonNull IBinder transition,
+ @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) {
+ mPendingEnter = new TransitSession(transition, callback);
if (remoteTransition != null) {
// Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
@@ -183,32 +210,35 @@ class SplitScreenTransitions {
mTransitions.getMainExecutor(), remoteTransition);
mPendingRemoteHandler.setTransition(transition);
}
- return transition;
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
+ + " deduced Enter split screen");
}
/** Starts a transition to dismiss split. */
- IBinder startDismissTransition(@Nullable IBinder transition, WindowContainerTransaction wct,
+ IBinder startDismissTransition(WindowContainerTransaction wct,
Transitions.TransitionHandler handler, @SplitScreen.StageType int dismissTop,
@SplitScreenController.ExitReason int reason) {
final int type = reason == EXIT_REASON_DRAG_DIVIDER
? TRANSIT_SPLIT_DISMISS_SNAP : TRANSIT_SPLIT_DISMISS;
- if (transition == null) {
- transition = mTransitions.startTransition(type, wct, handler);
- }
+ IBinder transition = mTransitions.startTransition(type, wct, handler);
+ setDismissTransition(transition, dismissTop, reason);
+ return transition;
+ }
+
+ /** Sets a transition to dismiss split. */
+ void setDismissTransition(@NonNull IBinder transition, @SplitScreen.StageType int dismissTop,
+ @SplitScreenController.ExitReason int reason) {
mPendingDismiss = new DismissTransition(transition, reason, dismissTop);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
+ " deduced Dismiss due to %s. toTop=%s",
exitReasonToString(reason), stageTypeToString(dismissTop));
- return transition;
}
- IBinder startRecentTransition(@Nullable IBinder transition, WindowContainerTransaction wct,
- Transitions.TransitionHandler handler, @Nullable RemoteTransition remoteTransition) {
- if (transition == null) {
- transition = mTransitions.startTransition(TRANSIT_OPEN, wct, handler);
- }
- mPendingRecent = transition;
+ void setRecentTransition(@NonNull IBinder transition,
+ @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) {
+ mPendingRecent = new TransitSession(transition, callback);
if (remoteTransition != null) {
// Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
@@ -219,44 +249,93 @@ class SplitScreenTransitions {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
+ " deduced Enter recent panel");
- return transition;
}
void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t,
IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) {
- if (mergeTarget == mAnimatingTransition && mActiveRemoteHandler != null) {
+ if (mergeTarget != mAnimatingTransition) return;
+
+ if (isPendingEnter(transition) && isPendingRecent(mergeTarget)) {
+ mPendingRecent.mCallback = new TransitionCallback() {
+ @Override
+ public void onTransitionFinished(WindowContainerTransaction finishWct,
+ SurfaceControl.Transaction finishT) {
+ // Since there's an entering transition merged, recent transition no longer
+ // need to handle entering split screen after the transition finished.
+ }
+ };
+ }
+
+ if (mActiveRemoteHandler != null) {
mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+ } else {
+ for (int i = mAnimations.size() - 1; i >= 0; --i) {
+ final Animator anim = mAnimations.get(i);
+ mTransitions.getAnimExecutor().execute(anim::end);
+ }
+ }
+ }
+
+ boolean end() {
+ // If its remote, there's nothing we can do right now.
+ if (mActiveRemoteHandler != null) return false;
+ for (int i = mAnimations.size() - 1; i >= 0; --i) {
+ final Animator anim = mAnimations.get(i);
+ mTransitions.getAnimExecutor().execute(anim::end);
+ }
+ return true;
+ }
+
+ void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishT) {
+ if (isPendingEnter(transition)) {
+ if (!aborted) {
+ // An enter transition got merged, appends the rest operations to finish entering
+ // split screen.
+ mStageCoordinator.finishEnterSplitScreen(finishT);
+ mPendingRemoteHandler = null;
+ }
+
+ mPendingEnter.mCallback.onTransitionConsumed(aborted);
+ mPendingEnter = null;
+ mPendingRemoteHandler = null;
+ } else if (isPendingDismiss(transition)) {
+ mPendingDismiss.mCallback.onTransitionConsumed(aborted);
+ mPendingDismiss = null;
+ } else if (isPendingRecent(transition)) {
+ mPendingRecent.mCallback.onTransitionConsumed(aborted);
+ mPendingRecent = null;
+ mPendingRemoteHandler = null;
}
}
void onFinish(WindowContainerTransaction wct, WindowContainerTransactionCallback wctCB) {
if (!mAnimations.isEmpty()) return;
- if (mAnimatingTransition == mPendingEnter) {
+
+ TransitionCallback callback = null;
+ if (isPendingEnter(mAnimatingTransition)) {
+ callback = mPendingEnter.mCallback;
mPendingEnter = null;
}
- if (mPendingDismiss != null && mPendingDismiss.mTransition == mAnimatingTransition) {
+ if (isPendingDismiss(mAnimatingTransition)) {
+ callback = mPendingDismiss.mCallback;
mPendingDismiss = null;
}
- if (mAnimatingTransition == mPendingRecent) {
- // If the clean-up wct is null when finishing recent transition, it indicates it's
- // returning to home and thus no need to reorder tasks.
- final boolean returnToHome = wct == null;
- if (returnToHome) {
- wct = new WindowContainerTransaction();
- }
- mStageCoordinator.onRecentTransitionFinished(returnToHome, wct, mFinishTransaction);
+ if (isPendingRecent(mAnimatingTransition)) {
+ callback = mPendingRecent.mCallback;
mPendingRecent = null;
}
+
+ if (callback != null) {
+ if (wct == null) wct = new WindowContainerTransaction();
+ callback.onTransitionFinished(wct, mFinishTransaction);
+ }
+
mPendingRemoteHandler = null;
mActiveRemoteHandler = null;
mAnimatingTransition = null;
mOnFinish.run();
- if (mFinishTransaction != null) {
- mFinishTransaction.apply();
- mTransactionPool.release(mFinishTransaction);
- mFinishTransaction = null;
- }
if (mFinishCallback != null) {
mFinishCallback.onTransitionFinished(wct /* wct */, wctCB /* wctCB */);
mFinishCallback = null;
@@ -353,17 +432,34 @@ class SplitScreenTransitions {
|| info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
}
- /** Bundled information of dismiss transition. */
- static class DismissTransition {
- IBinder mTransition;
+ /** Clean-up callbacks for transition. */
+ interface TransitionCallback {
+ /** Calls when the transition got consumed. */
+ default void onTransitionConsumed(boolean aborted) {}
- int mReason;
+ /** Calls when the transition finished. */
+ default void onTransitionFinished(WindowContainerTransaction finishWct,
+ SurfaceControl.Transaction finishT) {}
+ }
+
+ /** Session for a transition and its clean-up callback. */
+ static class TransitSession {
+ final IBinder mTransition;
+ TransitionCallback mCallback;
+
+ TransitSession(IBinder transition, @Nullable TransitionCallback callback) {
+ mTransition = transition;
+ mCallback = callback != null ? callback : new TransitionCallback() {};
+ }
+ }
- @SplitScreen.StageType
- int mDismissTop;
+ /** Bundled information of dismiss transition. */
+ static class DismissTransition extends TransitSession {
+ final int mReason;
+ final @SplitScreen.StageType int mDismissTop;
DismissTransition(IBinder transition, int reason, int dismissTop) {
- this.mTransition = transition;
+ super(transition, null /* callback */);
this.mReason = reason;
this.mDismissTop = dismissTop;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
index 3e7a1004ed7a..2dc4a0441b06 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
@@ -16,7 +16,9 @@
package com.android.wm.shell.splitscreen;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
@@ -28,6 +30,10 @@ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED_
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_DRAG;
+import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER;
+import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE;
+import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_UNKNOWN;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
@@ -38,6 +44,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 android.annotation.Nullable;
import android.util.Slog;
import com.android.internal.logging.InstanceId;
@@ -59,7 +66,7 @@ public class SplitscreenEventLogger {
// Drag info
private @SplitPosition int mDragEnterPosition;
- private InstanceId mDragEnterSessionId;
+ private @Nullable InstanceId mEnterSessionId;
// For deduping async events
private int mLastMainStagePosition = -1;
@@ -67,6 +74,7 @@ public class SplitscreenEventLogger {
private int mLastSideStagePosition = -1;
private int mLastSideStageUid = -1;
private float mLastSplitRatio = -1f;
+ private @SplitScreenController.SplitEnterReason int mEnterReason = ENTER_REASON_UNKNOWN;
public SplitscreenEventLogger() {
mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE);
@@ -79,12 +87,35 @@ public class SplitscreenEventLogger {
return mLoggerSessionId != null;
}
+ public boolean isEnterRequestedByDrag() {
+ return mEnterReason == ENTER_REASON_DRAG;
+ }
+
/**
* May be called before logEnter() to indicate that the session was started from a drag.
*/
- public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) {
+ public void enterRequestedByDrag(@SplitPosition int position, InstanceId enterSessionId) {
mDragEnterPosition = position;
- mDragEnterSessionId = dragSessionId;
+ enterRequested(enterSessionId, ENTER_REASON_DRAG);
+ }
+
+ /**
+ * May be called before logEnter() to indicate that the session was started from launcher.
+ * This specifically is for all the scenarios where split started without a drag interaction
+ */
+ public void enterRequested(@Nullable InstanceId enterSessionId,
+ @SplitScreenController.SplitEnterReason int enterReason) {
+ mEnterSessionId = enterSessionId;
+ mEnterReason = enterReason;
+ }
+
+ /**
+ * @return if an enterSessionId has been set via either
+ * {@link #enterRequested(InstanceId, int)} or
+ * {@link #enterRequestedByDrag(int, InstanceId)}
+ */
+ public boolean hasValidEnterSessionId() {
+ return mEnterSessionId != null;
}
/**
@@ -95,9 +126,7 @@ public class SplitscreenEventLogger {
@SplitPosition int sideStagePosition, int sideStageUid,
boolean isLandscape) {
mLoggerSessionId = mIdSequence.newInstanceId();
- int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED
- ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape)
- : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+ int enterReason = getLoggerEnterReason(isLandscape);
updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
mainStageUid);
updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
@@ -112,10 +141,24 @@ public class SplitscreenEventLogger {
mLastMainStageUid,
mLastSideStagePosition,
mLastSideStageUid,
- mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0,
+ mEnterSessionId != null ? mEnterSessionId.getId() : 0,
mLoggerSessionId.getId());
}
+ private int getLoggerEnterReason(boolean isLandscape) {
+ switch (mEnterReason) {
+ case ENTER_REASON_MULTI_INSTANCE:
+ return SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE;
+ case ENTER_REASON_LAUNCHER:
+ return SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER;
+ case ENTER_REASON_DRAG:
+ return getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape);
+ case ENTER_REASON_UNKNOWN:
+ default:
+ return SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER;
+ }
+ }
+
/**
* Returns the framework logging constant given a splitscreen exit reason.
*/
@@ -176,11 +219,12 @@ public class SplitscreenEventLogger {
// Reset states
mLoggerSessionId = null;
mDragEnterPosition = SPLIT_POSITION_UNDEFINED;
- mDragEnterSessionId = null;
+ mEnterSessionId = null;
mLastMainStagePosition = -1;
mLastMainStageUid = -1;
mLastSideStagePosition = -1;
mLastSideStageUid = -1;
+ mEnterReason = ENTER_REASON_UNKNOWN;
}
/**
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 30f316efb2b3..c17f8226c925 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
@@ -18,25 +18,36 @@ package com.android.wm.shell.splitscreen;
import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED;
+import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
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_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.transitTypeToString;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
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.ENTER_REASON_LAUNCHER;
+import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
@@ -47,7 +58,6 @@ 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.splitscreen.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR;
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;
@@ -62,11 +72,12 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
-import android.app.ActivityTaskManager;
+import android.app.IActivityTaskManager;
import android.app.PendingIntent;
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.hardware.devicestate.DeviceStateManager;
@@ -74,6 +85,7 @@ import android.os.Bundle;
import android.os.Debug;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.ServiceManager;
import android.util.Log;
import android.util.Slog;
import android.view.Choreographer;
@@ -84,6 +96,8 @@ import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.view.WindowManager;
+import android.widget.Toast;
+import android.window.DisplayAreaInfo;
import android.window.RemoteTransition;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
@@ -93,7 +107,9 @@ import android.window.WindowContainerTransaction;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.ArrayUtils;
import com.android.launcher3.icons.IconProvider;
+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.DisplayImeController;
@@ -109,16 +125,16 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.splitscreen.SplitScreen.StageType;
import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
+import com.android.wm.shell.transition.DefaultMixedHandler;
+import com.android.wm.shell.transition.LegacyTransitions;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.StagedSplitBounds;
+import com.android.wm.shell.util.SplitBounds;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
-import javax.inject.Provider;
-
/**
* Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
* {@link SideStage} stages.
@@ -132,9 +148,9 @@ import javax.inject.Provider;
* This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and
* {@link #onStageHasChildrenChanged(StageListenerImpl).}
*/
-class StageCoordinator implements SplitLayout.SplitLayoutHandler,
+public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
DisplayController.OnDisplaysChangedListener, Transitions.TransitionHandler,
- ShellTaskOrganizer.TaskListener {
+ ShellTaskOrganizer.TaskListener, ShellTaskOrganizer.FocusListener {
private static final String TAG = StageCoordinator.class.getSimpleName();
@@ -142,10 +158,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private final MainStage mMainStage;
private final StageListenerImpl mMainStageListener = new StageListenerImpl();
- private final StageTaskUnfoldController mMainUnfoldController;
private final SideStage mSideStage;
private final StageListenerImpl mSideStageListener = new StageListenerImpl();
- private final StageTaskUnfoldController mSideUnfoldController;
private final DisplayLayout mDisplayLayout;
@SplitPosition
private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -168,6 +182,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private final ShellExecutor mMainExecutor;
private final Optional<RecentTasksController> mRecentTasks;
+ private final Rect mTempRect1 = new Rect();
+ private final Rect mTempRect2 = new Rect();
+
+ private ActivityManager.RunningTaskInfo mFocusingTaskInfo;
+
/**
* A single-top root task which the split divider attached to.
*/
@@ -181,12 +200,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private boolean mShouldUpdateRecents;
private boolean mExitSplitScreenOnHide;
private boolean mIsDividerRemoteAnimating;
- private boolean mResizingSplits;
+ private boolean mIsExiting;
/** The target stage to dismiss to when unlock after folded. */
@StageType
private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+ private DefaultMixedHandler mMixedHandler;
+
private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
new SplitWindowManager.ParentContainerCallbacks() {
@Override
@@ -196,27 +217,57 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Override
public void onLeashReady(SurfaceControl leash) {
- mSyncQueue.runInSync(t -> applyDividerVisibility(t));
+ // This is for avoiding divider invisible due to delay of creating so only need
+ // to do when divider should visible case.
+ if (mDividerVisible) {
+ mSyncQueue.runInSync(t -> applyDividerVisibility(t));
+ }
}
};
- StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
+ private final SplitScreenTransitions.TransitionCallback mRecentTransitionCallback =
+ new SplitScreenTransitions.TransitionCallback() {
+ @Override
+ public void onTransitionFinished(WindowContainerTransaction finishWct,
+ SurfaceControl.Transaction finishT) {
+ // Check if the recent transition is finished by returning to the current split, so we
+ // can restore the divider bar.
+ for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
+ final WindowContainerTransaction.HierarchyOp op =
+ finishWct.getHierarchyOps().get(i);
+ final IBinder container = op.getContainer();
+ if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
+ && (mMainStage.containsContainer(container)
+ || mSideStage.containsContainer(container))) {
+ updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */);
+ setDividerVisibility(true, finishT);
+ return;
+ }
+ }
+
+ // Dismiss the split screen if it's not returning to split.
+ prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct);
+ setSplitsVisible(false);
+ setDividerVisibility(false, finishT);
+ logExit(EXIT_REASON_UNKNOWN);
+ }
+ };
+
+ protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
ShellTaskOrganizer taskOrganizer, DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController, Transitions transitions,
- TransactionPool transactionPool, SplitscreenEventLogger logger,
+ TransactionPool transactionPool,
IconProvider iconProvider, ShellExecutor mainExecutor,
- Optional<RecentTasksController> recentTasks,
- Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+ Optional<RecentTasksController> recentTasks) {
mContext = context;
mDisplayId = displayId;
mSyncQueue = syncQueue;
mTaskOrganizer = taskOrganizer;
- mLogger = logger;
+ mLogger = new SplitscreenEventLogger();
mMainExecutor = mainExecutor;
mRecentTasks = recentTasks;
- mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
- mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
+
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */);
mMainStage = new MainStage(
@@ -226,8 +277,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mMainStageListener,
mSyncQueue,
mSurfaceSession,
- iconProvider,
- mMainUnfoldController);
+ iconProvider);
mSideStage = new SideStage(
mContext,
mTaskOrganizer,
@@ -235,8 +285,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSideStageListener,
mSyncQueue,
mSurfaceSession,
- iconProvider,
- mSideUnfoldController);
+ iconProvider);
mDisplayController = displayController;
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
@@ -250,6 +299,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDisplayController.addDisplayWindowListener(this);
mDisplayLayout = new DisplayLayout(displayController.getDisplayLayout(displayId));
transitions.addHandler(this);
+ mTaskOrganizer.addFocusListener(this);
}
@VisibleForTesting
@@ -258,9 +308,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
DisplayController displayController, DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
Transitions transitions, TransactionPool transactionPool,
- SplitscreenEventLogger logger, ShellExecutor mainExecutor,
- Optional<RecentTasksController> recentTasks,
- Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+ ShellExecutor mainExecutor,
+ Optional<RecentTasksController> recentTasks) {
mContext = context;
mDisplayId = displayId;
mSyncQueue = syncQueue;
@@ -274,9 +323,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitLayout = splitLayout;
mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
this::onTransitionAnimationComplete, this);
- mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
- mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
- mLogger = logger;
+ mLogger = new SplitscreenEventLogger();
mMainExecutor = mainExecutor;
mRecentTasks = recentTasks;
mDisplayController.addDisplayWindowListener(this);
@@ -284,6 +331,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
transitions.addHandler(this);
}
+ public void setMixedHandler(DefaultMixedHandler mixedHandler) {
+ mMixedHandler = mixedHandler;
+ }
+
@VisibleForTesting
SplitScreenTransitions getSplitTransitions() {
return mSplitTransitions;
@@ -329,15 +380,23 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
targetStage.evictAllChildren(evictWct);
targetStage.addTask(task, wct);
- if (!evictWct.isEmpty()) {
- wct.merge(evictWct, true /* transfer */);
- }
if (ENABLE_SHELL_TRANSITIONS) {
prepareEnterSplitScreen(wct);
- mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE,
- wct, null, this);
+ mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct,
+ null, this, new SplitScreenTransitions.TransitionCallback() {
+ @Override
+ public void onTransitionFinished(WindowContainerTransaction finishWct,
+ SurfaceControl.Transaction finishT) {
+ if (!evictWct.isEmpty()) {
+ finishWct.merge(evictWct, true);
+ }
+ }
+ });
} else {
+ if (!evictWct.isEmpty()) {
+ wct.merge(evictWct, true /* transfer */);
+ }
mTaskOrganizer.applyTransaction(wct);
}
return true;
@@ -357,21 +416,126 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return result;
}
+ SplitscreenEventLogger getLogger() {
+ return mLogger;
+ }
+
+ /** Launches an activity into split. */
+ void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
+ @Nullable Bundle options) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+ prepareEvictChildTasks(position, evictWct);
+
+ options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
+ wct.sendPendingIntent(intent, fillInIntent, options);
+ prepareEnterSplitScreen(wct, null /* taskInfo */, position);
+
+ mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, null, this,
+ new SplitScreenTransitions.TransitionCallback() {
+ @Override
+ public void onTransitionConsumed(boolean aborted) {
+ // Switch the split position if launching as MULTIPLE_TASK failed.
+ if (aborted
+ && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
+ setSideStagePositionAnimated(
+ SplitLayout.reversePosition(mSideStagePosition));
+ }
+ }
+
+ @Override
+ public void onTransitionFinished(WindowContainerTransaction finishWct,
+ SurfaceControl.Transaction finishT) {
+ if (!evictWct.isEmpty()) {
+ finishWct.merge(evictWct, true);
+ }
+ }
+ });
+ }
+
+ /** Launches an activity into split by legacy transition. */
+ void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
+ @SplitPosition int position, @androidx.annotation.Nullable Bundle options) {
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+ prepareEvictChildTasks(position, evictWct);
+
+ LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
+ @Override
+ public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+ IRemoteAnimationFinishedCallback finishedCallback,
+ SurfaceControl.Transaction t) {
+ if (apps == null || apps.length == 0) {
+ if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
+ mMainExecutor.execute(() ->
+ exitSplitScreen(mMainStage.getChildCount() == 0
+ ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
+ } else {
+ // Switch the split position if launching as MULTIPLE_TASK failed.
+ if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
+ setSideStagePosition(SplitLayout.reversePosition(
+ getSideStagePosition()), null);
+ }
+ }
+
+ // Do nothing when the animation was cancelled.
+ t.apply();
+ return;
+ }
+
+ for (int i = 0; i < apps.length; ++i) {
+ if (apps[i].mode == MODE_OPENING) {
+ t.show(apps[i].leash);
+ }
+ }
+ t.apply();
+
+ if (finishedCallback != null) {
+ try {
+ finishedCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error finishing legacy transition: ", e);
+ }
+ }
+
+ mSyncQueue.queue(evictWct);
+ }
+ };
+
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct);
+
+ // If split still not active, apply windows bounds first to avoid surface reset to
+ // wrong pos by SurfaceAnimator from wms.
+ if (!mMainStage.isActive() && mLogger.isEnterRequestedByDrag()) {
+ updateWindowBounds(mSplitLayout, wct);
+ }
+
+ wct.sendPendingIntent(intent, fillInIntent, options);
+ mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
+ }
+
/** Starts 2 tasks in one transition. */
void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId,
@Nullable Bundle sideOptions, @SplitPosition int sidePosition, float splitRatio,
- @Nullable RemoteTransition remoteTransition) {
+ @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
mainOptions = mainOptions != null ? mainOptions : new Bundle();
sideOptions = sideOptions != null ? sideOptions : new Bundle();
setSideStagePosition(sidePosition, wct);
+ if (mMainStage.isActive()) {
+ mMainStage.evictAllChildren(wct);
+ mSideStage.evictAllChildren(wct);
+ } else {
+ // Build a request WCT that will launch both apps such that task 0 is on the main stage
+ // while task 1 is on the side stage.
+ mMainStage.activate(wct, false /* reparent */);
+ }
mSplitLayout.setDivideRatio(splitRatio);
- // Build a request WCT that will launch both apps such that task 0 is on the main stage
- // while task 1 is on the side stage.
- mMainStage.activate(wct, false /* reparent */);
updateWindowBounds(mSplitLayout, wct);
wct.reorder(mRootTaskInfo.token, true);
+ wct.setForceTranslucent(mRootTaskInfo.token, false);
// Make sure the launch options will put tasks in the corresponding split roots
addActivityOptions(mainOptions, mMainStage);
@@ -382,42 +546,70 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
wct.startTask(sideTaskId, sideOptions);
mSplitTransitions.startEnterTransition(
- TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this);
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null);
+ setEnterInstanceId(instanceId);
}
/** Starts 2 tasks in one legacy transition. */
void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
- float splitRatio, RemoteAnimationAdapter adapter) {
- startWithLegacyTransition(mainTaskId, sideTaskId, null /* pendingIntent */,
- null /* fillInIntent */, mainOptions, sideOptions, sidePosition, splitRatio,
- adapter);
+ float splitRatio, RemoteAnimationAdapter adapter,
+ InstanceId instanceId) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (sideOptions == null) sideOptions = new Bundle();
+ addActivityOptions(sideOptions, mSideStage);
+ wct.startTask(sideTaskId, sideOptions);
+
+ startWithLegacyTransition(wct, mainTaskId, mainOptions, sidePosition, splitRatio, adapter,
+ instanceId);
}
/** Start an intent and a task ordered by {@code intentFirst}. */
void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent,
int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions,
- @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter) {
- startWithLegacyTransition(taskId, INVALID_TASK_ID, pendingIntent, fillInIntent,
- mainOptions, sideOptions, sidePosition, splitRatio, adapter);
+ @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+ InstanceId instanceId) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (sideOptions == null) sideOptions = new Bundle();
+ addActivityOptions(sideOptions, mSideStage);
+ wct.sendPendingIntent(pendingIntent, fillInIntent, sideOptions);
+
+ startWithLegacyTransition(wct, taskId, mainOptions, sidePosition, splitRatio, adapter,
+ instanceId);
}
- private void startWithLegacyTransition(int mainTaskId, int sideTaskId,
- @Nullable PendingIntent pendingIntent, @Nullable Intent fillInIntent,
- @Nullable Bundle mainOptions, @Nullable Bundle sideOptions,
- @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter) {
- final boolean withIntent = pendingIntent != null && fillInIntent != null;
+ void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
+ int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions,
+ @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+ InstanceId instanceId) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (sideOptions == null) sideOptions = new Bundle();
+ addActivityOptions(sideOptions, mSideStage);
+ wct.startShortcut(mContext.getPackageName(), shortcutInfo, sideOptions);
+
+ startWithLegacyTransition(wct, taskId, mainOptions, sidePosition, splitRatio, adapter,
+ instanceId);
+ }
+
+ /**
+ * @param instanceId if {@code null}, will not log. Otherwise it will be used in
+ * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
+ */
+ private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId,
+ @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio,
+ RemoteAnimationAdapter adapter, InstanceId instanceId) {
// Init divider first to make divider leash for remote animation target.
mSplitLayout.init();
+ mSplitLayout.setDivideRatio(splitRatio);
+
// Set false to avoid record new bounds with old task still on top;
mShouldUpdateRecents = false;
mIsDividerRemoteAnimating = true;
- final WindowContainerTransaction wct = new WindowContainerTransaction();
+
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
prepareEvictChildTasks(SPLIT_POSITION_TOP_OR_LEFT, evictWct);
prepareEvictChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, evictWct);
- // Need to add another wrapper here in shell so that we can inject the divider bar
- // and also manage the process elevation via setRunningRemote
+
IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
@Override
public void onAnimationStart(@WindowManager.TransitionOldType int transit,
@@ -425,31 +617,19 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
RemoteAnimationTarget[] wallpapers,
RemoteAnimationTarget[] nonApps,
final IRemoteAnimationFinishedCallback finishedCallback) {
- RemoteAnimationTarget[] augmentedNonApps =
- new RemoteAnimationTarget[nonApps.length + 1];
- for (int i = 0; i < nonApps.length; ++i) {
- augmentedNonApps[i] = nonApps[i];
- }
- augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget();
-
IRemoteAnimationFinishedCallback wrapCallback =
new IRemoteAnimationFinishedCallback.Stub() {
@Override
public void onAnimationFinished() throws RemoteException {
- onRemoteAnimationFinishedOrCancelled(evictWct);
+ onRemoteAnimationFinishedOrCancelled(false /* cancel */, evictWct);
finishedCallback.onAnimationFinished();
}
};
+ Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication());
try {
- try {
- ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(
- adapter.getCallingApplication());
- } catch (SecurityException e) {
- Slog.e(TAG, "Unable to boost animation thread. This should only happen"
- + " during unit tests");
- }
adapter.getRunner().onAnimationStart(transit, apps, wallpapers,
- augmentedNonApps, wrapCallback);
+ ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps,
+ getDividerBarLegacyTarget()), wrapCallback);
} catch (RemoteException e) {
Slog.e(TAG, "Error starting remote animation", e);
}
@@ -457,7 +637,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Override
public void onAnimationCancelled(boolean isKeyguardOccluded) {
- onRemoteAnimationFinishedOrCancelled(evictWct);
+ onRemoteAnimationFinishedOrCancelled(true /* cancel */, evictWct);
try {
adapter.getRunner().onAnimationCancelled(isKeyguardOccluded);
} catch (RemoteException e) {
@@ -476,47 +656,44 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mainOptions = mainActivityOptions.toBundle();
}
- sideOptions = sideOptions != null ? sideOptions : new Bundle();
setSideStagePosition(sidePosition, wct);
-
- mSplitLayout.setDivideRatio(splitRatio);
if (!mMainStage.isActive()) {
- // Build a request WCT that will launch both apps such that task 0 is on the main stage
- // while task 1 is on the side stage.
mMainStage.activate(wct, false /* reparent */);
}
- updateWindowBounds(mSplitLayout, wct);
- wct.reorder(mRootTaskInfo.token, true);
- // Make sure the launch options will put tasks in the corresponding split roots
+ if (mainOptions == null) mainOptions = new Bundle();
addActivityOptions(mainOptions, mMainStage);
- addActivityOptions(sideOptions, mSideStage);
-
- // Add task launch requests
+ updateWindowBounds(mSplitLayout, wct);
wct.startTask(mainTaskId, mainOptions);
- if (withIntent) {
- wct.sendPendingIntent(pendingIntent, fillInIntent, sideOptions);
- } else {
- wct.startTask(sideTaskId, sideOptions);
- }
- // Using legacy transitions, so we can't use blast sync since it conflicts.
- mTaskOrganizer.applyTransaction(wct);
+ wct.reorder(mRootTaskInfo.token, true);
+ wct.setForceTranslucent(mRootTaskInfo.token, false);
+
+ mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
setDividerVisibility(true, t);
updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
});
+
+ setEnterInstanceId(instanceId);
+ }
+
+ private void setEnterInstanceId(InstanceId instanceId) {
+ if (instanceId != null) {
+ mLogger.enterRequested(instanceId, ENTER_REASON_LAUNCHER);
+ }
}
- private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) {
+ private void onRemoteAnimationFinishedOrCancelled(boolean cancel,
+ WindowContainerTransaction evictWct) {
mIsDividerRemoteAnimating = false;
mShouldUpdateRecents = true;
// If any stage has no child after animation finished, it means that split will display
// nothing, such status will happen if task and intent is same app but not support
- // multi-instagce, we should exit split and expand that app as full screen.
- if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
+ // multi-instance, we should exit split and expand that app as full screen.
+ if (!cancel && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) {
mMainExecutor.execute(() ->
exitSplitScreen(mMainStage.getChildCount() == 0
- ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
+ ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
} else {
mSyncQueue.queue(evictWct);
}
@@ -534,6 +711,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
+ void prepareEvictNonOpeningChildTasks(@SplitPosition int position, RemoteAnimationTarget[] apps,
+ WindowContainerTransaction wct) {
+ if (position == mSideStagePosition) {
+ mSideStage.evictNonOpeningChildren(apps, wct);
+ } else {
+ mMainStage.evictNonOpeningChildren(apps, wct);
+ }
+ }
+
void prepareEvictInvisibleChildTasks(WindowContainerTransaction wct) {
mMainStage.evictInvisibleChildren(wct);
mSideStage.evictInvisibleChildren(wct);
@@ -603,11 +789,28 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
int getTaskId(@SplitPosition int splitPosition) {
- if (mSideStagePosition == splitPosition) {
- return mSideStage.getTopVisibleChildTaskId();
- } else {
- return mMainStage.getTopVisibleChildTaskId();
+ if (splitPosition == SPLIT_POSITION_UNDEFINED) {
+ return INVALID_TASK_ID;
}
+
+ return mSideStagePosition == splitPosition
+ ? mSideStage.getTopVisibleChildTaskId()
+ : mMainStage.getTopVisibleChildTaskId();
+ }
+
+ void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) {
+ if (mSideStagePosition == sideStagePosition) return;
+ SurfaceControl.Transaction t = mTransactionPool.acquire();
+ final StageTaskListener topLeftStage =
+ mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+ final StageTaskListener bottomRightStage =
+ mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+ mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash,
+ () -> {
+ setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition),
+ null /* wct */);
+ mTransactionPool.release(t);
+ });
}
void setSideStagePosition(@SplitPosition int sideStagePosition,
@@ -627,7 +830,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
onLayoutSizeChanged(mSplitLayout);
} else {
updateWindowBounds(mSplitLayout, wct);
- updateUnfoldBounds();
+ sendOnBoundsChanged();
}
}
}
@@ -642,7 +845,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (ENABLE_SHELL_TRANSITIONS) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct);
- mSplitTransitions.startDismissTransition(null /* transition */, wct, this,
+ mSplitTransitions.startDismissTransition(wct, this,
mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED);
} else {
exitSplitScreen(
@@ -675,8 +878,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final int dismissTop = mainStageVisible ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
final WindowContainerTransaction wct = new WindowContainerTransaction();
prepareExitSplitScreen(dismissTop, wct);
- mSplitTransitions.startDismissTransition(null /* transition */, wct, this,
- dismissTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
+ mSplitTransitions.startDismissTransition(wct, this, dismissTop,
+ EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
}
}
}
@@ -712,7 +915,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private void applyExitSplitScreen(@Nullable StageTaskListener childrenToTop,
WindowContainerTransaction wct, @ExitReason int exitReason) {
- if (!mMainStage.isActive()) return;
+ if (!mMainStage.isActive() || mIsExiting) return;
+
+ onSplitScreenExit();
mRecentTasks.ifPresent(recentTasks -> {
// Notify recents if we are exiting in a way that breaks the pair, and disable further
@@ -723,26 +928,54 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
});
mShouldUpdateRecents = false;
+ mIsDividerRemoteAnimating = false;
- // When the exit split-screen is caused by one of the task enters auto pip,
- // we want the tasks to be put to bottom instead of top, otherwise it will end up
- // a fullscreen plus a pinned task instead of pinned only at the end of the transition.
- final boolean fromEnteringPip = exitReason == EXIT_REASON_CHILD_TASK_ENTER_PIP;
- mSideStage.removeAllTasks(wct, !fromEnteringPip && mSideStage == childrenToTop);
- mMainStage.deactivate(wct, !fromEnteringPip && mMainStage == childrenToTop);
- wct.reorder(mRootTaskInfo.token, false /* onTop */);
- mTaskOrganizer.applyTransaction(wct);
+ mSplitLayout.getInvisibleBounds(mTempRect1);
+ if (childrenToTop == null) {
+ mSideStage.removeAllTasks(wct, false /* toTop */);
+ mMainStage.deactivate(wct, false /* toTop */);
+ wct.reorder(mRootTaskInfo.token, false /* onTop */);
+ wct.setForceTranslucent(mRootTaskInfo.token, true);
+ wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
+ onTransitionAnimationComplete();
+ } else {
+ // Expand to top side split as full screen for fading out decor animation and dismiss
+ // another side split(Moving its children to bottom).
+ mIsExiting = true;
+ childrenToTop.resetBounds(wct);
+ wct.reorder(childrenToTop.mRootTaskInfo.token, true);
+ wct.setSmallestScreenWidthDp(childrenToTop.mRootTaskInfo.token,
+ SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
+ }
+ mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
- setResizingSplits(false /* resizing */);
t.setWindowCrop(mMainStage.mRootLeash, null)
.setWindowCrop(mSideStage.mRootLeash, null);
+ t.hide(mMainStage.mDimLayer).hide(mSideStage.mDimLayer);
setDividerVisibility(false, t);
+
+ if (childrenToTop == null) {
+ t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right);
+ } else {
+ // In this case, exit still under progress, fade out the split decor after first WCT
+ // done and do remaining WCT after animation finished.
+ childrenToTop.fadeOutDecor(() -> {
+ WindowContainerTransaction finishedWCT = new WindowContainerTransaction();
+ mIsExiting = false;
+ mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */);
+ mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */);
+ finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */);
+ finishedWCT.setForceTranslucent(mRootTaskInfo.token, true);
+ finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
+ mSyncQueue.queue(finishedWCT);
+ mSyncQueue.runInSync(at -> {
+ at.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right);
+ });
+ onTransitionAnimationComplete();
+ });
+ }
});
- // Hide divider and reset its position.
- mSplitLayout.resetDividerPosition();
- mSplitLayout.release();
- mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
Slog.i(TAG, "applyExitSplitScreen, reason = " + exitReasonToString(exitReason));
// Log the exit
if (childrenToTop != null) {
@@ -753,6 +986,47 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
/**
+ * Overridden by child classes.
+ */
+ protected void onSplitScreenEnter() {
+ }
+
+ /**
+ * Overridden by child classes.
+ */
+ protected void onSplitScreenExit() {
+ }
+
+ /**
+ * Exits the split screen by finishing one of the tasks.
+ */
+ protected void exitStage(@SplitPosition int stageToClose) {
+ if (ENABLE_SHELL_TRANSITIONS) {
+ StageTaskListener stageToTop = mSideStagePosition == stageToClose
+ ? mMainStage
+ : mSideStage;
+ exitSplitScreen(stageToTop, EXIT_REASON_APP_FINISHED);
+ } else {
+ boolean toEnd = stageToClose == SPLIT_POSITION_BOTTOM_OR_RIGHT;
+ mSplitLayout.flingDividerToDismiss(toEnd, EXIT_REASON_APP_FINISHED);
+ }
+ }
+
+ /**
+ * Grants focus to the main or the side stages.
+ */
+ protected void grantFocusToStage(@SplitPosition int stageToFocus) {
+ IActivityTaskManager activityTaskManagerService = IActivityTaskManager.Stub.asInterface(
+ ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE));
+ try {
+ activityTaskManagerService.setFocusedTask(getTaskId(stageToFocus));
+ } catch (RemoteException | NullPointerException e) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "%s: Unable to update focus on the chosen stage, %s", TAG, e);
+ }
+ }
+
+ /**
* Returns whether the split pair in the recent tasks list should be broken.
*/
private boolean shouldBreakPairedTaskInRecents(@ExitReason int exitReason) {
@@ -799,6 +1073,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition) {
if (mMainStage.isActive()) return;
+ onSplitScreenEnter();
if (taskInfo != null) {
setSideStagePosition(startPosition, wct);
mSideStage.addTask(taskInfo, wct);
@@ -806,12 +1081,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mMainStage.activate(wct, true /* includingTopTask */);
updateWindowBounds(mSplitLayout, wct);
wct.reorder(mRootTaskInfo.token, true);
+ wct.setForceTranslucent(mRootTaskInfo.token, false);
}
void finishEnterSplitScreen(SurfaceControl.Transaction t) {
mSplitLayout.init();
setDividerVisibility(true, t);
updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+ t.show(mRootTaskLeash);
setSplitsVisible(true);
mShouldUpdateRecents = true;
updateRecentTasksSplitPair();
@@ -840,6 +1117,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private void addActivityOptions(Bundle opts, StageTaskListener stage) {
opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token);
+ // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split
+ // will be canceled.
+ opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true);
+ opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true);
}
void updateActivityOptions(Bundle opts, @SplitPosition int position) {
@@ -860,6 +1141,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
listener.onSplitVisibilityChanged(isSplitScreenVisible());
+ if (mSplitLayout != null) {
+ listener.onSplitBoundsChanged(mSplitLayout.getRootBounds(), getMainStageBounds(),
+ getSideStageBounds());
+ }
mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
}
@@ -872,6 +1157,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
+ private void sendOnBoundsChanged() {
+ if (mSplitLayout == null) return;
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onSplitBoundsChanged(mSplitLayout.getRootBounds(),
+ getMainStageBounds(), getSideStageBounds());
+ }
+ }
+
private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId,
boolean present, boolean visible) {
int stage;
@@ -897,9 +1190,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
- private void onStageChildTaskEnterPip(StageListenerImpl stageListener, int taskId) {
- exitSplitScreen(stageListener == mMainStageListener ? mMainStage : mSideStage,
- EXIT_REASON_CHILD_TASK_ENTER_PIP);
+ private void onStageChildTaskEnterPip() {
+ // When the exit split-screen is caused by one of the task enters auto pip,
+ // we want both tasks to be put to bottom instead of top, otherwise it will end up
+ // a fullscreen plus a pinned task instead of pinned only at the end of the transition.
+ exitSplitScreen(null, EXIT_REASON_CHILD_TASK_ENTER_PIP);
}
private void updateRecentTasksSplitPair() {
@@ -921,7 +1216,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
leftTopTaskId = mainStageTopTaskId;
rightBottomTaskId = sideStageTopTaskId;
}
- StagedSplitBounds splitBounds = new StagedSplitBounds(topLeftBounds, bottomRightBounds,
+ SplitBounds splitBounds = new SplitBounds(topLeftBounds, bottomRightBounds,
leftTopTaskId, rightBottomTaskId);
if (mainStageTopTaskId != INVALID_TASK_ID && sideStageTopTaskId != INVALID_TASK_ID) {
// Update the pair for the top tasks
@@ -935,12 +1230,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final SplitScreen.SplitScreenListener l = mListeners.get(i);
l.onSplitVisibilityChanged(mDividerVisible);
}
-
- if (mMainUnfoldController != null && mSideUnfoldController != null) {
- mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible);
- mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible);
- updateUnfoldBounds();
- }
+ sendOnBoundsChanged();
}
@Override
@@ -961,11 +1251,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
}
- if (mMainUnfoldController != null && mSideUnfoldController != null) {
- mMainUnfoldController.init();
- mSideUnfoldController.init();
- }
-
onRootTaskAppeared();
}
@@ -979,13 +1264,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mRootTaskInfo = taskInfo;
if (mSplitLayout != null
&& mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)
- && mMainStage.isActive()) {
- // TODO(b/204925795): With Shell transition, We are handling split bounds rotation at
- // onRotateDisplay. But still need to handle unfold case.
- if (ENABLE_SHELL_TRANSITIONS) {
- updateUnfoldBounds();
- return;
- }
+ && mMainStage.isActive()
+ && !ENABLE_SHELL_TRANSITIONS) {
// Clear the divider remote animating flag as the divider will be re-rendered to apply
// the new rotation config.
mIsDividerRemoteAnimating = false;
@@ -1009,6 +1289,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
mRootTaskInfo = null;
+ mRootTaskLeash = null;
}
@@ -1025,10 +1306,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
wct.reparent(mMainStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
// Make the stages adjacent to each other so they occlude what's behind them.
- wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token,
- true /* moveTogether */);
+ wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
- mTaskOrganizer.applyTransaction(wct);
+ wct.setForceTranslucent(mRootTaskInfo.token, true);
+ mSplitLayout.getInvisibleBounds(mTempRect1);
+ wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
+ mSyncQueue.queue(wct);
+ mSyncQueue.runInSync(t -> {
+ t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top);
+ });
}
private void onRootTaskVanished() {
@@ -1136,12 +1422,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDividerFadeInAnimator.cancel();
return;
}
+ mSplitLayout.getRefDividerBounds(mTempRect1);
transaction.show(dividerLeash);
transaction.setAlpha(dividerLeash, 0);
transaction.setLayer(dividerLeash, Integer.MAX_VALUE);
- transaction.setPosition(dividerLeash,
- mSplitLayout.getRefDividerBounds().left,
- mSplitLayout.getRefDividerBounds().top);
+ transaction.setPosition(dividerLeash, mTempRect1.left, mTempRect1.top);
transaction.apply();
}
@@ -1161,27 +1446,58 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
final boolean hasChildren = stageListener.mHasChildren;
final boolean isSideStage = stageListener == mSideStageListener;
- if (!hasChildren) {
+ if (!hasChildren && !mIsExiting && mMainStage.isActive()) {
if (isSideStage && mMainStageListener.mVisible) {
// Exit to main stage if side stage no longer has children.
- exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED);
+ if (ENABLE_SHELL_TRANSITIONS) {
+ exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED);
+ } else {
+ mSplitLayout.flingDividerToDismiss(
+ mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ EXIT_REASON_APP_FINISHED);
+ }
} else if (!isSideStage && mSideStageListener.mVisible) {
// Exit to side stage if main stage no longer has children.
- exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED);
+ if (ENABLE_SHELL_TRANSITIONS) {
+ exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED);
+ } else {
+ mSplitLayout.flingDividerToDismiss(
+ mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ EXIT_REASON_APP_FINISHED);
+ }
}
- } else if (isSideStage && !mMainStage.isActive()) {
+ } else if (isSideStage && hasChildren && !mMainStage.isActive()) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
mSplitLayout.init();
- prepareEnterSplitScreen(wct);
+ if (mLogger.isEnterRequestedByDrag()) {
+ prepareEnterSplitScreen(wct);
+ } else {
+ // TODO (b/238697912) : Add the validation to prevent entering non-recovered status
+ onSplitScreenEnter();
+ mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+ mMainStage.activate(wct, true /* includingTopTask */);
+ updateWindowBounds(mSplitLayout, wct);
+ wct.reorder(mRootTaskInfo.token, true);
+ wct.setForceTranslucent(mRootTaskInfo.token, false);
+ }
+
mSyncQueue.queue(wct);
- mSyncQueue.runInSync(t ->
- updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */));
+ mSyncQueue.runInSync(t -> {
+ if (mLogger.isEnterRequestedByDrag()) {
+ updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+ } else {
+ mSplitLayout.flingDividerToCenter();
+ }
+ });
}
if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) {
mShouldUpdateRecents = true;
updateRecentTasksSplitPair();
if (!mLogger.hasStartedSession()) {
+ if (!mLogger.hasValidEnterSessionId()) {
+ mLogger.enterRequested(null /*enterSessionId*/, ENTER_REASON_MULTI_INSTANCE);
+ }
mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
getMainStagePosition(), mMainStage.getTopChildTaskUid(),
getSideStagePosition(), mSideStage.getTopChildTaskUid(),
@@ -1190,27 +1506,43 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
+ boolean isValidToEnterSplitScreen(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
+ return taskInfo.supportsMultiWindow
+ && ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType())
+ && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode());
+ }
+
+ ActivityManager.RunningTaskInfo getFocusingTaskInfo() {
+ return mFocusingTaskInfo;
+ }
+
+ @Override
+ public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ mFocusingTaskInfo = taskInfo;
+ }
+
@Override
- public void onSnappedToDismiss(boolean bottomOrRight) {
+ public void onSnappedToDismiss(boolean bottomOrRight, int reason) {
final boolean mainStageToTop =
bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
: mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
if (!ENABLE_SHELL_TRANSITIONS) {
- exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, EXIT_REASON_DRAG_DIVIDER);
+ exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, reason);
return;
}
- setResizingSplits(false /* resizing */);
final int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
final WindowContainerTransaction wct = new WindowContainerTransaction();
prepareExitSplitScreen(dismissTop, wct);
- mSplitTransitions.startDismissTransition(
- null /* transition */, wct, this, dismissTop, EXIT_REASON_DRAG_DIVIDER);
+ if (mRootTaskInfo != null) {
+ wct.setDoNotPip(mRootTaskInfo.token);
+ }
+ mSplitTransitions.startDismissTransition(wct, this, dismissTop, EXIT_REASON_DRAG_DIVIDER);
}
@Override
public void onDoubleTappedDivider() {
- setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition), null /* wct */);
+ setSideStagePositionAnimated(SplitLayout.reversePosition(mSideStagePosition));
mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
getSideStagePosition(), mSideStage.getTopChildTaskUid(),
mSplitLayout.isLandscape());
@@ -1229,10 +1561,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
public void onLayoutSizeChanging(SplitLayout layout) {
final SurfaceControl.Transaction t = mTransactionPool.acquire();
t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
- setResizingSplits(true /* resizing */);
updateSurfaceBounds(layout, t, true /* applyResizingOffset */);
- mMainStage.onResizing(getMainStageBounds(), t);
- mSideStage.onResizing(getSideStageBounds(), t);
+ getMainStageBounds(mTempRect1);
+ getSideStageBounds(mTempRect2);
+ mMainStage.onResizing(mTempRect1, mTempRect2, t);
+ mSideStage.onResizing(mTempRect2, mTempRect1, t);
t.apply();
mTransactionPool.release(t);
}
@@ -1241,10 +1574,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
public void onLayoutSizeChanged(SplitLayout layout) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
updateWindowBounds(layout, wct);
- updateUnfoldBounds();
+ sendOnBoundsChanged();
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
- setResizingSplits(false /* resizing */);
updateSurfaceBounds(layout, t, false /* applyResizingOffset */);
mMainStage.onResized(t);
mSideStage.onResized(t);
@@ -1252,15 +1584,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
}
- private void updateUnfoldBounds() {
- if (mMainUnfoldController != null && mSideUnfoldController != null) {
- mMainUnfoldController.onLayoutChanged(getMainStageBounds(), getMainStagePosition(),
- isLandscape());
- mSideUnfoldController.onLayoutChanged(getSideStageBounds(), getSideStagePosition(),
- isLandscape());
- }
- }
-
private boolean isLandscape() {
return mSplitLayout.isLandscape();
}
@@ -1288,16 +1611,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
applyResizingOffset);
}
- void setResizingSplits(boolean resizing) {
- if (resizing == mResizingSplits) return;
- try {
- ActivityTaskManager.getService().setSplitScreenResizing(resizing);
- mResizingSplits = resizing;
- } catch (RemoteException e) {
- Slog.w(TAG, "Error calling setSplitScreenResizing", e);
- }
- }
-
@Override
public int getSplitItemPosition(WindowContainerToken token) {
if (token == null) {
@@ -1329,7 +1642,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (displayId != DEFAULT_DISPLAY) {
return;
}
- mDisplayController.addDisplayChangingController(this::onRotateDisplay);
+ mDisplayController.addDisplayChangingController(this::onDisplayChange);
}
@Override
@@ -1340,16 +1653,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDisplayLayout.set(mDisplayController.getDisplayLayout(displayId));
}
- private void onRotateDisplay(int displayId, int fromRotation, int toRotation,
- WindowContainerTransaction wct) {
+ void updateSurfaces(SurfaceControl.Transaction transaction) {
+ updateSurfaceBounds(mSplitLayout, transaction, /* applyResizingOffset */ false);
+ mSplitLayout.update(transaction);
+ }
+
+ private void onDisplayChange(int displayId, int fromRotation, int toRotation,
+ @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
if (!mMainStage.isActive()) return;
- // Only do this when shell transition
- if (!ENABLE_SHELL_TRANSITIONS) return;
mDisplayLayout.rotateTo(mContext.getResources(), toRotation);
mSplitLayout.rotateTo(toRotation, mDisplayLayout.stableInsets());
+ if (newDisplayAreaInfo != null) {
+ mSplitLayout.updateConfiguration(newDisplayAreaInfo.configuration);
+ }
updateWindowBounds(mSplitLayout, wct);
- updateUnfoldBounds();
+ sendOnBoundsChanged();
}
private void onFoldedStateChanged(boolean folded) {
@@ -1373,6 +1692,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
? mSplitLayout.getBounds2() : mSplitLayout.getBounds1();
}
+ private void getSideStageBounds(Rect rect) {
+ if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) {
+ mSplitLayout.getBounds1(rect);
+ } else {
+ mSplitLayout.getBounds2(rect);
+ }
+ }
+
+ private void getMainStageBounds(Rect rect) {
+ if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) {
+ mSplitLayout.getBounds2(rect);
+ } else {
+ mSplitLayout.getBounds1(rect);
+ }
+ }
+
/**
* Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain
* this task (yet) so this can also be used to identify which stage to put a task into.
@@ -1400,7 +1735,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Nullable TransitionRequestInfo request) {
final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
if (triggerTask == null) {
- if (mMainStage.isActive()) {
+ if (isSplitActive()) {
+ // Check if the display is rotating.
final TransitionRequestInfo.DisplayChange displayChange =
request.getDisplayChange();
if (request.getType() == TRANSIT_CHANGE && displayChange != null
@@ -1427,7 +1763,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mRecentTasks.ifPresent(recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId));
}
- if (mMainStage.isActive()) {
+ if (isSplitActive()) {
// Try to handle everything while in split-screen, so return a WCT even if it's empty.
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " split is active so using split"
+ "Transition to handle request. triggerTask=%d type=%s mainChildren=%d"
@@ -1442,7 +1778,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN ? STAGE_TYPE_SIDE
: STAGE_TYPE_MAIN;
prepareExitSplitScreen(dismissTop, out);
- mSplitTransitions.startDismissTransition(transition, out, this, dismissTop,
+ mSplitTransitions.setDismissTransition(transition, dismissTop,
EXIT_REASON_APP_FINISHED);
}
} else if (isOpening && inFullscreen) {
@@ -1452,13 +1788,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
} else if (activityType == ACTIVITY_TYPE_HOME
|| activityType == ACTIVITY_TYPE_RECENTS) {
// Enter overview panel, so start recent transition.
- mSplitTransitions.startRecentTransition(transition, out, this,
- request.getRemoteTransition());
- } else {
- // Occluded by the other fullscreen task, so dismiss both.
+ mSplitTransitions.setRecentTransition(transition, request.getRemoteTransition(),
+ mRecentTransitionCallback);
+ } else if (mSplitTransitions.mPendingRecent == null) {
+ // If split-task is not controlled by recents animation
+ // and occluded by the other fullscreen task, dismiss both.
prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out);
- mSplitTransitions.startDismissTransition(transition, out, this,
- STAGE_TYPE_UNDEFINED, EXIT_REASON_UNKNOWN);
+ mSplitTransitions.setDismissTransition(
+ transition, STAGE_TYPE_UNDEFINED, EXIT_REASON_UNKNOWN);
}
}
} else {
@@ -1466,12 +1803,40 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// One task is appearing into split, prepare to enter split screen.
out = new WindowContainerTransaction();
prepareEnterSplitScreen(out);
- mSplitTransitions.mPendingEnter = transition;
+ mSplitTransitions.setEnterTransition(
+ transition, request.getRemoteTransition(), null /* callback */);
}
}
return out;
}
+ /**
+ * This is used for mixed scenarios. For such scenarios, just make sure to include exiting
+ * split or entering split when appropriate.
+ */
+ public void addEnterOrExitIfNeeded(@Nullable TransitionRequestInfo request,
+ @NonNull WindowContainerTransaction outWCT) {
+ final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
+ if (triggerTask != null && triggerTask.displayId != mDisplayId) {
+ // Skip handling task on the other display.
+ return;
+ }
+ final @WindowManager.TransitionType int type = request.getType();
+ if (isSplitActive() && !isOpeningType(type)
+ && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " One of the splits became "
+ + "empty during a mixed transition (one not handled by split),"
+ + " so make sure split-screen state is cleaned-up. "
+ + "mainStageCount=%d sideStageCount=%d", mMainStage.getChildCount(),
+ mSideStage.getChildCount());
+ prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, outWCT);
+ }
+ }
+
+ public boolean isSplitActive() {
+ return mMainStage.isActive();
+ }
+
@Override
public void mergeAnimation(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction t, IBinder mergeTarget,
@@ -1479,17 +1844,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitTransitions.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
}
+ /** Jump the current transition animation to the end. */
+ public boolean end() {
+ return mSplitTransitions.end();
+ }
+
@Override
- public void onTransitionMerged(@NonNull IBinder transition) {
- // Once the pending enter transition got merged, make sure to bring divider bar visible and
- // clear the pending transition from cache to prevent mess-up the following state.
- if (transition == mSplitTransitions.mPendingEnter) {
- final SurfaceControl.Transaction t = mTransactionPool.acquire();
- finishEnterSplitScreen(t);
- mSplitTransitions.mPendingEnter = null;
- t.apply();
- mTransactionPool.release(t);
- }
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishT) {
+ mSplitTransitions.onTransitionConsumed(transition, aborted, finishT);
}
@Override
@@ -1498,10 +1861,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (transition != mSplitTransitions.mPendingEnter
- && transition != mSplitTransitions.mPendingRecent
- && (mSplitTransitions.mPendingDismiss == null
- || mSplitTransitions.mPendingDismiss.mTransition != transition)) {
+ if (!mSplitTransitions.isPendingTransition(transition)) {
// Not entering or exiting, so just do some house-keeping and validation.
// If we're not in split-mode, just abort so something else can handle it.
@@ -1545,28 +1905,56 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Use normal animations.
return false;
+ } else if (mMixedHandler != null && hasDisplayChange(info)) {
+ // A display-change has been un-expectedly inserted into the transition. Redirect
+ // handling to the mixed-handler to deal with splitting it up.
+ if (mMixedHandler.animatePendingSplitWithDisplayChange(transition, info,
+ startTransaction, finishTransaction, finishCallback)) {
+ return true;
+ }
}
+ return startPendingAnimation(transition, info, startTransaction, finishTransaction,
+ finishCallback);
+ }
+
+ /** Starts the pending transition animation. */
+ public boolean startPendingAnimation(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
boolean shouldAnimate = true;
- if (mSplitTransitions.mPendingEnter == transition) {
- shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction);
- } else if (mSplitTransitions.mPendingRecent == transition) {
+ if (mSplitTransitions.isPendingEnter(transition)) {
+ shouldAnimate = startPendingEnterAnimation(
+ transition, info, startTransaction, finishTransaction);
+ } else if (mSplitTransitions.isPendingRecent(transition)) {
shouldAnimate = startPendingRecentAnimation(transition, info, startTransaction);
- } else if (mSplitTransitions.mPendingDismiss != null
- && mSplitTransitions.mPendingDismiss.mTransition == transition) {
+ } else if (mSplitTransitions.isPendingDismiss(transition)) {
shouldAnimate = startPendingDismissAnimation(
mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction);
}
if (!shouldAnimate) return false;
mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction,
- finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+ finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token,
+ mRootTaskInfo.token);
return true;
}
- void onTransitionAnimationComplete() {
+ private boolean hasDisplayChange(TransitionInfo info) {
+ boolean has = false;
+ for (int iC = 0; iC < info.getChanges().size() && !has; ++iC) {
+ final TransitionInfo.Change change = info.getChanges().get(iC);
+ has = change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0;
+ }
+ return has;
+ }
+
+ /** Called to clean-up state and do house-keeping after the animation is done. */
+ public void onTransitionAnimationComplete() {
// If still playing, let it finish.
- if (!mMainStage.isActive()) {
+ if (!mMainStage.isActive() && !mIsExiting) {
// Update divider state after animation so that it is still around and positioned
// properly for the animation itself.
mSplitLayout.release();
@@ -1576,7 +1964,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
private boolean startPendingEnterAnimation(@NonNull IBinder transition,
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction finishT) {
// First, verify that we actually have opened apps in both splits.
TransitionInfo.Change mainChild = null;
TransitionInfo.Change sideChild = null;
@@ -1623,13 +2012,13 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
+ " before startAnimation().");
}
- finishEnterSplitScreen(t);
- addDividerBarToTransition(info, t, true /* show */);
+ finishEnterSplitScreen(finishT);
+ addDividerBarToTransition(info, finishT, true /* show */);
return true;
}
- private boolean startPendingDismissAnimation(
- @NonNull SplitScreenTransitions.DismissTransition dismissTransition,
+ /** Synchronize split-screen state with transition and make appropriate preparations. */
+ public void prepareDismissAnimation(@StageType int toStage, @ExitReason int dismissReason,
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
@NonNull SurfaceControl.Transaction finishT) {
// Make some noise if things aren't totally expected. These states shouldn't effect
@@ -1662,7 +2051,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mRecentTasks.ifPresent(recentTasks -> {
// Notify recents if we are exiting in a way that breaks the pair, and disable further
// updates to splits in the recents until we enter split again
- if (shouldBreakPairedTaskInRecents(dismissTransition.mReason) && mShouldUpdateRecents) {
+ if (shouldBreakPairedTaskInRecents(dismissReason) && mShouldUpdateRecents) {
for (TransitionInfo.Change change : info.getChanges()) {
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
if (taskInfo != null
@@ -1679,30 +2068,37 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Wait until after animation to update divider
// Reset crops so they don't interfere with subsequent launches
- t.setWindowCrop(mMainStage.mRootLeash, null);
- t.setWindowCrop(mSideStage.mRootLeash, null);
+ t.setCrop(mMainStage.mRootLeash, null);
+ t.setCrop(mSideStage.mRootLeash, null);
+
+ if (toStage == STAGE_TYPE_UNDEFINED) {
+ logExit(dismissReason);
+ } else {
+ logExitToStage(dismissReason, toStage == STAGE_TYPE_MAIN);
+ }
+
+ // Hide divider and dim layer on transition finished.
+ setDividerVisibility(false, finishT);
+ finishT.hide(mMainStage.mDimLayer);
+ finishT.hide(mSideStage.mDimLayer);
+ }
+ private boolean startPendingDismissAnimation(
+ @NonNull SplitScreenTransitions.DismissTransition dismissTransition,
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction finishT) {
+ prepareDismissAnimation(dismissTransition.mDismissTop, dismissTransition.mReason, info,
+ t, finishT);
if (dismissTransition.mDismissTop == STAGE_TYPE_UNDEFINED) {
- logExit(dismissTransition.mReason);
// TODO: Have a proper remote for this. Until then, though, reset state and use the
// normal animation stuff (which falls back to the normal launcher remote).
+ t.hide(mSplitLayout.getDividerLeash());
mSplitLayout.release(t);
mSplitTransitions.mPendingDismiss = null;
return false;
- } else {
- logExitToStage(dismissTransition.mReason,
- dismissTransition.mDismissTop == STAGE_TYPE_MAIN);
}
- addDividerBarToTransition(info, t, false /* show */);
- // We're dismissing split by moving the other one to fullscreen.
- // Since we don't have any animations for this yet, just use the internal example
- // animations.
-
- // Hide divider and dim layer on transition finished.
- setDividerVisibility(false, finishT);
- finishT.hide(mMainStage.mDimLayer);
- finishT.hide(mSideStage.mDimLayer);
+ addDividerBarToTransition(info, finishT, false /* show */);
return true;
}
@@ -1712,46 +2108,26 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return true;
}
- void onRecentTransitionFinished(boolean returnToHome, WindowContainerTransaction wct,
- SurfaceControl.Transaction finishT) {
- // Exclude the case that the split screen has been dismissed already.
- if (!mMainStage.isActive()) {
- // The latest split dismissing transition might be a no-op transition and thus won't
- // callback startAnimation, update split visibility here to cover this kind of no-op
- // transition case.
- setSplitsVisible(false);
- return;
- }
-
- if (returnToHome) {
- // When returning to home from recent apps, the splitting tasks are already hidden, so
- // append the reset of dismissing operations into the clean-up wct.
- prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
- setSplitsVisible(false);
- logExit(EXIT_REASON_RETURN_HOME);
- } else {
- setDividerVisibility(true, finishT);
- }
- }
-
private void addDividerBarToTransition(@NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t, boolean show) {
+ @NonNull SurfaceControl.Transaction finishT, boolean show) {
final SurfaceControl leash = mSplitLayout.getDividerLeash();
final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash);
- final Rect bounds = mSplitLayout.getDividerBounds();
- barChange.setStartAbsBounds(bounds);
- barChange.setEndAbsBounds(bounds);
+ mSplitLayout.getRefDividerBounds(mTempRect1);
+ barChange.setStartAbsBounds(mTempRect1);
+ barChange.setEndAbsBounds(mTempRect1);
barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
barChange.setFlags(FLAG_IS_DIVIDER_BAR);
// Technically this should be order-0, but this is running after layer assignment
// and it's a special case, so just add to end.
info.addChange(barChange);
- // Be default, make it visible. The remote animator can adjust alpha if it plans to animate.
+
if (show) {
- t.setAlpha(leash, 1.f);
- t.setLayer(leash, Integer.MAX_VALUE);
- t.setPosition(leash, bounds.left, bounds.top);
- t.show(leash);
+ finishT.setLayer(leash, Integer.MAX_VALUE);
+ finishT.setPosition(leash, mTempRect1.left, mTempRect1.top);
+ finishT.show(leash);
+ // Ensure divider surface are re-parented back into the hierarchy at the end of the
+ // transition. See Transition#buildFinishTransaction for more detail.
+ finishT.reparent(leash, mRootTaskLeash);
}
}
@@ -1855,8 +2231,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
@Override
- public void onChildTaskEnterPip(int taskId) {
- StageCoordinator.this.onStageChildTaskEnterPip(this, taskId);
+ public void onChildTaskEnterPip() {
+ StageCoordinator.this.onStageChildTaskEnterPip();
}
@Override
@@ -1868,19 +2244,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Override
public void onNoLongerSupportMultiWindow() {
if (mMainStage.isActive()) {
+ final Toast splitUnsupportedToast = Toast.makeText(mContext,
+ R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
final boolean isMainStage = mMainStageListener == this;
if (!ENABLE_SHELL_TRANSITIONS) {
StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
+ splitUnsupportedToast.show();
return;
}
final int stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
final WindowContainerTransaction wct = new WindowContainerTransaction();
prepareExitSplitScreen(stageType, wct);
- mSplitTransitions.startDismissTransition(null /* transition */, wct,
- StageCoordinator.this, stageType,
+ mSplitTransitions.startDismissTransition(wct,StageCoordinator.this, stageType,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
+ splitUnsupportedToast.show();
}
}
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 949bf5f55808..6b90eabe3bd2 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
@@ -17,12 +17,13 @@
package com.android.wm.shell.splitscreen;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-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.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE;
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import android.annotation.CallSuper;
@@ -31,7 +32,10 @@ import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.Slog;
import android.util.SparseArray;
+import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.WindowContainerToken;
@@ -39,6 +43,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
+import com.android.internal.util.ArrayUtils;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.SurfaceUtils;
@@ -47,6 +52,7 @@ import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.splitscreen.SplitScreen.StageType;
import java.io.PrintWriter;
+import java.util.function.Predicate;
/**
* Base class that handle common task org. related for split-screen stages.
@@ -60,12 +66,6 @@ import java.io.PrintWriter;
class StageTaskListener implements ShellTaskOrganizer.TaskListener {
private static final String TAG = StageTaskListener.class.getSimpleName();
- protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD};
- protected static final int[] CONTROLLED_WINDOWING_MODES =
- {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
- protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
- {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
-
/** Callback interface for listening to changes in a split-screen stage. */
public interface StageListenerCallbacks {
void onRootTaskAppeared();
@@ -74,7 +74,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
void onChildTaskStatusChanged(int taskId, boolean present, boolean visible);
- void onChildTaskEnterPip(int taskId);
+ void onChildTaskEnterPip();
void onRootTaskVanished();
@@ -95,18 +95,14 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
// TODO(b/204308910): Extracts SplitDecorManager related code to common package.
private SplitDecorManager mSplitDecorManager;
- private final StageTaskUnfoldController mStageTaskUnfoldController;
-
StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
- SurfaceSession surfaceSession, IconProvider iconProvider,
- @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+ SurfaceSession surfaceSession, IconProvider iconProvider) {
mContext = context;
mCallbacks = callbacks;
mSyncQueue = syncQueue;
mSurfaceSession = surfaceSession;
mIconProvider = iconProvider;
- mStageTaskUnfoldController = stageTaskUnfoldController;
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
}
@@ -119,63 +115,53 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
boolean containsToken(WindowContainerToken token) {
- if (token.equals(mRootTaskInfo.token)) {
- return true;
- }
-
- for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
- if (token.equals(mChildrenTaskInfo.valueAt(i).token)) {
- return true;
- }
- }
+ return contains(t -> t.token.equals(token));
+ }
- return false;
+ boolean containsContainer(IBinder binder) {
+ return contains(t -> t.token.asBinder() == binder);
}
/**
* Returns the top visible child task's id.
*/
int getTopVisibleChildTaskId() {
- for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
- final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i);
- if (info.isVisible) {
- return info.taskId;
- }
- }
- return INVALID_TASK_ID;
+ final ActivityManager.RunningTaskInfo taskInfo = getChildTaskInfo(t -> t.isVisible);
+ return taskInfo != null ? taskInfo.taskId : INVALID_TASK_ID;
}
/**
* Returns the top activity uid for the top child task.
*/
int getTopChildTaskUid() {
- for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
- final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i);
- if (info.topActivityInfo == null) {
- continue;
- }
- return info.topActivityInfo.applicationInfo.uid;
- }
- return 0;
+ final ActivityManager.RunningTaskInfo taskInfo =
+ getChildTaskInfo(t -> t.topActivityInfo != null);
+ return taskInfo != null ? taskInfo.topActivityInfo.applicationInfo.uid : 0;
}
/** @return {@code true} if this listener contains the currently focused task. */
boolean isFocused() {
- if (mRootTaskInfo == null) {
- return false;
- }
+ return contains(t -> t.isFocused);
+ }
- if (mRootTaskInfo.isFocused) {
+ private boolean contains(Predicate<ActivityManager.RunningTaskInfo> predicate) {
+ if (mRootTaskInfo != null && predicate.test(mRootTaskInfo)) {
return true;
}
+ return getChildTaskInfo(predicate) != null;
+ }
+
+ @Nullable
+ private ActivityManager.RunningTaskInfo getChildTaskInfo(
+ Predicate<ActivityManager.RunningTaskInfo> predicate) {
for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
- if (mChildrenTaskInfo.valueAt(i).isFocused) {
- return true;
+ final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
+ if (predicate.test(taskInfo)) {
+ return taskInfo;
}
}
-
- return false;
+ return null;
}
@Override
@@ -207,20 +193,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
}
-
- if (mStageTaskUnfoldController != null) {
- mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash);
- }
}
@Override
@CallSuper
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- if (!taskInfo.supportsMultiWindow) {
- // Leave split screen if the task no longer supports multi window.
- mCallbacks.onNoLongerSupportMultiWindow();
- return;
- }
if (mRootTaskInfo.taskId == taskInfo.taskId) {
// Inflates split decor view only when the root task is visible.
if (mRootTaskInfo.isVisible != taskInfo.isVisible) {
@@ -233,6 +210,15 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
mRootTaskInfo = taskInfo;
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
+ if (!taskInfo.supportsMultiWindow
+ || !ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType())
+ || !ArrayUtils.contains(CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
+ taskInfo.getWindowingMode())) {
+ // Leave split screen if the task no longer supports multi window or have
+ // uncontrolled task.
+ mCallbacks.onNoLongerSupportMultiWindow();
+ return;
+ }
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */,
taskInfo.isVisible);
@@ -258,6 +244,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
if (mRootTaskInfo.taskId == taskId) {
mCallbacks.onRootTaskVanished();
mRootTaskInfo = null;
+ mRootLeash = null;
mSyncQueue.runInSync(t -> {
t.remove(mDimLayer);
mSplitDecorManager.release(t);
@@ -266,22 +253,18 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
mChildrenTaskInfo.remove(taskId);
mChildrenLeashes.remove(taskId);
mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible);
- if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
- mCallbacks.onChildTaskEnterPip(taskId);
- }
if (ENABLE_SHELL_TRANSITIONS) {
// Status is managed/synchronized by the transition lifecycle.
return;
}
+ if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
+ mCallbacks.onChildTaskEnterPip();
+ }
sendStatusChanged();
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
}
-
- if (mStageTaskUnfoldController != null) {
- mStageTaskUnfoldController.onTaskVanished(taskInfo);
- }
}
@Override
@@ -305,9 +288,9 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
}
- void onResizing(Rect newBounds, SurfaceControl.Transaction t) {
+ void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t) {
if (mSplitDecorManager != null && mRootTaskInfo != null) {
- mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, t);
+ mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t);
}
}
@@ -317,6 +300,14 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
}
+ void fadeOutDecor(Runnable finishedCallback) {
+ if (mSplitDecorManager != null) {
+ mSplitDecorManager.fadeOutDecor(finishedCallback);
+ } else {
+ finishedCallback.run();
+ }
+ }
+
void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) {
// Clear overridden bounds and windowing mode to make sure the child task can inherit
// windowing mode and bounds from split root.
@@ -341,6 +332,19 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
}
+ void evictNonOpeningChildren(RemoteAnimationTarget[] apps, WindowContainerTransaction wct) {
+ final SparseArray<ActivityManager.RunningTaskInfo> toBeEvict = mChildrenTaskInfo.clone();
+ for (int i = 0; i < apps.length; i++) {
+ if (apps[i].mode == MODE_OPENING) {
+ toBeEvict.remove(apps[i].taskId);
+ }
+ }
+ for (int i = toBeEvict.size() - 1; i >= 0; i--) {
+ final ActivityManager.RunningTaskInfo taskInfo = toBeEvict.valueAt(i);
+ wct.reparent(taskInfo.token, null /* parent */, false /* onTop */);
+ }
+ }
+
void evictInvisibleChildren(WindowContainerTransaction wct) {
for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) {
final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
@@ -350,6 +354,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
}
+ void resetBounds(WindowContainerTransaction wct) {
+ wct.setBounds(mRootTaskInfo.token, null);
+ wct.setAppBounds(mRootTaskInfo.token, null);
+ }
+
void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener,
@StageType int stage) {
for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
@@ -363,7 +372,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
SurfaceControl leash, boolean firstAppeared) {
final Point taskPositionInParent = taskInfo.positionInParent;
mSyncQueue.runInSync(t -> {
- t.setWindowCrop(leash, null);
+ // The task surface might be released before running in the sync queue for the case like
+ // trampoline launch, so check if the surface is valid before processing it.
+ if (!leash.isValid()) {
+ Slog.w(TAG, "Skip updating invalid child task surface of task#" + taskInfo.taskId);
+ return;
+ }
+ t.setCrop(leash, null);
t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) {
t.setAlpha(leash, 1f);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl
deleted file mode 100644
index 45f6d3c8b154..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
-import android.window.RemoteTransition;
-
-import com.android.wm.shell.stagesplit.ISplitScreenListener;
-
-/**
- * Interface that is exposed to remote callers to manipulate the splitscreen feature.
- */
-interface ISplitScreen {
-
- /**
- * Registers a split screen listener.
- */
- oneway void registerSplitScreenListener(in ISplitScreenListener listener) = 1;
-
- /**
- * Unregisters a split screen listener.
- */
- oneway void unregisterSplitScreenListener(in ISplitScreenListener listener) = 2;
-
- /**
- * Hides the side-stage if it is currently visible.
- */
- oneway void setSideStageVisibility(boolean visible) = 3;
-
- /**
- * Removes a task from the side stage.
- */
- oneway void removeFromSideStage(int taskId) = 4;
-
- /**
- * Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID
- * to indicate leaving no top task after leaving split-screen.
- */
- oneway void exitSplitScreen(int toTopTaskId) = 5;
-
- /**
- * @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible.
- */
- oneway void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) = 6;
-
- /**
- * Starts a task in a stage.
- */
- oneway void startTask(int taskId, int stage, int position, in Bundle options) = 7;
-
- /**
- * Starts a shortcut in a stage.
- */
- oneway void startShortcut(String packageName, String shortcutId, int stage, int position,
- in Bundle options, in UserHandle user) = 8;
-
- /**
- * Starts an activity in a stage.
- */
- oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int stage,
- int position, in Bundle options) = 9;
-
- /**
- * Starts tasks simultaneously in one transition.
- */
- oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
- in Bundle sideOptions, int sidePosition, in RemoteTransition remoteTransition) = 10;
-
- /**
- * Version of startTasks using legacy transition system.
- */
- oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions,
- int sideTaskId, in Bundle sideOptions, int sidePosition,
- in RemoteAnimationAdapter adapter) = 11;
-
- /**
- * Blocking call that notifies and gets additional split-screen targets when entering
- * recents (for example: the dividerBar).
- * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled).
- * @param appTargets apps that will be re-parented to display area
- */
- RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
- in RemoteAnimationTarget[] appTargets) = 12;
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java
deleted file mode 100644
index 83855be91e04..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-
-import android.annotation.Nullable;
-import android.graphics.Rect;
-import android.view.SurfaceSession;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-/**
- * Main stage for split-screen mode. When split-screen is active all standard activity types launch
- * on the main stage, except for task that are explicitly pinned to the {@link SideStage}.
- * @see StageCoordinator
- */
-class MainStage extends StageTaskListener {
- private static final String TAG = MainStage.class.getSimpleName();
-
- private boolean mIsActive = false;
-
- MainStage(ShellTaskOrganizer taskOrganizer, int displayId,
- StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
- SurfaceSession surfaceSession,
- @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
- super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
- stageTaskUnfoldController);
- }
-
- boolean isActive() {
- return mIsActive;
- }
-
- void activate(Rect rootBounds, WindowContainerTransaction wct) {
- if (mIsActive) return;
-
- final WindowContainerToken rootToken = mRootTaskInfo.token;
- wct.setBounds(rootToken, rootBounds)
- .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW)
- .setLaunchRoot(
- rootToken,
- CONTROLLED_WINDOWING_MODES,
- CONTROLLED_ACTIVITY_TYPES)
- .reparentTasks(
- null /* currentParent */,
- rootToken,
- CONTROLLED_WINDOWING_MODES,
- CONTROLLED_ACTIVITY_TYPES,
- true /* onTop */)
- // Moving the root task to top after the child tasks were re-parented , or the root
- // task cannot be visible and focused.
- .reorder(rootToken, true /* onTop */);
-
- mIsActive = true;
- }
-
- void deactivate(WindowContainerTransaction wct) {
- deactivate(wct, false /* toTop */);
- }
-
- void deactivate(WindowContainerTransaction wct, boolean toTop) {
- if (!mIsActive) return;
- mIsActive = false;
-
- if (mRootTaskInfo == null) return;
- final WindowContainerToken rootToken = mRootTaskInfo.token;
- wct.setLaunchRoot(
- rootToken,
- null,
- null)
- .reparentTasks(
- rootToken,
- null /* newParent */,
- CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
- CONTROLLED_ACTIVITY_TYPES,
- toTop)
- // We want this re-order to the bottom regardless since we are re-parenting
- // all its tasks.
- .reorder(rootToken, false /* onTop */);
- }
-
- void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) {
- wct.setBounds(mRootTaskInfo.token, bounds)
- .setWindowingMode(mRootTaskInfo.token, windowingMode);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS
deleted file mode 100644
index 264e88f32bff..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# WM shell sub-modules stagesplit owner
-chenghsiuchang@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java
deleted file mode 100644
index 8fbad52c630f..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-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;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.os.Binder;
-import android.view.IWindow;
-import android.view.InsetsSource;
-import android.view.InsetsState;
-import android.view.LayoutInflater;
-import android.view.SurfaceControl;
-import android.view.SurfaceControlViewHost;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.view.WindowlessWindowManager;
-import android.widget.FrameLayout;
-
-import com.android.wm.shell.R;
-
-/**
- * Handles drawing outline of the bounds of provided root surface. The outline will be drown with
- * the consideration of display insets like status bar, navigation bar and display cutout.
- */
-class OutlineManager extends WindowlessWindowManager {
- private static final String WINDOW_NAME = "SplitOutlineLayer";
- private final Context mContext;
- private final Rect mRootBounds = new Rect();
- private final Rect mTempRect = new Rect();
- private final Rect mLastOutlineBounds = new Rect();
- private final InsetsState mInsetsState = new InsetsState();
- private final int mExpandedTaskBarHeight;
- private OutlineView mOutlineView;
- private SurfaceControlViewHost mViewHost;
- private SurfaceControl mHostLeash;
- private SurfaceControl mLeash;
-
- OutlineManager(Context context, Configuration configuration) {
- super(configuration, null /* rootSurface */, null /* hostInputToken */);
- mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
- null /* options */);
- mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.taskbar_frame_height);
- }
-
- @Override
- protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
- b.setParent(mHostLeash);
- }
-
- void inflate(SurfaceControl rootLeash, Rect rootBounds) {
- if (mLeash != null || mViewHost != null) return;
-
- mHostLeash = rootLeash;
- mRootBounds.set(rootBounds);
- mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
-
- final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext)
- .inflate(R.layout.split_outline, null);
- mOutlineView = rootLayout.findViewById(R.id.split_outline);
-
- final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
- 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
- FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
- lp.width = mRootBounds.width();
- lp.height = mRootBounds.height();
- lp.token = new Binder();
- lp.setTitle(WINDOW_NAME);
- 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.
- mViewHost.setView(rootLayout, lp);
- mLeash = getSurfaceControl(mViewHost.getWindowToken());
-
- drawOutline();
- }
-
- void release() {
- if (mViewHost != null) {
- mViewHost.release();
- mViewHost = null;
- }
- mRootBounds.setEmpty();
- mLastOutlineBounds.setEmpty();
- mOutlineView = null;
- mHostLeash = null;
- mLeash = null;
- }
-
- @Nullable
- SurfaceControl getOutlineLeash() {
- return mLeash;
- }
-
- void setVisibility(boolean visible) {
- if (mOutlineView != null) {
- mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
- }
- }
-
- void setRootBounds(Rect rootBounds) {
- if (mViewHost == null || mViewHost.getView() == null) {
- return;
- }
-
- if (!mRootBounds.equals(rootBounds)) {
- WindowManager.LayoutParams lp =
- (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
- lp.width = rootBounds.width();
- lp.height = rootBounds.height();
- mViewHost.relayout(lp);
- mRootBounds.set(rootBounds);
- drawOutline();
- }
- }
-
- void onInsetsChanged(InsetsState insetsState) {
- if (!mInsetsState.equals(insetsState)) {
- mInsetsState.set(insetsState);
- drawOutline();
- }
- }
-
- private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) {
- outBounds.set(rootBounds);
- final InsetsSource taskBarInsetsSource =
- insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
- // Only insets the divider bar with task bar when it's expanded so that the rounded corners
- // will be drawn against task bar.
- if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
- outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds));
- }
-
- // Offset the coordinate from screen based to surface based.
- outBounds.offset(-rootBounds.left, -rootBounds.top);
- }
-
- void drawOutline() {
- if (mOutlineView == null) {
- return;
- }
-
- computeOutlineBounds(mRootBounds, mInsetsState, mTempRect);
- if (mTempRect.equals(mLastOutlineBounds)) {
- return;
- }
-
- ViewGroup.MarginLayoutParams lp =
- (ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams();
- lp.leftMargin = mTempRect.left;
- lp.topMargin = mTempRect.top;
- lp.width = mTempRect.width();
- lp.height = mTempRect.height();
- mOutlineView.setLayoutParams(lp);
- mLastOutlineBounds.set(mTempRect);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java
deleted file mode 100644
index 92b1381fc808..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
-import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
-import static android.view.RoundedCorner.POSITION_TOP_LEFT;
-import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.util.AttributeSet;
-import android.view.RoundedCorner;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.internal.R;
-
-/** View for drawing split outline. */
-public class OutlineView extends View {
- private final Paint mPaint = new Paint();
- private final Path mPath = new Path();
- private final float[] mRadii = new float[8];
-
- public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setStrokeWidth(
- getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width));
- mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null));
- }
-
- @Override
- protected void onAttachedToWindow() {
- // TODO(b/200850654): match the screen corners with the actual display decor.
- mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT);
- mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT);
- mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT);
- mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT);
- }
-
- private int getCornerRadius(@RoundedCorner.Position int position) {
- final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position);
- return roundedCorner == null ? 0 : roundedCorner.getRadius();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- if (changed) {
- mPath.reset();
- mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW);
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawPath(mPath, mPaint);
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java
deleted file mode 100644
index 55c4f3aea19a..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import android.annotation.CallSuper;
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.graphics.Rect;
-import android.view.InsetsSourceControl;
-import android.view.InsetsState;
-import android.view.SurfaceControl;
-import android.view.SurfaceSession;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-/**
- * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up
- * here. All other task are launch in the {@link MainStage}.
- *
- * @see StageCoordinator
- */
-class SideStage extends StageTaskListener implements
- DisplayInsetsController.OnInsetsChangedListener {
- private static final String TAG = SideStage.class.getSimpleName();
- private final Context mContext;
- private OutlineManager mOutlineManager;
-
- SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
- StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
- SurfaceSession surfaceSession,
- @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
- super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
- stageTaskUnfoldController);
- mContext = context;
- }
-
- void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds,
- WindowContainerTransaction wct) {
- final WindowContainerToken rootToken = mRootTaskInfo.token;
- wct.setBounds(rootToken, rootBounds)
- .reparent(task.token, rootToken, true /* onTop*/)
- // Moving the root task to top after the child tasks were reparented , or the root
- // task cannot be visible and focused.
- .reorder(rootToken, true /* onTop */);
- }
-
- boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
- // No matter if the root task is empty or not, moving the root to bottom because it no
- // longer preserves visible child task.
- wct.reorder(mRootTaskInfo.token, false /* onTop */);
- if (mChildrenTaskInfo.size() == 0) return false;
- wct.reparentTasks(
- mRootTaskInfo.token,
- null /* newParent */,
- CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
- CONTROLLED_ACTIVITY_TYPES,
- toTop);
- return true;
- }
-
- boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) {
- final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId);
- if (task == null) return false;
- wct.reparent(task.token, newParent, false /* onTop */);
- return true;
- }
-
- @Nullable
- public SurfaceControl getOutlineLeash() {
- return mOutlineManager.getOutlineLeash();
- }
-
- @Override
- @CallSuper
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- super.onTaskAppeared(taskInfo, leash);
- if (isRootTask(taskInfo)) {
- mOutlineManager = new OutlineManager(mContext, taskInfo.configuration);
- enableOutline(true);
- }
- }
-
- @Override
- @CallSuper
- public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- super.onTaskInfoChanged(taskInfo);
- if (isRootTask(taskInfo)) {
- mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds());
- }
- }
-
- private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) {
- return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId;
- }
-
- void enableOutline(boolean enable) {
- if (mOutlineManager == null) {
- return;
- }
-
- if (enable) {
- if (mRootTaskInfo != null) {
- mOutlineManager.inflate(mRootLeash,
- mRootTaskInfo.configuration.windowConfiguration.getBounds());
- }
- } else {
- mOutlineManager.release();
- }
- }
-
- void setOutlineVisibility(boolean visible) {
- mOutlineManager.setVisibility(visible);
- }
-
- @Override
- public void insetsChanged(InsetsState insetsState) {
- mOutlineManager.onInsetsChanged(insetsState);
- }
-
- @Override
- public void insetsControlChanged(InsetsState insetsState,
- InsetsSourceControl[] activeControls) {
- insetsChanged(insetsState);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java
deleted file mode 100644
index c5d231262cd2..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-
-import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
-
-import java.util.concurrent.Executor;
-
-/**
- * Interface to engage split-screen feature.
- * TODO: Figure out which of these are actually needed outside of the Shell
- */
-@ExternalThread
-public interface SplitScreen {
- /**
- * Stage type isn't specified normally meaning to use what ever the default is.
- * E.g. exit split-screen and launch the app in fullscreen.
- */
- int STAGE_TYPE_UNDEFINED = -1;
- /**
- * The main stage type.
- * @see MainStage
- */
- int STAGE_TYPE_MAIN = 0;
-
- /**
- * The side stage type.
- * @see SideStage
- */
- int STAGE_TYPE_SIDE = 1;
-
- @IntDef(prefix = { "STAGE_TYPE_" }, value = {
- STAGE_TYPE_UNDEFINED,
- STAGE_TYPE_MAIN,
- STAGE_TYPE_SIDE
- })
- @interface StageType {}
-
- /** Callback interface for listening to changes in a split-screen stage. */
- interface SplitScreenListener {
- default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {}
- default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {}
- default void onSplitVisibilityChanged(boolean visible) {}
- }
-
- /** Registers listener that gets split screen callback. */
- void registerSplitScreenListener(@NonNull SplitScreenListener listener,
- @NonNull Executor executor);
-
- /** Unregisters listener that gets split screen callback. */
- void unregisterSplitScreenListener(@NonNull SplitScreenListener listener);
-
- /**
- * Returns a binder that can be passed to an external process to manipulate SplitScreen.
- */
- default ISplitScreen createExternalInterface() {
- return null;
- }
-
- /**
- * Called when the keyguard occluded state changes.
- * @param occluded Indicates if the keyguard is now occluded.
- */
- void onKeyguardOccludedChanged(boolean occluded);
-
- /**
- * Called when the visibility of the keyguard changes.
- * @param showing Indicates if the keyguard is now visible.
- */
- void onKeyguardVisibilityChanged(boolean showing);
-
- /** Get a string representation of a stage type */
- static String stageTypeToString(@StageType int stage) {
- switch (stage) {
- case STAGE_TYPE_UNDEFINED: return "UNDEFINED";
- case STAGE_TYPE_MAIN: return "MAIN";
- case STAGE_TYPE_SIDE: return "SIDE";
- default: return "UNKNOWN(" + stage + ")";
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java
deleted file mode 100644
index 07174051a344..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java
+++ /dev/null
@@ -1,595 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.RemoteAnimationTarget.MODE_OPENING;
-
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
-import static com.android.wm.shell.common.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.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
-
-import android.app.ActivityManager;
-import android.app.ActivityTaskManager;
-import android.app.PendingIntent;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.LauncherApps;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-import android.util.Slog;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.SurfaceSession;
-import android.view.WindowManager;
-import android.window.RemoteTransition;
-import android.window.WindowContainerTransaction;
-
-import androidx.annotation.BinderThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.internal.logging.InstanceId;
-import com.android.internal.util.FrameworkStatsLog;
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.RemoteCallable;
-import com.android.wm.shell.common.ShellExecutor;
-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.SplitPosition;
-import com.android.wm.shell.draganddrop.DragAndDropPolicy;
-import com.android.wm.shell.transition.LegacyTransitions;
-import com.android.wm.shell.transition.Transitions;
-
-import java.io.PrintWriter;
-import java.util.Arrays;
-import java.util.Optional;
-import java.util.concurrent.Executor;
-
-import javax.inject.Provider;
-
-/**
- * Class manages split-screen multitasking mode and implements the main interface
- * {@link SplitScreen}.
- * @see StageCoordinator
- */
-// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen.
-public class SplitScreenController implements DragAndDropPolicy.Starter,
- RemoteCallable<SplitScreenController> {
- private static final String TAG = SplitScreenController.class.getSimpleName();
-
- private final ShellTaskOrganizer mTaskOrganizer;
- private final SyncTransactionQueue mSyncQueue;
- private final Context mContext;
- private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
- private final ShellExecutor mMainExecutor;
- private final SplitScreenImpl mImpl = new SplitScreenImpl();
- private final DisplayImeController mDisplayImeController;
- private final DisplayInsetsController mDisplayInsetsController;
- private final Transitions mTransitions;
- private final TransactionPool mTransactionPool;
- private final SplitscreenEventLogger mLogger;
- private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
-
- private StageCoordinator mStageCoordinator;
-
- public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
- SyncTransactionQueue syncQueue, Context context,
- RootTaskDisplayAreaOrganizer rootTDAOrganizer,
- ShellExecutor mainExecutor, DisplayImeController displayImeController,
- DisplayInsetsController displayInsetsController,
- Transitions transitions, TransactionPool transactionPool,
- Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
- mTaskOrganizer = shellTaskOrganizer;
- mSyncQueue = syncQueue;
- mContext = context;
- mRootTDAOrganizer = rootTDAOrganizer;
- mMainExecutor = mainExecutor;
- mDisplayImeController = displayImeController;
- mDisplayInsetsController = displayInsetsController;
- mTransitions = transitions;
- mTransactionPool = transactionPool;
- mUnfoldControllerProvider = unfoldControllerProvider;
- mLogger = new SplitscreenEventLogger();
- }
-
- public SplitScreen asSplitScreen() {
- return mImpl;
- }
-
- @Override
- public Context getContext() {
- return mContext;
- }
-
- @Override
- public ShellExecutor getRemoteCallExecutor() {
- return mMainExecutor;
- }
-
- public void onOrganizerRegistered() {
- if (mStageCoordinator == null) {
- // TODO: Multi-display
- mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
- mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
- mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
- mUnfoldControllerProvider);
- }
- }
-
- public boolean isSplitScreenVisible() {
- return mStageCoordinator.isSplitScreenVisible();
- }
-
- public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) {
- final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId);
- if (task == null) {
- throw new IllegalArgumentException("Unknown taskId" + taskId);
- }
- return moveToSideStage(task, sideStagePosition);
- }
-
- public boolean moveToSideStage(ActivityManager.RunningTaskInfo task,
- @SplitPosition int sideStagePosition) {
- return mStageCoordinator.moveToSideStage(task, sideStagePosition);
- }
-
- public boolean removeFromSideStage(int taskId) {
- return mStageCoordinator.removeFromSideStage(taskId);
- }
-
- public void setSideStageOutline(boolean enable) {
- mStageCoordinator.setSideStageOutline(enable);
- }
-
- public void setSideStagePosition(@SplitPosition int sideStagePosition) {
- mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */);
- }
-
- public void setSideStageVisibility(boolean visible) {
- mStageCoordinator.setSideStageVisibility(visible);
- }
-
- public void enterSplitScreen(int taskId, boolean leftOrTop) {
- moveToSideStage(taskId,
- leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT);
- }
-
- public void exitSplitScreen(int toTopTaskId, int exitReason) {
- mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason);
- }
-
- public void onKeyguardOccludedChanged(boolean occluded) {
- mStageCoordinator.onKeyguardOccludedChanged(occluded);
- }
-
- public void onKeyguardVisibilityChanged(boolean showing) {
- mStageCoordinator.onKeyguardVisibilityChanged(showing);
- }
-
- public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
- mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide);
- }
-
- public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
- mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds);
- }
-
- public void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
- mStageCoordinator.registerSplitScreenListener(listener);
- }
-
- public void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) {
- mStageCoordinator.unregisterSplitScreenListener(listener);
- }
-
- public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
- options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
- null /* wct */);
-
- try {
- ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to launch task", e);
- }
- }
-
- public void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
- @Nullable Bundle options, UserHandle user) {
- options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
- null /* wct */);
-
- try {
- LauncherApps launcherApps =
- mContext.getSystemService(LauncherApps.class);
- launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
- options, user);
- } catch (ActivityNotFoundException e) {
- Slog.e(TAG, "Failed to launch shortcut", e);
- }
- }
-
- public void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
- @Nullable Bundle options) {
- if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
- startIntentLegacy(intent, fillInIntent, position, options);
- return;
- }
- mStageCoordinator.startIntent(intent, fillInIntent, STAGE_TYPE_UNDEFINED, position, options,
- null /* remote */);
- }
-
- private void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
- @SplitPosition int position, @Nullable Bundle options) {
- LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
- @Override
- public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback,
- SurfaceControl.Transaction t) {
- mStageCoordinator.updateSurfaceBounds(null /* layout */, t,
- false /* applyResizingOffset */);
-
- if (apps != null) {
- for (int i = 0; i < apps.length; ++i) {
- if (apps[i].mode == MODE_OPENING) {
- t.show(apps[i].leash);
- }
- }
- }
-
- t.apply();
- if (finishedCallback != null) {
- try {
- finishedCallback.onAnimationFinished();
- } catch (RemoteException e) {
- Slog.e(TAG, "Error finishing legacy transition: ", e);
- }
- }
- }
- };
- WindowContainerTransaction wct = new WindowContainerTransaction();
- options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct);
- wct.sendPendingIntent(intent, fillInIntent, options);
- mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
- }
-
- RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) {
- if (!isSplitScreenVisible()) return null;
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
- .setContainerLayer()
- .setName("RecentsAnimationSplitTasks")
- .setHidden(false)
- .setCallsite("SplitScreenController#onGoingtoRecentsLegacy");
- mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder);
- SurfaceControl sc = builder.build();
- SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
-
- // Ensure that we order these in the parent in the right z-order as their previous order
- Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex);
- int layer = 1;
- for (RemoteAnimationTarget appTarget : apps) {
- transaction.reparent(appTarget.leash, sc);
- transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left,
- appTarget.screenSpaceBounds.top);
- transaction.setLayer(appTarget.leash, layer++);
- }
- transaction.apply();
- transaction.close();
- return new RemoteAnimationTarget[]{
- mStageCoordinator.getDividerBarLegacyTarget(),
- mStageCoordinator.getOutlineLegacyTarget()};
- }
-
- /**
- * Sets drag info to be logged when splitscreen is entered.
- */
- public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
- mStageCoordinator.logOnDroppedToSplit(position, dragSessionId);
- }
-
- public void dump(@NonNull PrintWriter pw, String prefix) {
- pw.println(prefix + TAG);
- if (mStageCoordinator != null) {
- mStageCoordinator.dump(pw, prefix);
- }
- }
-
- /**
- * The interface for calls from outside the Shell, within the host process.
- */
- @ExternalThread
- private class SplitScreenImpl implements SplitScreen {
- private ISplitScreenImpl mISplitScreen;
- private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>();
- private final SplitScreenListener mListener = new SplitScreenListener() {
- @Override
- public void onStagePositionChanged(int stage, int position) {
- for (int i = 0; i < mExecutors.size(); i++) {
- final int index = i;
- mExecutors.valueAt(index).execute(() -> {
- mExecutors.keyAt(index).onStagePositionChanged(stage, position);
- });
- }
- }
-
- @Override
- public void onTaskStageChanged(int taskId, int stage, boolean visible) {
- for (int i = 0; i < mExecutors.size(); i++) {
- final int index = i;
- mExecutors.valueAt(index).execute(() -> {
- mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible);
- });
- }
- }
-
- @Override
- public void onSplitVisibilityChanged(boolean visible) {
- for (int i = 0; i < mExecutors.size(); i++) {
- final int index = i;
- mExecutors.valueAt(index).execute(() -> {
- mExecutors.keyAt(index).onSplitVisibilityChanged(visible);
- });
- }
- }
- };
-
- @Override
- public ISplitScreen createExternalInterface() {
- if (mISplitScreen != null) {
- mISplitScreen.invalidate();
- }
- mISplitScreen = new ISplitScreenImpl(SplitScreenController.this);
- return mISplitScreen;
- }
-
- @Override
- public void onKeyguardOccludedChanged(boolean occluded) {
- mMainExecutor.execute(() -> {
- SplitScreenController.this.onKeyguardOccludedChanged(occluded);
- });
- }
-
- @Override
- public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) {
- if (mExecutors.containsKey(listener)) return;
-
- mMainExecutor.execute(() -> {
- if (mExecutors.size() == 0) {
- SplitScreenController.this.registerSplitScreenListener(mListener);
- }
-
- mExecutors.put(listener, executor);
- });
-
- executor.execute(() -> {
- mStageCoordinator.sendStatusToListener(listener);
- });
- }
-
- @Override
- public void unregisterSplitScreenListener(SplitScreenListener listener) {
- mMainExecutor.execute(() -> {
- mExecutors.remove(listener);
-
- if (mExecutors.size() == 0) {
- SplitScreenController.this.unregisterSplitScreenListener(mListener);
- }
- });
- }
-
- @Override
- public void onKeyguardVisibilityChanged(boolean showing) {
- mMainExecutor.execute(() -> {
- SplitScreenController.this.onKeyguardVisibilityChanged(showing);
- });
- }
- }
-
- /**
- * The interface for calls from outside the host process.
- */
- @BinderThread
- private static class ISplitScreenImpl extends ISplitScreen.Stub {
- private SplitScreenController mController;
- private ISplitScreenListener mListener;
- private final SplitScreen.SplitScreenListener mSplitScreenListener =
- new SplitScreen.SplitScreenListener() {
- @Override
- public void onStagePositionChanged(int stage, int position) {
- try {
- if (mListener != null) {
- mListener.onStagePositionChanged(stage, position);
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "onStagePositionChanged", e);
- }
- }
-
- @Override
- public void onTaskStageChanged(int taskId, int stage, boolean visible) {
- try {
- if (mListener != null) {
- mListener.onTaskStageChanged(taskId, stage, visible);
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "onTaskStageChanged", e);
- }
- }
- };
- private final IBinder.DeathRecipient mListenerDeathRecipient =
- new IBinder.DeathRecipient() {
- @Override
- @BinderThread
- public void binderDied() {
- final SplitScreenController controller = mController;
- controller.getRemoteCallExecutor().execute(() -> {
- mListener = null;
- controller.unregisterSplitScreenListener(mSplitScreenListener);
- });
- }
- };
-
- public ISplitScreenImpl(SplitScreenController controller) {
- mController = controller;
- }
-
- /**
- * Invalidates this instance, preventing future calls from updating the controller.
- */
- void invalidate() {
- mController = null;
- }
-
- @Override
- public void registerSplitScreenListener(ISplitScreenListener listener) {
- executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener",
- (controller) -> {
- if (mListener != null) {
- mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- }
- if (listener != null) {
- try {
- listener.asBinder().linkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to link to death");
- return;
- }
- }
- mListener = listener;
- controller.registerSplitScreenListener(mSplitScreenListener);
- });
- }
-
- @Override
- public void unregisterSplitScreenListener(ISplitScreenListener listener) {
- executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener",
- (controller) -> {
- if (mListener != null) {
- mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- }
- mListener = null;
- controller.unregisterSplitScreenListener(mSplitScreenListener);
- });
- }
-
- @Override
- public void exitSplitScreen(int toTopTaskId) {
- executeRemoteCallWithTaskPermission(mController, "exitSplitScreen",
- (controller) -> {
- controller.exitSplitScreen(toTopTaskId,
- FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT);
- });
- }
-
- @Override
- public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
- executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide",
- (controller) -> {
- controller.exitSplitScreenOnHide(exitSplitScreenOnHide);
- });
- }
-
- @Override
- public void setSideStageVisibility(boolean visible) {
- executeRemoteCallWithTaskPermission(mController, "setSideStageVisibility",
- (controller) -> {
- controller.setSideStageVisibility(visible);
- });
- }
-
- @Override
- public void removeFromSideStage(int taskId) {
- executeRemoteCallWithTaskPermission(mController, "removeFromSideStage",
- (controller) -> {
- controller.removeFromSideStage(taskId);
- });
- }
-
- @Override
- public void startTask(int taskId, int stage, int position, @Nullable Bundle options) {
- executeRemoteCallWithTaskPermission(mController, "startTask",
- (controller) -> {
- controller.startTask(taskId, position, options);
- });
- }
-
- @Override
- public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
- int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
- RemoteAnimationAdapter adapter) {
- executeRemoteCallWithTaskPermission(mController, "startTasks",
- (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition(
- mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition,
- adapter));
- }
-
- @Override
- public void startTasks(int mainTaskId, @Nullable Bundle mainOptions,
- int sideTaskId, @Nullable Bundle sideOptions,
- @SplitPosition int sidePosition,
- @Nullable RemoteTransition remoteTransition) {
- executeRemoteCallWithTaskPermission(mController, "startTasks",
- (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions,
- sideTaskId, sideOptions, sidePosition, remoteTransition));
- }
-
- @Override
- public void startShortcut(String packageName, String shortcutId, int stage, int position,
- @Nullable Bundle options, UserHandle user) {
- executeRemoteCallWithTaskPermission(mController, "startShortcut",
- (controller) -> {
- controller.startShortcut(packageName, shortcutId, position,
- options, user);
- });
- }
-
- @Override
- public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position,
- @Nullable Bundle options) {
- executeRemoteCallWithTaskPermission(mController, "startIntent",
- (controller) -> {
- controller.startIntent(intent, fillInIntent, position, options);
- });
- }
-
- @Override
- public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
- RemoteAnimationTarget[] apps) {
- final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null};
- executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy",
- (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps),
- true /* blocking */);
- return out[0];
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java
deleted file mode 100644
index 018365420177..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_CLOSE;
-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 android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
-
-import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
-import static com.android.wm.shell.transition.Transitions.isOpeningType;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.graphics.Rect;
-import android.os.IBinder;
-import android.view.SurfaceControl;
-import android.view.WindowManager;
-import android.window.RemoteTransition;
-import android.window.TransitionInfo;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.transition.OneShotRemoteHandler;
-import com.android.wm.shell.transition.Transitions;
-
-import java.util.ArrayList;
-
-/** Manages transition animations for split-screen. */
-class SplitScreenTransitions {
- private static final String TAG = "SplitScreenTransitions";
-
- /** Flag applied to a transition change to identify it as a divider bar for animation. */
- public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
-
- private final TransactionPool mTransactionPool;
- private final Transitions mTransitions;
- private final Runnable mOnFinish;
-
- IBinder mPendingDismiss = null;
- IBinder mPendingEnter = null;
-
- private IBinder mAnimatingTransition = null;
- private OneShotRemoteHandler mRemoteHandler = null;
-
- private Transitions.TransitionFinishCallback mRemoteFinishCB = (wct, wctCB) -> {
- if (wct != null || wctCB != null) {
- throw new UnsupportedOperationException("finish transactions not supported yet.");
- }
- onFinish();
- };
-
- /** Keeps track of currently running animations */
- private final ArrayList<Animator> mAnimations = new ArrayList<>();
-
- private Transitions.TransitionFinishCallback mFinishCallback = null;
- private SurfaceControl.Transaction mFinishTransaction;
-
- SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions,
- @NonNull Runnable onFinishCallback) {
- mTransactionPool = pool;
- mTransitions = transitions;
- mOnFinish = onFinishCallback;
- }
-
- void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback,
- @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) {
- mFinishCallback = finishCallback;
- mAnimatingTransition = transition;
- if (mRemoteHandler != null) {
- mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction,
- mRemoteFinishCB);
- mRemoteHandler = null;
- return;
- }
- playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot);
- }
-
- private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot,
- @NonNull WindowContainerToken sideRoot) {
- mFinishTransaction = mTransactionPool.acquire();
-
- // Play some place-holder fade animations
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- final SurfaceControl leash = change.getLeash();
- final int mode = info.getChanges().get(i).getMode();
-
- if (mode == TRANSIT_CHANGE) {
- if (change.getParent() != null) {
- // This is probably reparented, so we want the parent to be immediately visible
- final TransitionInfo.Change parentChange = info.getChange(change.getParent());
- t.show(parentChange.getLeash());
- t.setAlpha(parentChange.getLeash(), 1.f);
- // and then animate this layer outside the parent (since, for example, this is
- // the home task animating from fullscreen to part-screen).
- t.reparent(leash, info.getRootLeash());
- t.setLayer(leash, info.getChanges().size() - i);
- // build the finish reparent/reposition
- mFinishTransaction.reparent(leash, parentChange.getLeash());
- mFinishTransaction.setPosition(leash,
- change.getEndRelOffset().x, change.getEndRelOffset().y);
- }
- // TODO(shell-transitions): screenshot here
- final Rect startBounds = new Rect(change.getStartAbsBounds());
- final Rect endBounds = new Rect(change.getEndAbsBounds());
- startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
- endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
- startExampleResizeAnimation(leash, startBounds, endBounds);
- }
- if (change.getParent() != null) {
- continue;
- }
-
- if (transition == mPendingEnter && (mainRoot.equals(change.getContainer())
- || sideRoot.equals(change.getContainer()))) {
- t.setWindowCrop(leash, change.getStartAbsBounds().width(),
- change.getStartAbsBounds().height());
- }
- boolean isOpening = isOpeningType(info.getType());
- if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
- // fade in
- startExampleAnimation(leash, true /* show */);
- } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
- // fade out
- if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
- // Dismissing via snap-to-top/bottom means that the dismissed task is already
- // not-visible (usually cropped to oblivion) so immediately set its alpha to 0
- // and don't animate it so it doesn't pop-in when reparented.
- t.setAlpha(leash, 0.f);
- } else {
- startExampleAnimation(leash, false /* show */);
- }
- }
- }
- t.apply();
- onFinish();
- }
-
- /** Starts a transition to enter split with a remote transition animator. */
- IBinder startEnterTransition(@WindowManager.TransitionType int transitType,
- @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition,
- @NonNull Transitions.TransitionHandler handler) {
- if (remoteTransition != null) {
- // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
- mRemoteHandler = new OneShotRemoteHandler(
- mTransitions.getMainExecutor(), remoteTransition);
- }
- final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
- mPendingEnter = transition;
- if (mRemoteHandler != null) {
- mRemoteHandler.setTransition(transition);
- }
- return transition;
- }
-
- /** Starts a transition for dismissing split after dragging the divider to a screen edge */
- IBinder startSnapToDismiss(@NonNull WindowContainerTransaction wct,
- @NonNull Transitions.TransitionHandler handler) {
- final IBinder transition = mTransitions.startTransition(
- TRANSIT_SPLIT_DISMISS_SNAP, wct, handler);
- mPendingDismiss = transition;
- return transition;
- }
-
- void onFinish() {
- if (!mAnimations.isEmpty()) return;
- mOnFinish.run();
- if (mFinishTransaction != null) {
- mFinishTransaction.apply();
- mTransactionPool.release(mFinishTransaction);
- mFinishTransaction = null;
- }
- mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
- mFinishCallback = null;
- if (mAnimatingTransition == mPendingEnter) {
- mPendingEnter = null;
- }
- if (mAnimatingTransition == mPendingDismiss) {
- mPendingDismiss = null;
- }
- mAnimatingTransition = null;
- }
-
- // TODO(shell-transitions): real animations
- private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) {
- final float end = show ? 1.f : 0.f;
- final float start = 1.f - end;
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- final ValueAnimator va = ValueAnimator.ofFloat(start, end);
- va.setDuration(500);
- va.addUpdateListener(animation -> {
- float fraction = animation.getAnimatedFraction();
- transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
- transaction.apply();
- });
- final Runnable finisher = () -> {
- transaction.setAlpha(leash, end);
- transaction.apply();
- mTransactionPool.release(transaction);
- mTransitions.getMainExecutor().execute(() -> {
- mAnimations.remove(va);
- onFinish();
- });
- };
- va.addListener(new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) { }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- finisher.run();
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- finisher.run();
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) { }
- });
- mAnimations.add(va);
- mTransitions.getAnimExecutor().execute(va::start);
- }
-
- // TODO(shell-transitions): real animations
- private void startExampleResizeAnimation(@NonNull SurfaceControl leash,
- @NonNull Rect startBounds, @NonNull Rect endBounds) {
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f);
- va.setDuration(500);
- va.addUpdateListener(animation -> {
- float fraction = animation.getAnimatedFraction();
- transaction.setWindowCrop(leash,
- (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction),
- (int) (startBounds.height() * (1.f - fraction)
- + endBounds.height() * fraction));
- transaction.setPosition(leash,
- startBounds.left * (1.f - fraction) + endBounds.left * fraction,
- startBounds.top * (1.f - fraction) + endBounds.top * fraction);
- transaction.apply();
- });
- final Runnable finisher = () -> {
- transaction.setWindowCrop(leash, 0, 0);
- transaction.setPosition(leash, endBounds.left, endBounds.top);
- transaction.apply();
- mTransactionPool.release(transaction);
- mTransitions.getMainExecutor().execute(() -> {
- mAnimations.remove(va);
- onFinish();
- });
- };
- va.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- finisher.run();
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- finisher.run();
- }
- });
- mAnimations.add(va);
- mTransitions.getAnimExecutor().execute(va::start);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java
deleted file mode 100644
index e1850396a5c0..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java
+++ /dev/null
@@ -1,324 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
-
-import com.android.internal.logging.InstanceId;
-import com.android.internal.logging.InstanceIdSequence;
-import com.android.internal.util.FrameworkStatsLog;
-import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
-
-/**
- * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent
- */
-public class SplitscreenEventLogger {
-
- // Used to generate instance ids for this drag if one is not provided
- private final InstanceIdSequence mIdSequence;
-
- // The instance id for the current splitscreen session (from start to end)
- private InstanceId mLoggerSessionId;
-
- // Drag info
- private @SplitPosition int mDragEnterPosition;
- private InstanceId mDragEnterSessionId;
-
- // For deduping async events
- private int mLastMainStagePosition = -1;
- private int mLastMainStageUid = -1;
- private int mLastSideStagePosition = -1;
- private int mLastSideStageUid = -1;
- private float mLastSplitRatio = -1f;
-
- public SplitscreenEventLogger() {
- mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE);
- }
-
- /**
- * Return whether a splitscreen session has started.
- */
- public boolean hasStartedSession() {
- return mLoggerSessionId != null;
- }
-
- /**
- * May be called before logEnter() to indicate that the session was started from a drag.
- */
- public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) {
- mDragEnterPosition = position;
- mDragEnterSessionId = dragSessionId;
- }
-
- /**
- * Logs when the user enters splitscreen.
- */
- public void logEnter(float splitRatio,
- @SplitPosition int mainStagePosition, int mainStageUid,
- @SplitPosition int sideStagePosition, int sideStageUid,
- boolean isLandscape) {
- mLoggerSessionId = mIdSequence.newInstanceId();
- int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED
- ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape)
- : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
- updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
- mainStageUid);
- updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
- sideStageUid);
- updateSplitRatioState(splitRatio);
- FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
- FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER,
- enterReason,
- 0 /* exitReason */,
- splitRatio,
- mLastMainStagePosition,
- mLastMainStageUid,
- mLastSideStagePosition,
- mLastSideStageUid,
- mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0,
- mLoggerSessionId.getId());
- }
-
- /**
- * Logs when the user exits splitscreen. Only one of the main or side stages should be
- * specified to indicate which position was focused as a part of exiting (both can be unset).
- */
- public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid,
- @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
- if (mLoggerSessionId == null) {
- // Ignore changes until we've started logging the session
- return;
- }
- if ((mainStagePosition != SPLIT_POSITION_UNDEFINED
- && sideStagePosition != SPLIT_POSITION_UNDEFINED)
- || (mainStageUid != 0 && sideStageUid != 0)) {
- throw new IllegalArgumentException("Only main or side stage should be set");
- }
- FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
- FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT,
- 0 /* enterReason */,
- exitReason,
- 0f /* splitRatio */,
- getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
- mainStageUid,
- getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
- sideStageUid,
- 0 /* dragInstanceId */,
- mLoggerSessionId.getId());
-
- // Reset states
- mLoggerSessionId = null;
- mDragEnterPosition = SPLIT_POSITION_UNDEFINED;
- mDragEnterSessionId = null;
- mLastMainStagePosition = -1;
- mLastMainStageUid = -1;
- mLastSideStagePosition = -1;
- mLastSideStageUid = -1;
- }
-
- /**
- * Logs when an app in the main stage changes.
- */
- public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid,
- boolean isLandscape) {
- if (mLoggerSessionId == null) {
- // Ignore changes until we've started logging the session
- return;
- }
- if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition,
- isLandscape), mainStageUid)) {
- // Ignore if there are no user perceived changes
- return;
- }
-
- FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
- FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
- 0 /* enterReason */,
- 0 /* exitReason */,
- 0f /* splitRatio */,
- mLastMainStagePosition,
- mLastMainStageUid,
- 0 /* sideStagePosition */,
- 0 /* sideStageUid */,
- 0 /* dragInstanceId */,
- mLoggerSessionId.getId());
- }
-
- /**
- * Logs when an app in the side stage changes.
- */
- public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid,
- boolean isLandscape) {
- if (mLoggerSessionId == null) {
- // Ignore changes until we've started logging the session
- return;
- }
- if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition,
- isLandscape), sideStageUid)) {
- // Ignore if there are no user perceived changes
- return;
- }
-
- FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
- FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
- 0 /* enterReason */,
- 0 /* exitReason */,
- 0f /* splitRatio */,
- 0 /* mainStagePosition */,
- 0 /* mainStageUid */,
- mLastSideStagePosition,
- mLastSideStageUid,
- 0 /* dragInstanceId */,
- mLoggerSessionId.getId());
- }
-
- /**
- * Logs when the splitscreen ratio changes.
- */
- public void logResize(float splitRatio) {
- if (mLoggerSessionId == null) {
- // Ignore changes until we've started logging the session
- return;
- }
- if (splitRatio <= 0f || splitRatio >= 1f) {
- // Don't bother reporting resizes that end up dismissing the split, that will be logged
- // via the exit event
- return;
- }
- if (!updateSplitRatioState(splitRatio)) {
- // Ignore if there are no user perceived changes
- return;
- }
- FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
- FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE,
- 0 /* enterReason */,
- 0 /* exitReason */,
- mLastSplitRatio,
- 0 /* mainStagePosition */, 0 /* mainStageUid */,
- 0 /* sideStagePosition */, 0 /* sideStageUid */,
- 0 /* dragInstanceId */,
- mLoggerSessionId.getId());
- }
-
- /**
- * Logs when the apps in splitscreen are swapped.
- */
- public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid,
- @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
- if (mLoggerSessionId == null) {
- // Ignore changes until we've started logging the session
- return;
- }
-
- updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
- mainStageUid);
- updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
- sideStageUid);
- FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
- FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP,
- 0 /* enterReason */,
- 0 /* exitReason */,
- 0f /* splitRatio */,
- mLastMainStagePosition,
- mLastMainStageUid,
- mLastSideStagePosition,
- mLastSideStageUid,
- 0 /* dragInstanceId */,
- mLoggerSessionId.getId());
- }
-
- private boolean updateMainStageState(int mainStagePosition, int mainStageUid) {
- boolean changed = (mLastMainStagePosition != mainStagePosition)
- || (mLastMainStageUid != mainStageUid);
- if (!changed) {
- return false;
- }
-
- mLastMainStagePosition = mainStagePosition;
- mLastMainStageUid = mainStageUid;
- return true;
- }
-
- private boolean updateSideStageState(int sideStagePosition, int sideStageUid) {
- boolean changed = (mLastSideStagePosition != sideStagePosition)
- || (mLastSideStageUid != sideStageUid);
- if (!changed) {
- return false;
- }
-
- mLastSideStagePosition = sideStagePosition;
- mLastSideStageUid = sideStageUid;
- return true;
- }
-
- private boolean updateSplitRatioState(float splitRatio) {
- boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0;
- if (!changed) {
- return false;
- }
-
- mLastSplitRatio = splitRatio;
- return true;
- }
-
- public int getDragEnterReasonFromSplitPosition(@SplitPosition int position,
- boolean isLandscape) {
- if (isLandscape) {
- return position == SPLIT_POSITION_TOP_OR_LEFT
- ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT
- : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT;
- } else {
- return position == SPLIT_POSITION_TOP_OR_LEFT
- ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP
- : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM;
- }
- }
-
- private int getMainStagePositionFromSplitPosition(@SplitPosition int position,
- boolean isLandscape) {
- if (position == SPLIT_POSITION_UNDEFINED) {
- return 0;
- }
- if (isLandscape) {
- return position == SPLIT_POSITION_TOP_OR_LEFT
- ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT
- : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT;
- } else {
- return position == SPLIT_POSITION_TOP_OR_LEFT
- ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP
- : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM;
- }
- }
-
- private int getSideStagePositionFromSplitPosition(@SplitPosition int position,
- boolean isLandscape) {
- if (position == SPLIT_POSITION_UNDEFINED) {
- return 0;
- }
- if (isLandscape) {
- return position == SPLIT_POSITION_TOP_OR_LEFT
- ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT
- : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT;
- } else {
- return position == SPLIT_POSITION_TOP_OR_LEFT
- ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP
- : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM;
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java
deleted file mode 100644
index de0feeecad4b..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java
+++ /dev/null
@@ -1,1333 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
-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 android.view.WindowManager.transitTypeToString;
-
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
-import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_MAIN;
-import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_SIDE;
-import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_UNDEFINED;
-import static com.android.wm.shell.stagesplit.SplitScreen.stageTypeToString;
-import static com.android.wm.shell.stagesplit.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR;
-import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
-import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
-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;
-import static com.android.wm.shell.transition.Transitions.isClosingType;
-import static com.android.wm.shell.transition.Transitions.isOpeningType;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.ActivityOptions;
-import android.app.ActivityTaskManager;
-import android.app.PendingIntent;
-import android.app.WindowConfiguration;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.hardware.devicestate.DeviceStateManager;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-import android.util.Slog;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.SurfaceSession;
-import android.view.WindowManager;
-import android.window.DisplayAreaInfo;
-import android.window.RemoteTransition;
-import android.window.TransitionInfo;
-import android.window.TransitionRequestInfo;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.logging.InstanceId;
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.split.SplitLayout;
-import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
-import com.android.wm.shell.common.split.SplitWindowManager;
-import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import com.android.wm.shell.transition.Transitions;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
-import javax.inject.Provider;
-
-/**
- * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
- * {@link SideStage} stages.
- * Some high-level rules:
- * - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at
- * least one child task.
- * - The {@link MainStage} should only have children if the coordinator is active.
- * - The {@link SplitLayout} divider is only visible if both the {@link MainStage}
- * and {@link SideStage} are visible.
- * - The {@link MainStage} configuration is fullscreen when the {@link SideStage} isn't visible.
- * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and
- * {@link #onStageHasChildrenChanged(StageListenerImpl).}
- */
-class StageCoordinator implements SplitLayout.SplitLayoutHandler,
- RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener, Transitions.TransitionHandler {
-
- private static final String TAG = StageCoordinator.class.getSimpleName();
-
- /** internal value for mDismissTop that represents no dismiss */
- private static final int NO_DISMISS = -2;
-
- private final SurfaceSession mSurfaceSession = new SurfaceSession();
-
- private final MainStage mMainStage;
- private final StageListenerImpl mMainStageListener = new StageListenerImpl();
- private final StageTaskUnfoldController mMainUnfoldController;
- private final SideStage mSideStage;
- private final StageListenerImpl mSideStageListener = new StageListenerImpl();
- private final StageTaskUnfoldController mSideUnfoldController;
- @SplitPosition
- private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
-
- private final int mDisplayId;
- private SplitLayout mSplitLayout;
- private boolean mDividerVisible;
- private final SyncTransactionQueue mSyncQueue;
- private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
- private final ShellTaskOrganizer mTaskOrganizer;
- private DisplayAreaInfo mDisplayAreaInfo;
- private final Context mContext;
- private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>();
- private final DisplayImeController mDisplayImeController;
- private final DisplayInsetsController mDisplayInsetsController;
- private final SplitScreenTransitions mSplitTransitions;
- private final SplitscreenEventLogger mLogger;
- private boolean mExitSplitScreenOnHide;
- private boolean mKeyguardOccluded;
-
- // TODO(b/187041611): remove this flag after totally deprecated legacy split
- /** Whether the device is supporting legacy split or not. */
- private boolean mUseLegacySplit;
-
- @SplitScreen.StageType private int mDismissTop = NO_DISMISS;
-
- /** The target stage to dismiss to when unlock after folded. */
- @SplitScreen.StageType private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
-
- private final Runnable mOnTransitionAnimationComplete = () -> {
- // If still playing, let it finish.
- if (!isSplitScreenVisible()) {
- // Update divider state after animation so that it is still around and positioned
- // properly for the animation itself.
- setDividerVisibility(false);
- mSplitLayout.resetDividerPosition();
- }
- mDismissTop = NO_DISMISS;
- };
-
- private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
- new SplitWindowManager.ParentContainerCallbacks() {
- @Override
- public void attachToParentSurface(SurfaceControl.Builder b) {
- mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b);
- }
-
- @Override
- public void onLeashReady(SurfaceControl leash) {
- mSyncQueue.runInSync(t -> applyDividerVisibility(t));
- }
- };
-
- StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
- RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
- DisplayImeController displayImeController,
- DisplayInsetsController displayInsetsController, Transitions transitions,
- TransactionPool transactionPool, SplitscreenEventLogger logger,
- Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
- mContext = context;
- mDisplayId = displayId;
- mSyncQueue = syncQueue;
- mRootTDAOrganizer = rootTDAOrganizer;
- mTaskOrganizer = taskOrganizer;
- mLogger = logger;
- mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
- mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
-
- mMainStage = new MainStage(
- mTaskOrganizer,
- mDisplayId,
- mMainStageListener,
- mSyncQueue,
- mSurfaceSession,
- mMainUnfoldController);
- mSideStage = new SideStage(
- mContext,
- mTaskOrganizer,
- mDisplayId,
- mSideStageListener,
- mSyncQueue,
- mSurfaceSession,
- mSideUnfoldController);
- mDisplayImeController = displayImeController;
- mDisplayInsetsController = displayInsetsController;
- mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage);
- mRootTDAOrganizer.registerListener(displayId, this);
- final DeviceStateManager deviceStateManager =
- mContext.getSystemService(DeviceStateManager.class);
- deviceStateManager.registerCallback(taskOrganizer.getExecutor(),
- new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged));
- mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
- mOnTransitionAnimationComplete);
- transitions.addHandler(this);
- }
-
- @VisibleForTesting
- StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
- RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
- MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController,
- DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
- Transitions transitions, TransactionPool transactionPool,
- SplitscreenEventLogger logger,
- Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
- mContext = context;
- mDisplayId = displayId;
- mSyncQueue = syncQueue;
- mRootTDAOrganizer = rootTDAOrganizer;
- mTaskOrganizer = taskOrganizer;
- mMainStage = mainStage;
- mSideStage = sideStage;
- mDisplayImeController = displayImeController;
- mDisplayInsetsController = displayInsetsController;
- mRootTDAOrganizer.registerListener(displayId, this);
- mSplitLayout = splitLayout;
- mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
- mOnTransitionAnimationComplete);
- mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
- mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
- mLogger = logger;
- transitions.addHandler(this);
- }
-
- @VisibleForTesting
- SplitScreenTransitions getSplitTransitions() {
- return mSplitTransitions;
- }
-
- boolean isSplitScreenVisible() {
- return mSideStageListener.mVisible && mMainStageListener.mVisible;
- }
-
- boolean moveToSideStage(ActivityManager.RunningTaskInfo task,
- @SplitPosition int sideStagePosition) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- setSideStagePosition(sideStagePosition, wct);
- mMainStage.activate(getMainStageBounds(), wct);
- mSideStage.addTask(task, getSideStageBounds(), wct);
- mSyncQueue.queue(wct);
- mSyncQueue.runInSync(
- t -> updateSurfaceBounds(null /* layout */, t, false /* applyResizingOffset */));
- return true;
- }
-
- boolean removeFromSideStage(int taskId) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
-
- /**
- * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the
- * {@link SideStage} no longer has children.
- */
- final boolean result = mSideStage.removeTask(taskId,
- mMainStage.isActive() ? mMainStage.mRootTaskInfo.token : null,
- wct);
- mTaskOrganizer.applyTransaction(wct);
- return result;
- }
-
- void setSideStageOutline(boolean enable) {
- mSideStage.enableOutline(enable);
- }
-
- /** Starts 2 tasks in one transition. */
- void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId,
- @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
- @Nullable RemoteTransition remoteTransition) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- mainOptions = mainOptions != null ? mainOptions : new Bundle();
- sideOptions = sideOptions != null ? sideOptions : new Bundle();
- setSideStagePosition(sidePosition, wct);
-
- // Build a request WCT that will launch both apps such that task 0 is on the main stage
- // while task 1 is on the side stage.
- mMainStage.activate(getMainStageBounds(), wct);
- mSideStage.setBounds(getSideStageBounds(), wct);
-
- // Make sure the launch options will put tasks in the corresponding split roots
- addActivityOptions(mainOptions, mMainStage);
- addActivityOptions(sideOptions, mSideStage);
-
- // Add task launch requests
- wct.startTask(mainTaskId, mainOptions);
- wct.startTask(sideTaskId, sideOptions);
-
- mSplitTransitions.startEnterTransition(
- TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this);
- }
-
- /** Starts 2 tasks in one legacy transition. */
- void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
- int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
- RemoteAnimationAdapter adapter) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- // Need to add another wrapper here in shell so that we can inject the divider bar
- // and also manage the process elevation via setRunningRemote
- IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
- @Override
- public void onAnimationStart(@WindowManager.TransitionOldType int transit,
- RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers,
- RemoteAnimationTarget[] nonApps,
- final IRemoteAnimationFinishedCallback finishedCallback) {
- RemoteAnimationTarget[] augmentedNonApps =
- new RemoteAnimationTarget[nonApps.length + 1];
- for (int i = 0; i < nonApps.length; ++i) {
- augmentedNonApps[i] = nonApps[i];
- }
- augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget();
- try {
- ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(
- adapter.getCallingApplication());
- adapter.getRunner().onAnimationStart(transit, apps, wallpapers, nonApps,
- finishedCallback);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error starting remote animation", e);
- }
- }
-
- @Override
- public void onAnimationCancelled(boolean isKeyguardOccluded) {
- try {
- adapter.getRunner().onAnimationCancelled(isKeyguardOccluded);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error starting remote animation", e);
- }
- }
- };
- RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(
- wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay());
-
- if (mainOptions == null) {
- mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle();
- } else {
- ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions);
- mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
- }
-
- sideOptions = sideOptions != null ? sideOptions : new Bundle();
- setSideStagePosition(sidePosition, wct);
-
- // Build a request WCT that will launch both apps such that task 0 is on the main stage
- // while task 1 is on the side stage.
- mMainStage.activate(getMainStageBounds(), wct);
- mSideStage.setBounds(getSideStageBounds(), wct);
-
- // Make sure the launch options will put tasks in the corresponding split roots
- addActivityOptions(mainOptions, mMainStage);
- addActivityOptions(sideOptions, mSideStage);
-
- // Add task launch requests
- wct.startTask(mainTaskId, mainOptions);
- wct.startTask(sideTaskId, sideOptions);
-
- // Using legacy transitions, so we can't use blast sync since it conflicts.
- mTaskOrganizer.applyTransaction(wct);
- }
-
- public void startIntent(PendingIntent intent, Intent fillInIntent,
- @SplitScreen.StageType int stage, @SplitPosition int position,
- @androidx.annotation.Nullable Bundle options,
- @Nullable RemoteTransition remoteTransition) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- options = resolveStartStage(stage, position, options, wct);
- wct.sendPendingIntent(intent, fillInIntent, options);
- mSplitTransitions.startEnterTransition(
- TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this);
- }
-
- Bundle resolveStartStage(@SplitScreen.StageType int stage,
- @SplitPosition int position, @androidx.annotation.Nullable Bundle options,
- @androidx.annotation.Nullable WindowContainerTransaction wct) {
- switch (stage) {
- case STAGE_TYPE_UNDEFINED: {
- // Use the stage of the specified position is valid.
- if (position != SPLIT_POSITION_UNDEFINED) {
- if (position == getSideStagePosition()) {
- options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct);
- } else {
- options = resolveStartStage(STAGE_TYPE_MAIN, position, options, wct);
- }
- } else {
- // Exit split-screen and launch fullscreen since stage wasn't specified.
- prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
- }
- break;
- }
- case STAGE_TYPE_SIDE: {
- if (position != SPLIT_POSITION_UNDEFINED) {
- setSideStagePosition(position, wct);
- } else {
- position = getSideStagePosition();
- }
- if (options == null) {
- options = new Bundle();
- }
- updateActivityOptions(options, position);
- break;
- }
- case STAGE_TYPE_MAIN: {
- if (position != SPLIT_POSITION_UNDEFINED) {
- // Set the side stage opposite of what we want to the main stage.
- final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT
- ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
- setSideStagePosition(sideStagePosition, wct);
- } else {
- position = getMainStagePosition();
- }
- if (options == null) {
- options = new Bundle();
- }
- updateActivityOptions(options, position);
- break;
- }
- default:
- throw new IllegalArgumentException("Unknown stage=" + stage);
- }
-
- return options;
- }
-
- @SplitPosition
- int getSideStagePosition() {
- return mSideStagePosition;
- }
-
- @SplitPosition
- int getMainStagePosition() {
- return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
- ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
- }
-
- void setSideStagePosition(@SplitPosition int sideStagePosition,
- @Nullable WindowContainerTransaction wct) {
- setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
- }
-
- private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds,
- @Nullable WindowContainerTransaction wct) {
- if (mSideStagePosition == sideStagePosition) return;
- mSideStagePosition = sideStagePosition;
- sendOnStagePositionChanged();
-
- if (mSideStageListener.mVisible && updateBounds) {
- if (wct == null) {
- // onLayoutSizeChanged builds/applies a wct with the contents of updateWindowBounds.
- onLayoutSizeChanged(mSplitLayout);
- } else {
- updateWindowBounds(mSplitLayout, wct);
- updateUnfoldBounds();
- }
- }
- }
-
- void setSideStageVisibility(boolean visible) {
- if (mSideStageListener.mVisible == visible) return;
-
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- mSideStage.setVisibility(visible, wct);
- mTaskOrganizer.applyTransaction(wct);
- }
-
- void onKeyguardOccludedChanged(boolean occluded) {
- // Do not exit split directly, because it needs to wait for task info update to determine
- // which task should remain on top after split dismissed.
- mKeyguardOccluded = occluded;
- }
-
- void onKeyguardVisibilityChanged(boolean showing) {
- if (!showing && mMainStage.isActive()
- && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) {
- exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage,
- SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED);
- }
- }
-
- void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
- mExitSplitScreenOnHide = exitSplitScreenOnHide;
- }
-
- void exitSplitScreen(int toTopTaskId, int exitReason) {
- StageTaskListener childrenToTop = null;
- if (mMainStage.containsTask(toTopTaskId)) {
- childrenToTop = mMainStage;
- } else if (mSideStage.containsTask(toTopTaskId)) {
- childrenToTop = mSideStage;
- }
-
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- if (childrenToTop != null) {
- childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct);
- }
- applyExitSplitScreen(childrenToTop, wct, exitReason);
- }
-
- private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- applyExitSplitScreen(childrenToTop, wct, exitReason);
- }
-
- private void applyExitSplitScreen(
- StageTaskListener childrenToTop,
- WindowContainerTransaction wct, int exitReason) {
- mSideStage.removeAllTasks(wct, childrenToTop == mSideStage);
- mMainStage.deactivate(wct, childrenToTop == mMainStage);
- mTaskOrganizer.applyTransaction(wct);
- mSyncQueue.runInSync(t -> t
- .setWindowCrop(mMainStage.mRootLeash, null)
- .setWindowCrop(mSideStage.mRootLeash, null));
- // Hide divider and reset its position.
- setDividerVisibility(false);
- mSplitLayout.resetDividerPosition();
- mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
- if (childrenToTop != null) {
- logExitToStage(exitReason, childrenToTop == mMainStage);
- } else {
- logExit(exitReason);
- }
- }
-
- /**
- * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates
- * an existing WindowContainerTransaction (rather than applying immediately). This is intended
- * to be used when exiting split might be bundled with other window operations.
- */
- void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop,
- @NonNull WindowContainerTransaction wct) {
- mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE);
- mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN);
- }
-
- void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
- outTopOrLeftBounds.set(mSplitLayout.getBounds1());
- outBottomOrRightBounds.set(mSplitLayout.getBounds2());
- }
-
- private void addActivityOptions(Bundle opts, StageTaskListener stage) {
- opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token);
- }
-
- void updateActivityOptions(Bundle opts, @SplitPosition int position) {
- addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage);
- }
-
- void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
- if (mListeners.contains(listener)) return;
- mListeners.add(listener);
- sendStatusToListener(listener);
- }
-
- void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) {
- mListeners.remove(listener);
- }
-
- void sendStatusToListener(SplitScreen.SplitScreenListener listener) {
- listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
- listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
- listener.onSplitVisibilityChanged(isSplitScreenVisible());
- mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
- mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
- }
-
- private void sendOnStagePositionChanged() {
- for (int i = mListeners.size() - 1; i >= 0; --i) {
- final SplitScreen.SplitScreenListener l = mListeners.get(i);
- l.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
- l.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
- }
- }
-
- private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId,
- boolean present, boolean visible) {
- int stage;
- if (present) {
- stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
- } else {
- // No longer on any stage
- stage = STAGE_TYPE_UNDEFINED;
- }
- if (stage == STAGE_TYPE_MAIN) {
- mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
- mSplitLayout.isLandscape());
- } else {
- mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
- mSplitLayout.isLandscape());
- }
-
- for (int i = mListeners.size() - 1; i >= 0; --i) {
- mListeners.get(i).onTaskStageChanged(taskId, stage, visible);
- }
- }
-
- private void sendSplitVisibilityChanged() {
- for (int i = mListeners.size() - 1; i >= 0; --i) {
- final SplitScreen.SplitScreenListener l = mListeners.get(i);
- l.onSplitVisibilityChanged(mDividerVisible);
- }
-
- if (mMainUnfoldController != null && mSideUnfoldController != null) {
- mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible);
- mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible);
- }
- }
-
- private void onStageRootTaskAppeared(StageListenerImpl stageListener) {
- if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) {
- mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit);
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- // Make the stages adjacent to each other so they occlude what's behind them.
- wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token,
- true /* moveTogether */);
-
- // Only sets side stage as launch-adjacent-flag-root when the device is not using legacy
- // split to prevent new split behavior confusing users.
- if (!mUseLegacySplit) {
- wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
- }
-
- mTaskOrganizer.applyTransaction(wct);
- }
- }
-
- private void onStageRootTaskVanished(StageListenerImpl stageListener) {
- if (stageListener == mMainStageListener || stageListener == mSideStageListener) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- // Deactivate the main stage if it no longer has a root task.
- mMainStage.deactivate(wct);
-
- if (!mUseLegacySplit) {
- wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
- }
-
- mTaskOrganizer.applyTransaction(wct);
- }
- }
-
- private void setDividerVisibility(boolean visible) {
- if (mDividerVisible == visible) return;
- mDividerVisible = visible;
- if (visible) {
- mSplitLayout.init();
- updateUnfoldBounds();
- } else {
- mSplitLayout.release();
- }
- sendSplitVisibilityChanged();
- }
-
- private void onStageVisibilityChanged(StageListenerImpl stageListener) {
- final boolean sideStageVisible = mSideStageListener.mVisible;
- final boolean mainStageVisible = mMainStageListener.mVisible;
- final boolean bothStageVisible = sideStageVisible && mainStageVisible;
- final boolean bothStageInvisible = !sideStageVisible && !mainStageVisible;
- final boolean sameVisibility = sideStageVisible == mainStageVisible;
- // Only add or remove divider when both visible or both invisible to avoid sometimes we only
- // got one stage visibility changed for a moment and it will cause flicker.
- if (sameVisibility) {
- setDividerVisibility(bothStageVisible);
- }
-
- if (bothStageInvisible) {
- if (mExitSplitScreenOnHide
- // Don't dismiss staged split when both stages are not visible due to sleeping display,
- // like the cases keyguard showing or screen off.
- || (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping)) {
- exitSplitScreen(null /* childrenToTop */,
- SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
- }
- } else if (mKeyguardOccluded) {
- // At least one of the stages is visible while keyguard occluded. Dismiss split because
- // there's show-when-locked activity showing on top of keyguard. Also make sure the
- // task contains show-when-locked activity remains on top after split dismissed.
- final StageTaskListener toTop =
- mainStageVisible ? mMainStage : (sideStageVisible ? mSideStage : null);
- exitSplitScreen(toTop, SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP);
- }
-
- mSyncQueue.runInSync(t -> {
- // Same above, we only set root tasks and divider leash visibility when both stage
- // change to visible or invisible to avoid flicker.
- if (sameVisibility) {
- t.setVisibility(mSideStage.mRootLeash, bothStageVisible)
- .setVisibility(mMainStage.mRootLeash, bothStageVisible);
- applyDividerVisibility(t);
- applyOutlineVisibility(t);
- }
- });
- }
-
- private void applyDividerVisibility(SurfaceControl.Transaction t) {
- final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
- if (dividerLeash == null) {
- return;
- }
-
- if (mDividerVisible) {
- t.show(dividerLeash)
- .setLayer(dividerLeash, Integer.MAX_VALUE)
- .setPosition(dividerLeash,
- mSplitLayout.getDividerBounds().left,
- mSplitLayout.getDividerBounds().top);
- } else {
- t.hide(dividerLeash);
- }
- }
-
- private void applyOutlineVisibility(SurfaceControl.Transaction t) {
- final SurfaceControl outlineLeash = mSideStage.getOutlineLeash();
- if (outlineLeash == null) {
- return;
- }
-
- if (mDividerVisible) {
- t.show(outlineLeash).setLayer(outlineLeash, Integer.MAX_VALUE);
- } else {
- t.hide(outlineLeash);
- }
- }
-
- private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
- final boolean hasChildren = stageListener.mHasChildren;
- final boolean isSideStage = stageListener == mSideStageListener;
- if (!hasChildren) {
- if (isSideStage && mMainStageListener.mVisible) {
- // Exit to main stage if side stage no longer has children.
- exitSplitScreen(mMainStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
- } else if (!isSideStage && mSideStageListener.mVisible) {
- // Exit to side stage if main stage no longer has children.
- exitSplitScreen(mSideStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
- }
- } else if (isSideStage) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- // Make sure the main stage is active.
- mMainStage.activate(getMainStageBounds(), wct);
- mSideStage.setBounds(getSideStageBounds(), wct);
- mTaskOrganizer.applyTransaction(wct);
- }
- if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren
- && mSideStageListener.mHasChildren) {
- mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
- getMainStagePosition(), mMainStage.getTopChildTaskUid(),
- getSideStagePosition(), mSideStage.getTopChildTaskUid(),
- mSplitLayout.isLandscape());
- }
- }
-
- @VisibleForTesting
- IBinder onSnappedToDismissTransition(boolean mainStageToTop) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- prepareExitSplitScreen(mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE, wct);
- return mSplitTransitions.startSnapToDismiss(wct, this);
- }
-
- @Override
- public void onSnappedToDismiss(boolean bottomOrRight) {
- final boolean mainStageToTop =
- bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
- : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
- if (ENABLE_SHELL_TRANSITIONS) {
- onSnappedToDismissTransition(mainStageToTop);
- return;
- }
- exitSplitScreen(mainStageToTop ? mMainStage : mSideStage,
- SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER);
- }
-
- @Override
- public void onDoubleTappedDivider() {
- setSideStagePosition(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
- ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT, null /* wct */);
- mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
- getSideStagePosition(), mSideStage.getTopChildTaskUid(),
- mSplitLayout.isLandscape());
- }
-
- @Override
- public void onLayoutPositionChanging(SplitLayout layout) {
- mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t, true /* applyResizingOffset */));
- }
-
- @Override
- public void onLayoutSizeChanging(SplitLayout layout) {
- mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t, true /* applyResizingOffset */));
- mSideStage.setOutlineVisibility(false);
- }
-
- @Override
- public void onLayoutSizeChanged(SplitLayout layout) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- updateWindowBounds(layout, wct);
- updateUnfoldBounds();
- mSyncQueue.queue(wct);
- mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t, false /* applyResizingOffset */));
- mSideStage.setOutlineVisibility(true);
- mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
- }
-
- private void updateUnfoldBounds() {
- if (mMainUnfoldController != null && mSideUnfoldController != null) {
- mMainUnfoldController.onLayoutChanged(getMainStageBounds());
- mSideUnfoldController.onLayoutChanged(getSideStageBounds());
- }
- }
-
- /**
- * Populates `wct` with operations that match the split windows to the current layout.
- * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied
- */
- private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
- final StageTaskListener topLeftStage =
- mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
- final StageTaskListener bottomRightStage =
- mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
- layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo);
- }
-
- void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t,
- boolean applyResizingOffset) {
- final StageTaskListener topLeftStage =
- mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
- final StageTaskListener bottomRightStage =
- mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
- (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash,
- bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer,
- applyResizingOffset);
- }
-
- @Override
- public int getSplitItemPosition(WindowContainerToken token) {
- if (token == null) {
- return SPLIT_POSITION_UNDEFINED;
- }
-
- if (token.equals(mMainStage.mRootTaskInfo.getToken())) {
- return getMainStagePosition();
- } else if (token.equals(mSideStage.mRootTaskInfo.getToken())) {
- return getSideStagePosition();
- }
-
- return SPLIT_POSITION_UNDEFINED;
- }
-
- @Override
- public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) {
- final StageTaskListener topLeftStage =
- mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
- final StageTaskListener bottomRightStage =
- mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo,
- bottomRightStage.mRootTaskInfo);
- mTaskOrganizer.applyTransaction(wct);
- }
-
- @Override
- public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) {
- mDisplayAreaInfo = displayAreaInfo;
- if (mSplitLayout == null) {
- mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext,
- mDisplayAreaInfo.configuration, this, mParentContainerCallbacks,
- mDisplayImeController, mTaskOrganizer, SplitLayout.PARALLAX_DISMISSING);
- mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
-
- if (mMainUnfoldController != null && mSideUnfoldController != null) {
- mMainUnfoldController.init();
- mSideUnfoldController.init();
- }
- }
- }
-
- @Override
- public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) {
- throw new IllegalStateException("Well that was unexpected...");
- }
-
- @Override
- public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) {
- mDisplayAreaInfo = displayAreaInfo;
- if (mSplitLayout != null
- && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)
- && mMainStage.isActive()) {
- onLayoutSizeChanged(mSplitLayout);
- }
- }
-
- private void onFoldedStateChanged(boolean folded) {
- mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
- if (!folded) return;
-
- if (mMainStage.isFocused()) {
- mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN;
- } else if (mSideStage.isFocused()) {
- mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE;
- }
- }
-
- private Rect getSideStageBounds() {
- return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
- ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2();
- }
-
- private Rect getMainStageBounds() {
- return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
- ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1();
- }
-
- /**
- * Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain
- * this task (yet) so this can also be used to identify which stage to put a task into.
- */
- private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) {
- // TODO(b/184679596): Find a way to either include task-org information in the transition,
- // or synchronize task-org callbacks so we can use stage.containsTask
- if (mMainStage.mRootTaskInfo != null
- && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) {
- return mMainStage;
- } else if (mSideStage.mRootTaskInfo != null
- && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
- return mSideStage;
- }
- return null;
- }
-
- @SplitScreen.StageType
- private int getStageType(StageTaskListener stage) {
- return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
- }
-
- @Override
- public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
- @Nullable TransitionRequestInfo request) {
- final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
- if (triggerTask == null) {
- // still want to monitor everything while in split-screen, so return non-null.
- return isSplitScreenVisible() ? new WindowContainerTransaction() : null;
- }
-
- WindowContainerTransaction out = null;
- final @WindowManager.TransitionType int type = request.getType();
- if (isSplitScreenVisible()) {
- // try to handle everything while in split-screen, so return a WCT even if it's empty.
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " split is active so using split"
- + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d"
- + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type),
- mMainStage.getChildCount(), mSideStage.getChildCount());
- out = new WindowContainerTransaction();
- final StageTaskListener stage = getStageOfTask(triggerTask);
- if (stage != null) {
- // dismiss split if the last task in one of the stages is going away
- if (isClosingType(type) && stage.getChildCount() == 1) {
- // The top should be the opposite side that is closing:
- mDismissTop = getStageType(stage) == STAGE_TYPE_MAIN
- ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
- }
- } else {
- if (triggerTask.getActivityType() == ACTIVITY_TYPE_HOME && isOpeningType(type)) {
- // Going home so dismiss both.
- mDismissTop = STAGE_TYPE_UNDEFINED;
- }
- }
- if (mDismissTop != NO_DISMISS) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
- + " deduced Dismiss from request. toTop=%s",
- stageTypeToString(mDismissTop));
- prepareExitSplitScreen(mDismissTop, out);
- mSplitTransitions.mPendingDismiss = transition;
- }
- } else {
- // Not in split mode, so look for an open into a split stage just so we can whine and
- // complain about how this isn't a supported operation.
- if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)) {
- if (getStageOfTask(triggerTask) != null) {
- throw new IllegalStateException("Entering split implicitly with only one task"
- + " isn't supported.");
- }
- }
- }
- return out;
- }
-
- @Override
- public boolean startAnimation(@NonNull IBinder transition,
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (transition != mSplitTransitions.mPendingDismiss
- && transition != mSplitTransitions.mPendingEnter) {
- // Not entering or exiting, so just do some house-keeping and validation.
-
- // If we're not in split-mode, just abort so something else can handle it.
- if (!isSplitScreenVisible()) return false;
-
- for (int iC = 0; iC < info.getChanges().size(); ++iC) {
- final TransitionInfo.Change change = info.getChanges().get(iC);
- final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
- if (taskInfo == null || !taskInfo.hasParentTask()) continue;
- final StageTaskListener stage = getStageOfTask(taskInfo);
- if (stage == null) continue;
- if (isOpeningType(change.getMode())) {
- if (!stage.containsTask(taskInfo.taskId)) {
- Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called"
- + " with " + taskInfo.taskId + " before startAnimation().");
- }
- } else if (isClosingType(change.getMode())) {
- if (stage.containsTask(taskInfo.taskId)) {
- Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called"
- + " with " + taskInfo.taskId + " before startAnimation().");
- }
- }
- }
- if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
- // TODO(shell-transitions): Implement a fallback behavior for now.
- throw new IllegalStateException("Somehow removed the last task in a stage"
- + " outside of a proper transition");
- // This can happen in some pathological cases. For example:
- // 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C]
- // 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time
- // In this case, the result *should* be that we leave split.
- // TODO(b/184679596): Find a way to either include task-org information in
- // the transition, or synchronize task-org callbacks.
- }
-
- // Use normal animations.
- return false;
- }
-
- boolean shouldAnimate = true;
- if (mSplitTransitions.mPendingEnter == transition) {
- shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction);
- } else if (mSplitTransitions.mPendingDismiss == transition) {
- shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction);
- }
- if (!shouldAnimate) return false;
-
- mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction,
- finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
- return true;
- }
-
- private boolean startPendingEnterAnimation(@NonNull IBinder transition,
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
- if (info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN) {
- // First, verify that we actually have opened 2 apps in split.
- TransitionInfo.Change mainChild = null;
- TransitionInfo.Change sideChild = null;
- for (int iC = 0; iC < info.getChanges().size(); ++iC) {
- final TransitionInfo.Change change = info.getChanges().get(iC);
- final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
- if (taskInfo == null || !taskInfo.hasParentTask()) continue;
- final @SplitScreen.StageType int stageType = getStageType(getStageOfTask(taskInfo));
- if (stageType == STAGE_TYPE_MAIN) {
- mainChild = change;
- } else if (stageType == STAGE_TYPE_SIDE) {
- sideChild = change;
- }
- }
- if (mainChild == null || sideChild == null) {
- throw new IllegalStateException("Launched 2 tasks in split, but didn't receive"
- + " 2 tasks in transition. Possibly one of them failed to launch");
- // TODO: fallback logic. Probably start a new transition to exit split before
- // applying anything here. Ideally consolidate with transition-merging.
- }
-
- // Update local states (before animating).
- setDividerVisibility(true);
- setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */,
- null /* wct */);
- setSplitsVisible(true);
-
- addDividerBarToTransition(info, t, true /* show */);
-
- // Make some noise if things aren't totally expected. These states shouldn't effect
- // transitions locally, but remotes (like Launcher) may get confused if they were
- // depending on listener callbacks. This can happen because task-organizer callbacks
- // aren't serialized with transition callbacks.
- // TODO(b/184679596): Find a way to either include task-org information in
- // the transition, or synchronize task-org callbacks.
- if (!mMainStage.containsTask(mainChild.getTaskInfo().taskId)) {
- Log.w(TAG, "Expected onTaskAppeared on " + mMainStage
- + " to have been called with " + mainChild.getTaskInfo().taskId
- + " before startAnimation().");
- }
- if (!mSideStage.containsTask(sideChild.getTaskInfo().taskId)) {
- Log.w(TAG, "Expected onTaskAppeared on " + mSideStage
- + " to have been called with " + sideChild.getTaskInfo().taskId
- + " before startAnimation().");
- }
- return true;
- } else {
- // TODO: other entry method animations
- throw new RuntimeException("Unsupported split-entry");
- }
- }
-
- private boolean startPendingDismissAnimation(@NonNull IBinder transition,
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
- // Make some noise if things aren't totally expected. These states shouldn't effect
- // transitions locally, but remotes (like Launcher) may get confused if they were
- // depending on listener callbacks. This can happen because task-organizer callbacks
- // aren't serialized with transition callbacks.
- // TODO(b/184679596): Find a way to either include task-org information in
- // the transition, or synchronize task-org callbacks.
- if (mMainStage.getChildCount() != 0) {
- final StringBuilder tasksLeft = new StringBuilder();
- for (int i = 0; i < mMainStage.getChildCount(); ++i) {
- tasksLeft.append(i != 0 ? ", " : "");
- tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i));
- }
- Log.w(TAG, "Expected onTaskVanished on " + mMainStage
- + " to have been called with [" + tasksLeft.toString()
- + "] before startAnimation().");
- }
- if (mSideStage.getChildCount() != 0) {
- final StringBuilder tasksLeft = new StringBuilder();
- for (int i = 0; i < mSideStage.getChildCount(); ++i) {
- tasksLeft.append(i != 0 ? ", " : "");
- tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i));
- }
- Log.w(TAG, "Expected onTaskVanished on " + mSideStage
- + " to have been called with [" + tasksLeft.toString()
- + "] before startAnimation().");
- }
-
- // Update local states.
- setSplitsVisible(false);
- // Wait until after animation to update divider
-
- if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
- // Reset crops so they don't interfere with subsequent launches
- t.setWindowCrop(mMainStage.mRootLeash, null);
- t.setWindowCrop(mSideStage.mRootLeash, null);
- }
-
- if (mDismissTop == STAGE_TYPE_UNDEFINED) {
- // Going home (dismissing both splits)
-
- // TODO: Have a proper remote for this. Until then, though, reset state and use the
- // normal animation stuff (which falls back to the normal launcher remote).
- t.hide(mSplitLayout.getDividerLeash());
- setDividerVisibility(false);
- mSplitTransitions.mPendingDismiss = null;
- return false;
- }
-
- addDividerBarToTransition(info, t, false /* show */);
- // We're dismissing split by moving the other one to fullscreen.
- // Since we don't have any animations for this yet, just use the internal example
- // animations.
- return true;
- }
-
- private void addDividerBarToTransition(@NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t, boolean show) {
- final SurfaceControl leash = mSplitLayout.getDividerLeash();
- final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash);
- final Rect bounds = mSplitLayout.getDividerBounds();
- barChange.setStartAbsBounds(bounds);
- barChange.setEndAbsBounds(bounds);
- barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
- barChange.setFlags(FLAG_IS_DIVIDER_BAR);
- // Technically this should be order-0, but this is running after layer assignment
- // and it's a special case, so just add to end.
- info.addChange(barChange);
- // Be default, make it visible. The remote animator can adjust alpha if it plans to animate.
- if (show) {
- t.setAlpha(leash, 1.f);
- t.setLayer(leash, Integer.MAX_VALUE);
- t.setPosition(leash, bounds.left, bounds.top);
- t.show(leash);
- }
- }
-
- RemoteAnimationTarget getDividerBarLegacyTarget() {
- final Rect bounds = mSplitLayout.getDividerBounds();
- return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
- mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */,
- null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
- new android.graphics.Point(0, 0) /* position */, bounds, bounds,
- new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
- null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
- }
-
- RemoteAnimationTarget getOutlineLegacyTarget() {
- final Rect bounds = mSideStage.mRootTaskInfo.configuration.windowConfiguration.getBounds();
- // Leverage TYPE_DOCK_DIVIDER type when wrapping outline remote animation target in order to
- // distinguish as a split auxiliary target in Launcher.
- return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
- mSideStage.getOutlineLeash(), false /* isTranslucent */, null /* clipRect */,
- null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
- new android.graphics.Point(0, 0) /* position */, bounds, bounds,
- new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
- null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
- }
-
- @Override
- public void dump(@NonNull PrintWriter pw, String prefix) {
- final String innerPrefix = prefix + " ";
- final String childPrefix = innerPrefix + " ";
- pw.println(prefix + TAG + " mDisplayId=" + mDisplayId);
- pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible);
- pw.println(innerPrefix + "MainStage");
- pw.println(childPrefix + "isActive=" + mMainStage.isActive());
- mMainStageListener.dump(pw, childPrefix);
- pw.println(innerPrefix + "SideStage");
- mSideStageListener.dump(pw, childPrefix);
- pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout);
- }
-
- /**
- * Directly set the visibility of both splits. This assumes hasChildren matches visibility.
- * This is intended for batch use, so it assumes other state management logic is already
- * handled.
- */
- private void setSplitsVisible(boolean visible) {
- mMainStageListener.mVisible = mSideStageListener.mVisible = visible;
- mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible;
- }
-
- /**
- * Sets drag info to be logged when splitscreen is next entered.
- */
- public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
- mLogger.enterRequestedByDrag(position, dragSessionId);
- }
-
- /**
- * Logs the exit of splitscreen.
- */
- private void logExit(int exitReason) {
- mLogger.logExit(exitReason,
- SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */,
- SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */,
- mSplitLayout.isLandscape());
- }
-
- /**
- * Logs the exit of splitscreen to a specific stage. This must be called before the exit is
- * executed.
- */
- private void logExitToStage(int exitReason, boolean toMainStage) {
- mLogger.logExit(exitReason,
- toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED,
- toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */,
- !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED,
- !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */,
- mSplitLayout.isLandscape());
- }
-
- class StageListenerImpl implements StageTaskListener.StageListenerCallbacks {
- boolean mHasRootTask = false;
- boolean mVisible = false;
- boolean mHasChildren = false;
-
- @Override
- public void onRootTaskAppeared() {
- mHasRootTask = true;
- StageCoordinator.this.onStageRootTaskAppeared(this);
- }
-
- @Override
- public void onStatusChanged(boolean visible, boolean hasChildren) {
- if (!mHasRootTask) return;
-
- if (mHasChildren != hasChildren) {
- mHasChildren = hasChildren;
- StageCoordinator.this.onStageHasChildrenChanged(this);
- }
- if (mVisible != visible) {
- mVisible = visible;
- StageCoordinator.this.onStageVisibilityChanged(this);
- }
- }
-
- @Override
- public void onChildTaskStatusChanged(int taskId, boolean present, boolean visible) {
- StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present, visible);
- }
-
- @Override
- public void onRootTaskVanished() {
- reset();
- StageCoordinator.this.onStageRootTaskVanished(this);
- }
-
- @Override
- public void onNoLongerSupportMultiWindow() {
- if (mMainStage.isActive()) {
- StageCoordinator.this.exitSplitScreen(null /* childrenToTop */,
- SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW);
- }
- }
-
- private void reset() {
- mHasRootTask = false;
- mVisible = false;
- mHasChildren = false;
- }
-
- public void dump(@NonNull PrintWriter pw, String prefix) {
- pw.println(prefix + "mHasRootTask=" + mHasRootTask);
- pw.println(prefix + "mVisible=" + mVisible);
- pw.println(prefix + "mHasChildren=" + mHasChildren);
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java
deleted file mode 100644
index 7b679580fa87..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java
+++ /dev/null
@@ -1,298 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
-import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
-
-import android.annotation.CallSuper;
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.util.SparseArray;
-import android.view.SurfaceControl;
-import android.view.SurfaceSession;
-import android.window.WindowContainerTransaction;
-
-import androidx.annotation.NonNull;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.SurfaceUtils;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-import java.io.PrintWriter;
-
-/**
- * Base class that handle common task org. related for split-screen stages.
- * Note that this class and its sub-class do not directly perform hierarchy operations.
- * They only serve to hold a collection of tasks and provide APIs like
- * {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator}
- * to perform operations in-sync with other containers.
- *
- * @see StageCoordinator
- */
-class StageTaskListener implements ShellTaskOrganizer.TaskListener {
- private static final String TAG = StageTaskListener.class.getSimpleName();
-
- protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD};
- protected static final int[] CONTROLLED_WINDOWING_MODES =
- {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
- protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
- {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
-
- /** Callback interface for listening to changes in a split-screen stage. */
- public interface StageListenerCallbacks {
- void onRootTaskAppeared();
-
- void onStatusChanged(boolean visible, boolean hasChildren);
-
- void onChildTaskStatusChanged(int taskId, boolean present, boolean visible);
-
- void onRootTaskVanished();
- void onNoLongerSupportMultiWindow();
- }
-
- private final StageListenerCallbacks mCallbacks;
- private final SurfaceSession mSurfaceSession;
- protected final SyncTransactionQueue mSyncQueue;
-
- protected ActivityManager.RunningTaskInfo mRootTaskInfo;
- protected SurfaceControl mRootLeash;
- protected SurfaceControl mDimLayer;
- protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>();
- private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>();
-
- private final StageTaskUnfoldController mStageTaskUnfoldController;
-
- StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId,
- StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
- SurfaceSession surfaceSession,
- @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
- mCallbacks = callbacks;
- mSyncQueue = syncQueue;
- mSurfaceSession = surfaceSession;
- mStageTaskUnfoldController = stageTaskUnfoldController;
- taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
- }
-
- int getChildCount() {
- return mChildrenTaskInfo.size();
- }
-
- boolean containsTask(int taskId) {
- return mChildrenTaskInfo.contains(taskId);
- }
-
- /**
- * Returns the top activity uid for the top child task.
- */
- int getTopChildTaskUid() {
- for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
- final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i);
- if (info.topActivityInfo == null) {
- continue;
- }
- return info.topActivityInfo.applicationInfo.uid;
- }
- return 0;
- }
-
- /** @return {@code true} if this listener contains the currently focused task. */
- boolean isFocused() {
- if (mRootTaskInfo == null) {
- return false;
- }
-
- if (mRootTaskInfo.isFocused) {
- return true;
- }
-
- for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
- if (mChildrenTaskInfo.valueAt(i).isFocused) {
- return true;
- }
- }
-
- return false;
- }
-
- @Override
- @CallSuper
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- if (mRootTaskInfo == null && !taskInfo.hasParentTask()) {
- mRootLeash = leash;
- mRootTaskInfo = taskInfo;
- mCallbacks.onRootTaskAppeared();
- sendStatusChanged();
- mSyncQueue.runInSync(t -> {
- t.hide(mRootLeash);
- mDimLayer =
- SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession);
- });
- } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
- final int taskId = taskInfo.taskId;
- mChildrenLeashes.put(taskId, leash);
- mChildrenTaskInfo.put(taskId, taskInfo);
- updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
- mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, taskInfo.isVisible);
- if (ENABLE_SHELL_TRANSITIONS) {
- // Status is managed/synchronized by the transition lifecycle.
- return;
- }
- sendStatusChanged();
- } else {
- throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
- + "\n mRootTaskInfo: " + mRootTaskInfo);
- }
-
- if (mStageTaskUnfoldController != null) {
- mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash);
- }
- }
-
- @Override
- @CallSuper
- public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- if (!taskInfo.supportsMultiWindow) {
- // Leave split screen if the task no longer supports multi window.
- mCallbacks.onNoLongerSupportMultiWindow();
- return;
- }
- if (mRootTaskInfo.taskId == taskInfo.taskId) {
- mRootTaskInfo = taskInfo;
- } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
- mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
- mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */,
- taskInfo.isVisible);
- if (!ENABLE_SHELL_TRANSITIONS) {
- updateChildTaskSurface(
- taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */);
- }
- } else {
- throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
- + "\n mRootTaskInfo: " + mRootTaskInfo);
- }
- if (ENABLE_SHELL_TRANSITIONS) {
- // Status is managed/synchronized by the transition lifecycle.
- return;
- }
- sendStatusChanged();
- }
-
- @Override
- @CallSuper
- public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- final int taskId = taskInfo.taskId;
- if (mRootTaskInfo.taskId == taskId) {
- mCallbacks.onRootTaskVanished();
- mSyncQueue.runInSync(t -> t.remove(mDimLayer));
- mRootTaskInfo = null;
- } else if (mChildrenTaskInfo.contains(taskId)) {
- mChildrenTaskInfo.remove(taskId);
- mChildrenLeashes.remove(taskId);
- mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible);
- if (ENABLE_SHELL_TRANSITIONS) {
- // Status is managed/synchronized by the transition lifecycle.
- return;
- }
- sendStatusChanged();
- } else {
- throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
- + "\n mRootTaskInfo: " + mRootTaskInfo);
- }
-
- if (mStageTaskUnfoldController != null) {
- mStageTaskUnfoldController.onTaskVanished(taskInfo);
- }
- }
-
- @Override
- public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
- b.setParent(findTaskSurface(taskId));
- }
-
- @Override
- public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc,
- SurfaceControl.Transaction t) {
- t.reparent(sc, findTaskSurface(taskId));
- }
-
- private SurfaceControl findTaskSurface(int taskId) {
- if (mRootTaskInfo.taskId == taskId) {
- return mRootLeash;
- } else if (mChildrenLeashes.contains(taskId)) {
- return mChildrenLeashes.get(taskId);
- } else {
- throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
- }
- }
-
- void setBounds(Rect bounds, WindowContainerTransaction wct) {
- wct.setBounds(mRootTaskInfo.token, bounds);
- }
-
- void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) {
- if (!containsTask(taskId)) {
- return;
- }
- wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */);
- }
-
- void setVisibility(boolean visible, WindowContainerTransaction wct) {
- wct.reorder(mRootTaskInfo.token, visible /* onTop */);
- }
-
- void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener,
- @SplitScreen.StageType int stage) {
- for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
- int taskId = mChildrenTaskInfo.keyAt(i);
- listener.onTaskStageChanged(taskId, stage,
- mChildrenTaskInfo.get(taskId).isVisible);
- }
- }
-
- private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo,
- SurfaceControl leash, boolean firstAppeared) {
- final Point taskPositionInParent = taskInfo.positionInParent;
- mSyncQueue.runInSync(t -> {
- t.setWindowCrop(leash, null);
- t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
- if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) {
- t.setAlpha(leash, 1f);
- t.setMatrix(leash, 1, 0, 0, 1);
- t.show(leash);
- }
- });
- }
-
- private void sendStatusChanged() {
- mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0);
- }
-
- @Override
- @CallSuper
- public void dump(@NonNull PrintWriter pw, String prefix) {
- final String innerPrefix = prefix + " ";
- final String childPrefix = innerPrefix + " ";
- pw.println(prefix + this);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java
deleted file mode 100644
index 62b9da6d4715..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.stagesplit;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import android.animation.RectEvaluator;
-import android.animation.TypeEvaluator;
-import android.annotation.NonNull;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.SparseArray;
-import android.view.InsetsSource;
-import android.view.InsetsState;
-import android.view.SurfaceControl;
-
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
-import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
-import com.android.wm.shell.unfold.UnfoldBackgroundController;
-
-import java.util.concurrent.Executor;
-
-/**
- * Controls transformations of the split screen task surfaces in response
- * to the unfolding/folding action on foldable devices
- */
-public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener {
-
- private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
- private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
-
- private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
- private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
- private final DisplayInsetsController mDisplayInsetsController;
- private final UnfoldBackgroundController mBackgroundController;
- private final Executor mExecutor;
- private final int mExpandedTaskBarHeight;
- private final float mWindowCornerRadiusPx;
- private final Rect mStageBounds = new Rect();
- private final TransactionPool mTransactionPool;
-
- private InsetsSource mTaskbarInsetsSource;
- private boolean mBothStagesVisible;
-
- public StageTaskUnfoldController(@NonNull Context context,
- @NonNull TransactionPool transactionPool,
- @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
- @NonNull DisplayInsetsController displayInsetsController,
- @NonNull UnfoldBackgroundController backgroundController,
- @NonNull Executor executor) {
- mUnfoldProgressProvider = unfoldProgressProvider;
- mTransactionPool = transactionPool;
- mExecutor = executor;
- mBackgroundController = backgroundController;
- mDisplayInsetsController = displayInsetsController;
- mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.taskbar_frame_height);
- }
-
- /**
- * Initializes the controller, starts listening for the external events
- */
- public void init() {
- mUnfoldProgressProvider.addListener(mExecutor, this);
- mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
- }
-
- @Override
- public void insetsChanged(InsetsState insetsState) {
- mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
- for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
- AnimationContext context = mAnimationContextByTaskId.valueAt(i);
- context.update();
- }
- }
-
- /**
- * Called when split screen task appeared
- * @param taskInfo info for the appeared task
- * @param leash surface leash for the appeared task
- */
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- AnimationContext context = new AnimationContext(leash);
- mAnimationContextByTaskId.put(taskInfo.taskId, context);
- }
-
- /**
- * Called when a split screen task vanished
- * @param taskInfo info for the vanished task
- */
- public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
- if (context != null) {
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- resetSurface(transaction, context);
- transaction.apply();
- mTransactionPool.release(transaction);
- }
- mAnimationContextByTaskId.remove(taskInfo.taskId);
- }
-
- @Override
- public void onStateChangeProgress(float progress) {
- if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return;
-
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- mBackgroundController.ensureBackground(transaction);
-
- for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
- AnimationContext context = mAnimationContextByTaskId.valueAt(i);
-
- context.mCurrentCropRect.set(RECT_EVALUATOR
- .evaluate(progress, context.mStartCropRect, context.mEndCropRect));
-
- transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
- .setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
- }
-
- transaction.apply();
-
- mTransactionPool.release(transaction);
- }
-
- @Override
- public void onStateChangeFinished() {
- resetTransformations();
- }
-
- /**
- * Called when split screen visibility changes
- * @param bothStagesVisible true if both stages of the split screen are visible
- */
- public void onSplitVisibilityChanged(boolean bothStagesVisible) {
- mBothStagesVisible = bothStagesVisible;
- if (!bothStagesVisible) {
- resetTransformations();
- }
- }
-
- /**
- * Called when split screen stage bounds changed
- * @param bounds new bounds for this stage
- */
- public void onLayoutChanged(Rect bounds) {
- mStageBounds.set(bounds);
-
- for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
- final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
- context.update();
- }
- }
-
- private void resetTransformations() {
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
-
- for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
- final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
- resetSurface(transaction, context);
- }
- mBackgroundController.removeBackground(transaction);
- transaction.apply();
-
- mTransactionPool.release(transaction);
- }
-
- private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) {
- transaction
- .setWindowCrop(context.mLeash, null)
- .setCornerRadius(context.mLeash, 0.0F);
- }
-
- private class AnimationContext {
- final SurfaceControl mLeash;
- final Rect mStartCropRect = new Rect();
- final Rect mEndCropRect = new Rect();
- final Rect mCurrentCropRect = new Rect();
-
- private AnimationContext(SurfaceControl leash) {
- this.mLeash = leash;
- update();
- }
-
- private void update() {
- mStartCropRect.set(mStageBounds);
-
- if (mTaskbarInsetsSource != null) {
- // Only insets the cropping window with taskbar when taskbar is expanded
- if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
- mStartCropRect.inset(mTaskbarInsetsSource
- .calculateVisibleInsets(mStartCropRect));
- }
- }
-
- // Offset to surface coordinates as layout bounds are in screen coordinates
- mStartCropRect.offsetTo(0, 0);
-
- mEndCropRect.set(mStartCropRect);
-
- int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height());
- int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION);
- mStartCropRect.inset(margin, margin, margin, margin);
- }
- }
-}
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 54d62edf2570..a0e176c7ea68 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
@@ -261,7 +261,8 @@ public class StartingSurfaceDrawer {
WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
params.setFitInsetsSides(0);
params.setFitInsetsTypes(0);
- params.format = PixelFormat.TRANSLUCENT;
+ params.format = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
+ ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT;
int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
index fbc992378e50..379af21ac956 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
@@ -42,10 +42,12 @@ import androidx.annotation.BinderThread;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.function.TriConsumer;
import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.ShellTaskOrganizer;
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.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
/**
* Implementation to draw the starting window to an application, and remove the starting window
@@ -74,6 +76,7 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo
private TriConsumer<Integer, Integer, Integer> mTaskLaunchingCallback;
private final StartingSurfaceImpl mImpl = new StartingSurfaceImpl();
private final Context mContext;
+ private final ShellTaskOrganizer mShellTaskOrganizer;
private final ShellExecutor mSplashScreenExecutor;
/**
* Need guarded because it has exposed to StartingSurface
@@ -81,14 +84,20 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo
@GuardedBy("mTaskBackgroundColors")
private final SparseIntArray mTaskBackgroundColors = new SparseIntArray();
- public StartingWindowController(Context context, ShellExecutor splashScreenExecutor,
- StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider,
+ public StartingWindowController(Context context,
+ ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
+ ShellExecutor splashScreenExecutor,
+ StartingWindowTypeAlgorithm startingWindowTypeAlgorithm,
+ IconProvider iconProvider,
TransactionPool pool) {
mContext = context;
+ mShellTaskOrganizer = shellTaskOrganizer;
mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor,
iconProvider, pool);
mStartingWindowTypeAlgorithm = startingWindowTypeAlgorithm;
mSplashScreenExecutor = splashScreenExecutor;
+ shellInit.addInitCallback(this::onInit, this);
}
/**
@@ -98,6 +107,10 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo
return mImpl;
}
+ private void onInit() {
+ mShellTaskOrganizer.initStartingWindow(this);
+ }
+
@Override
public Context getContext() {
return mContext;
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 95bc579a4a51..7b498e4f54ec 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
@@ -20,10 +20,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.graphics.Color.WHITE;
import static android.graphics.Color.alpha;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
-import static android.view.ViewRootImpl.LOCAL_LAYOUT;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
-import static android.view.WindowLayout.UNSPECIFIED_LENGTH;
import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
@@ -53,7 +51,6 @@ import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManager.TaskDescription;
import android.app.ActivityThread;
-import android.app.WindowConfiguration;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -80,7 +77,6 @@ import android.view.SurfaceSession;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
-import android.view.WindowLayout;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.window.ClientWindowFrames;
@@ -212,8 +208,6 @@ public class TaskSnapshotWindow {
final IWindowSession session = WindowManagerGlobal.getWindowSession();
final SurfaceControl surfaceControl = new SurfaceControl();
final ClientWindowFrames tmpFrames = new ClientWindowFrames();
- final WindowLayout windowLayout = new WindowLayout();
- final Rect displayCutoutSafe = new Rect();
final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0];
final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration();
@@ -234,11 +228,13 @@ public class TaskSnapshotWindow {
final InsetsState tmpInsetsState = new InsetsState();
final InputChannel tmpInputChannel = new InputChannel();
+ final float[] sizeCompatScale = { 1f };
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay");
final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId,
- info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls);
+ info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls,
+ new Rect(), sizeCompatScale);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (res < 0) {
Slog.w(TAG, "Failed to add snapshot starting window res=" + res);
@@ -250,25 +246,9 @@ public class TaskSnapshotWindow {
window.setOuter(snapshotSurface);
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout");
- if (LOCAL_LAYOUT) {
- if (!surfaceControl.isValid()) {
- session.updateVisibility(window, layoutParams, View.VISIBLE,
- tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls);
- }
- tmpInsetsState.getDisplayCutoutSafe(displayCutoutSafe);
- final WindowConfiguration winConfig =
- tmpMergedConfiguration.getMergedConfiguration().windowConfiguration;
- windowLayout.computeFrames(layoutParams, tmpInsetsState, displayCutoutSafe,
- winConfig.getBounds(), winConfig.getWindowingMode(), UNSPECIFIED_LENGTH,
- UNSPECIFIED_LENGTH, info.requestedVisibilities,
- null /* attachedWindowFrame */, 1f /* compatScale */, tmpFrames);
- session.updateLayout(window, layoutParams, 0 /* flags */, tmpFrames,
- UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH);
- } else {
- session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0,
- tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState,
- tmpControls, new Bundle());
- }
+ session.relayout(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();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ConfigurationChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ConfigurationChangeListener.java
new file mode 100644
index 000000000000..2fca8f0ecc76
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ConfigurationChangeListener.java
@@ -0,0 +1,51 @@
+/*
+ * 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.sysui;
+
+import android.content.res.Configuration;
+
+/**
+ * Callbacks for when the configuration changes.
+ */
+public interface ConfigurationChangeListener {
+
+ /**
+ * Called when a configuration changes. This precedes all the following callbacks.
+ */
+ default void onConfigurationChanged(Configuration newConfiguration) {}
+
+ /**
+ * Convenience method to the above, called when the density or font scale changes.
+ */
+ default void onDensityOrFontScaleChanged() {}
+
+ /**
+ * Convenience method to the above, called when the smallest screen width changes.
+ */
+ default void onSmallestScreenWidthChanged() {}
+
+ /**
+ * Convenience method to the above, called when the system theme changes, including dark/light
+ * UI_MODE changes.
+ */
+ default void onThemeChanged() {}
+
+ /**
+ * Convenience method to the above, called when the local list or layout direction changes.
+ */
+ default void onLocaleOrLayoutDirectionChanged() {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
new file mode 100644
index 000000000000..9df863163b50
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
@@ -0,0 +1,36 @@
+/*
+ * 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.sysui;
+
+/**
+ * Callbacks for when the keyguard changes.
+ */
+public interface KeyguardChangeListener {
+ /**
+ * Called when the keyguard is showing (and if so, whether it is occluded).
+ */
+ default void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
+ boolean animatingDismiss) {}
+
+ /**
+ * Called when the keyguard dismiss animation has finished.
+ *
+ * TODO(b/206741900) deprecate this path once we're able to animate the PiP window as part of
+ * keyguard dismiss animation.
+ */
+ default void onKeyguardDismissAnimationFinished() {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java
new file mode 100644
index 000000000000..2e6ddc363906
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.sysui;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT;
+
+import com.android.internal.protolog.common.ProtoLog;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.TreeMap;
+import java.util.function.BiConsumer;
+
+/**
+ * An entry point into the shell for dumping shell internal state and running adb commands.
+ *
+ * Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}.
+ */
+public final class ShellCommandHandler {
+ private static final String TAG = ShellCommandHandler.class.getSimpleName();
+
+ // We're using a TreeMap to keep them sorted by command name
+ private final TreeMap<String, BiConsumer<PrintWriter, String>> mDumpables = new TreeMap<>();
+ private final TreeMap<String, ShellCommandActionHandler> mCommands = new TreeMap<>();
+
+ public interface ShellCommandActionHandler {
+ /**
+ * Handles the given command.
+ *
+ * @param args the arguments starting with the action name, then the action arguments
+ * @param pw the write to print output to
+ */
+ boolean onShellCommand(String[] args, PrintWriter pw);
+
+ /**
+ * Prints the help for this class of commands. Implementations do not need to print the
+ * command class.
+ */
+ void printShellCommandHelp(PrintWriter pw, String prefix);
+ }
+
+
+ /**
+ * Adds a callback to run when the Shell is being dumped.
+ *
+ * @param callback the callback to be made when Shell is dumped, takes the print writer and
+ * a prefix
+ * @param instance used for debugging only
+ */
+ public <T> void addDumpCallback(BiConsumer<PrintWriter, String> callback, T instance) {
+ mDumpables.put(instance.getClass().getSimpleName(), callback);
+ ProtoLog.v(WM_SHELL_INIT, "Adding dump callback for %s",
+ instance.getClass().getSimpleName());
+ }
+
+ /**
+ * Adds an action callback to be invoked when the user runs that particular command from adb.
+ *
+ * @param commandClass the top level class of command to invoke
+ * @param actions the interface to callback when an action of this class is invoked
+ * @param instance used for debugging only
+ */
+ public <T> void addCommandCallback(String commandClass, ShellCommandActionHandler actions,
+ T instance) {
+ mCommands.put(commandClass, actions);
+ ProtoLog.v(WM_SHELL_INIT, "Adding command class %s for %s", commandClass,
+ instance.getClass().getSimpleName());
+ }
+
+ /** Dumps WM Shell internal state. */
+ public void dump(PrintWriter pw) {
+ for (String key : mDumpables.keySet()) {
+ final BiConsumer<PrintWriter, String> r = mDumpables.get(key);
+ r.accept(pw, "");
+ pw.println();
+ }
+ }
+
+
+ /** Returns {@code true} if command was found and executed. */
+ public boolean handleCommand(final String[] args, PrintWriter pw) {
+ if (args.length < 2) {
+ // Argument at position 0 is "WMShell".
+ return false;
+ }
+
+ final String cmdClass = args[1];
+ if (cmdClass.toLowerCase().equals("help")) {
+ return runHelp(pw);
+ }
+ if (!mCommands.containsKey(cmdClass)) {
+ return false;
+ }
+
+ // Only pass the actions onwards as arguments to the callback
+ final ShellCommandActionHandler actions = mCommands.get(args[1]);
+ final String[] cmdClassArgs = Arrays.copyOfRange(args, 2, args.length);
+ actions.onShellCommand(cmdClassArgs, pw);
+ return true;
+ }
+
+ private boolean runHelp(PrintWriter pw) {
+ pw.println("Window Manager Shell commands:");
+ for (String commandClass : mCommands.keySet()) {
+ pw.println(" " + commandClass);
+ mCommands.get(commandClass).printShellCommandHelp(pw, " ");
+ }
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" <no arguments provided>");
+ pw.println(" Dump all Window Manager Shell internal state");
+ return true;
+ }
+}
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
new file mode 100644
index 000000000000..57993948886b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -0,0 +1,278 @@
+/*
+ * 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.sysui;
+
+import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS;
+import static android.content.pm.ActivityInfo.CONFIG_FONT_SCALE;
+import static android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION;
+import static android.content.pm.ActivityInfo.CONFIG_LOCALE;
+import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
+import static android.content.pm.ActivityInfo.CONFIG_UI_MODE;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SYSUI_EVENTS;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ExternalThread;
+
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Handles event callbacks from SysUI that can be used within the Shell.
+ */
+public class ShellController {
+ private static final String TAG = ShellController.class.getSimpleName();
+
+ private final ShellInit mShellInit;
+ private final ShellCommandHandler mShellCommandHandler;
+ private final ShellExecutor mMainExecutor;
+ private final ShellInterfaceImpl mImpl = new ShellInterfaceImpl();
+
+ private final CopyOnWriteArrayList<ConfigurationChangeListener> mConfigChangeListeners =
+ new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<KeyguardChangeListener> mKeyguardChangeListeners =
+ new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners =
+ new CopyOnWriteArrayList<>();
+
+ private Configuration mLastConfiguration;
+
+
+ public ShellController(ShellInit shellInit, ShellCommandHandler shellCommandHandler,
+ ShellExecutor mainExecutor) {
+ mShellInit = shellInit;
+ mShellCommandHandler = shellCommandHandler;
+ mMainExecutor = mainExecutor;
+ }
+
+ /**
+ * Returns the external interface to this controller.
+ */
+ public ShellInterface asShell() {
+ return mImpl;
+ }
+
+ /**
+ * Adds a new configuration listener. The configuration change callbacks are not made in any
+ * particular order.
+ */
+ public void addConfigurationChangeListener(ConfigurationChangeListener listener) {
+ mConfigChangeListeners.remove(listener);
+ mConfigChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes an existing configuration listener.
+ */
+ public void removeConfigurationChangeListener(ConfigurationChangeListener listener) {
+ mConfigChangeListeners.remove(listener);
+ }
+
+ /**
+ * Adds a new Keyguard listener. The Keyguard change callbacks are not made in any
+ * particular order.
+ */
+ public void addKeyguardChangeListener(KeyguardChangeListener listener) {
+ mKeyguardChangeListeners.remove(listener);
+ mKeyguardChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes an existing Keyguard listener.
+ */
+ public void removeKeyguardChangeListener(KeyguardChangeListener listener) {
+ mKeyguardChangeListeners.remove(listener);
+ }
+
+ /**
+ * Adds a new user-change listener. The user change callbacks are not made in any
+ * particular order.
+ */
+ public void addUserChangeListener(UserChangeListener listener) {
+ mUserChangeListeners.remove(listener);
+ mUserChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes an existing user-change listener.
+ */
+ public void removeUserChangeListener(UserChangeListener listener) {
+ mUserChangeListeners.remove(listener);
+ }
+
+ @VisibleForTesting
+ void onConfigurationChanged(Configuration newConfig) {
+ // The initial config is send on startup and doesn't trigger listener callbacks
+ if (mLastConfiguration == null) {
+ mLastConfiguration = new Configuration(newConfig);
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Initial Configuration: %s", newConfig);
+ return;
+ }
+
+ final int diff = newConfig.diff(mLastConfiguration);
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "New configuration change: %s", newConfig);
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "\tchanges=%s",
+ Configuration.configurationDiffToString(diff));
+ final boolean densityFontScaleChanged = (diff & CONFIG_FONT_SCALE) != 0
+ || (diff & ActivityInfo.CONFIG_DENSITY) != 0;
+ final boolean smallestScreenWidthChanged = (diff & CONFIG_SMALLEST_SCREEN_SIZE) != 0;
+ final boolean themeChanged = (diff & CONFIG_ASSETS_PATHS) != 0
+ || (diff & CONFIG_UI_MODE) != 0;
+ final boolean localOrLayoutDirectionChanged = (diff & CONFIG_LOCALE) != 0
+ || (diff & CONFIG_LAYOUT_DIRECTION) != 0;
+
+ // Update the last configuration and call listeners
+ mLastConfiguration.updateFrom(newConfig);
+ for (ConfigurationChangeListener listener : mConfigChangeListeners) {
+ listener.onConfigurationChanged(newConfig);
+ if (densityFontScaleChanged) {
+ listener.onDensityOrFontScaleChanged();
+ }
+ if (smallestScreenWidthChanged) {
+ listener.onSmallestScreenWidthChanged();
+ }
+ if (themeChanged) {
+ listener.onThemeChanged();
+ }
+ if (localOrLayoutDirectionChanged) {
+ listener.onLocaleOrLayoutDirectionChanged();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard visibility changed: visible=%b "
+ + "occluded=%b animatingDismiss=%b", visible, occluded, animatingDismiss);
+ for (KeyguardChangeListener listener : mKeyguardChangeListeners) {
+ listener.onKeyguardVisibilityChanged(visible, occluded, animatingDismiss);
+ }
+ }
+
+ @VisibleForTesting
+ void onKeyguardDismissAnimationFinished() {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard dismiss animation finished");
+ for (KeyguardChangeListener listener : mKeyguardChangeListeners) {
+ listener.onKeyguardDismissAnimationFinished();
+ }
+ }
+
+ @VisibleForTesting
+ void onUserChanged(int newUserId, @NonNull Context userContext) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User changed: id=%d", newUserId);
+ for (UserChangeListener listener : mUserChangeListeners) {
+ listener.onUserChanged(newUserId, userContext);
+ }
+ }
+
+ @VisibleForTesting
+ void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User profiles changed");
+ for (UserChangeListener listener : mUserChangeListeners) {
+ listener.onUserProfilesChanged(profiles);
+ }
+ }
+
+ public void dump(@NonNull PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mConfigChangeListeners=" + mConfigChangeListeners.size());
+ pw.println(innerPrefix + "mLastConfiguration=" + mLastConfiguration);
+ pw.println(innerPrefix + "mKeyguardChangeListeners=" + mKeyguardChangeListeners.size());
+ pw.println(innerPrefix + "mUserChangeListeners=" + mUserChangeListeners.size());
+ }
+
+ /**
+ * The interface for calls from outside the Shell, within the host process.
+ */
+ @ExternalThread
+ private class ShellInterfaceImpl implements ShellInterface {
+
+ @Override
+ public void onInit() {
+ try {
+ mMainExecutor.executeBlocking(() -> mShellInit.init());
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to initialize the Shell in 2s", e);
+ }
+ }
+
+ @Override
+ public void dump(PrintWriter pw) {
+ try {
+ mMainExecutor.executeBlocking(() -> mShellCommandHandler.dump(pw));
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to dump the Shell in 2s", e);
+ }
+ }
+
+ @Override
+ public boolean handleCommand(String[] args, PrintWriter pw) {
+ try {
+ boolean[] result = new boolean[1];
+ mMainExecutor.executeBlocking(() -> {
+ result[0] = mShellCommandHandler.handleCommand(args, pw);
+ });
+ return result[0];
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to handle Shell command in 2s", e);
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfiguration) {
+ mMainExecutor.execute(() ->
+ ShellController.this.onConfigurationChanged(newConfiguration));
+ }
+
+ @Override
+ public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
+ boolean animatingDismiss) {
+ mMainExecutor.execute(() ->
+ ShellController.this.onKeyguardVisibilityChanged(visible, occluded,
+ animatingDismiss));
+ }
+
+ @Override
+ public void onKeyguardDismissAnimationFinished() {
+ mMainExecutor.execute(() ->
+ ShellController.this.onKeyguardDismissAnimationFinished());
+ }
+
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ mMainExecutor.execute(() ->
+ ShellController.this.onUserChanged(newUserId, userContext));
+ }
+
+ @Override
+ public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+ mMainExecutor.execute(() ->
+ ShellController.this.onUserProfilesChanged(profiles));
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
new file mode 100644
index 000000000000..ac52235375c4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.sysui;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT;
+
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Pair;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+
+import java.util.ArrayList;
+
+/**
+ * The entry point implementation into the shell for initializing shell internal state. Classes
+ * which need to initialize on start of the host SysUI should inject an instance of this class and
+ * add an init callback.
+ */
+public class ShellInit {
+ private static final String TAG = ShellInit.class.getSimpleName();
+
+ private final ShellExecutor mMainExecutor;
+
+ // An ordered list of init callbacks to be made once shell is first started
+ private final ArrayList<Pair<String, Runnable>> mInitCallbacks = new ArrayList<>();
+ private boolean mHasInitialized;
+
+
+ public ShellInit(ShellExecutor mainExecutor) {
+ mMainExecutor = mainExecutor;
+ }
+
+ /**
+ * Adds a callback to the ordered list of callbacks be made when Shell is first started. This
+ * can be used in class constructors when dagger is used to ensure that the initialization order
+ * matches the dependency order.
+ *
+ * @param r the callback to be made when Shell is initialized
+ * @param instance used for debugging only
+ */
+ public <T extends Object> void addInitCallback(Runnable r, T instance) {
+ if (mHasInitialized) {
+ if (Build.isDebuggable()) {
+ // All callbacks must be added prior to the Shell being initialized
+ throw new IllegalArgumentException("Can not add callback after init");
+ }
+ return;
+ }
+ final String className = instance.getClass().getSimpleName();
+ mInitCallbacks.add(new Pair<>(className, r));
+ ProtoLog.v(WM_SHELL_INIT, "Adding init callback for %s", className);
+ }
+
+ /**
+ * Calls all the init callbacks when the Shell is first starting.
+ */
+ @VisibleForTesting
+ public void init() {
+ ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size());
+ // Init in order of registration
+ for (int i = 0; i < mInitCallbacks.size(); i++) {
+ final Pair<String, Runnable> info = mInitCallbacks.get(i);
+ final long t1 = SystemClock.uptimeMillis();
+ info.second.run();
+ final long t2 = SystemClock.uptimeMillis();
+ ProtoLog.v(WM_SHELL_INIT, "\t%s init took %dms", info.first, (t2 - t1));
+ }
+ mInitCallbacks.clear();
+ mHasInitialized = true;
+ }
+}
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
new file mode 100644
index 000000000000..2108c824ac6f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
@@ -0,0 +1,77 @@
+/*
+ * 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.sysui;
+
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+
+import androidx.annotation.NonNull;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * General interface for notifying the Shell of common SysUI events like configuration or keyguard
+ * changes.
+ */
+public interface ShellInterface {
+
+ /**
+ * Initializes the shell state.
+ */
+ default void onInit() {}
+
+ /**
+ * Dumps the shell state.
+ */
+ default void dump(PrintWriter pw) {}
+
+ /**
+ * Handles a shell command.
+ */
+ default boolean handleCommand(final String[] args, PrintWriter pw) {
+ return false;
+ }
+
+ /**
+ * Notifies the Shell that the configuration has changed.
+ */
+ default void onConfigurationChanged(Configuration newConfiguration) {}
+
+ /**
+ * Notifies the Shell that the keyguard is showing (and if so, whether it is occluded) or not
+ * showing, and whether it is animating a dismiss.
+ */
+ default void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
+ boolean animatingDismiss) {}
+
+ /**
+ * Notifies the Shell when the keyguard dismiss animation has finished.
+ */
+ default void onKeyguardDismissAnimationFinished() {}
+
+ /**
+ * Notifies the Shell when the user changes.
+ */
+ default void onUserChanged(int newUserId, @NonNull Context userContext) {}
+
+ /**
+ * Notifies the Shell when a profile belonging to the user changes.
+ */
+ default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java
index 60123ab97fd7..3d0909f6128d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,23 +14,26 @@
* limitations under the License.
*/
-package com.android.wm.shell.hidedisplaycutout;
+package com.android.wm.shell.sysui;
-import android.content.res.Configuration;
+import android.content.Context;
+import android.content.pm.UserInfo;
import androidx.annotation.NonNull;
-import com.android.wm.shell.common.annotations.ExternalThread;
-
-import java.io.PrintWriter;
+import java.util.List;
/**
- * Interface to engage hide display cutout feature.
+ * Callbacks for when the user or user's profiles changes.
*/
-@ExternalThread
-public interface HideDisplayCutout {
+public interface UserChangeListener {
+ /**
+ * Called when the current (parent) user changes.
+ */
+ default void onUserChanged(int newUserId, @NonNull Context userContext) {}
+
/**
- * Notifies {@link Configuration} changed.
+ * Called when a profile belonging to the user changes.
*/
- void onConfigurationChanged(Configuration newConfig);
+ default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelper.java
deleted file mode 100644
index ad9dda619370..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelper.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.tasksurfacehelper;
-
-import android.app.ActivityManager.RunningTaskInfo;
-import android.graphics.Rect;
-import android.view.SurfaceControl;
-
-import java.util.concurrent.Executor;
-import java.util.function.Consumer;
-
-/**
- * Interface to communicate with a Task's SurfaceControl.
- */
-public interface TaskSurfaceHelper {
-
- /** Sets the METADATA_GAME_MODE for the layer corresponding to the task **/
- default void setGameModeForTask(int taskId, int gameMode) {}
-
- /** Takes a screenshot for a task **/
- default void screenshotTask(RunningTaskInfo taskInfo, Rect crop, Executor executor,
- Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) {}
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperController.java
deleted file mode 100644
index 064d9d1231c1..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperController.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.tasksurfacehelper;
-
-import android.app.ActivityManager.RunningTaskInfo;
-import android.graphics.Rect;
-import android.view.SurfaceControl;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.ShellExecutor;
-
-import java.util.concurrent.Executor;
-import java.util.function.Consumer;
-
-/**
- * Intermediary controller that communicates with {@link ShellTaskOrganizer} to send commands
- * to SurfaceControl.
- */
-public class TaskSurfaceHelperController {
-
- private final ShellTaskOrganizer mTaskOrganizer;
- private final ShellExecutor mMainExecutor;
- private final TaskSurfaceHelperImpl mImpl = new TaskSurfaceHelperImpl();
-
- public TaskSurfaceHelperController(ShellTaskOrganizer taskOrganizer,
- ShellExecutor mainExecutor) {
- mTaskOrganizer = taskOrganizer;
- mMainExecutor = mainExecutor;
- }
-
- public TaskSurfaceHelper asTaskSurfaceHelper() {
- return mImpl;
- }
-
- /**
- * Sends a Transaction to set the game mode metadata on the
- * corresponding SurfaceControl
- */
- public void setGameModeForTask(int taskId, int gameMode) {
- mTaskOrganizer.setSurfaceMetadata(taskId, SurfaceControl.METADATA_GAME_MODE, gameMode);
- }
-
- /**
- * Take screenshot of the specified task.
- */
- public void screenshotTask(RunningTaskInfo taskInfo, Rect crop,
- Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) {
- mTaskOrganizer.screenshotTask(taskInfo, crop, consumer);
- }
-
- private class TaskSurfaceHelperImpl implements TaskSurfaceHelper {
- @Override
- public void setGameModeForTask(int taskId, int gameMode) {
- mMainExecutor.execute(() -> {
- TaskSurfaceHelperController.this.setGameModeForTask(taskId, gameMode);
- });
- }
-
- @Override
- public void screenshotTask(RunningTaskInfo taskInfo, Rect crop, Executor executor,
- Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) {
- mMainExecutor.execute(() -> {
- TaskSurfaceHelperController.this.screenshotTask(taskInfo, crop,
- (t) -> executor.execute(() -> consumer.accept(t)));
- });
- }
- }
-}
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
new file mode 100644
index 000000000000..3cba92956f95
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -0,0 +1,398 @@
+/*
+ * 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.transition;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+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.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.phone.PipTouchHandler;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+import com.android.wm.shell.sysui.ShellInit;
+
+import java.util.ArrayList;
+import java.util.Optional;
+
+/**
+ * A handler for dealing with transitions involving multiple other handlers. For example: an
+ * activity in split-screen going into PiP.
+ */
+public class DefaultMixedHandler implements Transitions.TransitionHandler {
+
+ private final Transitions mPlayer;
+ private PipTransitionController mPipHandler;
+ private StageCoordinator mSplitHandler;
+
+ private static class MixedTransition {
+ static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
+
+ /** Both the display and split-state (enter/exit) is changing */
+ static final int TYPE_DISPLAY_AND_SPLIT_CHANGE = 2;
+
+ /** The default animation for this mixed transition. */
+ static final int ANIM_TYPE_DEFAULT = 0;
+
+ /** For ENTER_PIP_FROM_SPLIT, indicates that this is a to-home animation. */
+ static final int ANIM_TYPE_GOING_HOME = 1;
+
+ final int mType;
+ int mAnimType = 0;
+ final IBinder mTransition;
+
+ Transitions.TransitionFinishCallback mFinishCallback = null;
+ Transitions.TransitionHandler mLeftoversHandler = null;
+ WindowContainerTransaction mFinishWCT = null;
+
+ /**
+ * Mixed transitions are made up of multiple "parts". This keeps track of how many
+ * parts are currently animating.
+ */
+ int mInFlightSubAnimations = 0;
+
+ MixedTransition(int type, IBinder transition) {
+ mType = type;
+ mTransition = transition;
+ }
+ }
+
+ private final ArrayList<MixedTransition> mActiveTransitions = new ArrayList<>();
+
+ public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player,
+ Optional<SplitScreenController> splitScreenControllerOptional,
+ Optional<PipTouchHandler> pipTouchHandlerOptional) {
+ mPlayer = player;
+ if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent()
+ && splitScreenControllerOptional.isPresent()) {
+ // Add after dependencies because it is higher priority
+ shellInit.addInitCallback(() -> {
+ mPipHandler = pipTouchHandlerOptional.get().getTransitionHandler();
+ mSplitHandler = splitScreenControllerOptional.get().getTransitionHandler();
+ mPlayer.addHandler(this);
+ if (mSplitHandler != null) {
+ mSplitHandler.setMixedHandler(this);
+ }
+ }, this);
+ }
+ }
+
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitActive()) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a PiP-enter request while "
+ + "Split-Screen is active, so treat it as Mixed.");
+ if (request.getRemoteTransition() != null) {
+ throw new IllegalStateException("Unexpected remote transition in"
+ + "pip-enter-from-split request");
+ }
+ mActiveTransitions.add(new MixedTransition(MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT,
+ transition));
+
+ WindowContainerTransaction out = new WindowContainerTransaction();
+ mPipHandler.augmentRequest(transition, request, out);
+ mSplitHandler.addEnterOrExitIfNeeded(request, out);
+ return out;
+ }
+ return null;
+ }
+
+ private TransitionInfo subCopy(@NonNull TransitionInfo info,
+ @WindowManager.TransitionType int newType, boolean withChanges) {
+ final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
+ if (withChanges) {
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ out.getChanges().add(info.getChanges().get(i));
+ }
+ }
+ out.setRootLeash(info.getRootLeash(), info.getRootOffset().x, info.getRootOffset().y);
+ out.setAnimationOptions(info.getAnimationOptions());
+ return out;
+ }
+
+ private boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
+ return change.getTaskInfo() != null
+ && change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME;
+ }
+
+ private boolean isWallpaper(@NonNull TransitionInfo.Change change) {
+ return (change.getFlags() & FLAG_IS_WALLPAPER) != 0;
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ MixedTransition mixed = null;
+ for (int i = mActiveTransitions.size() - 1; i >= 0; --i) {
+ if (mActiveTransitions.get(i).mTransition != transition) continue;
+ mixed = mActiveTransitions.get(i);
+ break;
+ }
+ if (mixed == null) return false;
+
+ if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
+ return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction,
+ finishCallback);
+ } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
+ return false;
+ } else {
+ mActiveTransitions.remove(mixed);
+ throw new IllegalStateException("Starting mixed animation without a known mixed type? "
+ + mixed.mType);
+ }
+ }
+
+ private boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+ + "entering PIP while Split-Screen is active.");
+ TransitionInfo.Change pipChange = null;
+ TransitionInfo.Change wallpaper = null;
+ final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
+ boolean homeIsOpening = false;
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (mPipHandler.isEnteringPip(change, info.getType())) {
+ if (pipChange != null) {
+ throw new IllegalStateException("More than 1 pip-entering changes in one"
+ + " transition? " + info);
+ }
+ pipChange = change;
+ // going backwards, so remove-by-index is fine.
+ everythingElse.getChanges().remove(i);
+ } else if (isHomeOpening(change)) {
+ homeIsOpening = true;
+ } else if (isWallpaper(change)) {
+ wallpaper = change;
+ }
+ }
+ if (pipChange == null) {
+ // um, something probably went wrong.
+ return false;
+ }
+ final boolean isGoingHome = homeIsOpening;
+ mixed.mFinishCallback = finishCallback;
+ Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
+ --mixed.mInFlightSubAnimations;
+ if (mixed.mInFlightSubAnimations > 0) return;
+ mActiveTransitions.remove(mixed);
+ if (isGoingHome) {
+ mSplitHandler.onTransitionAnimationComplete();
+ }
+ mixed.mFinishCallback.onTransitionFinished(wct, wctCB);
+ };
+ if (isGoingHome) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
+ + "since entering-PiP caused us to leave split and return home.");
+ // We need to split the transition into 2 parts: the pip part (animated by pip)
+ // and the dismiss-part (animated by launcher).
+ mixed.mInFlightSubAnimations = 2;
+ // immediately make the wallpaper visible (so that we don't see it pop-in during
+ // the time it takes to start recents animation (which is remote).
+ if (wallpaper != null) {
+ startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f);
+ }
+ // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+ // we need a separate one to send over to launcher.
+ SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+ // Let split update internal state for dismiss.
+ mSplitHandler.prepareDismissAnimation(STAGE_TYPE_UNDEFINED,
+ 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 remove
+ // from transition info.
+ for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) {
+ if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR) != 0) {
+ everythingElse.getChanges().remove(i);
+ break;
+ }
+ }
+
+ mPipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
+ finishCB);
+ // Dispatch the rest of the transition normally. This will most-likely be taken by
+ // recents or default handler.
+ mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, everythingElse,
+ otherStartT, finishTransaction, finishCB, this);
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just "
+ + "forward animation to Pip-Handler.");
+ // This happens if the pip-ing activity is in a multi-activity task (and thus a
+ // new pip task is spawned). In this case, we don't actually exit split so we can
+ // just let pip transition handle the animation verbatim.
+ mixed.mInFlightSubAnimations = 1;
+ mPipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction,
+ finishCB);
+ }
+ return true;
+ }
+
+ private void unlinkMissingParents(TransitionInfo from) {
+ for (int i = 0; i < from.getChanges().size(); ++i) {
+ final TransitionInfo.Change chg = from.getChanges().get(i);
+ if (chg.getParent() == null) continue;
+ if (from.getChange(chg.getParent()) == null) {
+ from.getChanges().get(i).setParent(null);
+ }
+ }
+ }
+
+ private boolean isWithinTask(TransitionInfo info, TransitionInfo.Change chg) {
+ TransitionInfo.Change curr = chg;
+ while (curr != null) {
+ if (curr.getTaskInfo() != null) return true;
+ if (curr.getParent() == null) break;
+ curr = info.getChange(curr.getParent());
+ }
+ return false;
+ }
+
+ /**
+ * This is intended to be called by SplitCoordinator as a helper to mix an already-pending
+ * split transition with a display-change. The use-case for this is when a display
+ * change/rotation gets collected into a split-screen enter/exit transition which has already
+ * been claimed by StageCoordinator.handleRequest . This happens during launcher tests.
+ */
+ public boolean animatePendingSplitWithDisplayChange(@NonNull IBinder transition,
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ final TransitionInfo everythingElse = subCopy(info, info.getType(), true /* withChanges */);
+ final TransitionInfo displayPart = subCopy(info, TRANSIT_CHANGE, false /* withChanges */);
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (isWithinTask(info, change)) continue;
+ displayPart.addChange(change);
+ everythingElse.getChanges().remove(i);
+ }
+ if (displayPart.getChanges().isEmpty()) return false;
+ unlinkMissingParents(everythingElse);
+ final MixedTransition mixed = new MixedTransition(
+ MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition);
+ mixed.mFinishCallback = finishCallback;
+ mActiveTransitions.add(mixed);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change "
+ + "and split change.");
+ // We need to split the transition into 2 parts: the split part and the display part.
+ mixed.mInFlightSubAnimations = 2;
+
+ Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
+ --mixed.mInFlightSubAnimations;
+ if (wctCB != null) {
+ throw new IllegalArgumentException("Can't mix transitions that require finish"
+ + " sync callback");
+ }
+ if (wct != null) {
+ if (mixed.mFinishWCT == null) {
+ mixed.mFinishWCT = wct;
+ } else {
+ mixed.mFinishWCT.merge(wct, true /* transfer */);
+ }
+ }
+ if (mixed.mInFlightSubAnimations > 0) return;
+ mActiveTransitions.remove(mixed);
+ mixed.mFinishCallback.onTransitionFinished(mixed.mFinishWCT, null /* wctCB */);
+ };
+
+ // Dispatch the display change. This will most-likely be taken by the default handler.
+ // Do this first since the first handler used will apply the startT; the display change
+ // needs to take a screenshot before that happens so we need it to be the first handler.
+ mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, displayPart,
+ startT, finishT, finishCB, mSplitHandler);
+
+ // Note: at this point, startT has probably already been applied, so we are basically
+ // giving splitHandler an empty startT. This is currently OK because display-change will
+ // grab a screenshot and paste it on top anyways.
+ mSplitHandler.startPendingAnimation(
+ transition, everythingElse, startT, finishT, finishCB);
+ return true;
+ }
+
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ for (int i = 0; i < mActiveTransitions.size(); ++i) {
+ if (mActiveTransitions.get(i) != mergeTarget) continue;
+ MixedTransition mixed = mActiveTransitions.get(i);
+ if (mixed.mInFlightSubAnimations <= 0) {
+ // Already done, so no need to end it.
+ return;
+ }
+ if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
+ if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) {
+ boolean ended = mSplitHandler.end();
+ // If split couldn't end (because it is remote), then don't end everything else
+ // since we have to play out the animation anyways.
+ if (!ended) return;
+ mPipHandler.end();
+ if (mixed.mLeftoversHandler != null) {
+ mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+ finishCallback);
+ }
+ } else {
+ mPipHandler.end();
+ }
+ } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
+ // queue
+ } else {
+ throw new IllegalStateException("Playing a mixed transition with unknown type? "
+ + mixed.mType);
+ }
+ }
+ }
+
+ @Override
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishT) {
+ MixedTransition mixed = null;
+ for (int i = mActiveTransitions.size() - 1; i >= 0; --i) {
+ if (mActiveTransitions.get(i).mTransition != transition) continue;
+ mixed = mActiveTransitions.remove(i);
+ break;
+ }
+ if (mixed == null) return;
+ if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
+ mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+ }
+ }
+}
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 9154226b7b22..dbb2948de5db 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
@@ -18,7 +18,6 @@ package com.android.wm.shell.transition;
import static android.app.ActivityOptions.ANIM_CLIP_REVEAL;
import static android.app.ActivityOptions.ANIM_CUSTOM;
-import static android.app.ActivityOptions.ANIM_FROM_STYLE;
import static android.app.ActivityOptions.ANIM_NONE;
import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
import static android.app.ActivityOptions.ANIM_SCALE_UP;
@@ -42,7 +41,11 @@ import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_RELAUNCH;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
+import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+import static android.window.TransitionInfo.FLAG_FILLS_TASK;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
@@ -55,6 +58,10 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.sDisableCustomTaskAnimationProperty;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -62,6 +69,7 @@ import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
@@ -69,7 +77,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Canvas;
-import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.PixelFormat;
@@ -79,7 +86,6 @@ import android.graphics.drawable.Drawable;
import android.hardware.HardwareBuffer;
import android.os.Handler;
import android.os.IBinder;
-import android.os.SystemProperties;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.view.Choreographer;
@@ -107,6 +113,7 @@ import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellInit;
import java.util.ArrayList;
import java.util.List;
@@ -116,24 +123,10 @@ import java.util.function.Consumer;
public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private static final int MAX_ANIMATION_DURATION = 3000;
- /**
- * Restrict ability of activities overriding transition animation in a way such that
- * an activity can do it only when the transition happens within a same task.
- *
- * @see android.app.Activity#overridePendingTransition(int, int)
- */
- private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY =
- "persist.wm.disable_custom_task_animation";
-
- /**
- * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY
- */
- static boolean sDisableCustomTaskAnimationProperty =
- SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true);
-
private final TransactionPool mTransactionPool;
private final DisplayController mDisplayController;
private final Context mContext;
+ private final Handler mMainHandler;
private final ShellExecutor mMainExecutor;
private final ShellExecutor mAnimExecutor;
private final TransitionAnimation mTransitionAnimation;
@@ -150,8 +143,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private final int mCurrentUserId;
- private ScreenRotationAnimation mRotationAnimation;
-
private Drawable mEnterpriseThumbnailDrawable;
private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() {
@@ -165,27 +156,33 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
};
- DefaultTransitionHandler(@NonNull DisplayController displayController,
- @NonNull TransactionPool transactionPool, Context context,
+ DefaultTransitionHandler(@NonNull Context context,
+ @NonNull ShellInit shellInit,
+ @NonNull DisplayController displayController,
+ @NonNull TransactionPool transactionPool,
@NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor) {
mDisplayController = displayController;
mTransactionPool = transactionPool;
mContext = context;
+ mMainHandler = mainHandler;
mMainExecutor = mainExecutor;
mAnimExecutor = animExecutor;
mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG);
mCurrentUserId = UserHandle.myUserId();
+ mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
+ shellInit.addInitCallback(this::onInit, this);
+ }
- mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
+ private void onInit() {
updateEnterpriseThumbnailDrawable();
mContext.registerReceiver(
mEnterpriseResourceUpdatedReceiver,
new IntentFilter(ACTION_DEVICE_POLICY_RESOURCE_UPDATED),
/* broadcastPermission = */ null,
- mainHandler);
+ mMainHandler);
- AttributeCache.init(context);
+ AttributeCache.init(mContext);
}
private void updateEnterpriseThumbnailDrawable() {
@@ -195,14 +192,24 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
@VisibleForTesting
- static boolean isRotationSeamless(@NonNull TransitionInfo info,
- DisplayController displayController) {
+ static int getRotationAnimationHint(@NonNull TransitionInfo.Change displayChange,
+ @NonNull TransitionInfo info, @NonNull DisplayController displayController) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Display is changing, check if it should be seamless.");
- boolean checkedDisplayLayout = false;
- boolean hasTask = false;
- boolean displayExplicitSeamless = false;
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ "Display is changing, resolve the animation hint.");
+ // The explicit request of display has the highest priority.
+ if (displayChange.getRotationAnimation() == ROTATION_ANIMATION_SEAMLESS) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ " display requests explicit seamless");
+ return ROTATION_ANIMATION_SEAMLESS;
+ }
+
+ boolean allTasksSeamless = false;
+ boolean rejectSeamless = false;
+ ActivityManager.RunningTaskInfo topTaskInfo = null;
+ int animationHint = ROTATION_ANIMATION_ROTATE;
+ // Traverse in top-to-bottom order so that the first task is top-most.
+ final int size = info.getChanges().size();
+ for (int i = 0; i < size; ++i) {
final TransitionInfo.Change change = info.getChanges().get(i);
// Only look at changing things. showing/hiding don't need to rotate.
@@ -215,95 +222,69 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
if ((change.getFlags() & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
" display has system alert windows, so not seamless.");
- return false;
+ rejectSeamless = true;
}
- displayExplicitSeamless =
- change.getRotationAnimation() == ROTATION_ANIMATION_SEAMLESS;
} else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
" wallpaper is participating but isn't seamless.");
- return false;
+ rejectSeamless = true;
}
} else if (change.getTaskInfo() != null) {
- hasTask = true;
+ final int anim = change.getRotationAnimation();
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ final boolean isTopTask = topTaskInfo == null;
+ if (isTopTask) {
+ topTaskInfo = taskInfo;
+ if (anim != ROTATION_ANIMATION_UNSPECIFIED
+ && anim != ROTATION_ANIMATION_SEAMLESS) {
+ animationHint = anim;
+ }
+ }
// We only enable seamless rotation if all the visible task windows requested it.
- if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) {
+ if (anim != ROTATION_ANIMATION_SEAMLESS) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
" task %s isn't requesting seamless, so not seamless.",
- change.getTaskInfo().taskId);
- return false;
- }
-
- // This is the only way to get display-id currently, so we will check display
- // capabilities here
- if (!checkedDisplayLayout) {
- // only need to check display once.
- checkedDisplayLayout = true;
- final DisplayLayout displayLayout = displayController.getDisplayLayout(
- change.getTaskInfo().displayId);
- // For the upside down rotation we don't rotate seamlessly as the navigation
- // bar moves position. Note most apps (using orientation:sensor or user as
- // opposed to fullSensor) will not enter the reverse portrait orientation, so
- // actually the orientation won't change at all.
- int upsideDownRotation = displayLayout.getUpsideDownRotation();
- if (change.getStartRotation() == upsideDownRotation
- || change.getEndRotation() == upsideDownRotation) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- " rotation involves upside-down portrait, so not seamless.");
- return false;
- }
-
- // If the navigation bar can't change sides, then it will jump when we change
- // orientations and we don't rotate seamlessly - unless that is allowed, eg.
- // with gesture navigation where the navbar is low-profile enough that this
- // isn't very noticeable.
- if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving()
- && (!(displayLayout.navigationBarCanMove()
- && (change.getStartAbsBounds().width()
- != change.getStartAbsBounds().height())))) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- " nav bar changes sides, so not seamless.");
- return false;
- }
+ taskInfo.taskId);
+ allTasksSeamless = false;
+ } else if (isTopTask) {
+ allTasksSeamless = true;
}
}
}
- // ROTATION_ANIMATION_SEAMLESS can only be requested by task or display.
- if (hasTask || displayExplicitSeamless) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless.");
- return true;
+ if (!allTasksSeamless || rejectSeamless) {
+ return animationHint;
}
- return false;
- }
- /**
- * Gets the rotation animation for the topmost task. Assumes that seamless is checked
- * elsewhere, so it will default SEAMLESS to ROTATE.
- */
- private int getRotationAnimation(@NonNull TransitionInfo info) {
- // Traverse in top-to-bottom order so that the first task is top-most
- for (int i = 0; i < info.getChanges().size(); ++i) {
- final TransitionInfo.Change change = info.getChanges().get(i);
-
- // Only look at changing things. showing/hiding don't need to rotate.
- if (change.getMode() != TRANSIT_CHANGE) continue;
-
- // This container isn't rotating, so we can ignore it.
- if (change.getEndRotation() == change.getStartRotation()) continue;
+ // This is the only way to get display-id currently, so check display capabilities here.
+ final DisplayLayout displayLayout = displayController.getDisplayLayout(
+ topTaskInfo.displayId);
+ // For the upside down rotation we don't rotate seamlessly as the navigation bar moves
+ // position. Note most apps (using orientation:sensor or user as opposed to fullSensor)
+ // will not enter the reverse portrait orientation, so actually the orientation won't
+ // change at all.
+ final int upsideDownRotation = displayLayout.getUpsideDownRotation();
+ if (displayChange.getStartRotation() == upsideDownRotation
+ || displayChange.getEndRotation() == upsideDownRotation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ " rotation involves upside-down portrait, so not seamless.");
+ return animationHint;
+ }
- if (change.getTaskInfo() != null) {
- final int anim = change.getRotationAnimation();
- if (anim == ROTATION_ANIMATION_UNSPECIFIED
- // Fallback animation for seamless should also be default.
- || anim == ROTATION_ANIMATION_SEAMLESS) {
- return ROTATION_ANIMATION_ROTATE;
- }
- return anim;
- }
+ // If the navigation bar can't change sides, then it will jump when we change orientations
+ // and we don't rotate seamlessly - unless that is allowed, e.g. with gesture navigation
+ // where the navbar is low-profile enough that this isn't very noticeable.
+ if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving()
+ && (!(displayLayout.navigationBarCanMove()
+ && (displayChange.getStartAbsBounds().width()
+ != displayChange.getStartAbsBounds().height())))) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ " nav bar changes sides, so not seamless.");
+ return animationHint;
}
- return ROTATION_ANIMATION_ROTATE;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless.");
+ return ROTATION_ANIMATION_SEAMLESS;
}
@Override
@@ -330,12 +311,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
final Runnable onAnimFinish = () -> {
if (!animations.isEmpty()) return;
-
- if (mRotationAnimation != null) {
- mRotationAnimation.kill();
- mRotationAnimation = null;
- }
-
mAnimations.remove(transition);
finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
};
@@ -352,14 +327,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) {
if (info.getType() == TRANSIT_CHANGE) {
- isSeamlessDisplayChange = isRotationSeamless(info, mDisplayController);
- final int anim = getRotationAnimation(info);
+ final int anim = getRotationAnimationHint(change, info, mDisplayController);
+ isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS;
if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) {
- mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession,
- mTransactionPool, startTransaction, change, info.getRootLeash(),
- anim);
- mRotationAnimation.startAnimation(animations, onAnimFinish,
- mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor);
+ startRotationAnimation(startTransaction, change, info, anim, animations,
+ onAnimFinish);
continue;
}
} else {
@@ -398,11 +370,20 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
change.getEndAbsBounds().top - info.getRootOffset().y);
// Seamless display transition doesn't need to animate.
if (isSeamlessDisplayChange) continue;
- if (isTask) {
- // Skip non-tasks since those usually have null bounds.
+ if (isTask || (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)
+ && !change.hasFlags(FLAG_FILLS_TASK))) {
+ // Update Task and embedded split window crop bounds, otherwise we may see crop
+ // on previous bounds during the rotation animation.
startTransaction.setWindowCrop(change.getLeash(),
change.getEndAbsBounds().width(), change.getEndAbsBounds().height());
}
+ // Rotation change of independent non display window container.
+ if (change.getParent() == null
+ && change.getStartRotation() != change.getEndRotation()) {
+ startRotationAnimation(startTransaction, change, info,
+ ROTATION_ANIMATION_ROTATE, animations, onAnimFinish);
+ continue;
+ }
}
// Don't animate anything that isn't independent.
@@ -438,22 +419,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
cornerRadius = 0;
}
- if (a.getShowBackdrop()) {
- if (info.getAnimationOptions().getBackgroundColor() != 0) {
- // If available use the background color provided through AnimationOptions
- backgroundColorForTransition =
- info.getAnimationOptions().getBackgroundColor();
- } else if (a.getBackdropColor() != 0) {
- // Otherwise fallback on the background color provided through the animation
- // definition.
- backgroundColorForTransition = a.getBackdropColor();
- } else if (change.getBackgroundColor() != 0) {
- // Otherwise default to the window's background color if provided through
- // the theme as the background color for the animation - the top most window
- // with a valid background color and showBackground set takes precedence.
- backgroundColorForTransition = change.getBackgroundColor();
- }
- }
+ backgroundColorForTransition = getTransitionBackgroundColorIfSet(info, change, a,
+ backgroundColorForTransition);
boolean delayedEdgeExtension = false;
if (!isTask && a.hasExtension()) {
@@ -470,8 +437,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
final Rect clipRect = Transitions.isClosingType(change.getMode())
- ? mRotator.getEndBoundsInStartRotation(change)
- : change.getEndAbsBounds();
+ ? new Rect(mRotator.getEndBoundsInStartRotation(change))
+ : new Rect(change.getEndAbsBounds());
+ clipRect.offsetTo(0, 0);
if (delayedEdgeExtension) {
// If the edge extension needs to happen after the startTransition has been
@@ -480,11 +448,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
postStartTransactionCallbacks.add(t ->
startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
mTransactionPool, mMainExecutor, mAnimExecutor,
- null /* position */, cornerRadius, clipRect));
+ change.getEndRelOffset(), cornerRadius, clipRect));
} else {
startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
- mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */,
- cornerRadius, clipRect);
+ mTransactionPool, mMainExecutor, mAnimExecutor,
+ change.getEndRelOffset(), cornerRadius, clipRect);
}
if (info.getAnimationOptions() != null) {
@@ -520,9 +488,52 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
return true;
}
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ArrayList<Animator> anims = mAnimations.get(mergeTarget);
+ if (anims == null) return;
+ for (int i = anims.size() - 1; i >= 0; --i) {
+ final Animator anim = anims.get(i);
+ mAnimExecutor.execute(anim::end);
+ }
+ }
+
+ private void startRotationAnimation(SurfaceControl.Transaction startTransaction,
+ TransitionInfo.Change change, TransitionInfo info, int animHint,
+ ArrayList<Animator> animations, Runnable onAnimFinish) {
+ final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession,
+ mTransactionPool, startTransaction, change, info.getRootLeash(), animHint);
+ // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real
+ // content, and background color. The item of "animGroup" will be removed if the sub
+ // animation is finished. Then if the list becomes empty, the rotation animation is done.
+ final ArrayList<Animator> animGroup = new ArrayList<>(3);
+ final ArrayList<Animator> animGroupStore = new ArrayList<>(3);
+ final Runnable finishCallback = () -> {
+ if (!animGroup.isEmpty()) return;
+ anim.kill();
+ animations.removeAll(animGroupStore);
+ onAnimFinish.run();
+ };
+ anim.startAnimation(animGroup, finishCallback, mTransitionAnimationScaleSetting,
+ mMainExecutor, mAnimExecutor);
+ for (int i = animGroup.size() - 1; i >= 0; i--) {
+ final Animator animator = animGroup.get(i);
+ animGroupStore.add(animator);
+ animations.add(animator);
+ }
+ }
+
private void edgeExtendWindow(TransitionInfo.Change change,
Animation a, SurfaceControl.Transaction startTransaction,
SurfaceControl.Transaction finishTransaction) {
+ // Do not create edge extension surface for transfer starting window change.
+ // The app surface could be empty thus nothing can draw on the hardware renderer, which will
+ // block this thread when calling Surface#unlockCanvasAndPost.
+ if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
+ return;
+ }
final Transformation transformationAtStart = new Transformation();
a.getTransformationAt(0, transformationAtStart);
final Transformation transformationAtEnd = new Transformation();
@@ -631,29 +642,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
return edgeExtensionLayer;
}
- private void addBackgroundToTransition(
- @NonNull SurfaceControl rootLeash,
- @ColorInt int color,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction
- ) {
- final Color bgColor = Color.valueOf(color);
- final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() };
-
- final SurfaceControl animationBackgroundSurface = new SurfaceControl.Builder()
- .setName("Animation Background")
- .setParent(rootLeash)
- .setColorLayer()
- .setOpaque(true)
- .build();
-
- startTransaction
- .setLayer(animationBackgroundSurface, Integer.MIN_VALUE)
- .setColor(animationBackgroundSurface, colorArray)
- .show(animationBackgroundSurface);
- finishTransaction.remove(animationBackgroundSurface);
- }
-
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@@ -667,9 +655,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
@Nullable
- private Animation loadAnimation(TransitionInfo info, TransitionInfo.Change change,
- int wallpaperTransit) {
- Animation a = null;
+ private Animation loadAnimation(@NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change change, int wallpaperTransit) {
+ Animation a;
final int type = info.getType();
final int flags = info.getFlags();
@@ -680,7 +668,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
final boolean isTask = change.getTaskInfo() != null;
final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
final int overrideType = options != null ? options.getType() : ANIM_NONE;
- final boolean canCustomContainer = isTask ? !sDisableCustomTaskAnimationProperty : true;
+ final boolean canCustomContainer = !isTask || !sDisableCustomTaskAnimationProperty;
final Rect endBounds = Transitions.isClosingType(changeMode)
? mRotator.getEndBoundsInStartRotation(change)
: change.getEndAbsBounds();
@@ -724,72 +712,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
// This received a transferred starting window, so don't animate
return null;
} else {
- int animAttr = 0;
- boolean translucent = false;
- if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) {
- animAttr = enter
- ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation
- : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation;
- } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) {
- animAttr = enter
- ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation
- : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation;
- } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) {
- animAttr = enter
- ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation
- : R.styleable.WindowAnimation_wallpaperOpenExitAnimation;
- } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) {
- animAttr = enter
- ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation
- : R.styleable.WindowAnimation_wallpaperCloseExitAnimation;
- } else if (type == TRANSIT_OPEN) {
- // We will translucent open animation for translucent activities and tasks. Choose
- // WindowAnimation_activityOpenEnterAnimation and set translucent here, then
- // TransitionAnimation loads appropriate animation later.
- if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) {
- translucent = true;
- }
- if (isTask && !translucent) {
- animAttr = enter
- ? R.styleable.WindowAnimation_taskOpenEnterAnimation
- : R.styleable.WindowAnimation_taskOpenExitAnimation;
- } else {
- animAttr = enter
- ? R.styleable.WindowAnimation_activityOpenEnterAnimation
- : R.styleable.WindowAnimation_activityOpenExitAnimation;
- }
- } else if (type == TRANSIT_TO_FRONT) {
- animAttr = enter
- ? R.styleable.WindowAnimation_taskToFrontEnterAnimation
- : R.styleable.WindowAnimation_taskToFrontExitAnimation;
- } else if (type == TRANSIT_CLOSE) {
- if (isTask) {
- animAttr = enter
- ? R.styleable.WindowAnimation_taskCloseEnterAnimation
- : R.styleable.WindowAnimation_taskCloseExitAnimation;
- } else {
- if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) {
- translucent = true;
- }
- animAttr = enter
- ? R.styleable.WindowAnimation_activityCloseEnterAnimation
- : R.styleable.WindowAnimation_activityCloseExitAnimation;
- }
- } else if (type == TRANSIT_TO_BACK) {
- animAttr = enter
- ? R.styleable.WindowAnimation_taskToBackEnterAnimation
- : R.styleable.WindowAnimation_taskToBackExitAnimation;
- }
-
- if (animAttr != 0) {
- if (overrideType == ANIM_FROM_STYLE && canCustomContainer) {
- a = mTransitionAnimation
- .loadAnimationAttr(options.getPackageName(), options.getAnimations(),
- animAttr, translucent);
- } else {
- a = mTransitionAnimation.loadDefaultAnimationAttr(animAttr, translucent);
- }
- }
+ a = loadAttributeAnimation(info, change, wallpaperTransit, mTransitionAnimation);
}
if (a != null) {
@@ -834,13 +757,19 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
});
};
va.addListener(new AnimatorListenerAdapter() {
+ private boolean mFinished = false;
+
@Override
public void onAnimationEnd(Animator animation) {
+ if (mFinished) return;
+ mFinished = true;
finisher.run();
}
@Override
public void onAnimationCancel(Animator animation) {
+ if (mFinished) return;
+ mFinished = true;
finisher.run();
}
});
@@ -851,11 +780,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private void attachThumbnail(@NonNull ArrayList<Animator> animations,
@NonNull Runnable finishCallback, TransitionInfo.Change change,
TransitionInfo.AnimationOptions options, float cornerRadius) {
- final boolean isTask = change.getTaskInfo() != null;
final boolean isOpen = Transitions.isOpeningType(change.getMode());
final boolean isClose = Transitions.isClosingType(change.getMode());
if (isOpen) {
- if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS && isTask) {
+ if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) {
attachCrossProfileThumbnailAnimation(animations, finishCallback, change,
cornerRadius);
} else if (options.getType() == ANIM_THUMBNAIL_SCALE_UP) {
@@ -870,8 +798,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
@NonNull Runnable finishCallback, TransitionInfo.Change change, float cornerRadius) {
final Rect bounds = change.getEndAbsBounds();
// Show the right drawable depending on the user we're transitioning to.
- final Drawable thumbnailDrawable = change.getTaskInfo().userId == mCurrentUserId
- ? mContext.getDrawable(R.drawable.ic_account_circle) : mEnterpriseThumbnailDrawable;
+ final Drawable thumbnailDrawable = change.hasFlags(FLAG_CROSS_PROFILE_OWNER_THUMBNAIL)
+ ? mContext.getDrawable(R.drawable.ic_account_circle)
+ : change.hasFlags(FLAG_CROSS_PROFILE_WORK_THUMBNAIL)
+ ? mEnterpriseThumbnailDrawable : null;
+ if (thumbnailDrawable == null) {
+ return;
+ }
final HardwareBuffer thumbnail = mTransitionAnimation.createCrossProfileAppsThumbnail(
thumbnailDrawable, bounds);
if (thumbnail == null) {
@@ -896,7 +829,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
a.restrictDuration(MAX_ANIMATION_DURATION);
a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
- mMainExecutor, mAnimExecutor, new Point(bounds.left, bounds.top),
+ mMainExecutor, mAnimExecutor, change.getEndRelOffset(),
cornerRadius, change.getEndAbsBounds());
}
@@ -921,7 +854,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
a.restrictDuration(MAX_ANIMATION_DURATION);
a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
- mMainExecutor, mAnimExecutor, null /* position */,
+ mMainExecutor, mAnimExecutor, change.getEndRelOffset(),
cornerRadius, change.getEndAbsBounds());
}
@@ -954,7 +887,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private static void applyTransformation(long time, SurfaceControl.Transaction t,
SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix,
- Point position, float cornerRadius, @Nullable Rect clipRect) {
+ Point position, float cornerRadius, @Nullable Rect immutableClipRect) {
anim.getTransformation(time, transformation);
if (position != null) {
transformation.getMatrix().postTranslate(position.x, position.y);
@@ -962,6 +895,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
t.setMatrix(leash, transformation.getMatrix(), matrix);
t.setAlpha(leash, transformation.getAlpha());
+ final Rect clipRect = immutableClipRect == null ? null : new Rect(immutableClipRect);
Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE);
if (!extensionInsets.equals(Insets.NONE) && clipRect != null && !clipRect.isEmpty()) {
// Clip out any overflowing edge extension
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 3e2a0e635a75..4e1fa290270d 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
@@ -18,11 +18,9 @@ package com.android.wm.shell.transition;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.ActivityTaskManager;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
-import android.util.Slog;
import android.view.SurfaceControl;
import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
@@ -87,18 +85,14 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
});
}
};
+ Transitions.setRunningRemoteTransitionDelegate(mRemote.getAppThread());
try {
if (mRemote.asBinder() != null) {
mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */);
}
- try {
- ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(
- mRemote.getAppThread());
- } catch (SecurityException e) {
- Slog.e(Transitions.TAG, "Unable to boost animation thread. This should only happen"
- + " during unit tests");
- }
mRemote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb);
+ // assume that remote will apply the start transaction.
+ startTransaction.clear();
} catch (RemoteException e) {
Log.e(Transitions.TAG, "Error running remote transition.", e);
if (mRemote.asBinder() != null) {
@@ -120,6 +114,11 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
@Override
public void onTransitionFinished(WindowContainerTransaction wct,
SurfaceControl.Transaction sct) {
+ // We have merged, since we sent the transaction over binder, the one in this
+ // process won't be cleared if the remote applied it. We don't actually know if the
+ // remote applied the transaction, but applying twice will break surfaceflinger
+ // so just assume the worst-case and clear the local transaction.
+ t.clear();
mMainExecutor.execute(
() -> finishCallback.onTransitionFinished(wct, null /* wctCB */));
}
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 ece9f47e8788..9469529de8f1 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
@@ -18,7 +18,6 @@ package com.android.wm.shell.transition;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.ActivityTaskManager;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArrayMap;
@@ -83,7 +82,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
}
@Override
- public void onTransitionMerged(@NonNull IBinder transition) {
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishT) {
mRequestedRemotes.remove(transition);
}
@@ -129,16 +129,12 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
});
}
};
+ Transitions.setRunningRemoteTransitionDelegate(remote.getAppThread());
try {
handleDeath(remote.asBinder(), finishCallback);
- try {
- ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(
- remote.getAppThread());
- } catch (SecurityException e) {
- Log.e(Transitions.TAG, "Unable to boost animation thread. This should only happen"
- + " during unit tests");
- }
remote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb);
+ // assume that remote will apply the start transaction.
+ startTransaction.clear();
} catch (RemoteException e) {
Log.e(Transitions.TAG, "Error running remote transition.", e);
unhandleDeath(remote.asBinder(), finishCallback);
@@ -162,6 +158,11 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
@Override
public void onTransitionFinished(WindowContainerTransaction wct,
SurfaceControl.Transaction sct) {
+ // We have merged, since we sent the transaction over binder, the one in this
+ // process won't be cleared if the remote applied it. We don't actually know if the
+ // remote applied the transaction, but applying twice will break surfaceflinger
+ // so just assume the worst-case and clear the local transaction.
+ t.clear();
mMainExecutor.execute(() -> {
if (!mRequestedRemotes.containsKey(mergeTarget)) {
Log.e(TAG, "Merged transition finished after it's mergeTarget (the "
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 46f73fda37a1..b647f43da522 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -34,7 +34,6 @@ import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Color;
import android.graphics.ColorSpace;
-import android.graphics.GraphicBuffer;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
@@ -85,12 +84,8 @@ class ScreenRotationAnimation {
private final Context mContext;
private final TransactionPool mTransactionPool;
private final float[] mTmpFloats = new float[9];
- // Complete transformations being applied.
- private final Matrix mSnapshotInitialMatrix = new Matrix();
- /** The leash of display. */
+ /** The leash of the changing window container. */
private final SurfaceControl mSurfaceControl;
- private final Rect mStartBounds = new Rect();
- private final Rect mEndBounds = new Rect();
private final int mAnimHint;
private final int mStartWidth;
@@ -108,8 +103,7 @@ class ScreenRotationAnimation {
*/
private SurfaceControl mBackColorSurface;
/** The leash using to animate screenshot layer. */
- private SurfaceControl mAnimLeash;
- private Transaction mTransaction;
+ private final SurfaceControl mAnimLeash;
// The current active animation to move from the old to the new rotated
// state. Which animation is run here will depend on the old and new
@@ -137,9 +131,6 @@ class ScreenRotationAnimation {
mStartRotation = change.getStartRotation();
mEndRotation = change.getEndRotation();
- mStartBounds.set(change.getStartAbsBounds());
- mEndBounds.set(change.getEndAbsBounds());
-
mAnimLeash = new SurfaceControl.Builder(session)
.setParent(rootLeash)
.setEffectLayer()
@@ -148,53 +139,59 @@ class ScreenRotationAnimation {
.build();
try {
- SurfaceControl.LayerCaptureArgs args =
- new SurfaceControl.LayerCaptureArgs.Builder(mSurfaceControl)
- .setCaptureSecureLayers(true)
- .setAllowProtected(true)
- .setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight))
- .build();
- SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
- SurfaceControl.captureLayers(args);
- if (screenshotBuffer == null) {
- Slog.w(TAG, "Unable to take screenshot of display");
- return;
- }
-
- mScreenshotLayer = new SurfaceControl.Builder(session)
- .setParent(mAnimLeash)
- .setBLASTLayer()
- .setSecure(screenshotBuffer.containsSecureLayers())
- .setCallsite("ShellRotationAnimation")
- .setName("RotationLayer")
- .build();
+ if (change.getSnapshot() != null) {
+ mScreenshotLayer = change.getSnapshot();
+ t.reparent(mScreenshotLayer, mAnimLeash);
+ mStartLuma = change.getSnapshotLuma();
+ } else {
+ SurfaceControl.LayerCaptureArgs args =
+ new SurfaceControl.LayerCaptureArgs.Builder(mSurfaceControl)
+ .setCaptureSecureLayers(true)
+ .setAllowProtected(true)
+ .setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight))
+ .build();
+ SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
+ SurfaceControl.captureLayers(args);
+ if (screenshotBuffer == null) {
+ Slog.w(TAG, "Unable to take screenshot of display");
+ return;
+ }
+
+ mScreenshotLayer = new SurfaceControl.Builder(session)
+ .setParent(mAnimLeash)
+ .setBLASTLayer()
+ .setSecure(screenshotBuffer.containsSecureLayers())
+ .setOpaque(true)
+ .setCallsite("ShellRotationAnimation")
+ .setName("RotationLayer")
+ .build();
- GraphicBuffer buffer = GraphicBuffer.createFromHardwareBuffer(
- screenshotBuffer.getHardwareBuffer());
+ final ColorSpace colorSpace = screenshotBuffer.getColorSpace();
+ final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
+ t.setDataSpace(mScreenshotLayer, colorSpace.getDataSpace());
+ t.setBuffer(mScreenshotLayer, hardwareBuffer);
+ t.show(mScreenshotLayer);
+ if (!isCustomRotate()) {
+ mStartLuma = getMedianBorderLuma(hardwareBuffer, colorSpace);
+ }
+ }
t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE);
- t.setPosition(mAnimLeash, 0, 0);
- t.setAlpha(mAnimLeash, 1);
t.show(mAnimLeash);
-
- t.setBuffer(mScreenshotLayer, buffer);
- t.setColorSpace(mScreenshotLayer, screenshotBuffer.getColorSpace());
- t.show(mScreenshotLayer);
+ // Crop the real content in case it contains a larger child layer, e.g. wallpaper.
+ t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight));
if (!isCustomRotate()) {
mBackColorSurface = new SurfaceControl.Builder(session)
.setParent(rootLeash)
.setColorLayer()
+ .setOpaque(true)
.setCallsite("ShellRotationAnimation")
.setName("BackColorSurface")
.build();
- HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
- mStartLuma = getMedianBorderLuma(hardwareBuffer, screenshotBuffer.getColorSpace());
-
t.setLayer(mBackColorSurface, -1);
t.setColor(mBackColorSurface, new float[]{mStartLuma, mStartLuma, mStartLuma});
- t.setAlpha(mBackColorSurface, 1);
t.show(mBackColorSurface);
}
@@ -202,7 +199,7 @@ class ScreenRotationAnimation {
Slog.w(TAG, "Unable to allocate freeze surface", e);
}
- setRotation(t);
+ setScreenshotTransform(t);
t.apply();
}
@@ -210,19 +207,36 @@ class ScreenRotationAnimation {
return mAnimHint == ROTATION_ANIMATION_CROSSFADE || mAnimHint == ROTATION_ANIMATION_JUMPCUT;
}
- private void setRotation(SurfaceControl.Transaction t) {
- // Compute the transformation matrix that must be applied
- // to the snapshot to make it stay in the same original position
- // with the current screen rotation.
- int delta = deltaRotation(mEndRotation, mStartRotation);
- createRotationMatrix(delta, mStartWidth, mStartHeight, mSnapshotInitialMatrix);
- setRotationTransform(t, mSnapshotInitialMatrix);
- }
-
- private void setRotationTransform(SurfaceControl.Transaction t, Matrix matrix) {
+ private void setScreenshotTransform(SurfaceControl.Transaction t) {
if (mScreenshotLayer == null) {
return;
}
+ final Matrix matrix = new Matrix();
+ final int delta = deltaRotation(mEndRotation, mStartRotation);
+ if (delta != 0) {
+ // Compute the transformation matrix that must be applied to the snapshot to make it
+ // stay in the same original position with the current screen rotation.
+ switch (delta) {
+ case Surface.ROTATION_90:
+ matrix.setRotate(90, 0, 0);
+ matrix.postTranslate(mStartHeight, 0);
+ break;
+ case Surface.ROTATION_180:
+ matrix.setRotate(180, 0, 0);
+ matrix.postTranslate(mStartWidth, mStartHeight);
+ break;
+ case Surface.ROTATION_270:
+ matrix.setRotate(270, 0, 0);
+ matrix.postTranslate(0, mStartWidth);
+ break;
+ }
+ } else if ((mEndWidth > mStartWidth) == (mEndHeight > mStartHeight)
+ && (mEndWidth != mStartWidth || mEndHeight != mStartHeight)) {
+ // Display resizes without rotation change.
+ final float scale = Math.max((float) mEndWidth / mStartHeight,
+ (float) mEndHeight / mStartHeight);
+ matrix.setScale(scale, scale);
+ }
matrix.getValues(mTmpFloats);
float x = mTmpFloats[Matrix.MTRANS_X];
float y = mTmpFloats[Matrix.MTRANS_Y];
@@ -230,9 +244,6 @@ class ScreenRotationAnimation {
t.setMatrix(mScreenshotLayer,
mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y],
mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]);
-
- t.setAlpha(mScreenshotLayer, (float) 1.0);
- t.show(mScreenshotLayer);
}
/**
@@ -298,7 +309,6 @@ class ScreenRotationAnimation {
mRotateEnterAnimation.restrictDuration(MAX_ANIMATION_DURATION);
mRotateEnterAnimation.scaleCurrentDuration(animationScale);
- mTransaction = mTransactionPool.acquire();
if (customRotate) {
mRotateAlphaAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight);
mRotateAlphaAnimation.restrictDuration(MAX_ANIMATION_DURATION);
@@ -378,22 +388,16 @@ class ScreenRotationAnimation {
}
public void kill() {
- Transaction t = mTransaction != null ? mTransaction : mTransactionPool.acquire();
+ final Transaction t = mTransactionPool.acquire();
if (mAnimLeash.isValid()) {
t.remove(mAnimLeash);
}
- if (mScreenshotLayer != null) {
- if (mScreenshotLayer.isValid()) {
- t.remove(mScreenshotLayer);
- }
- mScreenshotLayer = null;
+ if (mScreenshotLayer != null && mScreenshotLayer.isValid()) {
+ t.remove(mScreenshotLayer);
}
- if (mBackColorSurface != null) {
- if (mBackColorSurface.isValid()) {
- t.remove(mBackColorSurface);
- }
- mBackColorSurface = null;
+ if (mBackColorSurface != null && mBackColorSurface.isValid()) {
+ t.remove(mBackColorSurface);
}
t.apply();
mTransactionPool.release(t);
@@ -486,27 +490,6 @@ class ScreenRotationAnimation {
return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
}
- private static void createRotationMatrix(int rotation, int width, int height,
- Matrix outMatrix) {
- switch (rotation) {
- case Surface.ROTATION_0:
- outMatrix.reset();
- break;
- case Surface.ROTATION_90:
- outMatrix.setRotate(90, 0, 0);
- outMatrix.postTranslate(height, 0);
- break;
- case Surface.ROTATION_180:
- outMatrix.setRotate(180, 0, 0);
- outMatrix.postTranslate(width, height);
- break;
- case Surface.ROTATION_270:
- outMatrix.setRotate(270, 0, 0);
- outMatrix.postTranslate(0, width);
- break;
- }
- }
-
private static void applyColor(int startColor, int endColor, float[] rgbFloat,
float fraction, SurfaceControl surface, SurfaceControl.Transaction t) {
final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
new file mode 100644
index 000000000000..efee6f40b53e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -0,0 +1,220 @@
+/*
+ * 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.transition;
+
+import static android.app.ActivityOptions.ANIM_FROM_STYLE;
+import static android.app.ActivityOptions.ANIM_NONE;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+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 android.view.WindowManager.transitTypeToString;
+import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE;
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN;
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Color;
+import android.os.SystemProperties;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.window.TransitionInfo;
+
+import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+/** The helper class that provides methods for adding styles to transition animations. */
+public class TransitionAnimationHelper {
+
+ /**
+ * Restrict ability of activities overriding transition animation in a way such that
+ * an activity can do it only when the transition happens within a same task.
+ *
+ * @see android.app.Activity#overridePendingTransition(int, int)
+ */
+ private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY =
+ "persist.wm.disable_custom_task_animation";
+
+ /**
+ * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY
+ */
+ static final boolean sDisableCustomTaskAnimationProperty =
+ SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true);
+
+ /** Loads the animation that is defined through attribute id for the given transition. */
+ @Nullable
+ public static Animation loadAttributeAnimation(@NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change change, int wallpaperTransit,
+ @NonNull TransitionAnimation transitionAnimation) {
+ final int type = info.getType();
+ final int changeMode = change.getMode();
+ final int changeFlags = change.getFlags();
+ final boolean enter = Transitions.isOpeningType(changeMode);
+ final boolean isTask = change.getTaskInfo() != null;
+ final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+ final int overrideType = options != null ? options.getType() : ANIM_NONE;
+ final boolean canCustomContainer = !isTask || !sDisableCustomTaskAnimationProperty;
+ final boolean isDream =
+ isTask && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM;
+ int animAttr = 0;
+ boolean translucent = false;
+ if (isDream) {
+ if (type == TRANSIT_OPEN) {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_dreamActivityOpenEnterAnimation
+ : R.styleable.WindowAnimation_dreamActivityOpenExitAnimation;
+ } else if (type == TRANSIT_CLOSE) {
+ animAttr = enter
+ ? 0
+ : R.styleable.WindowAnimation_dreamActivityCloseExitAnimation;
+ }
+ } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation
+ : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation;
+ } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation
+ : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation;
+ } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation
+ : R.styleable.WindowAnimation_wallpaperOpenExitAnimation;
+ } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation
+ : R.styleable.WindowAnimation_wallpaperCloseExitAnimation;
+ } else if (type == TRANSIT_OPEN) {
+ // We will translucent open animation for translucent activities and tasks. Choose
+ // WindowAnimation_activityOpenEnterAnimation and set translucent here, then
+ // TransitionAnimation loads appropriate animation later.
+ if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) {
+ translucent = true;
+ }
+ if (isTask && !translucent) {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_taskOpenEnterAnimation
+ : R.styleable.WindowAnimation_taskOpenExitAnimation;
+ } else {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_activityOpenEnterAnimation
+ : R.styleable.WindowAnimation_activityOpenExitAnimation;
+ }
+ } else if (type == TRANSIT_TO_FRONT) {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_taskToFrontEnterAnimation
+ : R.styleable.WindowAnimation_taskToFrontExitAnimation;
+ } else if (type == TRANSIT_CLOSE) {
+ if (isTask) {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_taskCloseEnterAnimation
+ : R.styleable.WindowAnimation_taskCloseExitAnimation;
+ } else {
+ if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) {
+ translucent = true;
+ }
+ animAttr = enter
+ ? R.styleable.WindowAnimation_activityCloseEnterAnimation
+ : R.styleable.WindowAnimation_activityCloseExitAnimation;
+ }
+ } else if (type == TRANSIT_TO_BACK) {
+ animAttr = enter
+ ? R.styleable.WindowAnimation_taskToBackEnterAnimation
+ : R.styleable.WindowAnimation_taskToBackExitAnimation;
+ }
+
+ Animation a = null;
+ if (animAttr != 0) {
+ if (overrideType == ANIM_FROM_STYLE && canCustomContainer) {
+ a = transitionAnimation
+ .loadAnimationAttr(options.getPackageName(), options.getAnimations(),
+ animAttr, translucent);
+ } else {
+ a = transitionAnimation.loadDefaultAnimationAttr(animAttr, translucent);
+ }
+ }
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "loadAnimation: anim=%s animAttr=0x%x type=%s isEntrance=%b", a, animAttr,
+ transitTypeToString(type),
+ enter);
+ return a;
+ }
+
+ /**
+ * Gets the background {@link ColorInt} for the given transition animation if it is set.
+ *
+ * @param defaultColor {@link ColorInt} to return if there is no background color specified by
+ * the given transition animation.
+ */
+ @ColorInt
+ public static int getTransitionBackgroundColorIfSet(@NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change change, @NonNull Animation a,
+ @ColorInt int defaultColor) {
+ if (!a.getShowBackdrop()) {
+ return defaultColor;
+ }
+ if (info.getAnimationOptions() != null
+ && info.getAnimationOptions().getBackgroundColor() != 0) {
+ // If available use the background color provided through AnimationOptions
+ return info.getAnimationOptions().getBackgroundColor();
+ } else if (a.getBackdropColor() != 0) {
+ // Otherwise fallback on the background color provided through the animation
+ // definition.
+ return a.getBackdropColor();
+ } else if (change.getBackgroundColor() != 0) {
+ // Otherwise default to the window's background color if provided through
+ // the theme as the background color for the animation - the top most window
+ // with a valid background color and showBackground set takes precedence.
+ return change.getBackgroundColor();
+ }
+ return defaultColor;
+ }
+
+ /**
+ * Adds the given {@code backgroundColor} as the background color to the transition animation.
+ */
+ public static void addBackgroundToTransition(@NonNull SurfaceControl rootLeash,
+ @ColorInt int backgroundColor, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ if (backgroundColor == 0) {
+ // No background color.
+ return;
+ }
+ final Color bgColor = Color.valueOf(backgroundColor);
+ final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() };
+ final SurfaceControl animationBackgroundSurface = new SurfaceControl.Builder()
+ .setName("Animation Background")
+ .setParent(rootLeash)
+ .setColorLayer()
+ .setOpaque(true)
+ .build();
+ startTransaction
+ .setLayer(animationBackgroundSurface, Integer.MIN_VALUE)
+ .setColor(animationBackgroundSurface, colorArray)
+ .show(animationBackgroundSurface);
+ finishTransaction.remove(animationBackgroundSurface);
+ }
+}
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 435d67087f34..29d25bc39223 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
@@ -23,6 +23,8 @@ import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
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 android.view.WindowManager.fixScale;
+import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
@@ -30,6 +32,8 @@ import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTas
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityTaskManager;
+import android.app.IApplicationThread;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
@@ -56,13 +60,13 @@ import androidx.annotation.BinderThread;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
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.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellInit;
import java.util.ArrayList;
import java.util.Arrays;
@@ -97,11 +101,18 @@ public class Transitions implements RemoteCallable<Transitions> {
/** Transition type for dismissing split-screen. */
public static final int TRANSIT_SPLIT_DISMISS = TRANSIT_FIRST_CUSTOM + 7;
+ /** Transition type for freeform to maximize transition. */
+ public static final int TRANSIT_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 8;
+
+ /** Transition type for maximize to freeform transition. */
+ public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9;
+
private final WindowOrganizer mOrganizer;
private final Context mContext;
private final ShellExecutor mMainExecutor;
private final ShellExecutor mAnimExecutor;
private final TransitionPlayerImpl mPlayerImpl;
+ private final DefaultTransitionHandler mDefaultTransitionHandler;
private final RemoteTransitionHandler mRemoteTransitionHandler;
private final DisplayController mDisplayController;
private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
@@ -109,6 +120,8 @@ public class Transitions implements RemoteCallable<Transitions> {
/** List of possible handlers. Ordered by specificity (eg. tapped back to front). */
private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>();
+ private final ArrayList<TransitionObserver> mObservers = new ArrayList<>();
+
/** List of {@link Runnable} instances to run when the last active transition has finished. */
private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>();
@@ -127,8 +140,11 @@ public class Transitions implements RemoteCallable<Transitions> {
/** Keeps track of currently playing transitions in the order of receipt. */
private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>();
- public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool,
- @NonNull DisplayController displayController, @NonNull Context context,
+ public Transitions(@NonNull Context context,
+ @NonNull ShellInit shellInit,
+ @NonNull WindowOrganizer organizer,
+ @NonNull TransactionPool pool,
+ @NonNull DisplayController displayController,
@NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor) {
mOrganizer = organizer;
@@ -137,33 +153,40 @@ public class Transitions implements RemoteCallable<Transitions> {
mAnimExecutor = animExecutor;
mDisplayController = displayController;
mPlayerImpl = new TransitionPlayerImpl();
+ mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit,
+ displayController, pool, mainExecutor, mainHandler, animExecutor);
+ mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
// The very last handler (0 in the list) should be the default one.
- mHandlers.add(new DefaultTransitionHandler(displayController, pool, context, mainExecutor,
- mainHandler, animExecutor));
+ mHandlers.add(mDefaultTransitionHandler);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default");
// Next lowest priority is remote transitions.
- mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor);
mHandlers.add(mRemoteTransitionHandler);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
- ContentResolver resolver = context.getContentResolver();
- mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
- Settings.Global.TRANSITION_ANIMATION_SCALE,
- context.getResources().getFloat(
- R.dimen.config_appTransitionAnimationDurationScaleDefault));
+ ContentResolver resolver = mContext.getContentResolver();
+ mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
dispatchAnimScaleSetting(mTransitionAnimationScaleSetting);
resolver.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false,
new SettingsObserver());
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ // Register this transition handler with Core
+ mOrganizer.registerTransitionPlayer(mPlayerImpl);
+ // Pre-load the instance.
+ TransitionMetrics.getInstance();
+ }
}
- private Transitions() {
- mOrganizer = null;
- mContext = null;
- mMainExecutor = null;
- mAnimExecutor = null;
- mDisplayController = null;
- mPlayerImpl = null;
- mRemoteTransitionHandler = null;
+ private float getTransitionAnimationScaleSetting() {
+ return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+ Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
+ R.dimen.config_appTransitionAnimationDurationScaleDefault)));
}
public ShellTransitions asRemoteTransitions() {
@@ -186,20 +209,20 @@ public class Transitions implements RemoteCallable<Transitions> {
}
}
- /** Register this transition handler with Core */
- public void register(ShellTaskOrganizer taskOrganizer) {
- if (mPlayerImpl == null) return;
- taskOrganizer.registerTransitionPlayer(mPlayerImpl);
- // Pre-load the instance.
- TransitionMetrics.getInstance();
- }
-
/**
* Adds a handler candidate.
* @see TransitionHandler
*/
public void addHandler(@NonNull TransitionHandler handler) {
+ if (mHandlers.isEmpty()) {
+ throw new RuntimeException("Unexpected handler added prior to initialization, please "
+ + "use ShellInit callbacks to ensure proper ordering");
+ }
mHandlers.add(handler);
+ // Set initial scale settings.
+ handler.setAnimScaleSetting(mTransitionAnimationScaleSetting);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: %s",
+ handler.getClass().getSimpleName());
}
public ShellExecutor getMainExecutor() {
@@ -227,6 +250,29 @@ public class Transitions implements RemoteCallable<Transitions> {
mRemoteTransitionHandler.removeFiltered(remoteTransition);
}
+ /** Registers an observer on the lifecycle of transitions. */
+ public void registerObserver(@NonNull TransitionObserver observer) {
+ mObservers.add(observer);
+ }
+
+ /** Unregisters the observer. */
+ public void unregisterObserver(@NonNull TransitionObserver observer) {
+ mObservers.remove(observer);
+ }
+
+ /** Boosts the process priority of remote animation player. */
+ public static void setRunningRemoteTransitionDelegate(IApplicationThread appThread) {
+ if (appThread == null) return;
+ try {
+ ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(appThread);
+ } catch (SecurityException e) {
+ Log.e(TAG, "Unable to boost animation process. This should only happen"
+ + " during unit tests");
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Runs the given {@code runnable} when the last active transition has finished, or immediately
* if there are currently no active transitions.
@@ -287,12 +333,14 @@ public class Transitions implements RemoteCallable<Transitions> {
finishT.setAlpha(leash, 1.f);
}
} else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
- // Wallpaper is a bit of an anomaly: it's visibility is tied to other WindowStates.
- // As a result, we actually can't hide it's WindowToken because there may not be a
- // transition associated with it becoming visible again. Fortunately, since it is
- // always z-ordered to the back, we don't have to worry about it flickering to the
- // front during reparenting, so the hide here isn't necessary for it.
- if ((change.getFlags() & FLAG_IS_WALLPAPER) == 0) {
+ // Wallpaper/IME are anomalies: their visibility is tied to other WindowStates.
+ // As a result, we actually can't hide their WindowTokens because there may not be a
+ // transition associated with them becoming visible again. Fortunately, since
+ // wallpapers are always z-ordered to the back, we don't have to worry about it
+ // flickering to the front during reparenting. Similarly, the IME is reparented to
+ // the associated app, so its visibility is coupled. So, an explicit hide is not
+ // needed visually anyways.
+ if ((change.getFlags() & (FLAG_IS_WALLPAPER | FLAG_IS_INPUT_METHOD)) == 0) {
finishT.hide(leash);
}
}
@@ -309,13 +357,14 @@ public class Transitions implements RemoteCallable<Transitions> {
if (info.getRootLeash().isValid()) {
t.show(info.getRootLeash());
}
+ final int numChanges = info.getChanges().size();
// Put animating stuff above this line and put static stuff below it.
- int zSplitLine = info.getChanges().size();
+ final int zSplitLine = numChanges + 1;
// changes should be ordered top-to-bottom in z
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ for (int i = numChanges - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
final SurfaceControl leash = change.getLeash();
- final int mode = info.getChanges().get(i).getMode();
+ final int mode = change.getMode();
// Don't reparent anything that isn't independent within its parents
if (!TransitionInfo.isIndependent(change, info)) {
@@ -329,26 +378,31 @@ public class Transitions implements RemoteCallable<Transitions> {
t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x,
change.getStartAbsBounds().top - info.getRootOffset().y);
}
+ final int layer;
// Put all the OPEN/SHOW on top
- if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
+ if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
+ // Wallpaper is always at the bottom.
+ layer = -zSplitLine;
+ } else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
if (isOpening) {
// put on top
- t.setLayer(leash, zSplitLine + info.getChanges().size() - i);
+ layer = zSplitLine + numChanges - i;
} else {
// put on bottom
- t.setLayer(leash, zSplitLine - i);
+ layer = zSplitLine - i;
}
} else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
if (isOpening) {
// put on bottom and leave visible
- t.setLayer(leash, zSplitLine - i);
+ layer = zSplitLine - i;
} else {
// put on top
- t.setLayer(leash, zSplitLine + info.getChanges().size() - i);
+ layer = zSplitLine + numChanges - i;
}
} else { // CHANGE or other
- t.setLayer(leash, zSplitLine + info.getChanges().size() - i);
+ layer = zSplitLine + numChanges - i;
}
+ t.setLayer(leash, layer);
}
}
@@ -371,12 +425,18 @@ public class Transitions implements RemoteCallable<Transitions> {
+ Arrays.toString(mActiveTransitions.stream().map(
activeTransition -> activeTransition.mToken).toArray()));
}
+
+ for (int i = 0; i < mObservers.size(); ++i) {
+ mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT);
+ }
+
if (!info.getRootLeash().isValid()) {
// Invalid root-leash implies that the transition is empty/no-op, so just do
// housekeeping and return.
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Invalid root leash (%s): %s",
transitionToken, info);
t.apply();
+ finishT.apply();
onAbort(transitionToken);
return;
}
@@ -400,6 +460,7 @@ public class Transitions implements RemoteCallable<Transitions> {
}
if (nonTaskChange && transferStartingWindow) {
t.apply();
+ finishT.apply();
// Treat this as an abort since we are bypassing any merge logic and effectively
// finishing immediately.
onAbort(transitionToken);
@@ -435,33 +496,46 @@ public class Transitions implements RemoteCallable<Transitions> {
playing.mToken, (wct, cb) -> onFinish(merging.mToken, wct, cb));
}
- boolean startAnimation(@NonNull ActiveTransition active, TransitionHandler handler) {
- return handler.startAnimation(active.mToken, active.mInfo, active.mStartT, active.mFinishT,
- (wct, cb) -> onFinish(active.mToken, wct, cb));
- }
+ private void playTransition(@NonNull ActiveTransition active) {
+ for (int i = 0; i < mObservers.size(); ++i) {
+ mObservers.get(i).onTransitionStarting(active.mToken);
+ }
- void playTransition(@NonNull ActiveTransition active) {
setupAnimHierarchy(active.mInfo, active.mStartT, active.mFinishT);
// If a handler already chose to run this animation, try delegating to it first.
if (active.mHandler != null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try firstHandler %s",
active.mHandler);
- if (startAnimation(active, active.mHandler)) {
+ boolean consumed = active.mHandler.startAnimation(active.mToken, active.mInfo,
+ active.mStartT, active.mFinishT, (wct, cb) -> onFinish(active.mToken, wct, cb));
+ if (consumed) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler");
return;
}
}
- // Otherwise give every other handler a chance (in order)
+ // Otherwise give every other handler a chance
+ active.mHandler = dispatchTransition(active.mToken, active.mInfo, active.mStartT,
+ active.mFinishT, (wct, cb) -> onFinish(active.mToken, wct, cb), active.mHandler);
+ }
+
+ /**
+ * Gives every handler (in order) a chance to animate until one consumes the transition.
+ * @return the handler which consumed the transition.
+ */
+ TransitionHandler dispatchTransition(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT,
+ @NonNull TransitionFinishCallback finishCB, @Nullable TransitionHandler skip) {
for (int i = mHandlers.size() - 1; i >= 0; --i) {
- if (mHandlers.get(i) == active.mHandler) continue;
+ if (mHandlers.get(i) == skip) continue;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try handler %s",
mHandlers.get(i));
- if (startAnimation(active, mHandlers.get(i))) {
+ boolean consumed = mHandlers.get(i).startAnimation(transition, info, startT, finishT,
+ finishCB);
+ if (consumed) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s",
mHandlers.get(i));
- active.mHandler = mHandlers.get(i);
- return;
+ return mHandlers.get(i);
}
}
throw new IllegalStateException(
@@ -496,15 +570,29 @@ public class Transitions implements RemoteCallable<Transitions> {
active.mMerged = true;
active.mAborted = abort;
if (active.mHandler != null) {
- active.mHandler.onTransitionMerged(active.mToken);
+ active.mHandler.onTransitionConsumed(
+ active.mToken, abort, abort ? null : active.mFinishT);
+ }
+ for (int i = 0; i < mObservers.size(); ++i) {
+ mObservers.get(i).onTransitionMerged(
+ active.mToken, mActiveTransitions.get(0).mToken);
}
return;
}
- mActiveTransitions.get(activeIdx).mAborted = abort;
+ final ActiveTransition active = mActiveTransitions.get(activeIdx);
+ active.mAborted = abort;
+ if (active.mAborted && active.mHandler != null) {
+ // Notifies to clean-up the aborted transition.
+ active.mHandler.onTransitionConsumed(
+ transition, true /* aborted */, null /* finishTransaction */);
+ }
+ for (int i = 0; i < mObservers.size(); ++i) {
+ mObservers.get(i).onTransitionFinished(active.mToken, active.mAborted);
+ }
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
"Transition animation finished (abort=%b), notifying core %s", abort, transition);
// Merge all relevant transactions together
- SurfaceControl.Transaction fullFinish = mActiveTransitions.get(activeIdx).mFinishT;
+ SurfaceControl.Transaction fullFinish = active.mFinishT;
for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) {
final ActiveTransition toMerge = mActiveTransitions.get(iA);
if (!toMerge.mMerged) break;
@@ -533,7 +621,15 @@ public class Transitions implements RemoteCallable<Transitions> {
while (mActiveTransitions.size() > activeIdx
&& mActiveTransitions.get(activeIdx).mAborted) {
ActiveTransition aborted = mActiveTransitions.remove(activeIdx);
+ // Notifies to clean-up the aborted transition.
+ if (aborted.mHandler != null) {
+ aborted.mHandler.onTransitionConsumed(
+ transition, true /* aborted */, null /* finishTransaction */);
+ }
mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */);
+ for (int i = 0; i < mObservers.size(); ++i) {
+ mObservers.get(i).onTransitionFinished(active.mToken, true);
+ }
}
if (mActiveTransitions.size() <= activeIdx) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations "
@@ -615,8 +711,9 @@ public class Transitions implements RemoteCallable<Transitions> {
if (wct == null) {
wct = new WindowContainerTransaction();
}
- mDisplayController.getChangeController().dispatchOnRotateDisplay(wct,
- change.getDisplayId(), change.getStartRotation(), change.getEndRotation());
+ mDisplayController.getChangeController().dispatchOnDisplayChange(wct,
+ change.getDisplayId(), change.getStartRotation(), change.getEndRotation(),
+ null /* newDisplayAreaInfo */);
}
}
active.mToken = mOrganizer.startTransition(
@@ -714,9 +811,15 @@ public class Transitions implements RemoteCallable<Transitions> {
/**
* Called when a transition which was already "claimed" by this handler has been merged
- * into another animation. Gives this handler a chance to clean-up any expectations.
+ * into another animation or has been aborted. Gives this handler a chance to clean-up any
+ * expectations.
+ *
+ * @param transition The transition been consumed.
+ * @param aborted Whether the transition is aborted or not.
+ * @param finishTransaction The transaction to be applied after the transition animated.
*/
- default void onTransitionMerged(@NonNull IBinder transition) { }
+ default void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishTransaction) { }
/**
* Sets transition animation scale settings value to handler.
@@ -726,6 +829,52 @@ public class Transitions implements RemoteCallable<Transitions> {
default void setAnimScaleSetting(float scale) {}
}
+ /**
+ * Interface for something that needs to know the lifecycle of some transitions, but never
+ * handles any transition by itself.
+ */
+ public interface TransitionObserver {
+ /**
+ * Called when the transition is ready to play. It may later be merged into other
+ * transitions. Note this doesn't mean this transition will be played anytime soon.
+ *
+ * @param transition the unique token of this transition
+ * @param startTransaction the transaction given to the handler to be applied before the
+ * transition animation. This will be applied when the transition
+ * handler that handles this transition starts the transition.
+ * @param finishTransaction the transaction given to the handler to be applied after the
+ * transition animation. The Transition system will apply it when
+ * finishCallback is called by the transition handler.
+ */
+ void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction);
+
+ /**
+ * Called when the transition is starting to play. It isn't called for merged transitions.
+ *
+ * @param transition the unique token of this transition
+ */
+ void onTransitionStarting(@NonNull IBinder transition);
+
+ /**
+ * Called when a transition is merged into another transition. There won't be any following
+ * lifecycle calls for the merged transition.
+ *
+ * @param merged the unique token of the transition that's merged to another one
+ * @param playing the unique token of the transition that accepts the merge
+ */
+ void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing);
+
+ /**
+ * Called when the transition is finished. This isn't called for merged transitions.
+ *
+ * @param transition the unique token of this transition
+ * @param aborted {@code true} if this transition is aborted; {@code false} otherwise.
+ */
+ void onTransitionFinished(@NonNull IBinder transition, boolean aborted);
+ }
+
@BinderThread
private class TransitionPlayerImpl extends ITransitionPlayer.Stub {
@Override
@@ -820,9 +969,7 @@ public class Transitions implements RemoteCallable<Transitions> {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
- mTransitionAnimationScaleSetting = Settings.Global.getFloat(
- mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE,
- mTransitionAnimationScaleSetting);
+ mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
mMainExecutor.execute(() -> dispatchAnimScaleSetting(mTransitionAnimationScaleSetting));
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
new file mode 100644
index 000000000000..6b59e313b01b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
@@ -0,0 +1,234 @@
+/*
+ * 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.unfold;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.TaskInfo;
+import android.util.SparseArray;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
+
+import java.util.List;
+import java.util.Optional;
+
+import dagger.Lazy;
+
+/**
+ * Manages fold/unfold animations of tasks on foldable devices.
+ * When folding or unfolding a foldable device we play animations that
+ * transform task cropping/scaling/rounded corners.
+ *
+ * This controller manages:
+ * 1) Folding/unfolding when Shell transitions disabled
+ * 2) Folding when Shell transitions enabled, unfolding is managed by
+ * {@link com.android.wm.shell.unfold.UnfoldTransitionHandler}
+ */
+public class UnfoldAnimationController implements UnfoldListener {
+
+ private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
+ private final ShellExecutor mExecutor;
+ private final TransactionPool mTransactionPool;
+ private final List<UnfoldTaskAnimator> mAnimators;
+ private final Lazy<Optional<UnfoldTransitionHandler>> mUnfoldTransitionHandler;
+
+ private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>();
+ private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>();
+
+ public UnfoldAnimationController(
+ @NonNull ShellInit shellInit,
+ @NonNull TransactionPool transactionPool,
+ @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
+ @NonNull List<UnfoldTaskAnimator> animators,
+ @NonNull Lazy<Optional<UnfoldTransitionHandler>> unfoldTransitionHandler,
+ @NonNull ShellExecutor executor) {
+ mUnfoldProgressProvider = unfoldProgressProvider;
+ mUnfoldTransitionHandler = unfoldTransitionHandler;
+ mTransactionPool = transactionPool;
+ mExecutor = executor;
+ mAnimators = animators;
+ // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
+ // override for this controller from the base module
+ if (unfoldProgressProvider != ShellUnfoldProgressProvider.NO_PROVIDER) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
+ }
+
+ /**
+ * Initializes the controller, starts listening for the external events
+ */
+ public void onInit() {
+ mUnfoldProgressProvider.addListener(mExecutor, this);
+
+ for (int i = 0; i < mAnimators.size(); i++) {
+ final UnfoldTaskAnimator animator = mAnimators.get(i);
+ animator.init();
+ // TODO(b/238217847): See #provideSplitTaskUnfoldAnimatorBase
+ mExecutor.executeDelayed(animator::start, 0);
+ }
+ }
+
+ /**
+ * Called when a task appeared
+ * @param taskInfo info for the appeared task
+ * @param leash surface leash for the appeared task
+ */
+ public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
+ mTaskSurfaces.put(taskInfo.taskId, leash);
+
+ // Find the first matching animator
+ for (int i = 0; i < mAnimators.size(); i++) {
+ final UnfoldTaskAnimator animator = mAnimators.get(i);
+ if (animator.isApplicableTask(taskInfo)) {
+ mAnimatorsByTaskId.put(taskInfo.taskId, animator);
+ animator.onTaskAppeared(taskInfo, leash);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Called when task info changed
+ * @param taskInfo info for the changed task
+ */
+ public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+ final UnfoldTaskAnimator animator = mAnimatorsByTaskId.get(taskInfo.taskId);
+ final boolean isCurrentlyApplicable = animator != null;
+
+ if (isCurrentlyApplicable) {
+ final boolean isApplicable = animator.isApplicableTask(taskInfo);
+ if (isApplicable) {
+ // Still applicable, send update
+ animator.onTaskChanged(taskInfo);
+ } else {
+ // Became inapplicable
+ resetTask(animator, taskInfo);
+ animator.onTaskVanished(taskInfo);
+ mAnimatorsByTaskId.remove(taskInfo.taskId);
+ }
+ } else {
+ // Find the first matching animator
+ for (int i = 0; i < mAnimators.size(); i++) {
+ final UnfoldTaskAnimator currentAnimator = mAnimators.get(i);
+ if (currentAnimator.isApplicableTask(taskInfo)) {
+ // Became applicable
+ mAnimatorsByTaskId.put(taskInfo.taskId, currentAnimator);
+
+ SurfaceControl leash = mTaskSurfaces.get(taskInfo.taskId);
+ currentAnimator.onTaskAppeared(taskInfo, leash);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when a task vanished
+ * @param taskInfo info for the vanished task
+ */
+ public void onTaskVanished(RunningTaskInfo taskInfo) {
+ mTaskSurfaces.remove(taskInfo.taskId);
+
+ final UnfoldTaskAnimator animator = mAnimatorsByTaskId.get(taskInfo.taskId);
+ final boolean isCurrentlyApplicable = animator != null;
+
+ if (isCurrentlyApplicable) {
+ resetTask(animator, taskInfo);
+ animator.onTaskVanished(taskInfo);
+ mAnimatorsByTaskId.remove(taskInfo.taskId);
+ }
+ }
+
+ @Override
+ public void onStateChangeStarted() {
+ if (mUnfoldTransitionHandler.get().get().willHandleTransition()) {
+ return;
+ }
+
+ SurfaceControl.Transaction transaction = null;
+ for (int i = 0; i < mAnimators.size(); i++) {
+ final UnfoldTaskAnimator animator = mAnimators.get(i);
+ if (animator.hasActiveTasks()) {
+ if (transaction == null) transaction = mTransactionPool.acquire();
+ animator.prepareStartTransaction(transaction);
+ }
+ }
+
+ if (transaction != null) {
+ transaction.apply();
+ mTransactionPool.release(transaction);
+ }
+ }
+
+ @Override
+ public void onStateChangeProgress(float progress) {
+ if (mUnfoldTransitionHandler.get().get().willHandleTransition()) {
+ return;
+ }
+
+ SurfaceControl.Transaction transaction = null;
+ for (int i = 0; i < mAnimators.size(); i++) {
+ final UnfoldTaskAnimator animator = mAnimators.get(i);
+ if (animator.hasActiveTasks()) {
+ if (transaction == null) transaction = mTransactionPool.acquire();
+ animator.applyAnimationProgress(progress, transaction);
+ }
+ }
+
+ if (transaction != null) {
+ transaction.apply();
+ mTransactionPool.release(transaction);
+ }
+ }
+
+ @Override
+ public void onStateChangeFinished() {
+ if (mUnfoldTransitionHandler.get().get().willHandleTransition()) {
+ return;
+ }
+
+ final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+
+ for (int i = 0; i < mAnimators.size(); i++) {
+ final UnfoldTaskAnimator animator = mAnimators.get(i);
+ animator.resetAllSurfaces(transaction);
+ animator.prepareFinishTransaction(transaction);
+ }
+
+ transaction.apply();
+
+ mTransactionPool.release(transaction);
+ }
+
+ private void resetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
+ if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
+ // PiP task has its own cleanup path, ignore surface reset to avoid conflict.
+ return;
+ }
+ final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+ animator.resetSurface(taskInfo, transaction);
+ transaction.apply();
+ mTransactionPool.release(transaction);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
index 9faf454261d3..86ca292399cb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
@@ -79,7 +79,7 @@ public class UnfoldBackgroundController {
}
private float[] getBackgroundColor(Context context) {
- int colorInt = context.getResources().getColor(R.color.unfold_transition_background);
+ int colorInt = context.getResources().getColor(R.color.taskbar_background);
return new float[]{
(float) red(colorInt) / 255.0F,
(float) green(colorInt) / 255.0F,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
index 639603941c18..5d7b62905d3b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
@@ -16,8 +16,6 @@
package com.android.wm.shell.unfold;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.view.WindowManager.TRANSIT_CHANGE;
import android.os.IBinder;
@@ -30,15 +28,24 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.transition.Transitions.TransitionFinishCallback;
import com.android.wm.shell.transition.Transitions.TransitionHandler;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
+import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;
+import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+/**
+ * Transition handler that is responsible for animating app surfaces when unfolding of foldable
+ * devices. It does not handle the folding animation, which is done in
+ * {@link UnfoldAnimationController}.
+ */
public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListener {
private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
@@ -51,17 +58,37 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
@Nullable
private IBinder mTransition;
- private final List<TransitionInfo.Change> mAnimatedFullscreenTasks = new ArrayList<>();
+ private final List<UnfoldTaskAnimator> mAnimators = new ArrayList<>();
- public UnfoldTransitionHandler(ShellUnfoldProgressProvider unfoldProgressProvider,
- TransactionPool transactionPool, Executor executor, Transitions transitions) {
+ public UnfoldTransitionHandler(ShellInit shellInit,
+ ShellUnfoldProgressProvider unfoldProgressProvider,
+ FullscreenUnfoldTaskAnimator fullscreenUnfoldAnimator,
+ SplitTaskUnfoldAnimator splitUnfoldTaskAnimator,
+ TransactionPool transactionPool,
+ Executor executor,
+ Transitions transitions) {
mUnfoldProgressProvider = unfoldProgressProvider;
mTransactionPool = transactionPool;
mExecutor = executor;
mTransitions = transitions;
+
+ mAnimators.add(splitUnfoldTaskAnimator);
+ mAnimators.add(fullscreenUnfoldAnimator);
+ // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
+ // override for this controller from the base module
+ if (unfoldProgressProvider != ShellUnfoldProgressProvider.NO_PROVIDER
+ && Transitions.ENABLE_SHELL_TRANSITIONS) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
}
- public void init() {
+ /**
+ * Called when the transition handler is initialized.
+ */
+ public void onInit() {
+ for (int i = 0; i < mAnimators.size(); i++) {
+ mAnimators.get(i).init();
+ }
mTransitions.addHandler(this);
mUnfoldProgressProvider.addListener(mExecutor, this);
}
@@ -71,49 +98,69 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull TransitionFinishCallback finishCallback) {
-
if (transition != mTransition) return false;
- startTransaction.apply();
-
- mAnimatedFullscreenTasks.clear();
- info.getChanges().forEach(change -> {
- final boolean allowedToAnimate = change.getTaskInfo() != null
- && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FULLSCREEN
- && change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME
- && change.getMode() == TRANSIT_CHANGE;
-
- if (allowedToAnimate) {
- mAnimatedFullscreenTasks.add(change);
+ for (int i = 0; i < mAnimators.size(); i++) {
+ final UnfoldTaskAnimator animator = mAnimators.get(i);
+ animator.clearTasks();
+
+ info.getChanges().forEach(change -> {
+ if (change.getTaskInfo() != null
+ && change.getMode() == TRANSIT_CHANGE
+ && animator.isApplicableTask(change.getTaskInfo())) {
+ animator.onTaskAppeared(change.getTaskInfo(), change.getLeash());
+ }
+ });
+
+ if (animator.hasActiveTasks()) {
+ animator.prepareStartTransaction(startTransaction);
+ animator.prepareFinishTransaction(finishTransaction);
+ animator.start();
}
- });
+ }
+ startTransaction.apply();
mFinishCallback = finishCallback;
- mTransition = null;
return true;
}
@Override
public void onStateChangeProgress(float progress) {
- mAnimatedFullscreenTasks.forEach(change -> {
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+ if (mTransition == null) return;
- // TODO: this is a placeholder animation, replace with a spec version in the next CLs
- final float testScale = 0.8f + 0.2f * progress;
- transaction.setScale(change.getLeash(), testScale, testScale);
+ SurfaceControl.Transaction transaction = null;
+ for (int i = 0; i < mAnimators.size(); i++) {
+ final UnfoldTaskAnimator animator = mAnimators.get(i);
+
+ if (animator.hasActiveTasks()) {
+ if (transaction == null) {
+ transaction = mTransactionPool.acquire();
+ }
+
+ animator.applyAnimationProgress(progress, transaction);
+ }
+ }
+
+ if (transaction != null) {
transaction.apply();
mTransactionPool.release(transaction);
- });
+ }
}
@Override
public void onStateChangeFinished() {
- if (mFinishCallback != null) {
- mFinishCallback.onTransitionFinished(null, null);
- mFinishCallback = null;
- mAnimatedFullscreenTasks.clear();
+ if (mFinishCallback == null) return;
+
+ for (int i = 0; i < mAnimators.size(); i++) {
+ final UnfoldTaskAnimator animator = mAnimators.get(i);
+ animator.clearTasks();
+ animator.stop();
}
+
+ mFinishCallback.onTransitionFinished(null, null);
+ mFinishCallback = null;
+ mTransition = null;
}
@Nullable
@@ -127,4 +174,8 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
}
return null;
}
+
+ public boolean willHandleTransition() {
+ return mTransition != null;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
index aa3868cfca84..eab82f00e962 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,16 +14,16 @@
* limitations under the License.
*/
-package com.android.wm.shell.fullscreen;
+package com.android.wm.shell.unfold.animation;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.util.MathUtils.lerp;
import static android.view.Display.DEFAULT_DISPLAY;
import android.animation.RectEvaluator;
import android.animation.TypeEvaluator;
import android.annotation.NonNull;
-import android.app.ActivityManager;
import android.app.TaskInfo;
import android.content.Context;
import android.graphics.Matrix;
@@ -32,22 +32,26 @@ import android.util.SparseArray;
import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
import com.android.wm.shell.unfold.UnfoldBackgroundController;
-import java.util.concurrent.Executor;
-
/**
- * Controls full screen app unfold transition: animating cropping window and scaling when
- * folding or unfolding a foldable device.
+ * This helper class contains logic that calculates scaling and cropping parameters
+ * for the folding/unfolding animation. As an input it receives TaskInfo objects and
+ * surfaces leashes and as an output it could fill surface transactions with required
+ * transformations.
+ *
+ * This class is used by
+ * {@link com.android.wm.shell.unfold.UnfoldTransitionHandler} and
+ * {@link UnfoldAnimationController}. They use independent
+ * instances of FullscreenUnfoldTaskAnimator.
*/
-public final class FullscreenUnfoldController implements UnfoldListener,
- OnInsetsChangedListener {
+public class FullscreenUnfoldTaskAnimator implements UnfoldTaskAnimator,
+ DisplayInsetsController.OnInsetsChangedListener {
private static final float[] FLOAT_9 = new float[9];
private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
@@ -57,49 +61,77 @@ public final class FullscreenUnfoldController implements UnfoldListener,
private static final float END_SCALE = 1f;
private static final float START_SCALE = END_SCALE - VERTICAL_START_MARGIN * 2;
- private final Executor mExecutor;
- private final ShellUnfoldProgressProvider mProgressProvider;
- private final DisplayInsetsController mDisplayInsetsController;
-
private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
+ private final int mExpandedTaskBarHeight;
+ private final float mWindowCornerRadiusPx;
+ private final DisplayInsetsController mDisplayInsetsController;
private final UnfoldBackgroundController mBackgroundController;
private InsetsSource mTaskbarInsetsSource;
- private final float mWindowCornerRadiusPx;
- private final float mExpandedTaskBarHeight;
-
- private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
-
- public FullscreenUnfoldController(
- @NonNull Context context,
- @NonNull Executor executor,
+ public FullscreenUnfoldTaskAnimator(Context context,
@NonNull UnfoldBackgroundController backgroundController,
- @NonNull ShellUnfoldProgressProvider progressProvider,
- @NonNull DisplayInsetsController displayInsetsController
- ) {
- mExecutor = executor;
- mProgressProvider = progressProvider;
+ DisplayInsetsController displayInsetsController) {
mDisplayInsetsController = displayInsetsController;
- mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
+ mBackgroundController = backgroundController;
mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.taskbar_frame_height);
- mBackgroundController = backgroundController;
+ mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
}
- /**
- * Initializes the controller
- */
public void init() {
- mProgressProvider.addListener(mExecutor, this);
mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
}
@Override
- public void onStateChangeProgress(float progress) {
- if (mAnimationContextByTaskId.size() == 0) return;
+ public void insetsChanged(InsetsState insetsState) {
+ mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+ AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+ context.update(mTaskbarInsetsSource, context.mTaskInfo);
+ }
+ }
+
+ public boolean hasActiveTasks() {
+ return mAnimationContextByTaskId.size() > 0;
+ }
+
+ @Override
+ public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {
+ AnimationContext animationContext = new AnimationContext(leash, mTaskbarInsetsSource,
+ taskInfo);
+ mAnimationContextByTaskId.put(taskInfo.taskId, animationContext);
+ }
+
+ @Override
+ public void onTaskChanged(TaskInfo taskInfo) {
+ AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId);
+ if (animationContext != null) {
+ animationContext.update(mTaskbarInsetsSource, taskInfo);
+ }
+ }
+
+ @Override
+ public void onTaskVanished(TaskInfo taskInfo) {
+ mAnimationContextByTaskId.remove(taskInfo.taskId);
+ }
- mBackgroundController.ensureBackground(mTransaction);
+ @Override
+ public void clearTasks() {
+ mAnimationContextByTaskId.clear();
+ }
+
+ @Override
+ public void resetSurface(TaskInfo taskInfo, Transaction transaction) {
+ final AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
+ if (context != null) {
+ resetSurface(context, transaction);
+ }
+ }
+
+ @Override
+ public void applyAnimationProgress(float progress, Transaction transaction) {
+ if (mAnimationContextByTaskId.size() == 0) return;
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
@@ -111,75 +143,41 @@ public final class FullscreenUnfoldController implements UnfoldListener,
context.mMatrix.setScale(scale, scale, context.mCurrentCropRect.exactCenterX(),
context.mCurrentCropRect.exactCenterY());
- mTransaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
+ transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
.setMatrix(context.mLeash, context.mMatrix, FLOAT_9)
- .setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
+ .setCornerRadius(context.mLeash, mWindowCornerRadiusPx)
+ .show(context.mLeash);
}
-
- mTransaction.apply();
}
@Override
- public void onStateChangeFinished() {
- for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
- final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
- resetSurface(context);
- }
-
- mBackgroundController.removeBackground(mTransaction);
- mTransaction.apply();
+ public void prepareStartTransaction(Transaction transaction) {
+ mBackgroundController.ensureBackground(transaction);
}
@Override
- public void insetsChanged(InsetsState insetsState) {
- mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
- for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
- AnimationContext context = mAnimationContextByTaskId.valueAt(i);
- context.update(mTaskbarInsetsSource, context.mTaskInfo);
- }
+ public void prepareFinishTransaction(Transaction transaction) {
+ mBackgroundController.removeBackground(transaction);
}
- /**
- * Called when a new matching task appeared
- */
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- AnimationContext animationContext = new AnimationContext(leash, mTaskbarInsetsSource,
- taskInfo);
- mAnimationContextByTaskId.put(taskInfo.taskId, animationContext);
- }
-
- /**
- * Called when matching task changed
- */
- public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId);
- if (animationContext != null) {
- animationContext.update(mTaskbarInsetsSource, taskInfo);
- }
+ @Override
+ public boolean isApplicableTask(TaskInfo taskInfo) {
+ return taskInfo != null && taskInfo.isVisible()
+ && taskInfo.realActivity != null // to filter out parents created by organizer
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+ && taskInfo.getActivityType() != ACTIVITY_TYPE_HOME;
}
- /**
- * Called when matching task vanished
- */
- public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId);
- if (animationContext != null) {
- // PiP task has its own cleanup path, ignore surface reset to avoid conflict.
- if (taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED) {
- resetSurface(animationContext);
- }
- mAnimationContextByTaskId.remove(taskInfo.taskId);
- }
-
- if (mAnimationContextByTaskId.size() == 0) {
- mBackgroundController.removeBackground(mTransaction);
+ @Override
+ public void resetAllSurfaces(Transaction transaction) {
+ for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+ final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+ resetSurface(context, transaction);
}
-
- mTransaction.apply();
}
- private void resetSurface(AnimationContext context) {
- mTransaction
+ private void resetSurface(AnimationContext context, Transaction transaction) {
+ transaction
.setWindowCrop(context.mLeash, null)
.setCornerRadius(context.mLeash, 0.0F)
.setMatrix(context.mLeash, 1.0F, 0.0F, 0.0F, 1.0F)
@@ -197,10 +195,9 @@ public final class FullscreenUnfoldController implements UnfoldListener,
TaskInfo mTaskInfo;
- private AnimationContext(SurfaceControl leash,
- InsetsSource taskBarInsetsSource,
- TaskInfo taskInfo) {
- this.mLeash = leash;
+ private AnimationContext(SurfaceControl leash, InsetsSource taskBarInsetsSource,
+ TaskInfo taskInfo) {
+ mLeash = leash;
update(taskBarInsetsSource, taskInfo);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
index 59eecb5db136..6e10ebe94c5d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,17 +14,19 @@
* limitations under the License.
*/
-package com.android.wm.shell.splitscreen;
+package com.android.wm.shell.unfold.animation;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import android.animation.RectEvaluator;
import android.animation.TypeEvaluator;
-import android.annotation.NonNull;
-import android.app.ActivityManager;
+import android.app.TaskInfo;
import android.content.Context;
import android.graphics.Insets;
import android.graphics.Rect;
@@ -32,67 +34,130 @@ import android.util.SparseArray;
import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
-import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.splitscreen.SplitScreen;
+import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
import com.android.wm.shell.unfold.UnfoldBackgroundController;
+import java.util.Optional;
import java.util.concurrent.Executor;
+import dagger.Lazy;
+
/**
- * Controls transformations of the split screen task surfaces in response
- * to the unfolding/folding action on foldable devices
+ * This helper class contains logic that calculates scaling and cropping parameters
+ * for the folding/unfolding animation. As an input it receives TaskInfo objects and
+ * surfaces leashes and as an output it could fill surface transactions with required
+ * transformations.
+ *
+ * This class is used by
+ * {@link com.android.wm.shell.unfold.UnfoldTransitionHandler} and
+ * {@link UnfoldAnimationController}.
+ * They use independent instances of SplitTaskUnfoldAnimator.
*/
-public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener {
+public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
+ DisplayInsetsController.OnInsetsChangedListener, SplitScreenListener {
private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
- private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
- private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
- private final DisplayInsetsController mDisplayInsetsController;
- private final UnfoldBackgroundController mBackgroundController;
private final Executor mExecutor;
+ private final DisplayInsetsController mDisplayInsetsController;
+ private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
private final int mExpandedTaskBarHeight;
private final float mWindowCornerRadiusPx;
- private final Rect mStageBounds = new Rect();
- private final TransactionPool mTransactionPool;
+ private final Lazy<Optional<SplitScreenController>> mSplitScreenController;
+ private final UnfoldBackgroundController mUnfoldBackgroundController;
+
+ private final Rect mMainStageBounds = new Rect();
+ private final Rect mSideStageBounds = new Rect();
+ private final Rect mRootStageBounds = new Rect();
private InsetsSource mTaskbarInsetsSource;
- private boolean mBothStagesVisible;
-
- public StageTaskUnfoldController(@NonNull Context context,
- @NonNull TransactionPool transactionPool,
- @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
- @NonNull DisplayInsetsController displayInsetsController,
- @NonNull UnfoldBackgroundController backgroundController,
- @NonNull Executor executor) {
- mUnfoldProgressProvider = unfoldProgressProvider;
- mTransactionPool = transactionPool;
- mExecutor = executor;
- mBackgroundController = backgroundController;
+
+ @SplitPosition
+ private int mMainStagePosition = SPLIT_POSITION_UNDEFINED;
+ @SplitPosition
+ private int mSideStagePosition = SPLIT_POSITION_UNDEFINED;
+
+ public SplitTaskUnfoldAnimator(Context context, Executor executor,
+ Lazy<Optional<SplitScreenController>> splitScreenController,
+ UnfoldBackgroundController unfoldBackgroundController,
+ DisplayInsetsController displayInsetsController) {
mDisplayInsetsController = displayInsetsController;
- mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
+ mExecutor = executor;
+ mUnfoldBackgroundController = unfoldBackgroundController;
+ mSplitScreenController = splitScreenController;
mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.taskbar_frame_height);
+ mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
}
- /**
- * Initializes the controller, starts listening for the external events
- */
+ /** Initializes the animator, this should be called only once */
+ @Override
public void init() {
- mUnfoldProgressProvider.addListener(mExecutor, this);
mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
}
+ /**
+ * Starts listening for split-screen changes and gets initial split-screen
+ * layout information through the listener
+ */
+ @Override
+ public void start() {
+ mSplitScreenController.get().get().asSplitScreen()
+ .registerSplitScreenListener(this, mExecutor);
+ }
+
+ /**
+ * Stops listening for the split-screen layout changes
+ */
+ @Override
+ public void stop() {
+ mSplitScreenController.get().get().asSplitScreen()
+ .unregisterSplitScreenListener(this);
+ }
+
@Override
public void insetsChanged(InsetsState insetsState) {
mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ updateContexts();
+ }
+
+ @Override
+ public void onTaskStageChanged(int taskId, int stage, boolean visible) {
+ final AnimationContext context = mAnimationContextByTaskId.get(taskId);
+ if (context != null) {
+ context.mStageType = stage;
+ context.update();
+ }
+ }
+
+ @Override
+ public void onStagePositionChanged(int stage, int position) {
+ if (stage == STAGE_TYPE_MAIN) {
+ mMainStagePosition = position;
+ } else {
+ mSideStagePosition = position;
+ }
+ updateContexts();
+ }
+
+ @Override
+ public void onSplitBoundsChanged(Rect rootBounds, Rect mainBounds, Rect sideBounds) {
+ mRootStageBounds.set(rootBounds);
+ mMainStageBounds.set(mainBounds);
+ mSideStageBounds.set(sideBounds);
+ updateContexts();
+ }
+
+ private void updateContexts() {
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
AnimationContext context = mAnimationContextByTaskId.valueAt(i);
context.update();
@@ -100,44 +165,73 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange
}
/**
- * Called when split screen task appeared
- * @param taskInfo info for the appeared task
- * @param leash surface leash for the appeared task
+ * Register a split task in the animator
+ * @param taskInfo info of the task
+ * @param leash the surface of the task
*/
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- // Only handle child task surface here.
- if (!taskInfo.hasParentTask()) return;
-
+ @Override
+ public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {
AnimationContext context = new AnimationContext(leash);
mAnimationContextByTaskId.put(taskInfo.taskId, context);
}
/**
- * Called when a split screen task vanished
- * @param taskInfo info for the vanished task
+ * Unregister the task from the unfold animation
+ * @param taskInfo info of the task
+ */
+ @Override
+ public void onTaskVanished(TaskInfo taskInfo) {
+ mAnimationContextByTaskId.remove(taskInfo.taskId);
+ }
+
+ @Override
+ public boolean isApplicableTask(TaskInfo taskInfo) {
+ return taskInfo.hasParentTask()
+ && taskInfo.isVisible
+ && taskInfo.realActivity != null // to filter out parents created by organizer
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW;
+ }
+
+ /**
+ * Clear all registered tasks
*/
- public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- if (!taskInfo.hasParentTask()) return;
+ @Override
+ public void clearTasks() {
+ mAnimationContextByTaskId.clear();
+ }
+ /**
+ * Reset transformations of the task that could have been applied by the animator
+ * @param taskInfo task to reset
+ * @param transaction a transaction to write the changes to
+ */
+ @Override
+ public void resetSurface(TaskInfo taskInfo, Transaction transaction) {
AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
if (context != null) {
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
resetSurface(transaction, context);
- transaction.apply();
- mTransactionPool.release(transaction);
}
- mAnimationContextByTaskId.remove(taskInfo.taskId);
}
+ /**
+ * Reset all surface transformation that could have been introduced by the animator
+ * @param transaction to write changes to
+ */
@Override
- public void onStateChangeProgress(float progress) {
- if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return;
-
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- mBackgroundController.ensureBackground(transaction);
+ public void resetAllSurfaces(Transaction transaction) {
+ for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+ final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+ resetSurface(transaction, context);
+ }
+ }
+ @Override
+ public void applyAnimationProgress(float progress, Transaction transaction) {
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+ if (context.mStageType == STAGE_TYPE_UNDEFINED) {
+ continue;
+ }
context.mCurrentCropRect.set(RECT_EVALUATOR
.evaluate(progress, context.mStartCropRect, context.mEndCropRect));
@@ -145,53 +239,25 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange
transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
.setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
}
-
- transaction.apply();
-
- mTransactionPool.release(transaction);
}
@Override
- public void onStateChangeFinished() {
- resetTransformations();
+ public void prepareStartTransaction(Transaction transaction) {
+ mUnfoldBackgroundController.ensureBackground(transaction);
+ mSplitScreenController.get().get().updateSplitScreenSurfaces(transaction);
}
- /**
- * Called when split screen visibility changes
- * @param bothStagesVisible true if both stages of the split screen are visible
- */
- public void onSplitVisibilityChanged(boolean bothStagesVisible) {
- mBothStagesVisible = bothStagesVisible;
- if (!bothStagesVisible) {
- resetTransformations();
- }
+ @Override
+ public void prepareFinishTransaction(Transaction transaction) {
+ mUnfoldBackgroundController.removeBackground(transaction);
}
/**
- * Called when split screen stage bounds changed
- * @param bounds new bounds for this stage
+ * @return true if there are tasks to animate
*/
- public void onLayoutChanged(Rect bounds, @SplitPosition int splitPosition,
- boolean isLandscape) {
- mStageBounds.set(bounds);
-
- for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
- final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
- context.update(splitPosition, isLandscape);
- }
- }
-
- private void resetTransformations() {
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
-
- for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
- final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
- resetSurface(transaction, context);
- }
- mBackgroundController.removeBackground(transaction);
- transaction.apply();
-
- mTransactionPool.release(transaction);
+ @Override
+ public boolean hasActiveTasks() {
+ return mAnimationContextByTaskId.size() > 0;
}
private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) {
@@ -202,26 +268,24 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange
private class AnimationContext {
final SurfaceControl mLeash;
+
final Rect mStartCropRect = new Rect();
final Rect mEndCropRect = new Rect();
final Rect mCurrentCropRect = new Rect();
- private @SplitPosition int mSplitPosition = SPLIT_POSITION_UNDEFINED;
- private boolean mIsLandscape = false;
+ @SplitScreen.StageType
+ int mStageType = STAGE_TYPE_UNDEFINED;
private AnimationContext(SurfaceControl leash) {
- this.mLeash = leash;
- update();
- }
-
- private void update(@SplitPosition int splitPosition, boolean isLandscape) {
- this.mSplitPosition = splitPosition;
- this.mIsLandscape = isLandscape;
+ mLeash = leash;
update();
}
private void update() {
- mStartCropRect.set(mStageBounds);
+ final Rect stageBounds = mStageType == STAGE_TYPE_MAIN
+ ? mMainStageBounds : mSideStageBounds;
+
+ mStartCropRect.set(stageBounds);
boolean taskbarExpanded = isTaskbarExpanded();
if (taskbarExpanded) {
@@ -239,7 +303,8 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange
// Sides adjacent to split bar or task bar are not be animated.
Insets margins;
- if (mIsLandscape) { // Left and right splits.
+ final boolean isLandscape = mRootStageBounds.width() > mRootStageBounds.height();
+ if (isLandscape) { // Left and right splits.
margins = getLandscapeMargins(margin, taskbarExpanded);
} else { // Top and bottom splits.
margins = getPortraitMargins(margin, taskbarExpanded);
@@ -251,7 +316,9 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange
int left = margin;
int right = margin;
int bottom = taskbarExpanded ? 0 : margin; // Taskbar margin.
- if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+ final int splitPosition = mStageType == STAGE_TYPE_MAIN
+ ? mMainStagePosition : mSideStagePosition;
+ if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
right = 0; // Divider margin.
} else {
left = 0; // Divider margin.
@@ -262,7 +329,9 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange
private Insets getPortraitMargins(int margin, boolean taskbarExpanded) {
int bottom = margin;
int top = margin;
- if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+ final int splitPosition = mStageType == STAGE_TYPE_MAIN
+ ? mMainStagePosition : mSideStagePosition;
+ if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
bottom = 0; // Divider margin.
} else { // Bottom split.
top = 0; // Divider margin.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java
new file mode 100644
index 000000000000..e1e366301b46
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java
@@ -0,0 +1,117 @@
+/*
+ * 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.unfold.animation;
+
+import android.app.TaskInfo;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+/**
+ * Interface for classes that handle animations of tasks when folding or unfolding
+ * foldable devices.
+ */
+public interface UnfoldTaskAnimator {
+ /**
+ * Initializes the animator, this should be called once in the lifetime of the animator
+ */
+ default void init() {}
+
+ /**
+ * Starts the animator, it might start listening for some events from the system.
+ * Applying animation should be done only when animator is started.
+ * Animator could be started/stopped several times.
+ */
+ default void start() {}
+
+ /**
+ * Stops the animator, it could unsubscribe from system events.
+ */
+ default void stop() {}
+
+ /**
+ * If this method returns true then task updates will be propagated to
+ * the animator using the onTaskAppeared/Changed/Vanished callbacks.
+ * @return true if this task should be animated by this animator
+ */
+ default boolean isApplicableTask(TaskInfo taskInfo) {
+ return false;
+ }
+
+ /**
+ * Called whenever a task applicable to this animator appeared
+ * (isApplicableTask returns true for this task)
+ *
+ * @param taskInfo info of the appeared task
+ * @param leash surface of the task
+ */
+ default void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {}
+
+ /**
+ * Called whenever a task applicable to this animator changed
+ * @param taskInfo info of the changed task
+ */
+ default void onTaskChanged(TaskInfo taskInfo) {}
+
+ /**
+ * Called whenever a task applicable to this animator vanished
+ * @param taskInfo info of the vanished task
+ */
+ default void onTaskVanished(TaskInfo taskInfo) {}
+
+ /**
+ * @return true if there tasks that could be potentially animated
+ */
+ default boolean hasActiveTasks() {
+ return false;
+ }
+
+ /**
+ * Clears all registered tasks in the animator
+ */
+ default void clearTasks() {}
+
+ /**
+ * Apply task surfaces transformations based on the current unfold progress
+ * @param progress unfold transition progress
+ * @param transaction to write changes to
+ */
+ default void applyAnimationProgress(float progress, Transaction transaction) {}
+
+ /**
+ * Apply task surfaces transformations that should be set before starting the animation
+ * @param transaction to write changes to
+ */
+ default void prepareStartTransaction(Transaction transaction) {}
+
+ /**
+ * Apply task surfaces transformations that should be set after finishing the animation
+ * @param transaction to write changes to
+ */
+ default void prepareFinishTransaction(Transaction transaction) {}
+
+ /**
+ * Resets task surface to its initial transformation
+ * @param transaction to write changes to
+ */
+ default void resetSurface(TaskInfo taskInfo, Transaction transaction) {}
+
+ /**
+ * Resets all task surfaces to their initial transformations
+ * @param transaction to write changes to
+ */
+ default void resetAllSurfaces(Transaction transaction) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java
index edeff6e37182..4c868305dcdb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,21 +14,16 @@
* limitations under the License.
*/
-package com.android.wm.shell.draganddrop;
+package com.android.wm.shell.unfold.qualifier;
-import android.content.res.Configuration;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import javax.inject.Qualifier;
/**
- * Interface for telling DragAndDrop stuff.
+ * Indicates that this class is used for the shell unfold transition
*/
-@ExternalThread
-public interface DragAndDrop {
-
- /** Called when the theme changes. */
- void onThemeChanged();
-
- /** Called when the configuration changes. */
- void onConfigChanged(Configuration newConfig);
-}
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UnfoldShellTransition {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java
index 46e4299f99fa..4d2b3e6f899b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,20 +14,17 @@
* limitations under the License.
*/
-package com.android.wm.shell.stagesplit;
+package com.android.wm.shell.unfold.qualifier;
-/**
- * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks.
- */
-oneway interface ISplitScreenListener {
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
- /**
- * Called when the stage position changes.
- */
- void onStagePositionChanged(int stage, int position);
+import javax.inject.Qualifier;
- /**
- * Called when a task changes stages.
- */
- void onTaskStageChanged(int taskId, int stage, boolean visible);
-} \ No newline at end of file
+/**
+ * Indicates that this class is used for unfold transition implemented
+ * without using Shell transitions
+ */
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UnfoldTransition {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java
index 603d05d78fc0..c045cebdf4e0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.util;
+import android.annotation.IntDef;
import android.app.ActivityManager;
import android.app.WindowConfiguration;
import android.os.Parcel;
@@ -24,40 +25,143 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import java.util.Arrays;
+import java.util.List;
+
/**
* Simple container for recent tasks. May contain either a single or pair of tasks.
*/
public class GroupedRecentTaskInfo implements Parcelable {
- public @NonNull ActivityManager.RecentTaskInfo mTaskInfo1;
- public @Nullable ActivityManager.RecentTaskInfo mTaskInfo2;
- public @Nullable StagedSplitBounds mStagedSplitBounds;
- public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1) {
- this(task1, null, null);
+ public static final int TYPE_SINGLE = 1;
+ public static final int TYPE_SPLIT = 2;
+ public static final int TYPE_FREEFORM = 3;
+
+ @IntDef(prefix = {"TYPE_"}, value = {
+ TYPE_SINGLE,
+ TYPE_SPLIT,
+ TYPE_FREEFORM
+ })
+ public @interface GroupType {}
+
+ @NonNull
+ private final ActivityManager.RecentTaskInfo[] mTasks;
+ @Nullable
+ private final SplitBounds mSplitBounds;
+ @GroupType
+ private final int mType;
+
+ /**
+ * Create new for a single task
+ */
+ public static GroupedRecentTaskInfo forSingleTask(
+ @NonNull ActivityManager.RecentTaskInfo task) {
+ return new GroupedRecentTaskInfo(new ActivityManager.RecentTaskInfo[]{task}, null,
+ TYPE_SINGLE);
+ }
+
+ /**
+ * Create new for a pair of tasks in split screen
+ */
+ public static GroupedRecentTaskInfo forSplitTasks(@NonNull ActivityManager.RecentTaskInfo task1,
+ @NonNull ActivityManager.RecentTaskInfo task2, @Nullable SplitBounds splitBounds) {
+ return new GroupedRecentTaskInfo(new ActivityManager.RecentTaskInfo[]{task1, task2},
+ splitBounds, TYPE_SPLIT);
}
- public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1,
- @Nullable ActivityManager.RecentTaskInfo task2,
- @Nullable StagedSplitBounds stagedSplitBounds) {
- mTaskInfo1 = task1;
- mTaskInfo2 = task2;
- mStagedSplitBounds = stagedSplitBounds;
+ /**
+ * Create new for a group of freeform tasks
+ */
+ public static GroupedRecentTaskInfo forFreeformTasks(
+ @NonNull ActivityManager.RecentTaskInfo... tasks) {
+ return new GroupedRecentTaskInfo(tasks, null, TYPE_FREEFORM);
+ }
+
+ private GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo[] tasks,
+ @Nullable SplitBounds splitBounds, @GroupType int type) {
+ mTasks = tasks;
+ mSplitBounds = splitBounds;
+ mType = type;
}
GroupedRecentTaskInfo(Parcel parcel) {
- mTaskInfo1 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR);
- mTaskInfo2 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR);
- mStagedSplitBounds = parcel.readTypedObject(StagedSplitBounds.CREATOR);
+ mTasks = parcel.createTypedArray(ActivityManager.RecentTaskInfo.CREATOR);
+ mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR);
+ mType = parcel.readInt();
+ }
+
+ /**
+ * Get primary {@link ActivityManager.RecentTaskInfo}
+ */
+ @NonNull
+ public ActivityManager.RecentTaskInfo getTaskInfo1() {
+ return mTasks[0];
+ }
+
+ /**
+ * Get secondary {@link ActivityManager.RecentTaskInfo}.
+ *
+ * Used in split screen.
+ */
+ @Nullable
+ public ActivityManager.RecentTaskInfo getTaskInfo2() {
+ if (mTasks.length > 1) {
+ return mTasks[1];
+ }
+ return null;
+ }
+
+ /**
+ * Get all {@link ActivityManager.RecentTaskInfo}s grouped together.
+ */
+ @NonNull
+ public List<ActivityManager.RecentTaskInfo> getTaskInfoList() {
+ return Arrays.asList(mTasks);
+ }
+
+ /**
+ * Return {@link SplitBounds} if this is a split screen entry or {@code null}
+ */
+ @Nullable
+ public SplitBounds getSplitBounds() {
+ return mSplitBounds;
+ }
+
+ /**
+ * Get type of this recents entry. One of {@link GroupType}
+ */
+ @GroupType
+ public int getType() {
+ return mType;
}
@Override
public String toString() {
- String taskString = "Task1: " + getTaskInfo(mTaskInfo1)
- + ", Task2: " + getTaskInfo(mTaskInfo2);
- if (mStagedSplitBounds != null) {
- taskString += ", SplitBounds: " + mStagedSplitBounds.toString();
+ StringBuilder taskString = new StringBuilder();
+ for (int i = 0; i < mTasks.length; i++) {
+ if (i == 0) {
+ taskString.append("Task");
+ } else {
+ taskString.append(", Task");
+ }
+ taskString.append(i + 1).append(": ").append(getTaskInfo(mTasks[i]));
+ }
+ if (mSplitBounds != null) {
+ taskString.append(", SplitBounds: ").append(mSplitBounds);
+ }
+ taskString.append(", Type=");
+ switch (mType) {
+ case TYPE_SINGLE:
+ taskString.append("TYPE_SINGLE");
+ break;
+ case TYPE_SPLIT:
+ taskString.append("TYPE_SPLIT");
+ break;
+ case TYPE_FREEFORM:
+ taskString.append("TYPE_FREEFORM");
+ break;
}
- return taskString;
+ return taskString.toString();
}
private String getTaskInfo(ActivityManager.RecentTaskInfo taskInfo) {
@@ -74,9 +178,9 @@ public class GroupedRecentTaskInfo implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int flags) {
- parcel.writeTypedObject(mTaskInfo1, flags);
- parcel.writeTypedObject(mTaskInfo2, flags);
- parcel.writeTypedObject(mStagedSplitBounds, flags);
+ parcel.writeTypedArray(mTasks, flags);
+ parcel.writeTypedObject(mSplitBounds, flags);
+ parcel.writeInt(mType);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/StagedSplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
index a0c84cc33ebd..e90389764af3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/StagedSplitBounds.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
@@ -25,7 +25,7 @@ import java.util.Objects;
* Container of various information needed to display split screen
* tasks/leashes/etc in Launcher
*/
-public class StagedSplitBounds implements Parcelable {
+public class SplitBounds implements Parcelable {
public final Rect leftTopBounds;
public final Rect rightBottomBounds;
/** This rect represents the actual gap between the two apps */
@@ -43,7 +43,7 @@ public class StagedSplitBounds implements Parcelable {
public final int leftTopTaskId;
public final int rightBottomTaskId;
- public StagedSplitBounds(Rect leftTopBounds, Rect rightBottomBounds,
+ public SplitBounds(Rect leftTopBounds, Rect rightBottomBounds,
int leftTopTaskId, int rightBottomTaskId) {
this.leftTopBounds = leftTopBounds;
this.rightBottomBounds = rightBottomBounds;
@@ -66,7 +66,7 @@ public class StagedSplitBounds implements Parcelable {
topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom;
}
- public StagedSplitBounds(Parcel parcel) {
+ public SplitBounds(Parcel parcel) {
leftTopBounds = parcel.readTypedObject(Rect.CREATOR);
rightBottomBounds = parcel.readTypedObject(Rect.CREATOR);
visualDividerBounds = parcel.readTypedObject(Rect.CREATOR);
@@ -96,11 +96,11 @@ public class StagedSplitBounds implements Parcelable {
@Override
public boolean equals(Object obj) {
- if (!(obj instanceof StagedSplitBounds)) {
+ if (!(obj instanceof SplitBounds)) {
return false;
}
// Only need to check the base fields (the other fields are derived from these)
- final StagedSplitBounds other = (StagedSplitBounds) obj;
+ final SplitBounds other = (SplitBounds) obj;
return Objects.equals(leftTopBounds, other.leftTopBounds)
&& Objects.equals(rightBottomBounds, other.rightBottomBounds)
&& leftTopTaskId == other.leftTopTaskId
@@ -120,15 +120,15 @@ public class StagedSplitBounds implements Parcelable {
+ "AppsVertical? " + appsStackedVertically;
}
- public static final Creator<StagedSplitBounds> CREATOR = new Creator<StagedSplitBounds>() {
+ public static final Creator<SplitBounds> CREATOR = new Creator<SplitBounds>() {
@Override
- public StagedSplitBounds createFromParcel(Parcel in) {
- return new StagedSplitBounds(in);
+ public SplitBounds createFromParcel(Parcel in) {
+ return new SplitBounds(in);
}
@Override
- public StagedSplitBounds[] newArray(int size) {
- return new StagedSplitBounds[size];
+ public SplitBounds[] newArray(int size) {
+ return new SplitBounds[size];
}
};
}
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
new file mode 100644
index 000000000000..9e49b51e1504
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -0,0 +1,260 @@
+/*
+ * 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.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.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityTaskManager;
+import android.content.Context;
+import android.os.Handler;
+import android.view.Choreographer;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+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.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.DesktopModeController;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.transition.Transitions;
+
+/**
+ * View model for the window decoration with a caption and shadows. Works with
+ * {@link CaptionWindowDecoration}.
+ */
+public class CaptionWindowDecorViewModel implements WindowDecorViewModel<CaptionWindowDecoration> {
+ private final ActivityTaskManager mActivityTaskManager;
+ private final ShellTaskOrganizer mTaskOrganizer;
+ private final Context mContext;
+ private final Handler mMainHandler;
+ private final Choreographer mMainChoreographer;
+ private final DisplayController mDisplayController;
+ private final SyncTransactionQueue mSyncQueue;
+ private FreeformTaskTransitionStarter mTransitionStarter;
+ private DesktopModeController mDesktopModeController;
+
+ public CaptionWindowDecorViewModel(
+ Context context,
+ Handler mainHandler,
+ Choreographer mainChoreographer,
+ ShellTaskOrganizer taskOrganizer,
+ DisplayController displayController,
+ SyncTransactionQueue syncQueue,
+ DesktopModeController desktopModeController) {
+ mContext = context;
+ mMainHandler = mainHandler;
+ mMainChoreographer = mainChoreographer;
+ mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
+ mTaskOrganizer = taskOrganizer;
+ mDisplayController = displayController;
+ mSyncQueue = syncQueue;
+ mDesktopModeController = desktopModeController;
+ }
+
+ @Override
+ public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
+ mTransitionStarter = transitionStarter;
+ }
+
+ @Override
+ public CaptionWindowDecoration createWindowDecoration(
+ ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ if (!shouldShowWindowDecor(taskInfo)) return null;
+ final CaptionWindowDecoration windowDecoration = new CaptionWindowDecoration(
+ mContext,
+ mDisplayController,
+ mTaskOrganizer,
+ taskInfo,
+ taskSurface,
+ mMainHandler,
+ mMainChoreographer,
+ mSyncQueue);
+ TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration);
+ CaptionTouchEventListener touchEventListener =
+ new CaptionTouchEventListener(taskInfo, taskPositioner);
+ windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
+ windowDecoration.setDragResizeCallback(taskPositioner);
+ setupWindowDecorationForTransition(taskInfo, startT, finishT, windowDecoration);
+ setupCaptionColor(taskInfo, windowDecoration);
+ return windowDecoration;
+ }
+
+ @Override
+ public CaptionWindowDecoration adoptWindowDecoration(AutoCloseable windowDecor) {
+ if (!(windowDecor instanceof CaptionWindowDecoration)) return null;
+ final CaptionWindowDecoration captionWindowDecor = (CaptionWindowDecoration) windowDecor;
+ if (!shouldShowWindowDecor(captionWindowDecor.mTaskInfo)) {
+ return null;
+ }
+ return captionWindowDecor;
+ }
+
+ @Override
+ public void onTaskInfoChanged(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) {
+ decoration.relayout(taskInfo);
+
+ setupCaptionColor(taskInfo, decoration);
+ }
+
+ private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) {
+ int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
+ decoration.setCaptionColor(statusBarColor);
+ }
+
+ @Override
+ public void setupWindowDecorationForTransition(
+ RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT,
+ CaptionWindowDecoration decoration) {
+ decoration.relayout(taskInfo, startT, finishT);
+ }
+
+ private class CaptionTouchEventListener implements
+ View.OnClickListener, View.OnTouchListener {
+
+ private final int mTaskId;
+ private final WindowContainerToken mTaskToken;
+ private final DragResizeCallback mDragResizeCallback;
+
+ private int mDragPointerId = -1;
+
+ private CaptionTouchEventListener(
+ RunningTaskInfo taskInfo,
+ DragResizeCallback dragResizeCallback) {
+ mTaskId = taskInfo.taskId;
+ mTaskToken = taskInfo.token;
+ mDragResizeCallback = dragResizeCallback;
+ }
+
+ @Override
+ public void onClick(View v) {
+ final int id = v.getId();
+ if (id == R.id.close_window) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.removeTask(mTaskToken);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitionStarter.startRemoveTransition(wct);
+ } else {
+ mSyncQueue.queue(wct);
+ }
+ } else if (id == R.id.maximize_window) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+ int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN
+ ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM;
+ int displayWindowingMode =
+ taskInfo.configuration.windowConfiguration.getDisplayWindowingMode();
+ wct.setWindowingMode(mTaskToken,
+ targetWindowingMode == displayWindowingMode
+ ? WINDOWING_MODE_UNDEFINED : targetWindowingMode);
+ if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+ wct.setBounds(mTaskToken, null);
+ }
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitionStarter.startWindowingModeTransition(targetWindowingMode, wct);
+ } else {
+ mSyncQueue.queue(wct);
+ }
+ } else if (id == R.id.minimize_window) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.reorder(mTaskToken, false);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitionStarter.startMinimizedModeTransition(wct);
+ } else {
+ mSyncQueue.queue(wct);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent e) {
+ if (v.getId() != R.id.caption) {
+ return false;
+ }
+ handleEventForMove(e);
+
+ if (e.getAction() != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+ if (taskInfo.isFocused) {
+ return false;
+ }
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.reorder(mTaskToken, true /* onTop */);
+ mSyncQueue.queue(wct);
+ return true;
+ }
+
+ private void handleEventForMove(MotionEvent e) {
+ RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+ int windowingMode = mDesktopModeController
+ .getDisplayAreaWindowingMode(taskInfo.displayId);
+ if (windowingMode == WINDOWING_MODE_FULLSCREEN) {
+ return;
+ }
+ switch (e.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mDragPointerId = e.getPointerId(0);
+ mDragResizeCallback.onDragResizeStart(
+ 0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
+ break;
+ case MotionEvent.ACTION_MOVE: {
+ int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+ mDragResizeCallback.onDragResizeMove(
+ e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+ int statusBarHeight = mDisplayController.getDisplayLayout(taskInfo.displayId)
+ .stableInsets().top;
+ mDragResizeCallback.onDragResizeEnd(
+ e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
+ if (e.getRawY(dragPointerIdx) <= statusBarHeight
+ && windowingMode == WINDOWING_MODE_FREEFORM) {
+ mDesktopModeController.setDesktopModeActive(false);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
+ if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
+ return DesktopModeStatus.IS_SUPPORTED
+ && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
+ && mDisplayController.getDisplayContext(taskInfo.displayId)
+ .getResources().getConfiguration().smallestScreenWidthDp >= 600;
+ }
+}
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
new file mode 100644
index 000000000000..733f6b7d5dbf
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -0,0 +1,217 @@
+/*
+ * 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.windowdecor;
+
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.VectorDrawable;
+import android.os.Handler;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.window.WindowContainerTransaction;
+
+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.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
+
+/**
+ * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
+ * {@link CaptionWindowDecorViewModel}. The caption bar contains maximize and close buttons.
+ *
+ * {@link CaptionWindowDecorViewModel} can change the color of the caption bar based on the foremost
+ * app's request through {@link #setCaptionColor(int)}, in which it changes the foreground color of
+ * caption buttons according to the luminance of the background.
+ *
+ * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
+ */
+public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
+ // The thickness of shadows of a window that has focus in DIP.
+ private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20;
+ // The thickness of shadows of a window that doesn't have focus in DIP.
+ private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5;
+
+ // Height of button (32dp) + 2 * margin (5dp each)
+ private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42;
+ private static final int RESIZE_HANDLE_IN_DIP = 30;
+
+ private static final Rect EMPTY_OUTSET = new Rect();
+ private static final Rect RESIZE_HANDLE_OUTSET = new Rect(
+ RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP);
+
+ private final Handler mHandler;
+ private final Choreographer mChoreographer;
+ private final SyncTransactionQueue mSyncQueue;
+
+ private View.OnClickListener mOnCaptionButtonClickListener;
+ private View.OnTouchListener mOnCaptionTouchListener;
+ private DragResizeCallback mDragResizeCallback;
+
+ private DragResizeInputListener mDragResizeListener;
+
+ private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
+ new WindowDecoration.RelayoutResult<>();
+
+ CaptionWindowDecoration(
+ Context context,
+ DisplayController displayController,
+ ShellTaskOrganizer taskOrganizer,
+ ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ Handler handler,
+ Choreographer choreographer,
+ SyncTransactionQueue syncQueue) {
+ super(context, displayController, taskOrganizer, taskInfo, taskSurface);
+
+ mHandler = handler;
+ mChoreographer = choreographer;
+ mSyncQueue = syncQueue;
+ }
+
+ void setCaptionListeners(
+ View.OnClickListener onCaptionButtonClickListener,
+ View.OnTouchListener onCaptionTouchListener) {
+ mOnCaptionButtonClickListener = onCaptionButtonClickListener;
+ mOnCaptionTouchListener = onCaptionTouchListener;
+ }
+
+ void setDragResizeCallback(DragResizeCallback dragResizeCallback) {
+ mDragResizeCallback = dragResizeCallback;
+ }
+
+ @Override
+ void relayout(ActivityManager.RunningTaskInfo taskInfo) {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ relayout(taskInfo, t, t);
+ mSyncQueue.runInSync(transaction -> {
+ transaction.merge(t);
+ t.close();
+ });
+ }
+
+ void relayout(ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
+ final int shadowRadiusDp = taskInfo.isFocused
+ ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP;
+ final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode()
+ == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+ final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable;
+ final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET;
+
+ WindowDecorLinearLayout oldRootView = mResult.mRootView;
+ final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ relayout(taskInfo, R.layout.caption_window_decoration, oldRootView,
+ DECOR_CAPTION_HEIGHT_IN_DIP, outset, shadowRadiusDp, startT, finishT, wct, mResult);
+ taskInfo = null; // Clear it just in case we use it accidentally
+
+ mTaskOrganizer.applyTransaction(wct);
+
+ if (mResult.mRootView == null) {
+ // This means something blocks the window decor from showing, e.g. the task is hidden.
+ // Nothing is set up in this case including the decoration surface.
+ return;
+ }
+ if (oldRootView != mResult.mRootView) {
+ setupRootView();
+ }
+
+ if (!isDragResizeable) {
+ closeDragResizeListener();
+ return;
+ }
+
+ if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) {
+ closeDragResizeListener();
+ mDragResizeListener = new DragResizeInputListener(
+ mContext,
+ mHandler,
+ mChoreographer,
+ mDisplay.getDisplayId(),
+ mDecorationContainerSurface,
+ mDragResizeCallback);
+ }
+
+ mDragResizeListener.setGeometry(
+ mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP));
+ }
+
+ /**
+ * Sets up listeners when a new root view is created.
+ */
+ private void setupRootView() {
+ View caption = mResult.mRootView.findViewById(R.id.caption);
+ caption.setOnTouchListener(mOnCaptionTouchListener);
+ View maximize = caption.findViewById(R.id.maximize_window);
+ if (DesktopModeStatus.IS_SUPPORTED) {
+ // Hide maximize button when desktop mode is available
+ maximize.setVisibility(View.GONE);
+ } else {
+ maximize.setVisibility(View.VISIBLE);
+ maximize.setOnClickListener(mOnCaptionButtonClickListener);
+ }
+ View close = caption.findViewById(R.id.close_window);
+ close.setOnClickListener(mOnCaptionButtonClickListener);
+ View minimize = caption.findViewById(R.id.minimize_window);
+ minimize.setOnClickListener(mOnCaptionButtonClickListener);
+ }
+
+ void setCaptionColor(int captionColor) {
+ if (mResult.mRootView == null) {
+ return;
+ }
+
+ View caption = mResult.mRootView.findViewById(R.id.caption);
+ GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground();
+ captionDrawable.setColor(captionColor);
+
+ int buttonTintColorRes =
+ Color.valueOf(captionColor).luminance() < 0.5
+ ? R.color.decor_button_light_color
+ : R.color.decor_button_dark_color;
+ ColorStateList buttonTintColor =
+ caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
+ View maximize = caption.findViewById(R.id.maximize_window);
+ VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground();
+ maximizeBackground.setTintList(buttonTintColor);
+
+ View close = caption.findViewById(R.id.close_window);
+ VectorDrawable closeBackground = (VectorDrawable) close.getBackground();
+ closeBackground.setTintList(buttonTintColor);
+ }
+
+ private void closeDragResizeListener() {
+ if (mDragResizeListener == null) {
+ return;
+ }
+ mDragResizeListener.close();
+ mDragResizeListener = null;
+ }
+
+ @Override
+ public void close() {
+ closeDragResizeListener();
+ super.close();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java
new file mode 100644
index 000000000000..ee160a15df19
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java
@@ -0,0 +1,46 @@
+/*
+ * 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.windowdecor;
+
+/**
+ * Callback called when receiving drag-resize or drag-move related input events.
+ */
+public interface DragResizeCallback {
+ /**
+ * Called when a drag resize starts.
+ *
+ * @param ctrlType {@link TaskPositioner.CtrlType} indicating the direction of resizing, use
+ * {@code 0} to indicate it's a move
+ * @param x x coordinate in window decoration coordinate system where the drag resize starts
+ * @param y y coordinate in window decoration coordinate system where the drag resize starts
+ */
+ void onDragResizeStart(@TaskPositioner.CtrlType int ctrlType, float x, float y);
+
+ /**
+ * Called when the pointer moves during a drag resize.
+ * @param x x coordinate in window decoration coordinate system of the new pointer location
+ * @param y y coordinate in window decoration coordinate system of the new pointer location
+ */
+ void onDragResizeMove(float x, float y);
+
+ /**
+ * Called when a drag resize stops.
+ * @param x x coordinate in window decoration coordinate system where the drag resize stops
+ * @param y y coordinate in window decoration coordinate system where the drag resize stops
+ */
+ void onDragResizeEnd(float x, float y);
+}
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
new file mode 100644
index 000000000000..3d014959a952
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -0,0 +1,295 @@
+/*
+ * 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.windowdecor;
+
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.hardware.input.InputManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.Choreographer;
+import android.view.IWindowSession;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.SurfaceControl;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.view.BaseIWindow;
+
+/**
+ * An input event listener registered to InputDispatcher to receive input events on task edges and
+ * convert them to drag resize requests.
+ */
+class DragResizeInputListener implements AutoCloseable {
+ private static final String TAG = "DragResizeInputListener";
+
+ private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession();
+ private final Handler mHandler;
+ private final Choreographer mChoreographer;
+ private final InputManager mInputManager;
+
+ private final int mDisplayId;
+ private final BaseIWindow mFakeWindow;
+ private final IBinder mFocusGrantToken;
+ private final SurfaceControl mDecorationSurface;
+ private final InputChannel mInputChannel;
+ private final TaskResizeInputEventReceiver mInputEventReceiver;
+ private final com.android.wm.shell.windowdecor.DragResizeCallback mCallback;
+
+ private int mWidth;
+ private int mHeight;
+ private int mResizeHandleThickness;
+
+ private int mDragPointerId = -1;
+
+ DragResizeInputListener(
+ Context context,
+ Handler handler,
+ Choreographer choreographer,
+ int displayId,
+ SurfaceControl decorationSurface,
+ DragResizeCallback callback) {
+ mInputManager = context.getSystemService(InputManager.class);
+ mHandler = handler;
+ mChoreographer = choreographer;
+ mDisplayId = displayId;
+ mDecorationSurface = decorationSurface;
+ // Use a fake window as the backing surface is a container layer and we don't want to create
+ // a buffer layer for it so we can't use ViewRootImpl.
+ mFakeWindow = new BaseIWindow();
+ mFakeWindow.setSession(mWindowSession);
+ mFocusGrantToken = new Binder();
+ mInputChannel = new InputChannel();
+ try {
+ mWindowSession.grantInputChannel(
+ mDisplayId,
+ mDecorationSurface,
+ mFakeWindow,
+ null /* hostInputToken */,
+ FLAG_NOT_FOCUSABLE,
+ PRIVATE_FLAG_TRUSTED_OVERLAY,
+ TYPE_APPLICATION,
+ mFocusGrantToken,
+ TAG + " of " + decorationSurface.toString(),
+ mInputChannel);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+
+ mInputEventReceiver = new TaskResizeInputEventReceiver(
+ mInputChannel, mHandler, mChoreographer);
+ mCallback = callback;
+ }
+
+ /**
+ * Updates geometry of this drag resize handler. Needs to be called every time there is a size
+ * change to notify the input event receiver it's ready to take the next input event. Otherwise
+ * it'll keep batching move events and the drag resize process is stalled.
+ *
+ * This is also used to update the touch regions of this handler every event dispatched here is
+ * a potential resize request.
+ *
+ * @param width The width of the drag resize handler in pixels, including resize handle
+ * thickness. That is task width + 2 * resize handle thickness.
+ * @param height The height of the drag resize handler in pixels, including resize handle
+ * thickness. That is task height + 2 * resize handle thickness.
+ * @param resizeHandleThickness The thickness of the resize handle in pixels.
+ */
+ void setGeometry(int width, int height, int resizeHandleThickness) {
+ if (mWidth == width && mHeight == height
+ && mResizeHandleThickness == resizeHandleThickness) {
+ return;
+ }
+
+ mWidth = width;
+ mHeight = height;
+ mResizeHandleThickness = resizeHandleThickness;
+
+ Region touchRegion = new Region();
+ final Rect topInputBounds = new Rect(0, 0, mWidth, mResizeHandleThickness);
+ touchRegion.union(topInputBounds);
+
+ final Rect leftInputBounds = new Rect(0, mResizeHandleThickness,
+ mResizeHandleThickness, mHeight - mResizeHandleThickness);
+ touchRegion.union(leftInputBounds);
+
+ final Rect rightInputBounds = new Rect(
+ mWidth - mResizeHandleThickness, mResizeHandleThickness,
+ mWidth, mHeight - mResizeHandleThickness);
+ touchRegion.union(rightInputBounds);
+
+ final Rect bottomInputBounds = new Rect(0, mHeight - mResizeHandleThickness,
+ mWidth, mHeight);
+ touchRegion.union(bottomInputBounds);
+
+ try {
+ mWindowSession.updateInputChannel(
+ mInputChannel.getToken(),
+ mDisplayId,
+ mDecorationSurface,
+ FLAG_NOT_FOCUSABLE,
+ PRIVATE_FLAG_TRUSTED_OVERLAY,
+ touchRegion);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void close() {
+ mInputChannel.dispose();
+ try {
+ mWindowSession.remove(mFakeWindow);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ private class TaskResizeInputEventReceiver extends InputEventReceiver {
+ private final Choreographer mChoreographer;
+ private final Runnable mConsumeBatchEventRunnable;
+ private boolean mConsumeBatchEventScheduled;
+
+ private TaskResizeInputEventReceiver(
+ InputChannel inputChannel, Handler handler, Choreographer choreographer) {
+ super(inputChannel, handler.getLooper());
+ mChoreographer = choreographer;
+
+ mConsumeBatchEventRunnable = () -> {
+ mConsumeBatchEventScheduled = false;
+ if (consumeBatchedInputEvents(mChoreographer.getFrameTimeNanos())) {
+ // If we consumed a batch here, we want to go ahead and schedule the
+ // consumption of batched input events on the next frame. Otherwise, we would
+ // wait until we have more input events pending and might get starved by other
+ // things occurring in the process.
+ scheduleConsumeBatchEvent();
+ }
+ };
+ }
+
+ @Override
+ public void onBatchedInputEventPending(int source) {
+ scheduleConsumeBatchEvent();
+ }
+
+ private void scheduleConsumeBatchEvent() {
+ if (mConsumeBatchEventScheduled) {
+ return;
+ }
+ mChoreographer.postCallback(
+ Choreographer.CALLBACK_INPUT, mConsumeBatchEventRunnable, null);
+ mConsumeBatchEventScheduled = true;
+ }
+
+ @Override
+ public void onInputEvent(InputEvent inputEvent) {
+ finishInputEvent(inputEvent, handleInputEvent(inputEvent));
+ }
+
+ private boolean handleInputEvent(InputEvent inputEvent) {
+ if (!(inputEvent instanceof MotionEvent)) {
+ return false;
+ }
+
+ MotionEvent e = (MotionEvent) inputEvent;
+ switch (e.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ mDragPointerId = e.getPointerId(0);
+ mCallback.onDragResizeStart(
+ calculateCtrlType(e.getX(0), e.getY(0)), e.getRawX(0), e.getRawY(0));
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ int dragPointerIndex = e.findPointerIndex(mDragPointerId);
+ mCallback.onDragResizeMove(
+ e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ int dragPointerIndex = e.findPointerIndex(mDragPointerId);
+ mCallback.onDragResizeEnd(
+ e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
+ mDragPointerId = -1;
+ break;
+ }
+ case MotionEvent.ACTION_HOVER_ENTER:
+ case MotionEvent.ACTION_HOVER_MOVE: {
+ updateCursorType(e.getXCursorPosition(), e.getYCursorPosition());
+ break;
+ }
+ case MotionEvent.ACTION_HOVER_EXIT:
+ mInputManager.setPointerIconType(PointerIcon.TYPE_DEFAULT);
+ break;
+ }
+ return true;
+ }
+
+ @TaskPositioner.CtrlType
+ private int calculateCtrlType(float x, float y) {
+ int ctrlType = 0;
+ if (x < mResizeHandleThickness) {
+ ctrlType |= TaskPositioner.CTRL_TYPE_LEFT;
+ }
+ if (x > mWidth - mResizeHandleThickness) {
+ ctrlType |= TaskPositioner.CTRL_TYPE_RIGHT;
+ }
+ if (y < mResizeHandleThickness) {
+ ctrlType |= TaskPositioner.CTRL_TYPE_TOP;
+ }
+ if (y > mHeight - mResizeHandleThickness) {
+ ctrlType |= TaskPositioner.CTRL_TYPE_BOTTOM;
+ }
+ return ctrlType;
+ }
+
+ private void updateCursorType(float x, float y) {
+ @TaskPositioner.CtrlType int ctrlType = calculateCtrlType(x, y);
+
+ int cursorType = PointerIcon.TYPE_DEFAULT;
+ switch (ctrlType) {
+ case TaskPositioner.CTRL_TYPE_LEFT:
+ case TaskPositioner.CTRL_TYPE_RIGHT:
+ cursorType = PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
+ break;
+ case TaskPositioner.CTRL_TYPE_TOP:
+ case TaskPositioner.CTRL_TYPE_BOTTOM:
+ cursorType = PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
+ break;
+ case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_TOP:
+ case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_BOTTOM:
+ cursorType = PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
+ break;
+ case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_BOTTOM:
+ case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_TOP:
+ cursorType = PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
+ break;
+ }
+ mInputManager.setPointerIconType(cursorType);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java
new file mode 100644
index 000000000000..1c61802bbd5c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java
@@ -0,0 +1,21 @@
+/*
+ * 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.windowdecor;
+
+interface TaskFocusStateConsumer {
+ void setTaskFocusState(boolean focused);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
new file mode 100644
index 000000000000..280569b05d87
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
@@ -0,0 +1,99 @@
+/*
+ * 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.windowdecor;
+
+import android.annotation.IntDef;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+
+class TaskPositioner implements DragResizeCallback {
+
+ @IntDef({CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM})
+ @interface CtrlType {}
+
+ static final int CTRL_TYPE_LEFT = 1;
+ static final int CTRL_TYPE_RIGHT = 2;
+ static final int CTRL_TYPE_TOP = 4;
+ static final int CTRL_TYPE_BOTTOM = 8;
+
+ private final ShellTaskOrganizer mTaskOrganizer;
+ private final WindowDecoration mWindowDecoration;
+
+ private final Rect mTaskBoundsAtDragStart = new Rect();
+ private final PointF mResizeStartPoint = new PointF();
+ private final Rect mResizeTaskBounds = new Rect();
+
+ private int mCtrlType;
+
+ TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration) {
+ mTaskOrganizer = taskOrganizer;
+ mWindowDecoration = windowDecoration;
+ }
+
+ @Override
+ public void onDragResizeStart(int ctrlType, float x, float y) {
+ mCtrlType = ctrlType;
+
+ mTaskBoundsAtDragStart.set(
+ mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
+ mResizeStartPoint.set(x, y);
+ }
+
+ @Override
+ public void onDragResizeMove(float x, float y) {
+ changeBounds(x, y);
+ }
+
+ @Override
+ public void onDragResizeEnd(float x, float y) {
+ changeBounds(x, y);
+
+ mCtrlType = 0;
+ mTaskBoundsAtDragStart.setEmpty();
+ mResizeStartPoint.set(0, 0);
+ }
+
+ private void changeBounds(float x, float y) {
+ float deltaX = x - mResizeStartPoint.x;
+ mResizeTaskBounds.set(mTaskBoundsAtDragStart);
+ if ((mCtrlType & CTRL_TYPE_LEFT) != 0) {
+ mResizeTaskBounds.left += deltaX;
+ }
+ if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) {
+ mResizeTaskBounds.right += deltaX;
+ }
+ float deltaY = y - mResizeStartPoint.y;
+ if ((mCtrlType & CTRL_TYPE_TOP) != 0) {
+ mResizeTaskBounds.top += deltaY;
+ }
+ if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) {
+ mResizeTaskBounds.bottom += deltaY;
+ }
+ if (mCtrlType == 0) {
+ mResizeTaskBounds.offset((int) deltaX, (int) deltaY);
+ }
+
+ if (!mResizeTaskBounds.isEmpty()) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds);
+ mTaskOrganizer.applyTransaction(wct);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorLinearLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorLinearLayout.java
new file mode 100644
index 000000000000..6d8001a2f92b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorLinearLayout.java
@@ -0,0 +1,72 @@
+/*
+ * 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.windowdecor;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.R;
+
+/**
+ * A {@link LinearLayout} that takes an additional task focused drawable state. The new state is
+ * used to select the correct background color for views in the window decoration.
+ */
+public class WindowDecorLinearLayout extends LinearLayout implements TaskFocusStateConsumer {
+ private static final int[] TASK_FOCUSED_STATE = { R.attr.state_task_focused };
+
+ private boolean mIsTaskFocused;
+
+ public WindowDecorLinearLayout(Context context) {
+ super(context);
+ }
+
+ public WindowDecorLinearLayout(Context context,
+ @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WindowDecorLinearLayout(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public WindowDecorLinearLayout(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void setTaskFocusState(boolean focused) {
+ mIsTaskFocused = focused;
+
+ refreshDrawableState();
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if (!mIsTaskFocused) {
+ return super.onCreateDrawableState(extraSpace);
+ }
+
+ final int[] states = super.onCreateDrawableState(extraSpace + 1);
+ mergeDrawableStates(states, TASK_FOCUSED_STATE);
+ return states;
+ }
+}
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
new file mode 100644
index 000000000000..d9697d288ab6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -0,0 +1,90 @@
+/*
+ * 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.windowdecor;
+
+import android.app.ActivityManager;
+import android.view.SurfaceControl;
+
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+
+/**
+ * The interface used by some {@link com.android.wm.shell.ShellTaskOrganizer.TaskListener} to help
+ * customize {@link WindowDecoration}. Its implementations are responsible to interpret user's
+ * interactions with UI widgets in window decorations and send corresponding requests to system
+ * servers.
+ *
+ * @param <T> The actual decoration type
+ */
+public interface WindowDecorViewModel<T extends AutoCloseable> {
+
+ /**
+ * Sets the transition starter that starts freeform task transitions.
+ *
+ * @param transitionStarter the transition starter that starts freeform task transitions
+ */
+ void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter);
+
+ /**
+ * Creates a window decoration for the given task.
+ * Can be {@code null} for Fullscreen tasks but not Freeform ones.
+ *
+ * @param taskInfo the initial task info of the task
+ * @param taskSurface the surface of the task
+ * @param startT the start transaction to be applied before the transition
+ * @param finishT the finish transaction to restore states after the transition
+ * @return the window decoration object
+ */
+ @Nullable T createWindowDecoration(
+ ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT);
+
+ /**
+ * Adopts the window decoration if possible.
+ * May be {@code null} if a window decor is not needed or the given one is incompatible.
+ *
+ * @param windowDecor the potential window decoration to adopt
+ * @return the window decoration if it can be adopted, or {@code null} otherwise.
+ */
+ @Nullable T adoptWindowDecoration(@Nullable AutoCloseable windowDecor);
+
+ /**
+ * Notifies a task info update on the given task, with the window decoration created previously
+ * for this task by {@link #createWindowDecoration}.
+ *
+ * @param taskInfo the new task info of the task
+ * @param windowDecoration the window decoration created for the task
+ */
+ void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo, T windowDecoration);
+
+ /**
+ * Notifies a transition is about to start about the given task to give the window decoration a
+ * chance to prepare for this transition.
+ *
+ * @param startT the start transaction to be applied before the transition
+ * @param finishT the finish transaction to restore states after the transition
+ * @param windowDecoration the window decoration created for the task
+ */
+ void setupWindowDecorationForTransition(
+ ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT,
+ T windowDecoration);
+}
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
new file mode 100644
index 000000000000..3e3a864f48c7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -0,0 +1,391 @@
+/*
+ * 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.windowdecor;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.InsetsState;
+import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+
+import java.util.function.Supplier;
+
+/**
+ * Manages a container surface and a windowless window to show window decoration. Responsible to
+ * update window decoration window state and layout parameters on task info changes and so that
+ * window decoration is in correct state and bounds.
+ *
+ * The container surface is a child of the task display area in the same display, so that window
+ * decorations can be drawn out of the task bounds and receive input events from out of the task
+ * bounds to support drag resizing.
+ *
+ * The windowless window that hosts window decoration is positioned in front of all activities, to
+ * allow the foreground activity to draw its own background behind window decorations, such as
+ * the window captions.
+ *
+ * @param <T> The type of the root view
+ */
+public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
+ implements AutoCloseable {
+ private static final int[] CAPTION_INSETS_TYPES = { InsetsState.ITYPE_CAPTION_BAR };
+
+ /**
+ * System-wide context. Only used to create context with overridden configurations.
+ */
+ final Context mContext;
+ final DisplayController mDisplayController;
+ final ShellTaskOrganizer mTaskOrganizer;
+ final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier;
+ final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
+ final Supplier<WindowContainerTransaction> mWindowContainerTransactionSupplier;
+ final SurfaceControlViewHostFactory mSurfaceControlViewHostFactory;
+ private final DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener =
+ new DisplayController.OnDisplaysChangedListener() {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ if (mTaskInfo.displayId != displayId) {
+ return;
+ }
+
+ mDisplayController.removeDisplayWindowListener(this);
+ relayout(mTaskInfo);
+ }
+ };
+
+ RunningTaskInfo mTaskInfo;
+ final SurfaceControl mTaskSurface;
+
+ Display mDisplay;
+ Context mDecorWindowContext;
+ SurfaceControl mDecorationContainerSurface;
+ SurfaceControl mTaskBackgroundSurface;
+
+ SurfaceControl mCaptionContainerSurface;
+ private CaptionWindowManager mCaptionWindowManager;
+ private SurfaceControlViewHost mViewHost;
+
+ private final Rect mCaptionInsetsRect = new Rect();
+ private final Rect mTaskSurfaceCrop = new Rect();
+ private final float[] mTmpColor = new float[3];
+
+ WindowDecoration(
+ Context context,
+ DisplayController displayController,
+ ShellTaskOrganizer taskOrganizer,
+ RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface) {
+ this(context, displayController, taskOrganizer, taskInfo, taskSurface,
+ SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
+ WindowContainerTransaction::new, new SurfaceControlViewHostFactory() {});
+ }
+
+ WindowDecoration(
+ Context context,
+ DisplayController displayController,
+ ShellTaskOrganizer taskOrganizer,
+ RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
+ Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
+ Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
+ SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
+ mContext = context;
+ mDisplayController = displayController;
+ mTaskOrganizer = taskOrganizer;
+ mTaskInfo = taskInfo;
+ mTaskSurface = taskSurface;
+ mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier;
+ mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
+ mWindowContainerTransactionSupplier = windowContainerTransactionSupplier;
+ mSurfaceControlViewHostFactory = surfaceControlViewHostFactory;
+
+ mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId);
+ mDecorWindowContext = mContext.createConfigurationContext(mTaskInfo.getConfiguration());
+ }
+
+ /**
+ * Used by {@link WindowDecoration} to trigger a new relayout because the requirements for a
+ * relayout weren't satisfied are satisfied now.
+ *
+ * @param taskInfo The previous {@link RunningTaskInfo} passed into {@link #relayout} or the
+ * constructor.
+ */
+ abstract void relayout(RunningTaskInfo taskInfo);
+
+ void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp,
+ Rect outsetsDp, float shadowRadiusDp, SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT, WindowContainerTransaction wct,
+ RelayoutResult<T> outResult) {
+ outResult.reset();
+
+ final Configuration oldTaskConfig = mTaskInfo.getConfiguration();
+ if (taskInfo != null) {
+ mTaskInfo = taskInfo;
+ }
+
+ if (!mTaskInfo.isVisible) {
+ releaseViews();
+ finishT.hide(mTaskSurface);
+ return;
+ }
+
+ if (rootView == null && layoutResId == 0) {
+ throw new IllegalArgumentException("layoutResId and rootView can't both be invalid.");
+ }
+
+ outResult.mRootView = rootView;
+ rootView = null; // Clear it just in case we use it accidentally
+ final Configuration taskConfig = mTaskInfo.getConfiguration();
+ if (oldTaskConfig.densityDpi != taskConfig.densityDpi
+ || mDisplay == null
+ || mDisplay.getDisplayId() != mTaskInfo.displayId) {
+ releaseViews();
+
+ if (!obtainDisplayOrRegisterListener()) {
+ outResult.mRootView = null;
+ return;
+ }
+ mDecorWindowContext = mContext.createConfigurationContext(taskConfig);
+ if (layoutResId != 0) {
+ outResult.mRootView =
+ (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+ }
+ }
+
+ if (outResult.mRootView == null) {
+ outResult.mRootView =
+ (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+ }
+
+ // DecorationContainerSurface
+ if (mDecorationContainerSurface == null) {
+ final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
+ mDecorationContainerSurface = builder
+ .setName("Decor container of Task=" + mTaskInfo.taskId)
+ .setContainerLayer()
+ .setParent(mTaskSurface)
+ .build();
+
+ startT.setTrustedOverlay(mDecorationContainerSurface, true);
+ }
+
+ final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
+ outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+ final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity);
+ final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity);
+ outResult.mWidth = taskBounds.width()
+ + (int) (outsetsDp.right * outResult.mDensity)
+ - decorContainerOffsetX;
+ outResult.mHeight = taskBounds.height()
+ + (int) (outsetsDp.bottom * outResult.mDensity)
+ - decorContainerOffsetY;
+ startT.setPosition(
+ mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY)
+ .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight)
+ // TODO(b/244455401): Change the z-order when it's better organized
+ .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1)
+ .show(mDecorationContainerSurface);
+
+ // TaskBackgroundSurface
+ if (mTaskBackgroundSurface == null) {
+ final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
+ mTaskBackgroundSurface = builder
+ .setName("Background of Task=" + mTaskInfo.taskId)
+ .setEffectLayer()
+ .setParent(mTaskSurface)
+ .build();
+ }
+
+ float shadowRadius = outResult.mDensity * shadowRadiusDp;
+ int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
+ mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
+ mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
+ mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
+ startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height())
+ .setShadowRadius(mTaskBackgroundSurface, shadowRadius)
+ .setColor(mTaskBackgroundSurface, mTmpColor)
+ // TODO(b/244455401): Change the z-order when it's better organized
+ .setLayer(mTaskBackgroundSurface, -1)
+ .show(mTaskBackgroundSurface);
+
+ // CaptionContainerSurface, CaptionWindowManager
+ if (mCaptionContainerSurface == null) {
+ final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
+ mCaptionContainerSurface = builder
+ .setName("Caption container of Task=" + mTaskInfo.taskId)
+ .setContainerLayer()
+ .setParent(mDecorationContainerSurface)
+ .build();
+ }
+
+ final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity);
+ startT.setPosition(
+ mCaptionContainerSurface, -decorContainerOffsetX, -decorContainerOffsetY)
+ .setWindowCrop(mCaptionContainerSurface, taskBounds.width(), captionHeight)
+ .show(mCaptionContainerSurface);
+
+ 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.
+ mCaptionWindowManager = new CaptionWindowManager(
+ mTaskInfo.getConfiguration(), mCaptionContainerSurface);
+ }
+
+ // Caption view
+ mCaptionWindowManager.setConfiguration(taskConfig);
+ final WindowManager.LayoutParams lp =
+ new WindowManager.LayoutParams(taskBounds.width(), captionHeight,
+ WindowManager.LayoutParams.TYPE_APPLICATION,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
+ lp.setTitle("Caption of Task=" + mTaskInfo.taskId);
+ lp.setTrustedOverlay();
+ if (mViewHost == null) {
+ mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay,
+ mCaptionWindowManager);
+ mViewHost.setView(outResult.mRootView, lp);
+ } else {
+ mViewHost.relayout(lp);
+ }
+
+ if (ViewRootImpl.CAPTION_ON_SHELL) {
+ outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
+
+ // Caption insets
+ mCaptionInsetsRect.set(taskBounds);
+ mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight;
+ wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES);
+ } else {
+ startT.hide(mCaptionContainerSurface);
+ }
+
+ // Task surface itself
+ Point taskPosition = mTaskInfo.positionInParent;
+ mTaskSurfaceCrop.set(
+ decorContainerOffsetX,
+ decorContainerOffsetY,
+ outResult.mWidth + decorContainerOffsetX,
+ outResult.mHeight + decorContainerOffsetY);
+ startT.show(mTaskSurface);
+ finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y)
+ .setCrop(mTaskSurface, mTaskSurfaceCrop);
+ }
+
+ /**
+ * Obtains the {@link Display} instance for the display ID in {@link #mTaskInfo} if it exists or
+ * registers {@link #mOnDisplaysChangedListener} if it doesn't.
+ *
+ * @return {@code true} if the {@link Display} instance exists; or {@code false} otherwise
+ */
+ private boolean obtainDisplayOrRegisterListener() {
+ mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId);
+ if (mDisplay == null) {
+ mDisplayController.addDisplayWindowListener(mOnDisplaysChangedListener);
+ return false;
+ }
+ return true;
+ }
+
+ private void releaseViews() {
+ if (mViewHost != null) {
+ mViewHost.release();
+ mViewHost = null;
+ }
+
+ mCaptionWindowManager = null;
+
+ final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+ boolean released = false;
+ if (mCaptionContainerSurface != null) {
+ t.remove(mCaptionContainerSurface);
+ mCaptionContainerSurface = null;
+ released = true;
+ }
+
+ if (mDecorationContainerSurface != null) {
+ t.remove(mDecorationContainerSurface);
+ mDecorationContainerSurface = null;
+ released = true;
+ }
+
+ if (mTaskBackgroundSurface != null) {
+ t.remove(mTaskBackgroundSurface);
+ mTaskBackgroundSurface = null;
+ released = true;
+ }
+
+ if (released) {
+ t.apply();
+ }
+
+ final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get();
+ wct.removeInsetsProvider(mTaskInfo.token, CAPTION_INSETS_TYPES);
+ mTaskOrganizer.applyTransaction(wct);
+ }
+
+ @Override
+ public void close() {
+ mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener);
+ releaseViews();
+ }
+
+ static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
+ int mWidth;
+ int mHeight;
+ float mDensity;
+ T mRootView;
+
+ void reset() {
+ mWidth = 0;
+ mHeight = 0;
+ mDensity = 0;
+ mRootView = null;
+ }
+ }
+
+ private static class CaptionWindowManager extends WindowlessWindowManager {
+ CaptionWindowManager(Configuration config, SurfaceControl rootSurface) {
+ super(config, rootSurface, null /* hostInputToken */);
+ }
+
+ @Override
+ public void setConfiguration(Configuration configuration) {
+ super.setConfiguration(configuration);
+ }
+ }
+
+ interface SurfaceControlViewHostFactory {
+ default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
+ return new SurfaceControlViewHost(c, d, wmm);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
index cb478c84c2b7..cba396a82a87 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
@@ -43,6 +43,84 @@ fun FlickerTestParameter.appPairsDividerBecomesVisible() {
}
}
+fun FlickerTestParameter.splitScreenDividerBecomesVisible() {
+ layerBecomesVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+}
+
+fun FlickerTestParameter.layerBecomesVisible(
+ component: FlickerComponentName
+) {
+ assertLayers {
+ this.isInvisible(component)
+ .then()
+ .isVisible(component)
+ }
+}
+
+fun FlickerTestParameter.layerIsVisibleAtEnd(
+ component: FlickerComponentName
+) {
+ assertLayersEnd {
+ this.isVisible(component)
+ }
+}
+
+fun FlickerTestParameter.splitAppLayerBoundsBecomesVisible(
+ rotation: Int,
+ component: FlickerComponentName,
+ splitLeftTop: Boolean
+) {
+ assertLayers {
+ val dividerRegion = this.last().layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
+ this.isInvisible(component)
+ .then()
+ .invoke("splitAppLayerBoundsBecomesVisible") {
+ it.visibleRegion(component).overlaps(
+ if (splitLeftTop) {
+ getSplitLeftTopRegion(dividerRegion, rotation)
+ } else {
+ getSplitRightBottomRegion(dividerRegion, rotation)
+ }
+ )
+ }
+ }
+}
+
+fun FlickerTestParameter.splitAppLayerBoundsIsVisibleAtEnd(
+ rotation: Int,
+ component: FlickerComponentName,
+ splitLeftTop: Boolean
+) {
+ assertLayersEnd {
+ val dividerRegion = layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
+ visibleRegion(component).overlaps(
+ if (splitLeftTop) {
+ getSplitLeftTopRegion(dividerRegion, rotation)
+ } else {
+ getSplitRightBottomRegion(dividerRegion, rotation)
+ }
+ )
+ }
+}
+
+fun FlickerTestParameter.appWindowBecomesVisible(
+ component: FlickerComponentName
+) {
+ assertWm {
+ this.isAppWindowInvisible(component)
+ .then()
+ .isAppWindowVisible(component)
+ }
+}
+
+fun FlickerTestParameter.appWindowIsVisibleAtEnd(
+ component: FlickerComponentName
+) {
+ assertWmEnd {
+ this.isAppWindowVisible(component)
+ }
+}
+
fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() {
assertLayersEnd {
this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
@@ -118,21 +196,53 @@ fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd(
fun getPrimaryRegion(dividerRegion: Region, rotation: Int): Region {
val displayBounds = WindowUtils.getDisplayBounds(rotation)
return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
- Region.from(0, 0, displayBounds.bounds.right,
- dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset)
+ Region.from(
+ 0, 0, displayBounds.bounds.right,
+ dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset
+ )
} else {
- Region.from(0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset,
- displayBounds.bounds.bottom)
+ Region.from(
+ 0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset,
+ displayBounds.bounds.bottom
+ )
}
}
fun getSecondaryRegion(dividerRegion: Region, rotation: Int): Region {
val displayBounds = WindowUtils.getDisplayBounds(rotation)
return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
- Region.from(0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset,
- displayBounds.bounds.right, displayBounds.bounds.bottom)
+ Region.from(
+ 0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset,
+ displayBounds.bounds.right, displayBounds.bounds.bottom
+ )
} else {
- Region.from(dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0,
- displayBounds.bounds.right, displayBounds.bounds.bottom)
+ Region.from(
+ dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0,
+ displayBounds.bounds.right, displayBounds.bounds.bottom
+ )
}
-} \ No newline at end of file
+}
+
+fun getSplitLeftTopRegion(dividerRegion: Region, rotation: Int): Region {
+ val displayBounds = WindowUtils.getDisplayBounds(rotation)
+ return if (displayBounds.width > displayBounds.height) {
+ Region.from(0, 0, dividerRegion.bounds.left, displayBounds.bounds.bottom)
+ } else {
+ Region.from(0, 0, displayBounds.bounds.right, dividerRegion.bounds.top)
+ }
+}
+
+fun getSplitRightBottomRegion(dividerRegion: Region, rotation: Int): Region {
+ val displayBounds = WindowUtils.getDisplayBounds(rotation)
+ return if (displayBounds.width > displayBounds.height) {
+ Region.from(
+ dividerRegion.bounds.right, 0, displayBounds.bounds.right,
+ displayBounds.bounds.bottom
+ )
+ } else {
+ Region.from(
+ 0, dividerRegion.bounds.bottom, displayBounds.bounds.right,
+ displayBounds.bounds.bottom
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
index 40891f36a5da..f56eb6e783aa 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
@@ -21,4 +21,5 @@ import com.android.server.wm.traces.common.FlickerComponentName
const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
val APP_PAIR_SPLIT_DIVIDER_COMPONENT = FlickerComponentName("", "AppPairSplitDivider#")
-val DOCKED_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider#") \ No newline at end of file
+val DOCKED_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider#")
+val SPLIT_SCREEN_DIVIDER_COMPONENT = FlickerComponentName("", "StageCoordinatorSplitDivider#")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt
deleted file mode 100644
index c9cab39b7d8b..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.apppairs
-
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd
-import com.android.wm.shell.flicker.helpers.AppPairsHelper
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
-import org.junit.After
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launch app from launcher. When the device doesn't support non-resizable in multi window
- * {@link Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW}, app pairs should not pair
- * non-resizable apps.
- *
- * To run this test: `atest WMShellFlickerTests:AppPairsTestCannotPairNonResizeableApps`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class AppPairsTestCannotPairNonResizeableApps(
- testSpec: FlickerTestParameter
-) : AppPairsTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- nonResizeableApp?.launchViaIntent(wmHelper)
- // TODO pair apps through normal UX flow
- executeShellCommand(
- composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true))
- nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) }
- }
- }
-
- @Before
- override fun setup() {
- super.setup()
- setSupportsNonResizableMultiWindow(instrumentation, -1)
- }
-
- @After
- override fun teardown() {
- super.teardown()
- resetMultiWindowConfig(instrumentation)
- }
-
- @Ignore
- @Test
- override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
-
- @Ignore
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- @Ignore
- @Test
- override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
-
- @Ignore
- @Test
- fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd()
-
- @Ignore
- @Test
- fun onlyResizeableAppWindowVisible() {
- val nonResizeableApp = nonResizeableApp
- require(nonResizeableApp != null) {
- "Non resizeable app not initialized"
- }
- testSpec.assertWmEnd {
- isAppWindowVisible(nonResizeableApp.component)
- isAppWindowInvisible(primaryApp.component)
- }
- }
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = AppPairsHelper.TEST_REPETITIONS)
- }
- }
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt
deleted file mode 100644
index 60c32c99d1ff..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.apppairs
-
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.AppPairsHelper
-import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launch app from launcher.
- * To run this test: `atest WMShellFlickerTests:AppPairsTestPairPrimaryAndSecondaryApps`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class AppPairsTestPairPrimaryAndSecondaryApps(
- testSpec: FlickerTestParameter
-) : AppPairsTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- // TODO pair apps through normal UX flow
- executeShellCommand(
- composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
- waitAppsShown(primaryApp, secondaryApp)
- }
- }
-
- @Ignore
- @Test
- override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
-
- @Ignore
- @Test
- override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
-
- @Ignore
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- @Ignore
- @Test
- fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
-
- @Ignore
- @Test
- fun bothAppWindowsVisible() {
- testSpec.assertWmEnd {
- isAppWindowVisible(primaryApp.component)
- isAppWindowVisible(secondaryApp.component)
- }
- }
-
- @Ignore
- @Test
- fun appsEndingBounds() {
- testSpec.assertLayersEnd {
- val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
- visibleRegion(primaryApp.component)
- .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion))
- visibleRegion(secondaryApp.component)
- .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion))
- }
- }
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = AppPairsHelper.TEST_REPETITIONS)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt
deleted file mode 100644
index 24869a802167..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.apppairs
-
-import android.view.Display
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.traces.common.WindowManagerConditionsFactory
-import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.AppPairsHelper
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
-import org.junit.After
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launch app from launcher. When the device supports non-resizable in multi window
- * {@link Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW}, app pairs can pair
- * non-resizable apps.
- *
- * To run this test: `atest WMShellFlickerTests:AppPairsTestSupportPairNonResizeableApps`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class AppPairsTestSupportPairNonResizeableApps(
- testSpec: FlickerTestParameter
-) : AppPairsTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- nonResizeableApp?.launchViaIntent(wmHelper)
- // TODO pair apps through normal UX flow
- executeShellCommand(
- composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true))
- val waitConditions = mutableListOf(
- WindowManagerConditionsFactory.isWindowVisible(primaryApp.component),
- WindowManagerConditionsFactory.isLayerVisible(primaryApp.component),
- WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
-
- nonResizeableApp?.let {
- waitConditions.add(
- WindowManagerConditionsFactory.isWindowVisible(nonResizeableApp.component))
- waitConditions.add(
- WindowManagerConditionsFactory.isLayerVisible(nonResizeableApp.component))
- }
- wmHelper.waitFor(*waitConditions.toTypedArray())
- }
- }
-
- @Before
- override fun setup() {
- super.setup()
- setSupportsNonResizableMultiWindow(instrumentation, 1)
- }
-
- @After
- override fun teardown() {
- super.teardown()
- resetMultiWindowConfig(instrumentation)
- }
-
- @Ignore
- @Test
- override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
-
- @Ignore
- @Test
- override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
-
- @Ignore
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- @Ignore
- @Test
- fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
-
- @Ignore
- @Test
- fun bothAppWindowVisible() {
- val nonResizeableApp = nonResizeableApp
- require(nonResizeableApp != null) {
- "Non resizeable app not initialized"
- }
- testSpec.assertWmEnd {
- isAppWindowVisible(nonResizeableApp.component)
- isAppWindowVisible(primaryApp.component)
- }
- }
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = AppPairsHelper.TEST_REPETITIONS)
- }
- }
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt
deleted file mode 100644
index 007415d19860..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.apppairs
-
-import android.os.SystemClock
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd
-import com.android.wm.shell.flicker.helpers.AppPairsHelper
-import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launch app from launcher.
- * To run this test: `atest WMShellFlickerTests:AppPairsTestUnpairPrimaryAndSecondaryApps`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class AppPairsTestUnpairPrimaryAndSecondaryApps(
- testSpec: FlickerTestParameter
-) : AppPairsTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- setup {
- eachRun {
- executeShellCommand(
- composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
- waitAppsShown(primaryApp, secondaryApp)
- }
- }
- transitions {
- // TODO pair apps through normal UX flow
- executeShellCommand(
- composePairsCommand(primaryTaskId, secondaryTaskId, pair = false))
- SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
- }
- }
-
- @Ignore
- @Test
- override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
-
- @Ignore
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- @Ignore
- @Test
- fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd()
-
- @Ignore
- @Test
- fun bothAppWindowsInvisible() {
- testSpec.assertWmEnd {
- isAppWindowInvisible(primaryApp.component)
- isAppWindowInvisible(secondaryApp.component)
- }
- }
-
- @Ignore
- @Test
- fun appsStartingBounds() {
- testSpec.assertLayersStart {
- val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
- visibleRegion(primaryApp.component)
- .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion))
- visibleRegion(secondaryApp.component)
- .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion))
- }
- }
-
- @Ignore
- @Test
- fun appsEndingBounds() {
- testSpec.assertLayersEnd {
- notContains(primaryApp.component)
- notContains(secondaryApp.component)
- }
- }
-
- @Ignore
- @Test
- override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = AppPairsHelper.TEST_REPETITIONS)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt
deleted file mode 100644
index 3e17948b4a84..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.apppairs
-
-import android.app.Instrumentation
-import android.content.Context
-import android.system.helpers.ActivityHelper
-import androidx.test.platform.app.InstrumentationRegistry
-import com.android.server.wm.flicker.FlickerBuilderProvider
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsVisible
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarLayerIsVisible
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.server.wm.traces.parser.toFlickerComponent
-import com.android.wm.shell.flicker.helpers.AppPairsHelper
-import com.android.wm.shell.flicker.helpers.BaseAppHelper
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setDevEnableNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import com.android.wm.shell.flicker.testapp.Components
-import org.junit.After
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Test
-
-abstract class AppPairsTransition(protected val testSpec: FlickerTestParameter) {
- protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
- protected val context: Context = instrumentation.context
- protected val activityHelper = ActivityHelper.getInstance()
- protected val appPairsHelper = AppPairsHelper(instrumentation,
- Components.SplitScreenActivity.LABEL,
- Components.SplitScreenActivity.COMPONENT.toFlickerComponent())
-
- protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation)
- protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation)
- protected open val nonResizeableApp: SplitScreenHelper? =
- SplitScreenHelper.getNonResizeable(instrumentation)
- protected var primaryTaskId = ""
- protected var secondaryTaskId = ""
- protected var nonResizeableTaskId = ""
- private var prevDevEnableNonResizableMultiWindow = 0
-
- @Before
- open fun setup() {
- prevDevEnableNonResizableMultiWindow = getDevEnableNonResizableMultiWindow(context)
- if (prevDevEnableNonResizableMultiWindow != 0) {
- // Turn off the development option
- setDevEnableNonResizableMultiWindow(context, 0)
- }
- }
-
- @After
- open fun teardown() {
- setDevEnableNonResizableMultiWindow(context, prevDevEnableNonResizableMultiWindow)
- }
-
- @FlickerBuilderProvider
- fun buildFlicker(): FlickerBuilder {
- return FlickerBuilder(instrumentation).apply {
- transition(this)
- }
- }
-
- internal open val transition: FlickerBuilder.() -> Unit
- get() = {
- setup {
- test {
- device.wakeUpAndGoToHomeScreen()
- }
- eachRun {
- this.setRotation(testSpec.startRotation)
- primaryApp.launchViaIntent(wmHelper)
- secondaryApp.launchViaIntent(wmHelper)
- nonResizeableApp?.launchViaIntent(wmHelper)
- updateTasksId()
- }
- }
- teardown {
- eachRun {
- executeShellCommand(composePairsCommand(
- primaryTaskId, secondaryTaskId, pair = false))
- executeShellCommand(composePairsCommand(
- primaryTaskId, nonResizeableTaskId, pair = false))
- primaryApp.exit(wmHelper)
- secondaryApp.exit(wmHelper)
- nonResizeableApp?.exit(wmHelper)
- }
- }
- }
-
- protected fun updateTasksId() {
- primaryTaskId = getTaskIdForActivity(
- primaryApp.component.packageName, primaryApp.component.className).toString()
- secondaryTaskId = getTaskIdForActivity(
- secondaryApp.component.packageName, secondaryApp.component.className).toString()
- val nonResizeableApp = nonResizeableApp
- if (nonResizeableApp != null) {
- nonResizeableTaskId = getTaskIdForActivity(
- nonResizeableApp.component.packageName,
- nonResizeableApp.component.className).toString()
- }
- }
-
- private fun getTaskIdForActivity(pkgName: String, activityName: String): Int {
- return activityHelper.getTaskIdForActivity(pkgName, activityName)
- }
-
- internal fun executeShellCommand(cmd: String) {
- BaseAppHelper.executeShellCommand(instrumentation, cmd)
- }
-
- internal fun composePairsCommand(
- primaryApp: String,
- secondaryApp: String,
- pair: Boolean
- ): String = buildString {
- // dumpsys activity service SystemUIService WMShell {pair|unpair} ${TASK_ID_1} ${TASK_ID_2}
- append("dumpsys activity service SystemUIService WMShell ")
- if (pair) {
- append("pair ")
- } else {
- append("unpair ")
- }
- append("$primaryApp $secondaryApp")
- }
-
- @Ignore
- @Test
- open fun navBarLayerIsVisible() {
- testSpec.navBarLayerIsVisible()
- }
-
- @Ignore
- @Test
- open fun statusBarLayerIsVisible() {
- testSpec.statusBarLayerIsVisible()
- }
-
- @Ignore
- @Test
- open fun navBarWindowIsVisible() {
- testSpec.navBarWindowIsVisible()
- }
-
- @Ignore
- @Test
- open fun statusBarWindowIsVisible() {
- testSpec.statusBarWindowIsVisible()
- }
-
- @Ignore
- @Test
- open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
-
- @Ignore
- @Test
- open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/OWNERS b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/OWNERS
deleted file mode 100644
index 8446b37dbf06..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# window manager > wm shell > Split Screen
-# Bug component: 928697
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt
deleted file mode 100644
index b0c3ba20d948..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.apppairs
-
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test open apps to app pairs and rotate.
- * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppsInAppPairsMode`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class RotateTwoLaunchedAppsInAppPairsMode(
- testSpec: FlickerTestParameter
-) : RotateTwoLaunchedAppsTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- executeShellCommand(composePairsCommand(
- primaryTaskId, secondaryTaskId, true /* pair */))
- waitAppsShown(primaryApp, secondaryApp)
- setRotation(testSpec.endRotation)
- }
- }
-
- @Ignore
- @Test
- override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
-
- @Ignore
- @Test
- override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
-
- @Ignore
- @Test
- fun bothAppWindowsVisible() {
- testSpec.assertWmEnd {
- isAppWindowVisible(primaryApp.component)
- isAppWindowVisible(secondaryApp.component)
- }
- }
-
- @Ignore
- @Test
- fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
-
- @Ignore
- @Test
- fun appPairsPrimaryBoundsIsVisibleAtEnd() =
- testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.endRotation,
- primaryApp.component)
-
- @Ignore
- @Test
- fun appPairsSecondaryBoundsIsVisibleAtEnd() =
- testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.endRotation,
- secondaryApp.component)
-
- @Ignore
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270)
- )
- }
- }
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt
deleted file mode 100644
index ae56c7732a4d..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.apppairs
-
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test open apps to app pairs and rotate.
- * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppsRotateAndEnterAppPairsMode`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode(
- testSpec: FlickerTestParameter
-) : RotateTwoLaunchedAppsTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- this.setRotation(testSpec.endRotation)
- executeShellCommand(
- composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
- waitAppsShown(primaryApp, secondaryApp)
- }
- }
-
- @Ignore
- @Test
- fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
-
- @Ignore
- @Test
- override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
-
- @Ignore
- @Test
- override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
-
- @Ignore
- @Test
- override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible()
-
- @Ignore
- @Test
- override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
-
- @Ignore
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- @Ignore
- @Test
- fun bothAppWindowsVisible() {
- testSpec.assertWmEnd {
- isAppWindowVisible(primaryApp.component)
- isAppWindowVisible(secondaryApp.component)
- }
- }
-
- @Ignore
- @Test
- fun appPairsPrimaryBoundsIsVisibleAtEnd() =
- testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.endRotation,
- primaryApp.component)
-
- @Ignore
- @Test
- fun appPairsSecondaryBoundsIsVisibleAtEnd() =
- testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.endRotation,
- secondaryApp.component)
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270)
- )
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt
deleted file mode 100644
index b1f1c9e539df..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.apppairs
-
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.Assume.assumeFalse
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Test
-
-abstract class RotateTwoLaunchedAppsTransition(
- testSpec: FlickerTestParameter
-) : AppPairsTransition(testSpec) {
- override val nonResizeableApp: SplitScreenHelper?
- get() = null
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setup {
- test {
- device.wakeUpAndGoToHomeScreen()
- this.setRotation(Surface.ROTATION_0)
- primaryApp.launchViaIntent(wmHelper)
- secondaryApp.launchViaIntent(wmHelper)
- updateTasksId()
- }
- }
- teardown {
- eachRun {
- executeShellCommand(composePairsCommand(
- primaryTaskId, secondaryTaskId, pair = false))
- primaryApp.exit(wmHelper)
- secondaryApp.exit(wmHelper)
- }
- }
- }
-
- @Before
- override fun setup() {
- // AppPairs hasn't been updated to Shell Transition. There will be conflict on rotation.
- assumeFalse(isShellTransitionsEnabled())
- super.setup()
- }
-
- @Ignore
- @Test
- override fun navBarLayerIsVisible() {
- super.navBarLayerIsVisible()
- }
-
- @Ignore
- @Test
- override fun navBarLayerRotatesAndScales() {
- super.navBarLayerRotatesAndScales()
- }
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
index e9d438a569d5..8157a4e453af 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
@@ -39,7 +39,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
) {
private val mediaSessionManager: MediaSessionManager
get() = context.getSystemService(MediaSessionManager::class.java)
- ?: error("Could not get MediaSessionManager")
+ ?: error("Could not get MediaSessionManager")
private val mediaController: MediaController?
get() = mediaSessionManager.getActiveSessions(null).firstOrNull {
@@ -69,8 +69,10 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
action: String? = null,
stringExtras: Map<String, String>
) {
- launchViaIntentAndWaitShown(wmHelper, expectedWindowName, action, stringExtras,
- waitConditions = arrayOf(WindowManagerStateHelper.pipShownCondition))
+ launchViaIntentAndWaitShown(
+ wmHelper, expectedWindowName, action, stringExtras,
+ waitConditions = arrayOf(WindowManagerStateHelper.pipShownCondition)
+ )
}
/**
@@ -85,7 +87,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
// from "the bottom".
repeat(FOCUS_ATTEMPTS) {
uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true }
- ?: error("The object we try to focus on is gone.")
+ ?: error("The object we try to focus on is gone.")
uiDevice.pressDPadDown()
uiDevice.waitForIdle()
@@ -100,29 +102,39 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
// Wait on WMHelper or simply wait for 3 seconds
wmHelper?.waitPipShown() ?: SystemClock.sleep(3_000)
// when entering pip, the dismiss button is visible at the start. to ensure the pip
- // animation is complete, wait until the pip dismiss button is no longer visible.
+ // animation is complete, wait until the pip dismiss button is no longer visible.
// b/176822698: dismiss-only state will be removed in the future
uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT)
}
+ fun enableEnterPipOnUserLeaveHint() {
+ clickObject(ENTER_PIP_ON_USER_LEAVE_HINT)
+ }
+
+ fun enableAutoEnterForPipActivity() {
+ clickObject(ENTER_PIP_AUTOENTER)
+ }
+
fun clickStartMediaSessionButton() {
clickObject(MEDIA_SESSION_START_RADIO_BUTTON_ID)
}
fun checkWithCustomActionsCheckbox() = uiDevice
- .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID))
- ?.takeIf { it.isCheckable }
- ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) }
- ?: error("'With custom actions' checkbox not found")
+ .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID))
+ ?.takeIf { it.isCheckable }
+ ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) }
+ ?: error("'With custom actions' checkbox not found")
fun pauseMedia() = mediaController?.transportControls?.pause()
- ?: error("No active media session found")
+ ?: error("No active media session found")
fun stopMedia() = mediaController?.transportControls?.stop()
- ?: error("No active media session found")
+ ?: error("No active media session found")
- @Deprecated("Use PipAppHelper.closePipWindow(wmHelper) instead",
- ReplaceWith("closePipWindow(wmHelper)"))
+ @Deprecated(
+ "Use PipAppHelper.closePipWindow(wmHelper) instead",
+ ReplaceWith("closePipWindow(wmHelper)")
+ )
fun closePipWindow() {
if (isTelevision) {
uiDevice.closeTvPipWindow()
@@ -152,7 +164,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss")
uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT)
val dismissPipObject = uiDevice.findObject(dismissSelector)
- ?: error("PIP window dismiss button not found")
+ ?: error("PIP window dismiss button not found")
val dismissButtonBounds = dismissPipObject.visibleBounds
uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY())
}
@@ -172,7 +184,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button")
uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT)
val expandPipObject = uiDevice.findObject(expandSelector)
- ?: error("PIP window expand button not found")
+ ?: error("PIP window expand button not found")
val expandButtonBounds = expandPipObject.visibleBounds
uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY())
wmHelper.waitPipGone()
@@ -194,5 +206,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
private const val ENTER_PIP_BUTTON_ID = "enter_pip"
private const val WITH_CUSTOM_ACTIONS_BUTTON_ID = "with_custom_actions"
private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start"
+ private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual"
+ private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter"
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
index 0ec9b2d869a8..49eca63a23ec 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
@@ -17,10 +17,21 @@
package com.android.wm.shell.flicker.helpers
import android.app.Instrumentation
-import android.content.res.Resources
+import android.graphics.Point
+import android.os.SystemClock
+import android.view.InputDevice
+import android.view.MotionEvent
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.server.wm.traces.common.FlickerComponentName
import com.android.server.wm.traces.parser.toFlickerComponent
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
import com.android.wm.shell.flicker.testapp.Components
+import org.junit.Assert
class SplitScreenHelper(
instrumentation: Instrumentation,
@@ -31,25 +42,170 @@ class SplitScreenHelper(
companion object {
const val TEST_REPETITIONS = 1
const val TIMEOUT_MS = 3_000L
+ const val DRAG_DURATION_MS = 1_000L
+ const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
+ const val GESTURE_STEP_MS = 16L
- // TODO: remove all legacy split screen flicker tests when legacy split screen is fully
- // deprecated.
- fun isUsingLegacySplit(): Boolean =
- Resources.getSystem().getBoolean(com.android.internal.R.bool.config_useLegacySplit)
+ private val notificationScrollerSelector: BySelector
+ get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
+ private val notificationContentSelector: BySelector
+ get() = By.text("Notification content")
fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper =
- SplitScreenHelper(instrumentation,
+ SplitScreenHelper(
+ instrumentation,
Components.SplitScreenActivity.LABEL,
- Components.SplitScreenActivity.COMPONENT.toFlickerComponent())
+ Components.SplitScreenActivity.COMPONENT.toFlickerComponent()
+ )
fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper =
- SplitScreenHelper(instrumentation,
+ SplitScreenHelper(
+ instrumentation,
Components.SplitScreenSecondaryActivity.LABEL,
- Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent())
+ Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent()
+ )
fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper =
- SplitScreenHelper(instrumentation,
+ SplitScreenHelper(
+ instrumentation,
Components.NonResizeableActivity.LABEL,
- Components.NonResizeableActivity.COMPONENT.toFlickerComponent())
+ Components.NonResizeableActivity.COMPONENT.toFlickerComponent()
+ )
+
+ fun getSendNotification(instrumentation: Instrumentation): SplitScreenHelper =
+ SplitScreenHelper(
+ instrumentation,
+ Components.SendNotificationActivity.LABEL,
+ Components.SendNotificationActivity.COMPONENT.toFlickerComponent()
+ )
+
+ fun dragFromNotificationToSplit(
+ instrumentation: Instrumentation,
+ device: UiDevice,
+ wmHelper: WindowManagerStateHelper
+ ) {
+ val displayBounds = wmHelper.currentState.layerState
+ .displays.firstOrNull { !it.isVirtual }
+ ?.layerStackSpace
+ ?: error("Display not found")
+
+ // Pull down the notifications
+ device.swipe(
+ displayBounds.centerX(), 5,
+ displayBounds.centerX(), displayBounds.bottom, 20 /* steps */
+ )
+ SystemClock.sleep(TIMEOUT_MS)
+
+ // Find the target notification
+ val notificationScroller = device.wait(
+ Until.findObject(notificationScrollerSelector), TIMEOUT_MS
+ )
+ var notificationContent = notificationScroller.findObject(notificationContentSelector)
+
+ while (notificationContent == null) {
+ device.swipe(
+ displayBounds.centerX(), displayBounds.centerY(),
+ displayBounds.centerX(), displayBounds.centerY() - 150, 20 /* steps */
+ )
+ notificationContent = notificationScroller.findObject(notificationContentSelector)
+ }
+
+ // Drag to split
+ var dragStart = notificationContent.visibleCenter
+ var dragMiddle = Point(dragStart.x + 50, dragStart.y)
+ var dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
+ val downTime = SystemClock.uptimeMillis()
+
+ touch(
+ instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime,
+ TIMEOUT_MS, dragStart
+ )
+ // It needs a horizontal movement to trigger the drag
+ touchMove(
+ instrumentation, downTime, SystemClock.uptimeMillis(),
+ DRAG_DURATION_MS, dragStart, dragMiddle
+ )
+ touchMove(
+ instrumentation, downTime, SystemClock.uptimeMillis(),
+ DRAG_DURATION_MS, dragMiddle, dragEnd
+ )
+ // Wait for a while to start splitting
+ SystemClock.sleep(TIMEOUT_MS)
+ touch(
+ instrumentation, MotionEvent.ACTION_UP, downTime, SystemClock.uptimeMillis(),
+ GESTURE_STEP_MS, dragEnd
+ )
+ SystemClock.sleep(TIMEOUT_MS)
+ }
+
+ fun touch(
+ instrumentation: Instrumentation,
+ action: Int,
+ downTime: Long,
+ eventTime: Long,
+ duration: Long,
+ point: Point
+ ) {
+ val motionEvent = MotionEvent.obtain(
+ downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0
+ )
+ motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
+ instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
+ motionEvent.recycle()
+ SystemClock.sleep(duration)
+ }
+
+ fun touchMove(
+ instrumentation: Instrumentation,
+ downTime: Long,
+ eventTime: Long,
+ duration: Long,
+ from: Point,
+ to: Point
+ ) {
+ val steps: Long = duration / GESTURE_STEP_MS
+ var currentTime = eventTime
+ var currentX = from.x.toFloat()
+ var currentY = from.y.toFloat()
+ val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
+ val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
+
+ for (i in 1..steps) {
+ val motionMove = MotionEvent.obtain(
+ downTime, currentTime, MotionEvent.ACTION_MOVE, currentX, currentY, 0
+ )
+ motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
+ instrumentation.uiAutomation.injectInputEvent(motionMove, true)
+ motionMove.recycle()
+
+ currentTime += GESTURE_STEP_MS
+ if (i == steps - 1) {
+ currentX = to.x.toFloat()
+ currentY = to.y.toFloat()
+ } else {
+ currentX += stepX
+ currentY += stepY
+ }
+ SystemClock.sleep(GESTURE_STEP_MS)
+ }
+ }
+
+ fun createShortcutOnHotseatIfNotExist(
+ taplInstrumentation: LauncherInstrumentation,
+ appName: String
+ ) {
+ taplInstrumentation.workspace
+ .deleteAppIcon(taplInstrumentation.workspace.getHotseatAppIcon(0))
+ val allApps = taplInstrumentation.workspace.switchToAllApps()
+ allApps.freeze()
+ try {
+ val appIconSrc = allApps.getAppIcon(appName)
+ Assert.assertNotNull("Unable to find app icon", appIconSrc)
+ val appIconDest = appIconSrc.dragToHotseat(0)
+ Assert.assertNotNull("Unable to drag app icon on hotseat", appIconDest)
+ } finally {
+ allApps.unfreeze()
+ }
+ }
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt
deleted file mode 100644
index c86a1229d8d8..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test open activity and dock to primary split screen
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenDockActivity`
- */
-@Presubmit
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group4
-class EnterSplitScreenDockActivity(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- device.launchSplitScreen(wmHelper)
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(LAUNCHER_COMPONENT, LIVE_WALLPAPER_COMPONENT,
- splitScreenApp.component, FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT, LAUNCHER_COMPONENT)
-
- @Presubmit
- @Test
- fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
- testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation,
- splitScreenApp.component)
-
- @Presubmit
- @Test
- fun dockedStackDividerBecomesVisible() = testSpec.dockedStackDividerBecomesVisible()
-
- @Presubmit
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun appWindowIsVisible() {
- testSpec.assertWmEnd {
- isAppWindowVisible(splitScreenApp.component)
- }
- }
-
- @FlakyTest
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0), // bugId = 179116910
- supportedNavigationModes = listOf(
- WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
- )
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt
deleted file mode 100644
index 2f9244be9c18..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test enter split screen from a detached recent task
- *
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenFromDetachedRecentTask`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group4
-class EnterSplitScreenFromDetachedRecentTask(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- cleanSetup(this)
- setup {
- eachRun {
- splitScreenApp.launchViaIntent(wmHelper)
- // Press back to remove the task, but it should still be shown in recent.
- device.pressBack()
- }
- }
- transitions {
- device.launchSplitScreen(wmHelper)
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(LAUNCHER_COMPONENT,
- FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT,
- splitScreenApp.component)
-
- @Presubmit
- @Test
- fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
-
- @Presubmit
- @Test
- fun appWindowIsVisible() {
- testSpec.assertWmEnd {
- isAppWindowVisible(splitScreenApp.component)
- }
- }
-
- @Presubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910
- )
- }
- }
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt
deleted file mode 100644
index 1740c3ec24ca..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test open activity to primary split screen and dock secondary activity to side
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenLaunchToSide`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group4
-class EnterSplitScreenLaunchToSide(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- device.launchSplitScreen(wmHelper)
- device.reopenAppFromOverview(wmHelper)
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component,
- secondaryApp.component, FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT)
-
- @Presubmit
- @Test
- fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
- testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation,
- splitScreenApp.component)
-
- @Presubmit
- @Test
- fun dockedStackSecondaryBoundsIsVisibleAtEnd() =
- testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.startRotation,
- secondaryApp.component)
-
- @Presubmit
- @Test
- fun dockedStackDividerBecomesVisible() = testSpec.dockedStackDividerBecomesVisible()
-
- @Presubmit
- @Test
- fun appWindowBecomesVisible() {
- testSpec.assertWm {
- // when the app is launched, first the activity becomes visible, then the
- // SnapshotStartingWindow appears and then the app window becomes visible.
- // Because we log WM once per frame, sometimes the activity and the window
- // become visible in the same entry, sometimes not, thus it is not possible to
- // assert the visibility of the activity here
- this.isAppWindowInvisible(secondaryApp.component)
- .then()
- // during re-parenting, the window may disappear and reappear from the
- // trace, this occurs because we log only 1x per frame
- .notContains(secondaryApp.component, isOptional = true)
- .then()
- .isAppWindowVisible(secondaryApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0) // bugId = 175687842
- )
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt
deleted file mode 100644
index 4c063b918e96..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.canSplitScreen
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.After
-import org.junit.Assert
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test enter split screen from non-resizable activity. When the device doesn't support
- * non-resizable in multi window, there should be no button to enter split screen for non-resizable
- * activity.
- *
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenNotSupportNonResizable`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group4
-class EnterSplitScreenNotSupportNonResizable(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- cleanSetup(this)
- setup {
- eachRun {
- nonResizeableApp.launchViaIntent(wmHelper)
- }
- }
- transitions {
- if (device.canSplitScreen(wmHelper)) {
- Assert.fail("Non-resizeable app should not enter split screen")
- }
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(LAUNCHER_COMPONENT,
- FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT,
- nonResizeableApp.component,
- splitScreenApp.component)
-
- @Before
- override fun setup() {
- super.setup()
- setSupportsNonResizableMultiWindow(instrumentation, -1)
- }
-
- @After
- override fun teardown() {
- super.teardown()
- resetMultiWindowConfig(instrumentation)
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt
deleted file mode 100644
index f75dee619564..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.After
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test enter split screen from non-resizable activity. When the device supports
- * non-resizable in multi window, there should be a button to enter split screen for non-resizable
- * activity.
- *
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenSupportNonResizable`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group2
-class EnterSplitScreenSupportNonResizable(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- cleanSetup(this)
- setup {
- eachRun {
- nonResizeableApp.launchViaIntent(wmHelper)
- }
- }
- transitions {
- device.launchSplitScreen(wmHelper)
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(LAUNCHER_COMPONENT,
- FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT,
- nonResizeableApp.component,
- splitScreenApp.component)
-
- @Before
- override fun setup() {
- super.setup()
- setSupportsNonResizableMultiWindow(instrumentation, 1)
- }
-
- @After
- override fun teardown() {
- super.teardown()
- resetMultiWindowConfig(instrumentation)
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
-
- @Presubmit
- @Test
- fun appWindowIsVisible() {
- testSpec.assertWmEnd {
- isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt
deleted file mode 100644
index ef7d65e8a732..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.exitSplitScreenFromBottom
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test open resizeable activity split in primary, and drag divider to bottom exit split screen
- * To run this test: `atest WMShellFlickerTests:ExitLegacySplitScreenFromBottom`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class ExitLegacySplitScreenFromBottom(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- setup {
- eachRun {
- splitScreenApp.launchViaIntent(wmHelper)
- device.launchSplitScreen(wmHelper)
- }
- }
- teardown {
- eachRun {
- splitScreenApp.exit(wmHelper)
- }
- }
- transitions {
- device.exitSplitScreenFromBottom(wmHelper)
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN,
- splitScreenApp.component, secondaryApp.component,
- FlickerComponentName.SNAPSHOT)
-
- @FlakyTest
- @Test
- fun layerBecomesInvisible() {
- testSpec.assertLayers {
- this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
- .then()
- .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
- }
- }
-
- @FlakyTest
- @Test
- fun appWindowBecomesInVisible() {
- testSpec.assertWm {
- this.isAppWindowVisible(secondaryApp.component)
- .then()
- .isAppWindowInvisible(secondaryApp.component)
- }
- }
-
- @FlakyTest
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @FlakyTest
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @FlakyTest
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @FlakyTest
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0) // b/175687842
- )
- }
- }
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt
deleted file mode 100644
index d913a6d85d3d..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test dock activity to primary split screen, and open secondary to side, exit primary split
- * and test secondary activity become full screen.
- * To run this test: `atest WMShellFlickerTests:ExitPrimarySplitScreenShowSecondaryFullscreen`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class ExitPrimarySplitScreenShowSecondaryFullscreen(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- teardown {
- eachRun {
- secondaryApp.exit(wmHelper)
- }
- }
- transitions {
- splitScreenApp.launchViaIntent(wmHelper)
- secondaryApp.launchViaIntent(wmHelper)
- device.launchSplitScreen(wmHelper)
- device.reopenAppFromOverview(wmHelper)
- // TODO(b/175687842) Can not find Split screen divider, use exit() instead
- splitScreenApp.exit(wmHelper)
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN,
- splitScreenApp.component, secondaryApp.component,
- FlickerComponentName.SNAPSHOT)
-
- @Presubmit
- @Test
- fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
-
- @FlakyTest
- @Test
- fun layerBecomesInvisible() {
- testSpec.assertLayers {
- this.isVisible(splitScreenApp.component)
- .then()
- .isInvisible(splitScreenApp.component)
- }
- }
-
- @FlakyTest
- @Test
- fun appWindowBecomesInVisible() {
- testSpec.assertWm {
- this.isAppWindowVisible(splitScreenApp.component)
- .then()
- .isAppWindowInvisible(splitScreenApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @Presubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910
- )
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt
deleted file mode 100644
index f3ff7b156aaf..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.After
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test launch non-resizable activity via intent in split screen mode. When the device does not
- * support non-resizable in multi window, it should trigger exit split screen.
- * To run this test: `atest WMShellFlickerTests:LegacySplitScreenFromIntentNotSupportNonResizable`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class LegacySplitScreenFromIntentNotSupportNonResizable(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- cleanSetup(this)
- setup {
- eachRun {
- splitScreenApp.launchViaIntent(wmHelper)
- device.launchSplitScreen(wmHelper)
- }
- }
- transitions {
- nonResizeableApp.launchViaIntent(wmHelper)
- wmHelper.waitForAppTransitionIdle()
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
- nonResizeableApp.component, splitScreenApp.component,
- FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT)
-
- @Before
- override fun setup() {
- super.setup()
- setSupportsNonResizableMultiWindow(instrumentation, -1)
- }
-
- @After
- override fun teardown() {
- super.teardown()
- resetMultiWindowConfig(instrumentation)
- }
-
- @Presubmit
- @Test
- fun resizableAppLayerBecomesInvisible() {
- testSpec.assertLayers {
- this.isVisible(splitScreenApp.component)
- .then()
- .isInvisible(splitScreenApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun nonResizableAppLayerBecomesVisible() {
- testSpec.assertLayers {
- this.notContains(nonResizeableApp.component)
- .then()
- .isInvisible(nonResizeableApp.component)
- .then()
- .isVisible(nonResizeableApp.component)
- }
- }
-
- /**
- * Assets that [splitScreenApp] exists at the start of the trace and, once it becomes
- * invisible, it remains invisible until the end of the trace.
- */
- @Presubmit
- @Test
- fun resizableAppWindowBecomesInvisible() {
- testSpec.assertWm {
- // when the activity gets PAUSED the window may still be marked as visible
- // it will be updated in the next log entry. This occurs because we record 1x
- // per frame, thus ignore activity check here
- this.isAppWindowVisible(splitScreenApp.component)
- .then()
- // immediately after the window (after onResume and before perform relayout)
- // the activity is invisible. This may or not be logged, since we record 1x
- // per frame, thus ignore activity check here
- .isAppWindowInvisible(splitScreenApp.component)
- }
- }
-
- /**
- * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then
- * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes
- * visible, it remains visible until the end of the trace.
- */
- @Presubmit
- @Test
- fun nonResizableAppWindowBecomesVisible() {
- testSpec.assertWm {
- this.notContains(nonResizeableApp.component)
- .then()
- // we log once per frame, upon logging, window may be visible or not depending
- // on what was processed until that moment. Both behaviors are correct
- .isAppWindowInvisible(nonResizeableApp.component, isOptional = true)
- .then()
- // immediately after the window (after onResume and before perform relayout)
- // the activity is invisible. This may or not be logged, since we record 1x
- // per frame, thus ignore activity check here
- .isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- /**
- * Asserts that both the app window and the activity are visible at the end of the trace
- */
- @Presubmit
- @Test
- fun nonResizableAppWindowBecomesVisibleAtEnd() {
- testSpec.assertWmEnd {
- isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
-
- @Presubmit
- @Test
- fun onlyNonResizableAppWindowIsVisibleAtEnd() {
- testSpec.assertWmEnd {
- isAppWindowInvisible(splitScreenApp.component)
- isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt
deleted file mode 100644
index 42e707ab0850..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.After
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test launch non-resizable activity via intent in split screen mode. When the device supports
- * non-resizable in multi window, it should show the non-resizable app in split screen.
- * To run this test: `atest WMShellFlickerTests:LegacySplitScreenFromIntentSupportNonResizable`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class LegacySplitScreenFromIntentSupportNonResizable(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- cleanSetup(this)
- setup {
- eachRun {
- splitScreenApp.launchViaIntent(wmHelper)
- device.launchSplitScreen(wmHelper)
- }
- }
- transitions {
- nonResizeableApp.launchViaIntent(wmHelper)
- wmHelper.waitForAppTransitionIdle()
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
- nonResizeableApp.component, splitScreenApp.component,
- FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT)
-
- @Before
- override fun setup() {
- super.setup()
- setSupportsNonResizableMultiWindow(instrumentation, 1)
- }
-
- @After
- override fun teardown() {
- super.teardown()
- resetMultiWindowConfig(instrumentation)
- }
-
- @Presubmit
- @Test
- fun nonResizableAppLayerBecomesVisible() {
- testSpec.assertLayers {
- this.isInvisible(nonResizeableApp.component)
- .then()
- .isVisible(nonResizeableApp.component)
- }
- }
-
- /**
- * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then
- * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes
- * visible, it remains visible until the end of the trace.
- */
- @Presubmit
- @Test
- fun nonResizableAppWindowBecomesVisible() {
- testSpec.assertWm {
- this.notContains(nonResizeableApp.component)
- .then()
- // we log once per frame, upon logging, window may be visible or not depending
- // on what was processed until that moment. Both behaviors are correct
- .isAppWindowInvisible(nonResizeableApp.component, isOptional = true)
- .then()
- // immediately after the window (after onResume and before perform relayout)
- // the activity is invisible. This may or not be logged, since we record 1x
- // per frame, thus ignore activity check here
- .isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
-
- @Presubmit
- @Test
- fun bothAppsWindowsAreVisibleAtEnd() {
- testSpec.assertWmEnd {
- isAppWindowVisible(splitScreenApp.component)
- isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt
deleted file mode 100644
index c1fba7d1530c..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.After
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test launch non-resizable activity via recent overview in split screen mode. When the device does
- * not support non-resizable in multi window, it should trigger exit split screen.
- * To run this test: `atest WMShellFlickerTests:LegacySplitScreenFromRecentNotSupportNonResizable`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class LegacySplitScreenFromRecentNotSupportNonResizable(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- cleanSetup(this)
- setup {
- eachRun {
- nonResizeableApp.launchViaIntent(wmHelper)
- splitScreenApp.launchViaIntent(wmHelper)
- device.launchSplitScreen(wmHelper)
- }
- }
- transitions {
- device.reopenAppFromOverview(wmHelper)
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
- TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component,
- FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT)
-
- @Before
- override fun setup() {
- super.setup()
- setSupportsNonResizableMultiWindow(instrumentation, -1)
- }
-
- @After
- override fun teardown() {
- super.teardown()
- resetMultiWindowConfig(instrumentation)
- }
-
- @Presubmit
- @Test
- fun resizableAppLayerBecomesInvisible() {
- testSpec.assertLayers {
- this.isVisible(splitScreenApp.component)
- .then()
- .isInvisible(splitScreenApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun nonResizableAppLayerBecomesVisible() {
- testSpec.assertLayers {
- this.isInvisible(nonResizeableApp.component)
- .then()
- .isVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun resizableAppWindowBecomesInvisible() {
- testSpec.assertWm {
- // when the activity gets PAUSED the window may still be marked as visible
- // it will be updated in the next log entry. This occurs because we record 1x
- // per frame, thus ignore activity check here
- this.isAppWindowVisible(splitScreenApp.component)
- .then()
- // immediately after the window (after onResume and before perform relayout)
- // the activity is invisible. This may or not be logged, since we record 1x
- // per frame, thus ignore activity check here
- .isAppWindowInvisible(splitScreenApp.component)
- }
- }
-
- @FlakyTest
- @Test
- fun nonResizableAppWindowBecomesVisible() {
- testSpec.assertWm {
- this.isAppWindowInvisible(nonResizeableApp.component)
- .then()
- .isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
-
- @Presubmit
- @Test
- fun onlyNonResizableAppWindowIsVisibleAtEnd() {
- testSpec.assertWmEnd {
- isAppWindowInvisible(splitScreenApp.component)
- isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt
deleted file mode 100644
index 6ac8683ac054..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.After
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test launch non-resizable activity via recent overview in split screen mode. When the device
- * supports non-resizable in multi window, it should show the non-resizable app in split screen.
- * To run this test: `atest WMShellFlickerTests:LegacySplitScreenFromRecentSupportNonResizable`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class LegacySplitScreenFromRecentSupportNonResizable(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- cleanSetup(this)
- setup {
- eachRun {
- nonResizeableApp.launchViaIntent(wmHelper)
- splitScreenApp.launchViaIntent(wmHelper)
- device.launchSplitScreen(wmHelper)
- }
- }
- transitions {
- device.reopenAppFromOverview(wmHelper)
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
- TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component,
- FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT)
-
- @Before
- override fun setup() {
- super.setup()
- setSupportsNonResizableMultiWindow(instrumentation, 1)
- }
-
- @After
- override fun teardown() {
- super.teardown()
- resetMultiWindowConfig(instrumentation)
- }
-
- @Presubmit
- @Test
- fun nonResizableAppLayerBecomesVisible() {
- testSpec.assertLayers {
- this.isInvisible(nonResizeableApp.component)
- .then()
- .isVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun nonResizableAppWindowBecomesVisible() {
- testSpec.assertWm {
- // when the app is launched, first the activity becomes visible, then the
- // SnapshotStartingWindow appears and then the app window becomes visible.
- // Because we log WM once per frame, sometimes the activity and the window
- // become visible in the same entry, sometimes not, thus it is not possible to
- // assert the visibility of the activity here
- this.isAppWindowInvisible(nonResizeableApp.component)
- .then()
- // during re-parenting, the window may disappear and reappear from the
- // trace, this occurs because we log only 1x per frame
- .notContains(nonResizeableApp.component, isOptional = true)
- .then()
- // if the window reappears after re-parenting it will most likely not
- // be visible in the first log entry (because we log only 1x per frame)
- .isAppWindowInvisible(nonResizeableApp.component, isOptional = true)
- .then()
- .isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
-
- @Presubmit
- @Test
- fun bothAppsWindowsAreVisibleAtEnd() {
- testSpec.assertWmEnd {
- isAppWindowVisible(splitScreenApp.component)
- isAppWindowVisible(nonResizeableApp.component)
- }
- }
-
- @Presubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenRotateTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenRotateTransition.kt
deleted file mode 100644
index b01f41c9e2ec..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenRotateTransition.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-
-abstract class LegacySplitScreenRotateTransition(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setup {
- eachRun {
- device.wakeUpAndGoToHomeScreen()
- device.openQuickStepAndClearRecentAppsFromOverview(wmHelper)
- secondaryApp.launchViaIntent(wmHelper)
- splitScreenApp.launchViaIntent(wmHelper)
- }
- }
- teardown {
- eachRun {
- splitScreenApp.exit(wmHelper)
- secondaryApp.exit(wmHelper)
- this.setRotation(Surface.ROTATION_0)
- }
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt
deleted file mode 100644
index fb1004bda0cb..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.entireScreenCovered
-import com.android.server.wm.flicker.helpers.exitSplitScreen
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsVisible
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarLayerIsVisible
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.dockedStackDividerBecomesInvisible
-import com.android.wm.shell.flicker.helpers.SimpleAppHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test open app to split screen.
- * To run this test: `atest WMShellFlickerTests:LegacySplitScreenToLauncher`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class LegacySplitScreenToLauncher(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
- private val testApp = SimpleAppHelper(instrumentation)
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setup {
- test {
- device.wakeUpAndGoToHomeScreen()
- device.openQuickStepAndClearRecentAppsFromOverview(wmHelper)
- }
- eachRun {
- testApp.launchViaIntent(wmHelper)
- this.setRotation(testSpec.endRotation)
- device.launchSplitScreen(wmHelper)
- device.waitForIdle()
- }
- }
- teardown {
- eachRun {
- testApp.exit(wmHelper)
- }
- }
- transitions {
- device.exitSplitScreen()
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT)
-
- @Presubmit
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
-
- @Presubmit
- @Test
- fun entireScreenCovered() = testSpec.entireScreenCovered()
-
- @Presubmit
- @Test
- fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
-
- @FlakyTest(bugId = 206753786)
- @Test
- fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-
- @Presubmit
- @Test
- fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
-
- @FlakyTest
- @Test
- fun dockedStackDividerBecomesInvisible() = testSpec.dockedStackDividerBecomesInvisible()
-
- @FlakyTest
- @Test
- fun layerBecomesInvisible() {
- testSpec.assertLayers {
- this.isVisible(testApp.component)
- .then()
- .isInvisible(testApp.component)
- }
- }
-
- @FlakyTest
- @Test
- fun focusDoesNotChange() {
- testSpec.assertEventLog {
- this.focusDoesNotChange()
- }
- }
-
- @Presubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- // b/161435597 causes the test not to work on 90 degrees
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt
deleted file mode 100644
index a4a1f617e497..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.legacysplitscreen
-
-import android.app.Instrumentation
-import android.content.Context
-import android.support.test.launcherhelper.LauncherStrategyFactory
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.platform.app.InstrumentationRegistry
-import com.android.server.wm.flicker.FlickerBuilderProvider
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setDevEnableNonResizableMultiWindow
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.After
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Test
-
-abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestParameter) {
- protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
- protected val context: Context = instrumentation.context
- protected val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation)
- protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation)
- protected val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation)
- protected val LAUNCHER_COMPONENT = FlickerComponentName("",
- LauncherStrategyFactory.getInstance(instrumentation)
- .launcherStrategy.supportedLauncherPackage)
- private var prevDevEnableNonResizableMultiWindow = 0
-
- @Before
- open fun setup() {
- // Only run legacy split tests when the system is using legacy split screen.
- assumeTrue(SplitScreenHelper.isUsingLegacySplit())
- // Legacy split is having some issue with Shell transition, and will be deprecated soon.
- assumeFalse(isShellTransitionsEnabled())
- prevDevEnableNonResizableMultiWindow = getDevEnableNonResizableMultiWindow(context)
- if (prevDevEnableNonResizableMultiWindow != 0) {
- // Turn off the development option
- setDevEnableNonResizableMultiWindow(context, 0)
- }
- }
-
- @After
- open fun teardown() {
- setDevEnableNonResizableMultiWindow(context, prevDevEnableNonResizableMultiWindow)
- }
-
- /**
- * List of windows that are ignored when verifying that visible elements appear on 2
- * consecutive entries in the trace.
- *
- * b/182720234
- */
- open val ignoredWindows: List<FlickerComponentName> = listOf(
- FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT)
-
- protected open val transition: FlickerBuilder.() -> Unit
- get() = {
- setup {
- eachRun {
- device.wakeUpAndGoToHomeScreen()
- device.openQuickStepAndClearRecentAppsFromOverview(wmHelper)
- secondaryApp.launchViaIntent(wmHelper)
- splitScreenApp.launchViaIntent(wmHelper)
- this.setRotation(testSpec.startRotation)
- }
- }
- teardown {
- eachRun {
- secondaryApp.exit(wmHelper)
- splitScreenApp.exit(wmHelper)
- this.setRotation(Surface.ROTATION_0)
- }
- }
- }
-
- @FlickerBuilderProvider
- fun buildFlicker(): FlickerBuilder {
- return FlickerBuilder(instrumentation).apply {
- transition(this)
- }
- }
-
- internal open val cleanSetup: FlickerBuilder.() -> Unit
- get() = {
- setup {
- eachRun {
- device.wakeUpAndGoToHomeScreen()
- device.openQuickStepAndClearRecentAppsFromOverview(wmHelper)
- this.setRotation(testSpec.startRotation)
- }
- }
- teardown {
- eachRun {
- nonResizeableApp.exit(wmHelper)
- splitScreenApp.exit(wmHelper)
- device.pressHome()
- this.setRotation(Surface.ROTATION_0)
- }
- }
- }
-
- @FlakyTest(bugId = 178447631)
- @Test
- open fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
- testSpec.assertWm {
- this.visibleWindowsShownMoreThanOneConsecutiveEntry(ignoredWindows)
- }
- }
-
- @FlakyTest(bugId = 178447631)
- @Test
- open fun visibleLayersShownMoreThanOneConsecutiveEntry() {
- testSpec.assertLayers {
- this.visibleLayersShownMoreThanOneConsecutiveEntry(ignoredWindows)
- }
- }
-
- companion object {
- internal val LIVE_WALLPAPER_COMPONENT = FlickerComponentName("",
- "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2")
- internal val LETTERBOX_COMPONENT = FlickerComponentName("", "Letterbox")
- internal val TOAST_COMPONENT = FlickerComponentName("", "Toast")
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OWNERS b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OWNERS
deleted file mode 100644
index 8446b37dbf06..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# window manager > wm shell > Split Screen
-# Bug component: 928697
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt
deleted file mode 100644
index 087b21c544c5..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.entireScreenCovered
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.statusBarLayerIsVisible
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.appPairsDividerBecomesVisible
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test open app to split screen.
- * To run this test: `atest WMShellFlickerTests:OpenAppToLegacySplitScreen`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class OpenAppToLegacySplitScreen(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- device.launchSplitScreen(wmHelper)
- wmHelper.waitForAppTransitionIdle()
- }
- }
-
- override val ignoredWindows: List<FlickerComponentName>
- get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component,
- FlickerComponentName.SPLASH_SCREEN,
- FlickerComponentName.SNAPSHOT)
-
- @FlakyTest
- @Test
- fun appWindowBecomesVisible() {
- testSpec.assertWm {
- this.isAppWindowInvisible(splitScreenApp.component)
- .then()
- .isAppWindowVisible(splitScreenApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun entireScreenCovered() = testSpec.entireScreenCovered()
-
- @Presubmit
- @Test
- fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
-
- @Presubmit
- @Test
- fun appPairsDividerBecomesVisible() = testSpec.appPairsDividerBecomesVisible()
-
- @FlakyTest
- @Test
- fun layerBecomesVisible() {
- testSpec.assertLayers {
- this.isInvisible(splitScreenApp.component)
- .then()
- .isVisible(splitScreenApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun focusChanges() {
- testSpec.assertEventLog {
- this.focusChanges(splitScreenApp.`package`,
- "recents_animation_input_consumer", "NexusLauncherActivity")
- }
- }
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910
- )
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt
deleted file mode 100644
index e2da1a4565c0..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.util.Rational
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import androidx.test.uiautomator.By
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.entireScreenCovered
-import com.android.server.wm.flicker.helpers.ImeAppHelper
-import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.resizeSplitScreen
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsVisible
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarLayerIsVisible
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.server.wm.traces.common.region.Region
-import com.android.server.wm.traces.parser.toFlickerComponent
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.helpers.SimpleAppHelper
-import com.android.wm.shell.flicker.testapp.Components
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test split screen resizing window transitions.
- * To run this test: `atest WMShellFlickerTests:ResizeLegacySplitScreen`
- *
- * Currently it runs only in 0 degrees because of b/156100803
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@FlakyTest(bugId = 159096424)
-@Group2
-class ResizeLegacySplitScreen(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenTransition(testSpec) {
- private val testAppTop = SimpleAppHelper(instrumentation)
- private val testAppBottom = ImeAppHelper(instrumentation)
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setup {
- eachRun {
- device.wakeUpAndGoToHomeScreen()
- this.setRotation(testSpec.startRotation)
- this.launcherStrategy.clearRecentAppsFromOverview()
- testAppBottom.launchViaIntent(wmHelper)
- device.pressHome()
- testAppTop.launchViaIntent(wmHelper)
- device.waitForIdle()
- device.launchSplitScreen(wmHelper)
- val snapshot =
- device.findObject(By.res(device.launcherPackageName, "snapshot"))
- snapshot.click()
- testAppBottom.openIME(device)
- device.pressBack()
- device.resizeSplitScreen(startRatio)
- }
- }
- teardown {
- eachRun {
- testAppTop.exit(wmHelper)
- testAppBottom.exit(wmHelper)
- }
- }
- transitions {
- device.resizeSplitScreen(stopRatio)
- }
- }
-
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @FlakyTest(bugId = 156223549)
- @Test
- fun topAppWindowIsAlwaysVisible() {
- testSpec.assertWm {
- this.isAppWindowVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent())
- }
- }
-
- @FlakyTest(bugId = 156223549)
- @Test
- fun bottomAppWindowIsAlwaysVisible() {
- testSpec.assertWm {
- this.isAppWindowVisible(Components.ImeActivity.COMPONENT.toFlickerComponent())
- }
- }
-
- @Test
- fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
-
- @Test
- fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
-
- @Test
- fun entireScreenCovered() = testSpec.entireScreenCovered()
-
- @Test
- fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
-
- @FlakyTest(bugId = 206753786)
- @Test
- fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-
- @Test
- fun topAppLayerIsAlwaysVisible() {
- testSpec.assertLayers {
- this.isVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent())
- }
- }
-
- @Test
- fun bottomAppLayerIsAlwaysVisible() {
- testSpec.assertLayers {
- this.isVisible(Components.ImeActivity.COMPONENT.toFlickerComponent())
- }
- }
-
- @Test
- fun dividerLayerIsAlwaysVisible() {
- testSpec.assertLayers {
- this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
- }
- }
-
- @FlakyTest
- @Test
- fun appsStartingBounds() {
- testSpec.assertLayersStart {
- val displayBounds = WindowUtils.displayBounds
- val dividerBounds =
- layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds
-
- val topAppBounds = Region.from(0, 0, dividerBounds.right,
- dividerBounds.top + WindowUtils.dockedStackDividerInset)
- val bottomAppBounds = Region.from(0,
- dividerBounds.bottom - WindowUtils.dockedStackDividerInset,
- displayBounds.right,
- displayBounds.bottom - WindowUtils.navigationBarFrameHeight)
- visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent())
- .coversExactly(topAppBounds)
- visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent())
- .coversExactly(bottomAppBounds)
- }
- }
-
- @FlakyTest
- @Test
- fun appsEndingBounds() {
- testSpec.assertLayersStart {
- val displayBounds = WindowUtils.displayBounds
- val dividerBounds =
- layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds
-
- val topAppBounds = Region.from(0, 0, dividerBounds.right,
- dividerBounds.top + WindowUtils.dockedStackDividerInset)
- val bottomAppBounds = Region.from(0,
- dividerBounds.bottom - WindowUtils.dockedStackDividerInset,
- displayBounds.right,
- displayBounds.bottom - WindowUtils.navigationBarFrameHeight)
-
- visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent())
- .coversExactly(topAppBounds)
- visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent())
- .coversExactly(bottomAppBounds)
- }
- }
-
- @Test
- fun focusDoesNotChange() {
- testSpec.assertEventLog {
- focusDoesNotChange()
- }
- }
-
- companion object {
- private val startRatio = Rational(1, 3)
- private val stopRatio = Rational(2, 3)
-
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
- .map {
- val description = (startRatio.toString().replace("/", "-") + "_to_" +
- stopRatio.toString().replace("/", "-"))
- val newName = "${FlickerTestParameter.defaultName(it)}_$description"
- FlickerTestParameter(it.config, nameOverride = newName)
- }
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt
deleted file mode 100644
index d703ea082c87..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test dock activity to primary split screen and rotate
- * To run this test: `atest WMShellFlickerTests:RotateOneLaunchedAppAndEnterSplitScreen`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class RotateOneLaunchedAppAndEnterSplitScreen(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenRotateTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- device.launchSplitScreen(wmHelper)
- this.setRotation(testSpec.startRotation)
- }
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
-
- @Presubmit
- @Test
- fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
- testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation,
- splitScreenApp.component)
-
- @Presubmit
- @Test
- fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
-
- @FlakyTest(bugId = 206753786)
- @Test
- fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-
- @Presubmit
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @FlakyTest
- @Test
- fun appWindowBecomesVisible() {
- testSpec.assertWm {
- this.isAppWindowInvisible(splitScreenApp.component)
- .then()
- .isAppWindowVisible(splitScreenApp.component)
- }
- }
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt
deleted file mode 100644
index 6b1883914e59..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Rotate
- * To run this test: `atest WMShellFlickerTests:RotateOneLaunchedAppInSplitScreenMode`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class RotateOneLaunchedAppInSplitScreenMode(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenRotateTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- this.setRotation(testSpec.startRotation)
- device.launchSplitScreen(wmHelper)
- }
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
-
- @Presubmit
- @Test
- fun dockedStackPrimaryBoundsIsVisibleAtEnd() = testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(
- testSpec.startRotation, splitScreenApp.component)
-
- @Presubmit
- @Test
- fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
-
- @FlakyTest(bugId = 206753786)
- @Test
- fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-
- @Presubmit
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @FlakyTest
- @Test
- fun appWindowBecomesVisible() {
- testSpec.assertWm {
- this.isAppWindowInvisible(splitScreenApp.component)
- .then()
- .isAppWindowVisible(splitScreenApp.component)
- }
- }
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt
deleted file mode 100644
index acd658b5ba56..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test open app to split screen.
- * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppAndEnterSplitScreen`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class RotateTwoLaunchedAppAndEnterSplitScreen(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenRotateTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- this.setRotation(testSpec.startRotation)
- device.launchSplitScreen(wmHelper)
- device.reopenAppFromOverview(wmHelper)
- }
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
-
- @Presubmit
- @Test
- fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
- testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation,
- splitScreenApp.component)
-
- @Presubmit
- @Test
- fun dockedStackSecondaryBoundsIsVisibleAtEnd() =
- testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.startRotation,
- secondaryApp.component)
-
- @Presubmit
- @Test
- fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
-
- @FlakyTest(bugId = 206753786)
- @Test
- fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-
- @Presubmit
- @Test
- fun appWindowBecomesVisible() {
- testSpec.assertWm {
- // when the app is launched, first the activity becomes visible, then the
- // SnapshotStartingWindow appears and then the app window becomes visible.
- // Because we log WM once per frame, sometimes the activity and the window
- // become visible in the same entry, sometimes not, thus it is not possible to
- // assert the visibility of the activity here
- this.isAppWindowInvisible(secondaryApp.component)
- .then()
- // during re-parenting, the window may disappear and reappear from the
- // trace, this occurs because we log only 1x per frame
- .notContains(secondaryApp.component, isOptional = true)
- .then()
- // if the window reappears after re-parenting it will most likely not
- // be visible in the first log entry (because we log only 1x per frame)
- .isAppWindowInvisible(secondaryApp.component, isOptional = true)
- .then()
- .isAppWindowVisible(secondaryApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt
deleted file mode 100644
index b40be8b5f401..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.legacysplitscreen
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test open app to split screen.
- * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppInSplitScreenMode`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
-class RotateTwoLaunchedAppInSplitScreenMode(
- testSpec: FlickerTestParameter
-) : LegacySplitScreenRotateTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- setup {
- eachRun {
- device.launchSplitScreen(wmHelper)
- device.reopenAppFromOverview(wmHelper)
- this.setRotation(testSpec.startRotation)
- }
- }
- transitions {
- this.setRotation(testSpec.startRotation)
- }
- }
-
- @Presubmit
- @Test
- fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
-
- @Presubmit
- @Test
- fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
- testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation,
- splitScreenApp.component)
-
- @Presubmit
- @Test
- fun dockedStackSecondaryBoundsIsVisibleAtEnd() =
- testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.startRotation,
- secondaryApp.component)
-
- @Presubmit
- @Test
- fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
-
- @FlakyTest(bugId = 206753786)
- @Test
- fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-
- @FlakyTest
- @Test
- fun appWindowBecomesVisible() {
- testSpec.assertWm {
- this.isAppWindowInvisible(secondaryApp.component)
- .then()
- .isAppWindowVisible(secondaryApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Presubmit
- @Test
- fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @Presubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
new file mode 100644
index 000000000000..ce624f2b5bbe
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.flicker.pip
+
+import android.platform.test.annotations.FlakyTest
+import androidx.test.filters.RequiresDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+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 an app via auto-enter property when navigating to home.
+ *
+ * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest`
+ *
+ * Actions:
+ * Launch an app in full screen
+ * Select "Auto-enter PiP" radio button
+ * Press Home button or swipe up to go Home and put [pipApp] in pip mode
+ *
+ * Notes:
+ * 1. All assertions are inherited from [EnterPipTest]
+ * 2. Part of the test setup occurs automatically via
+ * [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ * 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)
+@FlakyTest(bugId = 238367575)
+@Group3
+class AutoEnterPipOnGoToHomeTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) {
+ protected val taplInstrumentation = LauncherInstrumentation()
+ /**
+ * Defines the transition used to run the test
+ */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ setupAndTeardown(this)
+ setup {
+ eachRun {
+ pipApp.launchViaIntent(wmHelper)
+ pipApp.enableAutoEnterForPipActivity()
+ }
+ }
+ teardown {
+ eachRun {
+ // close gracefully so that onActivityUnpinned() can be called before force exit
+ pipApp.closePipWindow(wmHelper)
+ pipApp.exit(wmHelper)
+ }
+ }
+ transitions {
+ taplInstrumentation.goHome()
+ }
+ }
+
+ override fun pipLayerReduces() {
+ val layerName = pipApp.component.toLayerName()
+ testSpec.assertLayers {
+ val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+ pipLayerList.zipWithNext { previous, current ->
+ current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
+ }
+ }
+ }
+
+ /**
+ * Checks that [pipApp] window is animated towards default position in right bottom corner
+ */
+ @Test
+ fun pipLayerMovesTowardsRightBottomCorner() {
+ // in gestural nav the swipe makes PiP first go upwards
+ Assume.assumeFalse(testSpec.isGesturalNavigation)
+ val layerName = pipApp.component.toLayerName()
+ testSpec.assertLayers {
+ val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+ // Pip animates towards the right bottom corner, but because it is being resized at the
+ // same time, it is possible it shrinks first quickly below the default position and get
+ // moved up after that in just few last frames
+ pipLayerList.zipWithNext { previous, current ->
+ current.visibleRegion.isToTheRightBottom(previous.visibleRegion.region, 3)
+ }
+ }
+ }
+
+ override fun focusChanges() {
+ // in gestural nav the focus goes to different activity on swipe up
+ Assume.assumeFalse(testSpec.isGesturalNavigation)
+ super.focusChanges()
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
new file mode 100644
index 000000000000..953f59a1f70b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.flicker.pip
+
+import androidx.test.filters.RequiresDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.Assume
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+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`
+ *
+ * Actions:
+ * Launch an app in full screen
+ * Select "Via code behind" radio button
+ * Press Home button or swipe up to go Home and put [pipApp] in pip mode
+ *
+ * Notes:
+ * 1. All assertions are inherited from [EnterPipTest]
+ * 2. Part of the test setup occurs automatically via
+ * [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ * 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)
+@Group3
+class EnterPipOnUserLeaveHintTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) {
+ protected val taplInstrumentation = LauncherInstrumentation()
+ /**
+ * Defines the transition used to run the test
+ */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ setupAndTeardown(this)
+ setup {
+ eachRun {
+ pipApp.launchViaIntent(wmHelper)
+ pipApp.enableEnterPipOnUserLeaveHint()
+ }
+ }
+ teardown {
+ eachRun {
+ pipApp.exit(wmHelper)
+ }
+ }
+ transitions {
+ taplInstrumentation.goHome()
+ }
+ }
+
+ override fun pipAppLayerAlwaysVisible() {
+ if (!testSpec.isGesturalNavigation) super.pipAppLayerAlwaysVisible() else {
+ // pip layer in gesture nav will disappear during transition
+ testSpec.assertLayers {
+ this.isVisible(pipApp.component)
+ .then().isInvisible(pipApp.component)
+ .then().isVisible(pipApp.component)
+ }
+ }
+ }
+
+ override fun pipLayerReduces() {
+ // in gestural nav the pip enters through alpha animation
+ Assume.assumeFalse(testSpec.isGesturalNavigation)
+ super.pipLayerReduces()
+ }
+
+ override fun focusChanges() {
+ // in gestural nav the focus goes to different activity on swipe up
+ Assume.assumeFalse(testSpec.isGesturalNavigation)
+ super.focusChanges()
+ }
+
+ override fun pipLayerRemainInsideVisibleBounds() {
+ if (!testSpec.isGesturalNavigation) super.pipLayerRemainInsideVisibleBounds() else {
+ // pip layer in gesture nav will disappear during transition
+ testSpec.assertLayersStart {
+ this.visibleRegion(pipApp.component).coversAtMost(displayBounds)
+ }
+ testSpec.assertLayersEnd {
+ this.visibleRegion(pipApp.component).coversAtMost(displayBounds)
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
index 0640ac526bd0..61ac49835185 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
@@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.Presubmit
import android.view.Surface
-import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerParametersRunnerFactory
import com.android.server.wm.flicker.FlickerTestParameter
@@ -43,7 +42,7 @@ import org.junit.runners.Parameterized
*
* Notes:
* 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited [PipTransition]
+ * are inherited from [PipTransition]
* 2. Part of the test setup occurs automatically via
* [com.android.server.wm.flicker.TransitionRunnerWithRules],
* including configuring navigation mode, initial orientation and ensuring no
@@ -54,7 +53,7 @@ import org.junit.runners.Parameterized
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Group3
-class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
/**
* Defines the transition used to run the test
@@ -77,11 +76,6 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
}
}
- /** {@inheritDoc} */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
/**
* Checks [pipApp] window remains visible throughout the animation
*/
@@ -98,7 +92,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
*/
@Presubmit
@Test
- fun pipAppLayerAlwaysVisible() {
+ open fun pipAppLayerAlwaysVisible() {
testSpec.assertLayers {
this.isVisible(pipApp.component)
}
@@ -122,7 +116,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
*/
@Presubmit
@Test
- fun pipLayerRemainInsideVisibleBounds() {
+ open fun pipLayerRemainInsideVisibleBounds() {
testSpec.assertLayersVisibleRegion(pipApp.component) {
coversAtMost(displayBounds)
}
@@ -133,12 +127,12 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
*/
@Presubmit
@Test
- fun pipLayerReduces() {
+ open fun pipLayerReduces() {
val layerName = pipApp.component.toLayerName()
testSpec.assertLayers {
val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
pipLayerList.zipWithNext { previous, current ->
- current.visibleRegion.coversAtMost(previous.visibleRegion.region)
+ current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
}
}
}
@@ -175,7 +169,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
*/
@Presubmit
@Test
- fun focusChanges() {
+ open fun focusChanges() {
testSpec.assertEventLog {
this.focusChanges(pipApp.`package`, "NexusLauncherActivity")
}
@@ -192,8 +186,10 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
@JvmStatic
fun getParams(): List<FlickerTestParameter> {
return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 3)
+ .getConfigNonRotationTests(
+ supportedRotations = listOf(Surface.ROTATION_0),
+ repetitions = 3
+ )
}
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
index accb524d3de1..7680f4dfa1d5 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
@@ -28,13 +28,12 @@ import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.entireScreenCovered
import com.android.server.wm.flicker.helpers.WindowUtils
import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
import com.android.server.wm.traces.common.FlickerComponentName
import com.android.wm.shell.flicker.helpers.FixedAppHelper
import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT
-import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP
import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION
+import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -44,7 +43,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 EnterPipToOtherOrientationTest:EnterPipToOtherOrientationTest`
+ * To run this test: `atest WMShellFlickerTests:EnterPipToOtherOrientationTest`
*
* Actions:
* Launch [testApp] on a fixed portrait orientation
@@ -114,14 +113,6 @@ class EnterPipToOtherOrientationTest(
override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
/**
- * Checks that the [FlickerComponentName.STATUS_BAR] has the correct position at
- * the start and end of the transition
- */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-
- /**
* Checks that all parts of the screen are covered at the start and end of the transition
*
* TODO b/197726599 Prevents all states from being checked
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
index a3ed79bf0409..0768e82e491c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
@@ -79,11 +79,6 @@ class ExitPipViaExpandButtonClickTest(
}
/** {@inheritDoc} */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- /** {@inheritDoc} */
@FlakyTest(bugId = 197726610)
@Test
override fun pipLayerExpands() = super.pipLayerExpands()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
index 37e9344348d9..c6a705dacb8d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
@@ -80,7 +80,17 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit
/** {@inheritDoc} */
@FlakyTest(bugId = 206753786)
@Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
+ override fun statusBarLayerRotatesScales() {
+ Assume.assumeFalse(isShellTransitionsEnabled)
+ super.statusBarLayerRotatesScales()
+ }
+
+ @Presubmit
+ @Test
+ fun statusBarLayerRotatesScales_ShellTransit() {
+ Assume.assumeTrue(isShellTransitionsEnabled)
+ super.statusBarLayerRotatesScales()
+ }
/** {@inheritDoc} */
@FlakyTest(bugId = 197726610)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
index ab07ede5bb32..128703ad332c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
@@ -25,7 +25,6 @@ import com.android.server.wm.flicker.FlickerTestParameter
import com.android.server.wm.flicker.FlickerTestParameterFactory
import com.android.server.wm.flicker.annotation.Group3
import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -78,10 +77,6 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti
@Test
override fun pipLayerBecomesInvisible() = super.pipLayerBecomesInvisible()
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-
/**
* Checks that the focus doesn't change between windows during the transition
*/
@@ -108,4 +103,4 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti
repetitions = 3)
}
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt
index 1a21d32f568c..fe51228230cb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt
@@ -16,7 +16,7 @@
package com.android.wm.shell.flicker.pip
-import androidx.test.filters.FlakyTest
+import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerParametersRunnerFactory
import com.android.server.wm.flicker.FlickerTestParameter
@@ -35,7 +35,6 @@ import org.junit.runners.Parameterized
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Group4
-@FlakyTest(bugId = 217777115)
class PipKeyboardTestShellTransit(testSpec: FlickerTestParameter) : PipKeyboardTest(testSpec) {
@Before
@@ -43,7 +42,7 @@ class PipKeyboardTestShellTransit(testSpec: FlickerTestParameter) : PipKeyboardT
Assume.assumeTrue(isShellTransitionsEnabled)
}
- @FlakyTest(bugId = 214452854)
+ @Presubmit
@Test
override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt
deleted file mode 100644
index 21175a0767a5..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
-import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled
-import com.android.wm.shell.flicker.helpers.FixedAppHelper
-import com.android.wm.shell.flicker.helpers.ImeAppHelper
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
-import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip with split-screen.
- * To run this test: `atest WMShellFlickerTests:PipLegacySplitScreenTest`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group4
-class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
- private val imeApp = ImeAppHelper(instrumentation)
- private val testApp = FixedAppHelper(instrumentation)
-
- @Before
- open fun setup() {
- // Only run legacy split tests when the system is using legacy split screen.
- assumeTrue(SplitScreenHelper.isUsingLegacySplit())
- // Legacy split is having some issue with Shell transition, and will be deprecated soon.
- assumeFalse(isShellTransitionsEnabled())
- }
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setup {
- test {
- removeAllTasksButHome()
- device.wakeUpAndGoToHomeScreen()
- pipApp.launchViaIntent(stringExtras = mapOf(EXTRA_ENTER_PIP to "true"),
- wmHelper = wmHelper)
- }
- }
- transitions {
- testApp.launchViaIntent(wmHelper)
- device.launchSplitScreen(wmHelper)
- imeApp.launchViaIntent(wmHelper)
- }
- teardown {
- eachRun {
- imeApp.exit(wmHelper)
- testApp.exit(wmHelper)
- }
- test {
- removeAllTasksButHome()
- }
- }
- }
-
- /** {@inheritDoc} */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- @FlakyTest(bugId = 161435597)
- @Test
- fun pipWindowInsideDisplayBounds() {
- testSpec.assertWmVisibleRegion(pipApp.component) {
- coversAtMost(displayBounds)
- }
- }
-
- @Presubmit
- @Test
- fun bothAppWindowsVisible() {
- testSpec.assertWmEnd {
- isAppWindowVisible(testApp.component)
- isAppWindowVisible(imeApp.component)
- doNotOverlap(testApp.component, imeApp.component)
- }
- }
-
- @FlakyTest(bugId = 161435597)
- @Test
- fun pipLayerInsideDisplayBounds() {
- testSpec.assertLayersVisibleRegion(pipApp.component) {
- coversAtMost(displayBounds)
- }
- }
-
- @Presubmit
- @Test
- fun bothAppLayersVisible() {
- testSpec.assertLayersEnd {
- visibleRegion(testApp.component).coversAtMost(displayBounds)
- visibleRegion(imeApp.component).coversAtMost(displayBounds)
- }
- }
-
- @FlakyTest(bugId = 161435597)
- @Test
- override fun entireScreenCovered() = super.entireScreenCovered()
-
- companion object {
- const val TEST_REPETITIONS = 2
-
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = TEST_REPETITIONS
- )
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
index c1ee1a7cbb35..9fad4997e63a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
@@ -27,12 +27,9 @@ import com.android.server.wm.flicker.annotation.Group4
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.entireScreenCovered
import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
import com.android.wm.shell.flicker.helpers.FixedAppHelper
-import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -98,13 +95,6 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS
override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
/**
- * Checks the position of the status bar at the start and end of the transition
- */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
-
- /**
* Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition
*/
@Presubmit
@@ -141,14 +131,6 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS
@Presubmit
@Test
fun pipLayerRotates_StartingBounds() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- pipLayerRotates_StartingBounds_internal()
- }
-
- @FlakyTest(bugId = 228024285)
- @Test
- fun pipLayerRotates_StartingBounds_ShellTransit() {
- Assume.assumeTrue(isShellTransitionsEnabled)
pipLayerRotates_StartingBounds_internal()
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
index e40f2bc1ed5a..51339a1deb4b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
@@ -25,9 +25,9 @@ import com.android.server.wm.flicker.FlickerTestParameter
import com.android.server.wm.flicker.FlickerTestParameterFactory
import com.android.server.wm.flicker.annotation.Group4
import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.WindowUtils
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.helpers.WindowUtils
import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
import com.android.wm.shell.flicker.testapp.Components
@@ -113,10 +113,6 @@ open class SetRequestedOrientationWhilePinnedTest(
@Test
override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
@Presubmit
@Test
fun pipWindowInsideDisplay() {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
new file mode 100644
index 000000000000..702710caded7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.flicker.splitscreen
+
+import android.platform.test.annotations.Presubmit
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.helpers.SplitScreenHelper
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test enter split screen by dragging app icon from all apps.
+ * This test is only for large screen devices.
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromAllApps`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group1
+class EnterSplitScreenByDragFromAllApps(
+ testSpec: FlickerTestParameter
+) : SplitScreenBase(testSpec) {
+
+ @Before
+ open fun before() {
+ Assume.assumeTrue(taplInstrumentation.isTablet)
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ super.transition(this)
+ setup {
+ eachRun {
+ taplInstrumentation.goHome()
+ primaryApp.launchViaIntent(wmHelper)
+ }
+ }
+ transitions {
+ taplInstrumentation.launchedAppState.taskbar
+ .openAllApps()
+ .getAppIcon(secondaryApp.appName)
+ .dragToSplitscreen(secondaryApp.component.packageName,
+ primaryApp.component.packageName)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+
+ @Presubmit
+ @Test
+ fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp.component)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+ testSpec.endRotation, primaryApp.component, false /* splitLeftTop */)
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
+ testSpec.endRotation, secondaryApp.component, true /* splitLeftTop */)
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesVisible() =
+ testSpec.appWindowBecomesVisible(secondaryApp.component)
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTestParameter> {
+ return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+ repetitions = SplitScreenHelper.TEST_REPETITIONS,
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes =
+ listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY))
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
new file mode 100644
index 000000000000..7323d992ecd4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.flicker.splitscreen
+
+import android.platform.test.annotations.Presubmit
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.helpers.SplitScreenHelper
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test enter split screen by dragging app icon from notification.
+ * This test is only for large screen devices.
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromNotification`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group1
+class EnterSplitScreenByDragFromNotification(
+ testSpec: FlickerTestParameter
+) : SplitScreenBase(testSpec) {
+
+ private val sendNotificationApp = SplitScreenHelper.getSendNotification(instrumentation)
+
+ @Before
+ fun before() {
+ Assume.assumeTrue(taplInstrumentation.isTablet)
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ super.transition(this)
+ setup {
+ eachRun {
+ // Send a notification
+ sendNotificationApp.launchViaIntent(wmHelper)
+ val sendNotification = device.wait(
+ Until.findObject(By.text("Send Notification")),
+ SplitScreenHelper.TIMEOUT_MS
+ )
+ sendNotification?.click() ?: error("Send notification button not found")
+
+ taplInstrumentation.goHome()
+ primaryApp.launchViaIntent(wmHelper)
+ }
+ }
+ transitions {
+ SplitScreenHelper.dragFromNotificationToSplit(instrumentation, device, wmHelper)
+ }
+ teardown {
+ eachRun {
+ sendNotificationApp.exit(wmHelper)
+ }
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+
+ @Presubmit
+ @Test
+ fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerBecomesVisible() =
+ testSpec.layerBecomesVisible(sendNotificationApp.component)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+ testSpec.endRotation, primaryApp.component, false /* splitLeftTop */
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
+ testSpec.endRotation, sendNotificationApp.component, true /* splitLeftTop */
+ )
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowIsVisibleAtEnd() =
+ testSpec.appWindowIsVisibleAtEnd(sendNotificationApp.component)
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTestParameter> {
+ return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+ repetitions = SplitScreenHelper.TEST_REPETITIONS,
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes =
+ listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
new file mode 100644
index 000000000000..05c6e24ee89d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.flicker.splitscreen
+
+import android.platform.test.annotations.Presubmit
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.helpers.SplitScreenHelper
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test enter split screen by dragging app icon from taskbar.
+ * This test is only for large screen devices.
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromTaskbar`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group1
+class EnterSplitScreenByDragFromTaskbar(
+ testSpec: FlickerTestParameter
+) : SplitScreenBase(testSpec) {
+
+ @Before
+ fun before() {
+ Assume.assumeTrue(taplInstrumentation.isTablet)
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ super.transition(this)
+ setup {
+ eachRun {
+ taplInstrumentation.goHome()
+ SplitScreenHelper.createShortcutOnHotseatIfNotExist(
+ taplInstrumentation, secondaryApp.appName
+ )
+ primaryApp.launchViaIntent(wmHelper)
+ }
+ }
+ transitions {
+ taplInstrumentation.launchedAppState.taskbar
+ .getAppIcon(secondaryApp.appName)
+ .dragToSplitscreen(
+ secondaryApp.component.packageName,
+ primaryApp.component.packageName
+ )
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+
+ @Presubmit
+ @Test
+ fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp.component)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+ testSpec.endRotation, primaryApp.component, false /* splitLeftTop */
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
+ testSpec.endRotation, secondaryApp.component, true /* splitLeftTop */
+ )
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesVisible() =
+ testSpec.appWindowBecomesVisible(secondaryApp.component)
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTestParameter> {
+ return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+ repetitions = SplitScreenHelper.TEST_REPETITIONS,
+ supportedNavigationModes =
+ listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
new file mode 100644
index 000000000000..52c2daf96a3c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.flicker.splitscreen
+
+import android.app.Instrumentation
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.setRotation
+import com.android.wm.shell.flicker.helpers.SplitScreenHelper
+
+abstract class SplitScreenBase(protected val testSpec: FlickerTestParameter) {
+ protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ protected val taplInstrumentation = LauncherInstrumentation()
+ protected val context: Context = instrumentation.context
+ protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation)
+ protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation)
+
+ @FlickerBuilderProvider
+ fun buildFlicker(): FlickerBuilder {
+ return FlickerBuilder(instrumentation).apply {
+ transition(this)
+ }
+ }
+
+ protected open val transition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ test {
+ taplInstrumentation.setEnableRotation(true)
+ setRotation(testSpec.startRotation)
+ taplInstrumentation.setExpectedRotation(testSpec.startRotation)
+ }
+ }
+ teardown {
+ eachRun {
+ primaryApp.exit(wmHelper)
+ secondaryApp.exit(wmHelper)
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
index bd98585b67ec..bc0b0b6292b4 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
@@ -92,6 +92,17 @@
</intent-filter>
</activity>
+ <activity android:name=".SendNotificationActivity"
+ android:taskAffinity="com.android.wm.shell.flicker.testapp.SendNotificationActivity"
+ android:theme="@style/CutoutShortEdges"
+ android:label="SendNotificationApp"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
<activity android:name=".NonResizeableActivity"
android:resizeableActivity="false"
android:taskAffinity="com.android.wm.shell.flicker.testapp.NonResizeableActivity"
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml
new file mode 100644
index 000000000000..8d59b567e59b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@android:color/black">
+
+ <Button
+ android:id="@+id/button_send_notification"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:text="Send Notification" />
+</RelativeLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml
index 909b77c87894..229098313afa 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml
@@ -44,6 +44,39 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
+ android:checkedButton="@id/enter_pip_on_leave_disabled">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Enter PiP on home press"/>
+
+ <RadioButton
+ android:id="@+id/enter_pip_on_leave_disabled"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Disabled"
+ android:onClick="onAutoPipSelected"/>
+
+ <RadioButton
+ android:id="@+id/enter_pip_on_leave_manual"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Via code behind"
+ android:onClick="onAutoPipSelected"/>
+
+ <RadioButton
+ android:id="@+id/enter_pip_on_leave_autoenter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Auto-enter PiP"
+ android:onClick="onAutoPipSelected"/>
+ </RadioGroup>
+
+ <RadioGroup
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
android:checkedButton="@id/ratio_default">
<TextView
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
index 0ed59bdafd1d..a2b580da5898 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
@@ -88,6 +88,12 @@ public class Components {
PACKAGE_NAME + ".SplitScreenSecondaryActivity");
}
+ public static class SendNotificationActivity {
+ public static final String LABEL = "SendNotificationApp";
+ public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
+ PACKAGE_NAME + ".SendNotificationActivity");
+ }
+
public static class LaunchBubbleActivity {
public static final String LABEL = "LaunchBubbleApp";
public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java
index a6ba7823e22d..615b1730579c 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java
@@ -48,6 +48,7 @@ import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.CheckBox;
+import android.widget.RadioButton;
import java.util.ArrayList;
import java.util.Arrays;
@@ -201,6 +202,17 @@ public class PipActivity extends Activity {
super.onDestroy();
}
+ @Override
+ protected void onUserLeaveHint() {
+ // Only used when auto PiP is disabled. This is to simulate the behavior that an app
+ // supports regular PiP but not auto PiP.
+ final boolean manuallyEnterPip =
+ ((RadioButton) findViewById(R.id.enter_pip_on_leave_manual)).isChecked();
+ if (manuallyEnterPip) {
+ enterPictureInPictureMode();
+ }
+ }
+
private RemoteAction buildRemoteAction(Icon icon, String label, String action) {
final Intent intent = new Intent(action);
final PendingIntent pendingIntent =
@@ -216,6 +228,21 @@ public class PipActivity extends Activity {
enterPictureInPictureMode(mPipParamsBuilder.build());
}
+ public void onAutoPipSelected(View v) {
+ switch (v.getId()) {
+ case R.id.enter_pip_on_leave_manual:
+ // disable auto enter PiP
+ case R.id.enter_pip_on_leave_disabled:
+ mPipParamsBuilder.setAutoEnterEnabled(false);
+ setPictureInPictureParams(mPipParamsBuilder.build());
+ break;
+ case R.id.enter_pip_on_leave_autoenter:
+ mPipParamsBuilder.setAutoEnterEnabled(true);
+ setPictureInPictureParams(mPipParamsBuilder.build());
+ break;
+ }
+ }
+
public void onRatioSelected(View v) {
switch (v.getId()) {
case R.id.ratio_default:
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java
new file mode 100644
index 000000000000..8020ef2270a0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java
@@ -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.
+ */
+
+package com.android.wm.shell.flicker.testapp;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+public class SendNotificationActivity extends Activity {
+ private NotificationManager mNotificationManager;
+ private String mChannelId = "Channel id";
+ private String mChannelName = "Channel name";
+ private NotificationChannel mChannel;
+ private int mNotifyId = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_notification);
+ findViewById(R.id.button_send_notification).setOnClickListener(this::sendNotification);
+
+ mChannel = new NotificationChannel(mChannelId, mChannelName,
+ NotificationManager.IMPORTANCE_DEFAULT);
+ mNotificationManager = getSystemService(NotificationManager.class);
+ mNotificationManager.createNotificationChannel(mChannel);
+ }
+
+ private void sendNotification(View v) {
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
+ new Intent(this, SendNotificationActivity.class),
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ Notification notification = new Notification.Builder(this, mChannelId)
+ .setContentTitle("Notification App")
+ .setContentText("Notification content")
+ .setWhen(System.currentTimeMillis())
+ .setSmallIcon(R.drawable.ic_message)
+ .setContentIntent(pendingIntent)
+ .build();
+
+ mNotificationManager.notify(mNotifyId, notification);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index ea10be564351..1a8b9540cbd0 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -28,6 +28,9 @@ android_test {
"**/*.java",
"**/*.kt",
],
+ resource_dirs: [
+ "res",
+ ],
static_libs: [
"WindowManager-Shell",
@@ -65,4 +68,9 @@ android_test {
optimize: {
enabled: false,
},
+
+ aaptflags: [
+ "--extra-packages",
+ "com.android.wm.shell.tests",
+ ],
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java
new file mode 100644
index 000000000000..f8b3fb3def62
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import static org.mockito.Mockito.RETURNS_SELF;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.view.SurfaceControl;
+
+/**
+ * Helper class to provide mocks for {@link SurfaceControl.Builder} and
+ * {@link SurfaceControl.Transaction} with method chaining support.
+ */
+public class MockSurfaceControlHelper {
+ private MockSurfaceControlHelper() {}
+
+ /**
+ * Creates a mock {@link SurfaceControl.Builder} that supports method chaining and return the
+ * given {@link SurfaceControl} when calling {@link SurfaceControl.Builder#build()}.
+ *
+ * @param mockSurfaceControl the first {@link SurfaceControl} to return
+ * @return the mock of {@link SurfaceControl.Builder}
+ */
+ public static SurfaceControl.Builder createMockSurfaceControlBuilder(
+ SurfaceControl mockSurfaceControl) {
+ final SurfaceControl.Builder mockBuilder = mock(SurfaceControl.Builder.class, RETURNS_SELF);
+ doReturn(mockSurfaceControl)
+ .when(mockBuilder)
+ .build();
+ return mockBuilder;
+ }
+
+ /**
+ * Creates a mock {@link SurfaceControl.Transaction} that supports method chaining.
+ * @return the mock of {@link SurfaceControl.Transaction}
+ */
+ public static SurfaceControl.Transaction createMockSurfaceControlTransaction() {
+ return mock(SurfaceControl.Transaction.class, RETURNS_SELF);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java
new file mode 100644
index 000000000000..4bcdcaae230b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class ShellInitTest extends ShellTestCase {
+
+ @Mock private ShellExecutor mMainExecutor;
+
+ private ShellInit mImpl;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mImpl = new ShellInit(mMainExecutor);
+ }
+
+ @Test
+ public void testAddInitCallbacks_expectCalledInOrder() {
+ ArrayList<Integer> results = new ArrayList<>();
+ mImpl.addInitCallback(() -> {
+ results.add(1);
+ }, new Object());
+ mImpl.addInitCallback(() -> {
+ results.add(2);
+ }, new Object());
+ mImpl.addInitCallback(() -> {
+ results.add(3);
+ }, new Object());
+ mImpl.init();
+ assertTrue(results.get(0) == 1);
+ assertTrue(results.get(1) == 2);
+ assertTrue(results.get(2) == 3);
+ }
+
+ @Test
+ public void testNoInitCallbacksAfterInit_expectException() {
+ mImpl.init();
+ try {
+ mImpl.addInitCallback(() -> {}, new Object());
+ fail("Expected exception when adding callback after init");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testDoubleInit_expectNoOp() {
+ ArrayList<Integer> results = new ArrayList<>();
+ mImpl.addInitCallback(() -> {
+ results.add(1);
+ }, new Object());
+ mImpl.init();
+ assertTrue(results.size() == 1);
+ mImpl.init();
+ assertTrue(results.size() == 1);
+ }
+}
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 a6caefe6d3e7..7cbace5af48f 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,9 +16,13 @@
package com.android.wm.shell;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -26,19 +30,25 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_MULTI_WINDOW;
import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP;
+import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
-import android.content.Context;
+import android.app.WindowConfiguration;
import android.content.LocusId;
import android.content.pm.ParceledListSlice;
import android.os.Binder;
@@ -46,18 +56,21 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.SparseArray;
import android.view.SurfaceControl;
+import android.view.SurfaceSession;
import android.window.ITaskOrganizer;
import android.window.ITaskOrganizerController;
import android.window.TaskAppearedInfo;
import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransaction.Change;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
@@ -76,19 +89,19 @@ import java.util.Optional;
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class ShellTaskOrganizerTests {
+public class ShellTaskOrganizerTests extends ShellTestCase {
@Mock
private ITaskOrganizerController mTaskOrganizerController;
@Mock
- private Context mContext;
- @Mock
private CompatUIController mCompatUI;
+ @Mock
+ private ShellExecutor mTestExecutor;
+ @Mock
+ private ShellCommandHandler mShellCommandHandler;
- ShellTaskOrganizer mOrganizer;
- private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class);
- private final TransactionPool mTransactionPool = mock(TransactionPool.class);
- private final ShellExecutor mTestExecutor = mock(ShellExecutor.class);
+ private ShellTaskOrganizer mOrganizer;
+ private ShellInit mShellInit;
private class TrackingTaskListener implements ShellTaskOrganizer.TaskListener {
final ArrayList<RunningTaskInfo> appeared = new ArrayList<>();
@@ -132,18 +145,42 @@ public class ShellTaskOrganizerTests {
doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList())
.when(mTaskOrganizerController).registerTaskOrganizer(any());
} catch (RemoteException e) {}
- mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext,
- mCompatUI, Optional.empty()));
+ mShellInit = spy(new ShellInit(mTestExecutor));
+ mOrganizer = spy(new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
+ mTaskOrganizerController, mCompatUI, Optional.empty(), Optional.empty(),
+ mTestExecutor));
+ mShellInit.init();
}
@Test
- public void registerOrganizer_sendRegisterTaskOrganizer() throws RemoteException {
- mOrganizer.registerOrganizer();
+ public void instantiate_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void instantiate_addDumpCallback() {
+ verify(mShellCommandHandler, times(1)).addDumpCallback(any(), any());
+ }
+ @Test
+ public void testInit_sendRegisterTaskOrganizer() throws RemoteException {
verify(mTaskOrganizerController).registerTaskOrganizer(any(ITaskOrganizer.class));
}
@Test
+ public void testTaskLeashReleasedAfterVanished() throws RemoteException {
+ assumeFalse(ENABLE_SHELL_TRANSITIONS);
+ RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession())
+ .setName("task").build();
+ mOrganizer.registerOrganizer();
+ mOrganizer.onTaskAppeared(taskInfo, taskLeash);
+ assertTrue(taskLeash.isValid());
+ mOrganizer.onTaskVanished(taskInfo);
+ assertTrue(!taskLeash.isValid());
+ }
+
+ @Test
public void testOneListenerPerType() {
mOrganizer.addListenerForType(new TrackingTaskListener(), TASK_LISTENER_TYPE_MULTI_WINDOW);
try {
@@ -601,6 +638,99 @@ public class ShellTaskOrganizerTests {
verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token);
}
+ @Test
+ public void testPrepareClearBoundsForStandardTasks() {
+ MockToken token1 = new MockToken();
+ RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1);
+ mOrganizer.onTaskAppeared(task1, null);
+
+ MockToken token2 = new MockToken();
+ RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2);
+ mOrganizer.onTaskAppeared(task2, null);
+
+ MockToken otherDisplayToken = new MockToken();
+ RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED,
+ otherDisplayToken);
+ otherDisplayTask.displayId = 2;
+ mOrganizer.onTaskAppeared(otherDisplayTask, null);
+
+ WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1);
+
+ assertEquals(wct.getChanges().size(), 2);
+ Change boundsChange1 = wct.getChanges().get(token1.binder());
+ assertNotNull(boundsChange1);
+ assertNotEquals(
+ (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
+ assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty());
+
+ Change boundsChange2 = wct.getChanges().get(token2.binder());
+ assertNotNull(boundsChange2);
+ assertNotEquals(
+ (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
+ assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty());
+ }
+
+ @Test
+ public void testPrepareClearBoundsForStandardTasks_onlyClearActivityTypeStandard() {
+ MockToken token1 = new MockToken();
+ RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1);
+ mOrganizer.onTaskAppeared(task1, null);
+
+ MockToken token2 = new MockToken();
+ RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2);
+ task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME);
+ mOrganizer.onTaskAppeared(task2, null);
+
+ WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1);
+
+ // Only clear bounds for task1
+ assertEquals(1, wct.getChanges().size());
+ assertNotNull(wct.getChanges().get(token1.binder()));
+ }
+
+ @Test
+ public void testPrepareClearFreeformForStandardTasks() {
+ MockToken token1 = new MockToken();
+ RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1);
+ mOrganizer.onTaskAppeared(task1, null);
+
+ MockToken token2 = new MockToken();
+ RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW, token2);
+ mOrganizer.onTaskAppeared(task2, null);
+
+ MockToken otherDisplayToken = new MockToken();
+ RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM,
+ otherDisplayToken);
+ otherDisplayTask.displayId = 2;
+ mOrganizer.onTaskAppeared(otherDisplayTask, null);
+
+ WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1);
+
+ // Only task with freeform windowing mode and the right display should be updated
+ assertEquals(wct.getChanges().size(), 1);
+ Change wmModeChange1 = wct.getChanges().get(token1.binder());
+ assertNotNull(wmModeChange1);
+ assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED);
+ }
+
+ @Test
+ public void testPrepareClearFreeformForStandardTasks_onlyClearActivityTypeStandard() {
+ MockToken token1 = new MockToken();
+ RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1);
+ mOrganizer.onTaskAppeared(task1, null);
+
+ MockToken token2 = new MockToken();
+ RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FREEFORM, token2);
+ task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME);
+ mOrganizer.onTaskAppeared(task2, null);
+
+ WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1);
+
+ // Only clear freeform for task1
+ assertEquals(1, wct.getChanges().size());
+ assertNotNull(wct.getChanges().get(token1.binder()));
+ }
+
private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
RunningTaskInfo taskInfo = new RunningTaskInfo();
taskInfo.taskId = taskId;
@@ -608,4 +738,30 @@ public class ShellTaskOrganizerTests {
return taskInfo;
}
+ private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, MockToken token) {
+ RunningTaskInfo taskInfo = createTaskInfo(taskId, windowingMode);
+ taskInfo.displayId = 1;
+ taskInfo.token = token.token();
+ taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
+ return taskInfo;
+ }
+
+ private static class MockToken {
+ private final WindowContainerToken mToken;
+ private final IBinder mBinder;
+
+ MockToken() {
+ mToken = mock(WindowContainerToken.class);
+ mBinder = mock(IBinder.class);
+ when(mToken.asBinder()).thenReturn(mBinder);
+ }
+
+ WindowContainerToken token() {
+ return mToken;
+ }
+
+ IBinder binder() {
+ return mBinder;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
index 403dbf9d9554..b5ee037892ba 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
@@ -24,6 +24,8 @@ import android.testing.TestableContext;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.internal.protolog.common.ProtoLog;
+
import org.junit.After;
import org.junit.Before;
import org.mockito.MockitoAnnotations;
@@ -37,6 +39,9 @@ public abstract class ShellTestCase {
@Before
public void shellSetup() {
+ // Disable protolog tool when running the tests from studio
+ ProtoLog.REQUIRE_PROTOLOGTOOL = false;
+
MockitoAnnotations.initMocks(this);
final Context context =
InstrumentationRegistry.getInstrumentation().getTargetContext();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
index 32f1587752cb..ff1d2990a82a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
@@ -169,6 +169,7 @@ public class TaskViewTest extends ShellTestCase {
mTaskView.onTaskAppeared(mTaskInfo, mLeash);
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
+ assertThat(mTaskView.isInitialized()).isTrue();
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
}
@@ -178,6 +179,7 @@ public class TaskViewTest extends ShellTestCase {
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
verify(mViewListener).onInitialized();
+ assertThat(mTaskView.isInitialized()).isTrue();
// No task, no visibility change
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
}
@@ -189,6 +191,7 @@ public class TaskViewTest extends ShellTestCase {
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
verify(mViewListener).onInitialized();
+ assertThat(mTaskView.isInitialized()).isTrue();
verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(true));
}
@@ -223,6 +226,7 @@ public class TaskViewTest extends ShellTestCase {
verify(mOrganizer).removeListener(eq(mTaskView));
verify(mViewListener).onReleased();
+ assertThat(mTaskView.isInitialized()).isFalse();
}
@Test
@@ -270,6 +274,7 @@ public class TaskViewTest extends ShellTestCase {
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
verify(mViewListener, never()).onInitialized();
+ assertThat(mTaskView.isInitialized()).isFalse();
// If there's no surface the task should be made invisible
verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false));
}
@@ -281,6 +286,7 @@ public class TaskViewTest extends ShellTestCase {
verify(mTaskViewTransitions, never()).setTaskViewVisible(any(), anyBoolean());
verify(mViewListener).onInitialized();
+ assertThat(mTaskView.isInitialized()).isTrue();
// No task, no visibility change
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
}
@@ -353,6 +359,7 @@ public class TaskViewTest extends ShellTestCase {
verify(mOrganizer).removeListener(eq(mTaskView));
verify(mViewListener).onReleased();
+ assertThat(mTaskView.isInitialized()).isFalse();
verify(mTaskViewTransitions).removeTaskView(eq(mTaskView));
}
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 51eec27cfc0e..3672ae386dc4 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
@@ -25,8 +25,10 @@ import static org.mockito.Mockito.mock;
import android.app.ActivityManager;
import android.app.WindowConfiguration;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
+import android.view.Display;
import android.window.IWindowContainerToken;
import android.window.WindowContainerToken;
@@ -38,6 +40,11 @@ public final class TestRunningTaskInfoBuilder {
private int mParentTaskId = INVALID_TASK_ID;
private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD;
private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED;
+ private int mDisplayId = Display.DEFAULT_DISPLAY;
+ private ActivityManager.TaskDescription.Builder mTaskDescriptionBuilder = null;
+ private final Point mPositionInParent = new Point();
+ private boolean mIsVisible = false;
+ private long mLastActiveTime;
public static WindowContainerToken createMockWCToken() {
final IWindowContainerToken itoken = mock(IWindowContainerToken.class);
@@ -46,6 +53,11 @@ public final class TestRunningTaskInfoBuilder {
return new WindowContainerToken(itoken);
}
+ public TestRunningTaskInfoBuilder setToken(WindowContainerToken token) {
+ mToken = token;
+ return this;
+ }
+
public TestRunningTaskInfoBuilder setBounds(Rect bounds) {
mBounds.set(bounds);
return this;
@@ -68,17 +80,48 @@ public final class TestRunningTaskInfoBuilder {
return this;
}
+ public TestRunningTaskInfoBuilder setDisplayId(int displayId) {
+ mDisplayId = displayId;
+ return this;
+ }
+
+ public TestRunningTaskInfoBuilder setTaskDescriptionBuilder(
+ ActivityManager.TaskDescription.Builder builder) {
+ mTaskDescriptionBuilder = builder;
+ return this;
+ }
+
+ public TestRunningTaskInfoBuilder setPositionInParent(int x, int y) {
+ mPositionInParent.set(x, y);
+ return this;
+ }
+
+ public TestRunningTaskInfoBuilder setVisible(boolean isVisible) {
+ mIsVisible = isVisible;
+ return this;
+ }
+
+ public TestRunningTaskInfoBuilder setLastActiveTime(long lastActiveTime) {
+ mLastActiveTime = lastActiveTime;
+ return this;
+ }
+
public ActivityManager.RunningTaskInfo build() {
final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
- info.parentTaskId = INVALID_TASK_ID;
info.taskId = sNextTaskId++;
info.parentTaskId = mParentTaskId;
+ info.displayId = mDisplayId;
info.configuration.windowConfiguration.setBounds(mBounds);
info.configuration.windowConfiguration.setActivityType(mActivityType);
info.configuration.windowConfiguration.setWindowingMode(mWindowingMode);
info.token = mToken;
info.isResizeable = true;
info.supportsMultiWindow = true;
+ info.taskDescription =
+ mTaskDescriptionBuilder != null ? mTaskDescriptionBuilder.build() : null;
+ info.positionInParent = mPositionInParent;
+ info.isVisible = mIsVisible;
+ info.lastActiveTime = mLastActiveTime;
return info;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java
index da95c77d2b89..fe8b305093d7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java
@@ -48,9 +48,10 @@ public class TestShellExecutor implements ShellExecutor {
}
public void flushAll() {
- for (Runnable r : mRunnables) {
+ final ArrayList<Runnable> tmpRunnable = new ArrayList<>(mRunnables);
+ mRunnables.clear();
+ for (Runnable r : tmpRunnable) {
r.run();
}
- mRunnables.clear();
}
}
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
new file mode 100644
index 000000000000..98b59126227c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -0,0 +1,96 @@
+/*
+ * 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.activityembedding;
+
+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 org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.animation.Animator;
+import android.window.TransitionInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Tests for {@link ActivityEmbeddingAnimationRunner}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase {
+
+ @Before
+ public void setup() {
+ super.setUp();
+ doNothing().when(mController).onAnimationFinished(any());
+ }
+
+ @Test
+ public void testStartAnimation() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
+ info.addChange(embeddingChange);
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+
+ mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction);
+
+ final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
+ verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction),
+ finishCallback.capture());
+ verify(mStartTransaction).apply();
+ verify(mAnimator).start();
+ verifyNoMoreInteractions(mFinishTransaction);
+ verify(mController, never()).onAnimationFinished(any());
+
+ // Call onAnimationFinished() when the animation is finished.
+ finishCallback.getValue().run();
+
+ verify(mController).onAnimationFinished(mTransition);
+ }
+
+ @Test
+ public void testChangesBehindStartingWindow() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IS_BEHIND_STARTING_WINDOW);
+ info.addChange(embeddingChange);
+ final Animator animator = mAnimRunner.createAnimator(
+ info, mStartTransaction, mFinishTransaction,
+ () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */));
+
+ // The animation should be empty when it is behind starting window.
+ assertEquals(0, animator.getDuration());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
new file mode 100644
index 000000000000..3792e8361284
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -0,0 +1,107 @@
+/*
+ * 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.activityembedding;
+
+import static android.window.TransitionInfo.FLAG_FILLS_TASK;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
+
+import android.animation.Animator;
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** TestBase for ActivityEmbedding animation. */
+abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase {
+
+ @Mock
+ ShellInit mShellInit;
+ @Mock
+ Transitions mTransitions;
+ @Mock
+ IBinder mTransition;
+ @Mock
+ SurfaceControl.Transaction mStartTransaction;
+ @Mock
+ SurfaceControl.Transaction mFinishTransaction;
+ @Mock
+ Transitions.TransitionFinishCallback mFinishCallback;
+ @Mock
+ Animator mAnimator;
+
+ ActivityEmbeddingController mController;
+ ActivityEmbeddingAnimationRunner mAnimRunner;
+ ActivityEmbeddingAnimationSpec mAnimSpec;
+
+ @CallSuper
+ @Before
+ public void setUp() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+ MockitoAnnotations.initMocks(this);
+ mController = ActivityEmbeddingController.create(mContext, mShellInit, mTransitions);
+ assertNotNull(mController);
+ mAnimRunner = mController.mAnimationRunner;
+ assertNotNull(mAnimRunner);
+ mAnimSpec = mAnimRunner.mAnimationSpec;
+ assertNotNull(mAnimSpec);
+ spyOn(mController);
+ spyOn(mAnimRunner);
+ spyOn(mAnimSpec);
+ }
+
+ /** Creates a mock {@link TransitionInfo.Change}. */
+ static TransitionInfo.Change createChange() {
+ return new TransitionInfo.Change(mock(WindowContainerToken.class),
+ mock(SurfaceControl.class));
+ }
+
+ /**
+ * Creates a mock {@link TransitionInfo.Change} with
+ * {@link TransitionInfo#FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY} flag.
+ */
+ static TransitionInfo.Change createEmbeddedChange(@NonNull Rect startBounds,
+ @NonNull Rect endBounds, @NonNull Rect taskBounds) {
+ final TransitionInfo.Change change = createChange();
+ change.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
+ change.setStartAbsBounds(startBounds);
+ change.setEndAbsBounds(endBounds);
+ if (taskBounds.width() == startBounds.width()
+ && taskBounds.height() == startBounds.height()
+ && taskBounds.width() == endBounds.width()
+ && taskBounds.height() == endBounds.height()) {
+ change.setFlags(FLAG_FILLS_TASK);
+ }
+ return change;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
new file mode 100644
index 000000000000..baecf6fe6673
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -0,0 +1,188 @@
+/*
+ * 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.activityembedding;
+
+import static android.view.WindowManager.TRANSIT_OPEN;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.graphics.Rect;
+import android.window.TransitionInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link ActivityEmbeddingController}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:ActivityEmbeddingControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimationTestBase {
+
+ private static final Rect TASK_BOUNDS = new Rect(0, 0, 1000, 500);
+ private static final Rect EMBEDDED_LEFT_BOUNDS = new Rect(0, 0, 500, 500);
+ private static final Rect EMBEDDED_RIGHT_BOUNDS = new Rect(500, 0, 1000, 500);
+
+ @Before
+ public void setup() {
+ super.setUp();
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+ }
+
+ @Test
+ public void testInstantiate() {
+ verify(mShellInit).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void testOnInit() {
+ mController.onInit();
+
+ verify(mTransitions).addHandler(mController);
+ }
+
+ @Test
+ public void testSetAnimScaleSetting() {
+ mController.setAnimScaleSetting(1.0f);
+
+ verify(mAnimRunner).setAnimScaleSetting(1.0f);
+ verify(mAnimSpec).setAnimScaleSetting(1.0f);
+ }
+
+ @Test
+ public void testStartAnimation_containsNonActivityEmbeddingChange() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS,
+ EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
+ final TransitionInfo.Change nonEmbeddingChange = createChange();
+ info.addChange(embeddingChange);
+ info.addChange(nonEmbeddingChange);
+
+ // No-op because it contains non-embedded change.
+ assertFalse(mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any());
+ verifyNoMoreInteractions(mStartTransaction);
+ verifyNoMoreInteractions(mFinishTransaction);
+ verifyNoMoreInteractions(mFinishCallback);
+ }
+
+ @Test
+ public void testStartAnimation_containsOnlyFillTaskActivityEmbeddingChange() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createEmbeddedChange(TASK_BOUNDS, TASK_BOUNDS,
+ TASK_BOUNDS);
+ info.addChange(embeddingChange);
+
+ // No-op because it only contains embedded change that fills the Task. We will let the
+ // default handler to animate such transition.
+ assertFalse(mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any());
+ verifyNoMoreInteractions(mStartTransaction);
+ verifyNoMoreInteractions(mFinishTransaction);
+ verifyNoMoreInteractions(mFinishCallback);
+ }
+
+ @Test
+ public void testStartAnimation_containsActivityEmbeddingSplitChange() {
+ // Change that occupies only part of the Task.
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS,
+ EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
+ info.addChange(embeddingChange);
+
+ // ActivityEmbeddingController will handle such transition.
+ assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction);
+ verify(mStartTransaction).apply();
+ verifyNoMoreInteractions(mFinishTransaction);
+ }
+
+ @Test
+ public void testStartAnimation_containsChangeEnterActivityEmbeddingSplit() {
+ // Change that is entering ActivityEmbedding split.
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createEmbeddedChange(TASK_BOUNDS,
+ EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
+ info.addChange(embeddingChange);
+
+ // ActivityEmbeddingController will handle such transition.
+ assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction);
+ verify(mStartTransaction).apply();
+ verifyNoMoreInteractions(mFinishTransaction);
+ }
+
+ @Test
+ public void testStartAnimation_containsChangeExitActivityEmbeddingSplit() {
+ // Change that is exiting ActivityEmbedding split.
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS,
+ TASK_BOUNDS, TASK_BOUNDS);
+ info.addChange(embeddingChange);
+
+ // ActivityEmbeddingController will handle such transition.
+ assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction);
+ verify(mStartTransaction).apply();
+ verifyNoMoreInteractions(mFinishTransaction);
+ }
+
+ @Test
+ public void testOnAnimationFinished() {
+ // Should not call finish when there is no transition.
+ assertThrows(IllegalStateException.class,
+ () -> mController.onAnimationFinished(mTransition));
+
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS,
+ EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
+ info.addChange(embeddingChange);
+ mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback);
+
+ verify(mFinishCallback, never()).onTransitionFinished(any(), any());
+ mController.onAnimationFinished(mTransition);
+ verify(mFinishCallback).onTransitionFinished(any(), any());
+
+ // Should not call finish when the finish has already been called.
+ assertThrows(IllegalStateException.class,
+ () -> mController.onAnimationFinished(mTransition));
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java
deleted file mode 100644
index e73d9aaf190a..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.apppairs;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.hardware.display.DisplayManager;
-
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.TestRunningTaskInfoBuilder;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Tests for {@link AppPair}
- * Build/Install/Run:
- * atest WMShellUnitTests:AppPairTests
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AppPairTests extends ShellTestCase {
-
- private AppPairsController mController;
- @Mock private SyncTransactionQueue mSyncQueue;
- @Mock private ShellTaskOrganizer mTaskOrganizer;
- @Mock private DisplayController mDisplayController;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
- when(mDisplayController.getDisplay(anyInt())).thenReturn(
- mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
- mController = new TestAppPairsController(
- mTaskOrganizer,
- mSyncQueue,
- mDisplayController);
- spyOn(mController);
- }
-
- @After
- public void tearDown() {}
-
- @Test
- @UiThreadTest
- public void testContains() {
- final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
- final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
-
- final AppPair pair = mController.pairInner(task1, task2);
- assertThat(pair.contains(task1.taskId)).isTrue();
- assertThat(pair.contains(task2.taskId)).isTrue();
-
- pair.unpair();
- assertThat(pair.contains(task1.taskId)).isFalse();
- assertThat(pair.contains(task2.taskId)).isFalse();
- }
-
- @Test
- @UiThreadTest
- public void testVanishUnpairs() {
- final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
- final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
-
- final AppPair pair = mController.pairInner(task1, task2);
- assertThat(pair.contains(task1.taskId)).isTrue();
- assertThat(pair.contains(task2.taskId)).isTrue();
-
- pair.onTaskVanished(task1);
- assertThat(pair.contains(task1.taskId)).isFalse();
- assertThat(pair.contains(task2.taskId)).isFalse();
- }
-
- @Test
- @UiThreadTest
- public void testOnTaskInfoChanged_notSupportsMultiWindow() {
- final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
- final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
-
- final AppPair pair = mController.pairInner(task1, task2);
- assertThat(pair.contains(task1.taskId)).isTrue();
- assertThat(pair.contains(task2.taskId)).isTrue();
-
- task1.supportsMultiWindow = false;
- pair.onTaskInfoChanged(task1);
- verify(mController).unpair(pair.getRootTaskId());
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java
deleted file mode 100644
index 505c153eff9c..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.apppairs;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.hardware.display.DisplayManager;
-
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.TestRunningTaskInfoBuilder;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/** Tests for {@link AppPairsController} */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AppPairsControllerTests extends ShellTestCase {
- private TestAppPairsController mController;
- private TestAppPairsPool mPool;
- @Mock private SyncTransactionQueue mSyncQueue;
- @Mock private ShellTaskOrganizer mTaskOrganizer;
- @Mock private DisplayController mDisplayController;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
- when(mDisplayController.getDisplay(anyInt())).thenReturn(
- mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
- mController = new TestAppPairsController(
- mTaskOrganizer,
- mSyncQueue,
- mDisplayController);
- mPool = mController.getPool();
- }
-
- @After
- public void tearDown() {}
-
- @Test
- @UiThreadTest
- public void testPairUnpair() {
- final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
- final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
-
- final AppPair pair = mController.pairInner(task1, task2);
- assertThat(pair.contains(task1.taskId)).isTrue();
- assertThat(pair.contains(task2.taskId)).isTrue();
- assertThat(mPool.poolSize()).isGreaterThan(0);
-
- mController.unpair(task2.taskId);
- assertThat(pair.contains(task1.taskId)).isFalse();
- assertThat(pair.contains(task2.taskId)).isFalse();
- assertThat(mPool.poolSize()).isGreaterThan(1);
- }
-
- @Test
- @UiThreadTest
- public void testUnpair_DontReleaseToPool() {
- final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
- final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
-
- final AppPair pair = mController.pairInner(task1, task2);
- assertThat(pair.contains(task1.taskId)).isTrue();
- assertThat(pair.contains(task2.taskId)).isTrue();
-
- mController.unpair(task2.taskId, false /* releaseToPool */);
- assertThat(pair.contains(task1.taskId)).isFalse();
- assertThat(pair.contains(task2.taskId)).isFalse();
- assertThat(mPool.poolSize()).isEqualTo(1);
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java
deleted file mode 100644
index a3f134ee97ed..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.apppairs;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.when;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/** Tests for {@link AppPairsPool} */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AppPairsPoolTests extends ShellTestCase {
- private TestAppPairsController mController;
- private TestAppPairsPool mPool;
- @Mock private SyncTransactionQueue mSyncQueue;
- @Mock private ShellTaskOrganizer mTaskOrganizer;
- @Mock private DisplayController mDisplayController;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
- mController = new TestAppPairsController(
- mTaskOrganizer,
- mSyncQueue,
- mDisplayController);
- mPool = mController.getPool();
- }
-
- @After
- public void tearDown() {}
-
- @Test
- public void testInitialState() {
- // Pool should always start off with at least 1 entry.
- assertThat(mPool.poolSize()).isGreaterThan(0);
- }
-
- @Test
- public void testAcquireRelease() {
- assertThat(mPool.poolSize()).isGreaterThan(0);
- final AppPair appPair = mPool.acquire();
- assertThat(mPool.poolSize()).isGreaterThan(0);
- mPool.release(appPair);
- assertThat(mPool.poolSize()).isGreaterThan(1);
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java
deleted file mode 100644
index 294bc1276291..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.apppairs;
-
-import static org.mockito.Mockito.mock;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-public class TestAppPairsController extends AppPairsController {
- private TestAppPairsPool mPool;
-
- public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue,
- DisplayController displayController) {
- super(organizer, syncQueue, displayController, mock(ShellExecutor.class),
- mock(DisplayImeController.class), mock(DisplayInsetsController.class));
- mPool = new TestAppPairsPool(this);
- setPairsPool(mPool);
- }
-
- TestAppPairsPool getPool() {
- return mPool;
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java
deleted file mode 100644
index 1ee7fff44892..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.apppairs;
-
-import android.app.ActivityManager;
-
-import com.android.wm.shell.TestRunningTaskInfoBuilder;
-
-public class TestAppPairsPool extends AppPairsPool{
- TestAppPairsPool(AppPairsController controller) {
- super(controller);
- }
-
- @Override
- void incrementPool() {
- final AppPair entry = new AppPair(mController);
- final ActivityManager.RunningTaskInfo info =
- new TestRunningTaskInfoBuilder().build();
- entry.onTaskAppeared(info, null /* leash */);
- release(entry);
- }
-}
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 e7c5cb2183db..90a377309edd 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
@@ -20,6 +20,7 @@ import static android.window.BackNavigationInfo.KEY_TRIGGER_BACK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -28,6 +29,7 @@ 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;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -39,6 +41,7 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.os.Handler;
+import android.os.IBinder;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.provider.Settings;
@@ -51,13 +54,16 @@ import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.BackEvent;
import android.window.BackNavigationInfo;
+import android.window.IBackNaviAnimationController;
import android.window.IOnBackInvokedCallback;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Ignore;
@@ -74,10 +80,11 @@ import org.mockito.MockitoAnnotations;
@TestableLooper.RunWithLooper
@SmallTest
@RunWith(AndroidTestingRunner.class)
-public class BackAnimationControllerTest {
+public class BackAnimationControllerTest extends ShellTestCase {
private static final String ANIMATION_ENABLED = "1";
private final TestShellExecutor mShellExecutor = new TestShellExecutor();
+ private ShellInit mShellInit;
@Rule
public TestableContext mContext =
@@ -92,6 +99,9 @@ public class BackAnimationControllerTest {
@Mock
private IOnBackInvokedCallback mIOnBackInvokedCallback;
+ @Mock
+ private IBackNaviAnimationController mIBackNaviAnimationController;
+
private BackAnimationController mController;
private int mEventTime = 0;
@@ -107,10 +117,12 @@ public class BackAnimationControllerTest {
Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION,
ANIMATION_ENABLED);
mTestableLooper = TestableLooper.get(this);
- mController = new BackAnimationController(
+ mShellInit = spy(new ShellInit(mShellExecutor));
+ mController = new BackAnimationController(mShellInit,
mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
mActivityTaskManager, mContext,
mContentResolver);
+ mShellInit.init();
mEventTime = 0;
mShellExecutor.flushAll();
}
@@ -119,25 +131,24 @@ public class BackAnimationControllerTest {
SurfaceControl screenshotSurface,
HardwareBuffer hardwareBuffer,
int backType,
- IOnBackInvokedCallback onBackInvokedCallback) {
- BackNavigationInfo navigationInfo = new BackNavigationInfo(
- backType,
- topAnimationTarget,
- screenshotSurface,
- hardwareBuffer,
- new WindowConfiguration(),
- new RemoteCallback((bundle) -> {}),
- onBackInvokedCallback);
- try {
- doReturn(navigationInfo).when(mActivityTaskManager).startBackNavigation(anyBoolean());
- } catch (RemoteException ex) {
- ex.rethrowFromSystemServer();
- }
+ IOnBackInvokedCallback onBackInvokedCallback, boolean prepareAnimation) {
+ BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder()
+ .setType(backType)
+ .setDepartingAnimationTarget(topAnimationTarget)
+ .setScreenshotSurface(screenshotSurface)
+ .setScreenshotBuffer(hardwareBuffer)
+ .setTaskWindowConfiguration(new WindowConfiguration())
+ .setOnBackNavigationDone(new RemoteCallback((bundle) -> {}))
+ .setOnBackInvokedCallback(onBackInvokedCallback)
+ .setPrepareAnimation(prepareAnimation);
+
+ createNavigationInfo(builder);
}
private void createNavigationInfo(BackNavigationInfo.Builder builder) {
try {
- doReturn(builder.build()).when(mActivityTaskManager).startBackNavigation(anyBoolean());
+ doReturn(builder.build()).when(mActivityTaskManager)
+ .startBackNavigation(anyBoolean(), any(), any());
} catch (RemoteException ex) {
ex.rethrowFromSystemServer();
}
@@ -159,12 +170,17 @@ public class BackAnimationControllerTest {
}
@Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
@Ignore("b/207481538")
public void crossActivity_screenshotAttachedAndVisible() {
SurfaceControl screenshotSurface = new SurfaceControl();
HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
createNavigationInfo(createAnimationTarget(), screenshotSurface, hardwareBuffer,
- BackNavigationInfo.TYPE_CROSS_ACTIVITY, null);
+ BackNavigationInfo.TYPE_CROSS_ACTIVITY, null, true);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
verify(mTransaction).setBuffer(screenshotSurface, hardwareBuffer);
verify(mTransaction).setVisibility(screenshotSurface, true);
@@ -177,7 +193,7 @@ public class BackAnimationControllerTest {
HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
RemoteAnimationTarget animationTarget = createAnimationTarget();
createNavigationInfo(animationTarget, screenshotSurface, hardwareBuffer,
- BackNavigationInfo.TYPE_CROSS_ACTIVITY, null);
+ BackNavigationInfo.TYPE_CROSS_ACTIVITY, null, true);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
// b/207481538, we check that the surface is not moved for now, we can re-enable this once
@@ -211,15 +227,16 @@ public class BackAnimationControllerTest {
mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
RemoteAnimationTarget animationTarget = createAnimationTarget();
createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+ BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
// Check that back start and progress is dispatched when first move.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
verify(mIOnBackInvokedCallback).onBackStarted();
ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
- verify(mIOnBackInvokedCallback).onBackProgressed(backEventCaptor.capture());
+ verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget());
// Check that back invocation is dispatched.
@@ -232,17 +249,19 @@ public class BackAnimationControllerTest {
public void animationDisabledFromSettings() throws RemoteException {
// Toggle the setting off
Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0");
- mController = new BackAnimationController(
+ ShellInit shellInit = new ShellInit(mShellExecutor);
+ mController = new BackAnimationController(shellInit,
mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
mActivityTaskManager, mContext,
mContentResolver);
+ shellInit.init();
mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
RemoteAnimationTarget animationTarget = createAnimationTarget();
IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback);
+ BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback, false);
triggerBackGesture();
@@ -260,9 +279,10 @@ public class BackAnimationControllerTest {
mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
RemoteAnimationTarget animationTarget = createAnimationTarget();
createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+ BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true);
triggerBackGesture();
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
// Check that back invocation is dispatched.
verify(mIOnBackInvokedCallback).onBackInvoked();
@@ -271,11 +291,17 @@ public class BackAnimationControllerTest {
// the previous transition is finished.
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
verifyNoMoreInteractions(mIOnBackInvokedCallback);
+ mController.onBackToLauncherAnimationFinished();
+
+ // Verify that more events from a rejected swipe cannot start animation.
+ doMotionEvent(MotionEvent.ACTION_MOVE, 100);
+ doMotionEvent(MotionEvent.ACTION_UP, 0);
+ verifyNoMoreInteractions(mIOnBackInvokedCallback);
// Verify that we start accepting gestures again once transition finishes.
- mController.onBackToLauncherAnimationFinished();
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
verify(mIOnBackInvokedCallback).onBackStarted();
}
@@ -284,18 +310,49 @@ public class BackAnimationControllerTest {
mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
RemoteAnimationTarget animationTarget = createAnimationTarget();
createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+ BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true);
triggerBackGesture();
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
reset(mIOnBackInvokedCallback);
// Simulate transition timeout.
mShellExecutor.flushAll();
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
verify(mIOnBackInvokedCallback).onBackStarted();
}
+
+ @Test
+ public void cancelBackInvokeWhenLostFocus() throws RemoteException {
+ mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
+ RemoteAnimationTarget animationTarget = createAnimationTarget();
+
+ createNavigationInfo(animationTarget, null, null,
+ BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true);
+
+ doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+ // Check that back start and progress is dispatched when first move.
+ doMotionEvent(MotionEvent.ACTION_MOVE, 100);
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
+ verify(mIOnBackInvokedCallback).onBackStarted();
+
+ // Check that back invocation is dispatched.
+ mController.setTriggerBack(true); // Fake trigger back
+
+ // In case the focus has been changed.
+ IBinder token = mock(IBinder.class);
+ mController.mFocusObserver.focusLost(token);
+ mShellExecutor.flushAll();
+ verify(mIOnBackInvokedCallback).onBackCancelled();
+
+ // No more back invoke.
+ doMotionEvent(MotionEvent.ACTION_UP, 0);
+ verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+ }
+
private void doMotionEvent(int actionDown, int coordinate) {
mController.onMotionEvent(
coordinate, coordinate,
@@ -303,4 +360,14 @@ public class BackAnimationControllerTest {
BackEvent.EDGE_LEFT);
mEventTime += 10;
}
+
+ private void simulateRemoteAnimationStart(int type, RemoteAnimationTarget animationTarget)
+ throws RemoteException {
+ if (mController.mIBackAnimationRunner != null) {
+ final RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget};
+ mController.mIBackAnimationRunner.onAnimationStart(mIBackNaviAnimationController, type,
+ targets, null, null);
+ mShellExecutor.flushAll();
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java
new file mode 100644
index 000000000000..44ff35466ae2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.bubbles;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.floatThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.os.SystemClock;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test {@link MotionEvent} handling in {@link BubblesNavBarMotionEventHandler}.
+ * Verifies that swipe events
+ */
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner.class)
+public class BubblesNavBarMotionEventHandlerTest extends ShellTestCase {
+
+ private BubblesNavBarMotionEventHandler mMotionEventHandler;
+ @Mock
+ private WindowManager mWindowManager;
+ @Mock
+ private Runnable mInterceptTouchRunnable;
+ @Mock
+ private MotionEventListener mMotionEventListener;
+ private long mMotionEventTime;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ TestableBubblePositioner positioner = new TestableBubblePositioner(getContext(),
+ mWindowManager);
+ mMotionEventHandler = new BubblesNavBarMotionEventHandler(getContext(), positioner,
+ mInterceptTouchRunnable, mMotionEventListener);
+ mMotionEventTime = SystemClock.uptimeMillis();
+ }
+
+ @Test
+ public void testMotionEvent_swipeUpInGestureZone_handled() {
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 690));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 490));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 390));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 0, 390));
+
+ verify(mMotionEventListener).onDown(0, 990);
+ verify(mMotionEventListener).onMove(0, -300);
+ verify(mMotionEventListener).onMove(0, -500);
+ verify(mMotionEventListener).onMove(0, -600);
+ // Check that velocity up is about 5000
+ verify(mMotionEventListener).onUp(eq(0f), floatThat(f -> Math.round(f) == -5000));
+ verifyZeroInteractions(mMotionEventListener);
+ verify(mInterceptTouchRunnable).run();
+ }
+
+ @Test
+ public void testMotionEvent_swipeUpOutsideGestureZone_ignored() {
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 500));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 100));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 0, 100));
+
+ verifyZeroInteractions(mMotionEventListener);
+ verifyZeroInteractions(mInterceptTouchRunnable);
+ }
+
+ @Test
+ public void testMotionEvent_horizontalMoveMoreThanTouchSlop_handled() {
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 100, 990));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 100, 990));
+
+ verify(mMotionEventListener).onDown(0, 990);
+ verify(mMotionEventListener).onMove(100, 0);
+ verify(mMotionEventListener).onUp(0, 0);
+ verifyZeroInteractions(mMotionEventListener);
+ verify(mInterceptTouchRunnable).run();
+ }
+
+ @Test
+ public void testMotionEvent_moveLessThanTouchSlop_ignored() {
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 989));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 0, 989));
+
+ verify(mMotionEventListener).onDown(0, 990);
+ verifyNoMoreInteractions(mMotionEventListener);
+ verifyZeroInteractions(mInterceptTouchRunnable);
+ }
+
+ @Test
+ public void testMotionEvent_actionCancel_listenerNotified() {
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990));
+ mMotionEventHandler.onMotionEvent(newEvent(ACTION_CANCEL, 0, 990));
+ verify(mMotionEventListener).onDown(0, 990);
+ verify(mMotionEventListener).onCancel();
+ verifyNoMoreInteractions(mMotionEventListener);
+ verifyZeroInteractions(mInterceptTouchRunnable);
+ }
+
+ private MotionEvent newEvent(int actionDown, float x, float y) {
+ MotionEvent event = MotionEvent.obtain(0L, mMotionEventTime, actionDown, x, y, 0);
+ mMotionEventTime += 10;
+ return event;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java
index d5fbe556045a..0537d0ea2404 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java
@@ -20,7 +20,7 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
-import com.android.wm.shell.R;
+import com.android.wm.shell.tests.R;
/**
* Referenced by NotificationTestHelper#makeBubbleMetadata
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java
new file mode 100644
index 000000000000..991913afbb90
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.bubbles.animation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.bubbles.BubbleExpandedView;
+import com.android.wm.shell.bubbles.TestableBubblePositioner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class ExpandedViewAnimationControllerTest extends ShellTestCase {
+
+ private ExpandedViewAnimationController mController;
+
+ @Mock
+ private WindowManager mWindowManager;
+
+ @Mock
+ private BubbleExpandedView mMockExpandedView;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ TestableBubblePositioner positioner = new TestableBubblePositioner(getContext(),
+ mWindowManager);
+ mController = new ExpandedViewAnimationControllerImpl(getContext(), positioner);
+
+ mController.setExpandedView(mMockExpandedView);
+ when(mMockExpandedView.getContentHeight()).thenReturn(1000);
+ }
+
+ @Test
+ public void testUpdateDrag_expandedViewMovesUpAndClipped() {
+ // Drag by 50 pixels which corresponds to 10 pixels with overscroll
+ int dragDistance = 50;
+ int dampenedDistance = 10;
+
+ mController.updateDrag(dragDistance);
+
+ verify(mMockExpandedView).setTopClip(dampenedDistance);
+ verify(mMockExpandedView).setContentTranslationY(-dampenedDistance);
+ verify(mMockExpandedView).setManageButtonTranslationY(-dampenedDistance);
+ }
+
+ @Test
+ public void testUpdateDrag_zOrderUpdates() {
+ mController.updateDrag(10);
+ mController.updateDrag(20);
+
+ verify(mMockExpandedView, times(1)).setSurfaceZOrderedOnTop(true);
+ verify(mMockExpandedView, times(1)).setAnimating(true);
+ }
+
+ @Test
+ public void testUpdateDrag_moveBackToZero_zOrderRestored() {
+ mController.updateDrag(50);
+ reset(mMockExpandedView);
+ mController.updateDrag(0);
+ mController.updateDrag(0);
+
+ verify(mMockExpandedView, times(1)).setSurfaceZOrderedOnTop(false);
+ verify(mMockExpandedView, times(1)).setAnimating(false);
+ }
+
+ @Test
+ public void testUpdateDrag_hapticFeedbackOnlyOnce() {
+ // Drag by 10 which is below the collapse threshold - no feedback
+ mController.updateDrag(10);
+ verify(mMockExpandedView, times(0)).performHapticFeedback(anyInt());
+ // 150 takes it over the threshold - perform feedback
+ mController.updateDrag(150);
+ verify(mMockExpandedView, times(1)).performHapticFeedback(anyInt());
+ // Continue dragging, no more feedback
+ mController.updateDrag(200);
+ verify(mMockExpandedView, times(1)).performHapticFeedback(anyInt());
+ // Drag below threshold and over again - no more feedback
+ mController.updateDrag(10);
+ mController.updateDrag(150);
+ verify(mMockExpandedView, times(1)).performHapticFeedback(anyInt());
+ }
+
+ @Test
+ public void testShouldCollapse_doNotCollapseIfNotDragged() {
+ assertThat(mController.shouldCollapse()).isFalse();
+ }
+
+ @Test
+ public void testShouldCollapse_doNotCollapseIfVelocityDown() {
+ assumeTrue("Min fling velocity should be > 1 for this test", getMinFlingVelocity() > 1);
+ mController.setSwipeVelocity(getVelocityAboveMinFling());
+ assertThat(mController.shouldCollapse()).isFalse();
+ }
+
+ @Test
+ public void tesShouldCollapse_doNotCollapseIfVelocityUpIsSmall() {
+ assumeTrue("Min fling velocity should be > 1 for this test", getMinFlingVelocity() > 1);
+ mController.setSwipeVelocity(-getVelocityBelowMinFling());
+ assertThat(mController.shouldCollapse()).isFalse();
+ }
+
+ @Test
+ public void testShouldCollapse_collapseIfVelocityUpIsLarge() {
+ assumeTrue("Min fling velocity should be > 1 for this test", getMinFlingVelocity() > 1);
+ mController.setSwipeVelocity(-getVelocityAboveMinFling());
+ assertThat(mController.shouldCollapse()).isTrue();
+ }
+
+ @Test
+ public void testShouldCollapse_collapseIfPastThreshold() {
+ mController.updateDrag(500);
+ assertThat(mController.shouldCollapse()).isTrue();
+ }
+
+ @Test
+ public void testReset() {
+ mController.updateDrag(100);
+ reset(mMockExpandedView);
+ mController.reset();
+ verify(mMockExpandedView, atLeastOnce()).setAnimating(false);
+ verify(mMockExpandedView).setContentAlpha(1);
+ verify(mMockExpandedView).setBackgroundAlpha(1);
+ verify(mMockExpandedView).setManageButtonAlpha(1);
+ verify(mMockExpandedView).setManageButtonAlpha(1);
+ verify(mMockExpandedView).setTopClip(0);
+ verify(mMockExpandedView).setContentTranslationY(-0f);
+ verify(mMockExpandedView).setManageButtonTranslationY(-0f);
+ verify(mMockExpandedView).setBottomClip(0);
+ verify(mMockExpandedView).movePointerBy(0, 0);
+ assertThat(mController.shouldCollapse()).isFalse();
+ }
+
+ private int getVelocityBelowMinFling() {
+ return getMinFlingVelocity() - 1;
+ }
+
+ private int getVelocityAboveMinFling() {
+ return getMinFlingVelocity() + 1;
+ }
+
+ private int getMinFlingVelocity() {
+ return ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
index 0972cf2c032f..1636c5f73133 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
@@ -25,6 +25,7 @@ import com.android.wm.shell.bubbles.storage.BubbleXmlHelperTest.Companion.sparse
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertNotNull
import junit.framework.Assert.assertTrue
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -61,6 +62,12 @@ class BubblePersistentRepositoryTest : ShellTestCase() {
bubbles.put(1, user1Bubbles)
}
+ @After
+ fun teardown() {
+ // Clean up the any persisted bubbles for the next run
+ repository.persistsToDisk(SparseArray())
+ }
+
@Test
fun testReadWriteOperation() {
// Verify read before write doesn't cause FileNotFoundException
@@ -71,4 +78,4 @@ class BubblePersistentRepositoryTest : ShellTestCase() {
repository.persistsToDisk(bubbles)
assertTrue(sparseArraysEqual(bubbles, repository.readFromDisk()))
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java
new file mode 100644
index 000000000000..b8aa8e7cbc48
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java
@@ -0,0 +1,64 @@
+/*
+ * 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.common;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.view.IWindowManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the display change controller.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:DisplayChangeControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DisplayChangeControllerTests extends ShellTestCase {
+
+ private @Mock IWindowManager mWM;
+ private @Mock ShellInit mShellInit;
+ private @Mock ShellExecutor mMainExecutor;
+ private DisplayChangeController mController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mController = spy(new DisplayChangeController(mWM, mShellInit, mMainExecutor));
+ }
+
+ @Test
+ public void instantiate_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
new file mode 100644
index 000000000000..1e5e153fdfe1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
@@ -0,0 +1,65 @@
+/*
+ * 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.common;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.view.IWindowManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the display controller.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:DisplayControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DisplayControllerTests extends ShellTestCase {
+
+ private @Mock Context mContext;
+ private @Mock IWindowManager mWM;
+ private @Mock ShellInit mShellInit;
+ private @Mock ShellExecutor mMainExecutor;
+ private DisplayController mController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mController = new DisplayController(mContext, mWM, mShellInit, mMainExecutor);
+ }
+
+ @Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), eq(mController));
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index b88845044263..9967e5f47752 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -26,6 +26,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -39,26 +40,33 @@ import android.view.SurfaceControl;
import androidx.test.filters.SmallTest;
import com.android.internal.view.IInputMethodManager;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import java.util.concurrent.Executor;
@SmallTest
-public class DisplayImeControllerTest {
+public class DisplayImeControllerTest extends ShellTestCase {
+ @Mock
private SurfaceControl.Transaction mT;
- private DisplayImeController.PerDisplay mPerDisplay;
+ @Mock
private IInputMethodManager mMock;
+ @Mock
+ private ShellInit mShellInit;
+ private DisplayImeController.PerDisplay mPerDisplay;
private Executor mExecutor;
@Before
public void setUp() throws Exception {
- mT = mock(SurfaceControl.Transaction.class);
- mMock = mock(IInputMethodManager.class);
+ MockitoAnnotations.initMocks(this);
mExecutor = spy(Runnable::run);
- mPerDisplay = new DisplayImeController(null, null, null, mExecutor, new TransactionPool() {
+ mPerDisplay = new DisplayImeController(null, mShellInit, null, null, new TransactionPool() {
@Override
public SurfaceControl.Transaction acquire() {
return mT;
@@ -67,7 +75,7 @@ public class DisplayImeControllerTest {
@Override
public void release(SurfaceControl.Transaction t) {
}
- }) {
+ }, mExecutor) {
@Override
public IInputMethodManager getImms() {
return mMock;
@@ -78,6 +86,11 @@ public class DisplayImeControllerTest {
}
@Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
public void insetsControlChanged_schedulesNoWorkOnExecutor() {
mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl());
verifyZeroInteractions(mExecutor);
@@ -121,7 +134,7 @@ public class DisplayImeControllerTest {
private InsetsSourceControl[] insetsSourceControl() {
return new InsetsSourceControl[]{
new InsetsSourceControl(
- ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0), Insets.NONE)
+ ITYPE_IME, mock(SurfaceControl.class), false, new Point(0, 0), Insets.NONE)
};
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
index 3bf06cc0ede3..5f5a3c584ee0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -19,11 +19,13 @@ package com.android.wm.shell.common;
import static android.view.Display.DEFAULT_DISPLAY;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.content.ComponentName;
import android.os.RemoteException;
import android.util.SparseArray;
import android.view.IDisplayWindowInsetsController;
@@ -34,7 +36,9 @@ import android.view.InsetsVisibilities;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
@@ -45,7 +49,7 @@ import org.mockito.MockitoAnnotations;
import java.util.List;
@SmallTest
-public class DisplayInsetsControllerTest {
+public class DisplayInsetsControllerTest extends ShellTestCase {
private static final int SECOND_DISPLAY = DEFAULT_DISPLAY + 10;
@@ -53,6 +57,8 @@ public class DisplayInsetsControllerTest {
private IWindowManager mWm;
@Mock
private DisplayController mDisplayController;
+ @Mock
+ private ShellInit mShellInit;
private DisplayInsetsController mController;
private SparseArray<IDisplayWindowInsetsController> mInsetsControllersByDisplayId;
private TestShellExecutor mExecutor;
@@ -67,11 +73,16 @@ public class DisplayInsetsControllerTest {
mInsetsControllersByDisplayId = new SparseArray<>();
mDisplayIdCaptor = ArgumentCaptor.forClass(Integer.class);
mInsetsControllerCaptor = ArgumentCaptor.forClass(IDisplayWindowInsetsController.class);
- mController = new DisplayInsetsController(mWm, mDisplayController, mExecutor);
+ mController = new DisplayInsetsController(mWm, mShellInit, mDisplayController, mExecutor);
addDisplay(DEFAULT_DISPLAY);
}
@Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
public void testOnDisplayAdded_setsDisplayWindowInsetsControllerOnWMService()
throws RemoteException {
addDisplay(SECOND_DISPLAY);
@@ -164,7 +175,7 @@ public class DisplayInsetsControllerTest {
int hideInsetsCount = 0;
@Override
- public void topFocusedWindowChanged(String packageName,
+ public void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) {
topFocusedWindowChangedCount++;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
index 0ffa5b35331d..d467b399ebbb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
@@ -41,11 +41,13 @@ import androidx.test.filters.SmallTest;
import com.android.internal.R;
import com.android.internal.policy.SystemBarUtils;
+import com.android.wm.shell.ShellTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
/**
* Tests for {@link DisplayLayout}.
@@ -54,13 +56,14 @@ import org.mockito.MockitoSession;
* atest WMShellUnitTests:DisplayLayoutTest
*/
@SmallTest
-public class DisplayLayoutTest {
+public class DisplayLayoutTest extends ShellTestCase {
private MockitoSession mMockitoSession;
@Before
public void setup() {
mMockitoSession = mockitoSession()
.initMocks(this)
+ .strictness(Strictness.WARN)
.mockStatic(SystemBarUtils.class)
.startMocking();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
index 96938ebc27df..1347e061eb45 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
@@ -35,6 +35,8 @@ import android.window.TaskSnapshot;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.ShellTestCase;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -47,7 +49,7 @@ import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@SmallTest
-public class TaskStackListenerImplTest {
+public class TaskStackListenerImplTest extends ShellTestCase {
@Mock
private IActivityTaskManager mActivityTaskManager;
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 f1e602fcf778..695550dd8fa5 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
@@ -133,7 +133,7 @@ public class SplitLayoutTests extends ShellTestCase {
mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
waitDividerFlingFinished();
- verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false));
+ verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false), anyInt());
}
@Test
@@ -145,7 +145,7 @@ public class SplitLayoutTests extends ShellTestCase {
mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
waitDividerFlingFinished();
- verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true));
+ verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true), anyInt());
}
@Test
@@ -159,7 +159,8 @@ public class SplitLayoutTests extends ShellTestCase {
}
private void waitDividerFlingFinished() {
- verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture());
+ verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), anyInt(),
+ mRunnableCaptor.capture());
mRunnableCaptor.getValue().run();
}
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 596100dcdead..6292130ddec9 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
@@ -29,6 +29,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -53,6 +54,8 @@ 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.compatui.letterboxedu.LetterboxEduWindowManager;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
@@ -78,6 +81,8 @@ public class CompatUIControllerTest extends ShellTestCase {
private static final int TASK_ID = 12;
private CompatUIController mController;
+ private ShellInit mShellInit;
+ private @Mock ShellController mMockShellController;
private @Mock DisplayController mMockDisplayController;
private @Mock DisplayInsetsController mMockDisplayInsetsController;
private @Mock DisplayLayout mMockDisplayLayout;
@@ -105,9 +110,10 @@ public class CompatUIControllerTest extends ShellTestCase {
doReturn(TASK_ID).when(mMockLetterboxEduLayout).getTaskId();
doReturn(true).when(mMockLetterboxEduLayout).createLayout(anyBoolean());
doReturn(true).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean());
- mController = new CompatUIController(mContext, mMockDisplayController,
- mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor,
- mMockTransitionsLazy) {
+ mShellInit = spy(new ShellInit(mMockExecutor));
+ mController = new CompatUIController(mContext, mShellInit, mMockShellController,
+ mMockDisplayController, mMockDisplayInsetsController, mMockImeController,
+ mMockSyncQueue, mMockExecutor, mMockTransitionsLazy) {
@Override
CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
@@ -120,10 +126,21 @@ public class CompatUIControllerTest extends ShellTestCase {
return mMockLetterboxEduLayout;
}
};
+ mShellInit.init();
spyOn(mController);
}
@Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void instantiateController_registerKeyguardChangeListener() {
+ verify(mMockShellController, times(1)).addKeyguardChangeListener(any());
+ }
+
+ @Test
public void testListenerRegistered() {
verify(mMockDisplayController).addDisplayWindowListener(mController);
verify(mMockImeController).addPositionProcessor(mController);
@@ -324,7 +341,7 @@ public class CompatUIControllerTest extends ShellTestCase {
/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
// Verify that the restart button is hidden after keyguard becomes showing.
- mController.onKeyguardShowingChanged(true);
+ mController.onKeyguardVisibilityChanged(true, false, false);
verify(mMockCompatLayout).updateVisibility(false);
verify(mMockLetterboxEduLayout).updateVisibility(false);
@@ -340,7 +357,7 @@ public class CompatUIControllerTest extends ShellTestCase {
false);
// Verify button is shown after keyguard becomes not showing.
- mController.onKeyguardShowingChanged(false);
+ mController.onKeyguardVisibilityChanged(false, false, false);
verify(mMockCompatLayout).updateVisibility(true);
verify(mMockLetterboxEduLayout).updateVisibility(true);
@@ -352,7 +369,7 @@ public class CompatUIControllerTest extends ShellTestCase {
/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true);
- mController.onKeyguardShowingChanged(true);
+ mController.onKeyguardVisibilityChanged(true, false, false);
verify(mMockCompatLayout, times(2)).updateVisibility(false);
verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false);
@@ -360,7 +377,7 @@ public class CompatUIControllerTest extends ShellTestCase {
clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout);
// Verify button remains hidden after keyguard becomes not showing since IME is showing.
- mController.onKeyguardShowingChanged(false);
+ mController.onKeyguardVisibilityChanged(false, false, false);
verify(mMockCompatLayout).updateVisibility(false);
verify(mMockLetterboxEduLayout).updateVisibility(false);
@@ -378,7 +395,7 @@ public class CompatUIControllerTest extends ShellTestCase {
/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true);
- mController.onKeyguardShowingChanged(true);
+ mController.onKeyguardVisibilityChanged(true, false, false);
verify(mMockCompatLayout, times(2)).updateVisibility(false);
verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false);
@@ -392,7 +409,7 @@ public class CompatUIControllerTest extends ShellTestCase {
verify(mMockLetterboxEduLayout).updateVisibility(false);
// Verify button is shown after keyguard becomes not showing.
- mController.onKeyguardShowingChanged(false);
+ mController.onKeyguardVisibilityChanged(false, false, false);
verify(mMockCompatLayout).updateVisibility(true);
verify(mMockLetterboxEduLayout).updateVisibility(true);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
new file mode 100644
index 000000000000..dd23d97d9199
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.testing.AndroidTestingRunner;
+import android.window.DisplayAreaInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransaction.Change;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+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.ShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DesktopModeControllerTest extends ShellTestCase {
+
+ @Mock
+ private ShellTaskOrganizer mShellTaskOrganizer;
+ @Mock
+ private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+ @Mock
+ private ShellExecutor mTestExecutor;
+ @Mock
+ private Handler mMockHandler;
+ @Mock
+ private Transitions mMockTransitions;
+ private TestShellExecutor mExecutor;
+
+ private DesktopModeController mController;
+ private DesktopModeTaskRepository mDesktopModeTaskRepository;
+ private ShellInit mShellInit;
+ private StaticMockitoSession mMockitoSession;
+
+ @Before
+ public void setUp() {
+ mMockitoSession = mockitoSession().mockStatic(DesktopModeStatus.class).startMocking();
+ when(DesktopModeStatus.isActive(any())).thenReturn(true);
+
+ mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
+ mExecutor = new TestShellExecutor();
+
+ mDesktopModeTaskRepository = new DesktopModeTaskRepository();
+
+ mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer,
+ mRootTaskDisplayAreaOrganizer, mMockTransitions,
+ mDesktopModeTaskRepository, mMockHandler, mExecutor);
+
+ when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn(
+ new WindowContainerTransaction());
+
+ mShellInit.init();
+ clearInvocations(mShellTaskOrganizer);
+ clearInvocations(mRootTaskDisplayAreaOrganizer);
+ }
+
+ @After
+ public void tearDown() {
+ mMockitoSession.finishMocking();
+ }
+
+ @Test
+ public void instantiate_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() {
+ // Create a fake WCT to simulate setting task windowing mode to undefined
+ WindowContainerTransaction taskWct = new WindowContainerTransaction();
+ MockToken taskMockToken = new MockToken();
+ taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED);
+ when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(
+ mContext.getDisplayId())).thenReturn(taskWct);
+
+ // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly
+ MockToken displayMockToken = new MockToken();
+ DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken,
+ mContext.getDisplayId(), 0);
+ when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId()))
+ .thenReturn(displayAreaInfo);
+
+ // The test
+ mController.updateDesktopModeActive(true);
+
+ ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+ WindowContainerTransaction.class);
+ verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture());
+
+ // WCT should have 2 changes - clear task wm mode and set display wm mode
+ WindowContainerTransaction wct = arg.getValue();
+ assertThat(wct.getChanges()).hasSize(2);
+
+ // Verify executed WCT has a change for setting task windowing mode to undefined
+ Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder());
+ assertThat(taskWmModeChange).isNotNull();
+ assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+
+ // Verify executed WCT has a change for setting display windowing mode to freeform
+ Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder());
+ assertThat(displayWmModeChange).isNotNull();
+ assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM);
+ }
+
+ @Test
+ public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() {
+ // Create a fake WCT to simulate setting task windowing mode to undefined
+ WindowContainerTransaction taskWmWct = new WindowContainerTransaction();
+ MockToken taskWmMockToken = new MockToken();
+ taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED);
+ when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(
+ mContext.getDisplayId())).thenReturn(taskWmWct);
+
+ // Create a fake WCT to simulate clearing task bounds
+ WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction();
+ MockToken taskBoundsMockToken = new MockToken();
+ taskBoundsWct.setBounds(taskBoundsMockToken.token(), null);
+ when(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(
+ mContext.getDisplayId())).thenReturn(taskBoundsWct);
+
+ // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly
+ MockToken displayMockToken = new MockToken();
+ DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken,
+ mContext.getDisplayId(), 0);
+ when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId()))
+ .thenReturn(displayAreaInfo);
+
+ // The test
+ mController.updateDesktopModeActive(false);
+
+ ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+ WindowContainerTransaction.class);
+ verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture());
+
+ // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode
+ WindowContainerTransaction wct = arg.getValue();
+ assertThat(wct.getChanges()).hasSize(3);
+
+ // Verify executed WCT has a change for setting task windowing mode to undefined
+ Change taskWmMode = wct.getChanges().get(taskWmMockToken.binder());
+ assertThat(taskWmMode).isNotNull();
+ assertThat(taskWmMode.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+
+ // Verify executed WCT has a change for clearing task bounds
+ Change bounds = wct.getChanges().get(taskBoundsMockToken.binder());
+ assertThat(bounds).isNotNull();
+ assertThat(bounds.getWindowSetMask() & WINDOW_CONFIG_BOUNDS).isNotEqualTo(0);
+ assertThat(bounds.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue();
+
+ // Verify executed WCT has a change for setting display windowing mode to fullscreen
+ Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder());
+ assertThat(displayWmModeChange).isNotNull();
+ assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
+ }
+
+ @Test
+ public void testShowDesktopApps() {
+ // Set up two active tasks on desktop
+ mDesktopModeTaskRepository.addActiveTask(1);
+ mDesktopModeTaskRepository.addActiveTask(2);
+ MockToken token1 = new MockToken();
+ MockToken token2 = new MockToken();
+ ActivityManager.RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder().setToken(
+ token1.token()).setLastActiveTime(100).build();
+ ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder().setToken(
+ token2.token()).setLastActiveTime(200).build();
+ when(mShellTaskOrganizer.getRunningTaskInfo(1)).thenReturn(taskInfo1);
+ when(mShellTaskOrganizer.getRunningTaskInfo(2)).thenReturn(taskInfo2);
+
+ // Run show desktop apps logic
+ mController.showDesktopApps();
+ ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass(
+ WindowContainerTransaction.class);
+ verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture());
+ WindowContainerTransaction wct = wctCaptor.getValue();
+
+ // Check wct has reorder calls
+ assertThat(wct.getHierarchyOps()).hasSize(2);
+
+ // Task 2 has activity later, must be first
+ WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
+ assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+ assertThat(op1.getContainer()).isEqualTo(token2.binder());
+
+ // Task 1 should be second
+ WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(0);
+ assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+ assertThat(op2.getContainer()).isEqualTo(token2.binder());
+ }
+
+ private static class MockToken {
+ private final WindowContainerToken mToken;
+ private final IBinder mBinder;
+
+ MockToken() {
+ mToken = mock(WindowContainerToken.class);
+ mBinder = mock(IBinder.class);
+ when(mToken.asBinder()).thenReturn(mBinder);
+ }
+
+ WindowContainerToken token() {
+ return mToken;
+ }
+
+ IBinder binder() {
+ return mBinder;
+ }
+ }
+}
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
new file mode 100644
index 000000000000..9b28d11f6a9d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopModeTaskRepositoryTest : ShellTestCase() {
+
+ private lateinit var repo: DesktopModeTaskRepository
+
+ @Before
+ fun setUp() {
+ repo = DesktopModeTaskRepository()
+ }
+
+ @Test
+ fun addActiveTask_listenerNotifiedAndTaskIsActive() {
+ val listener = TestListener()
+ repo.addListener(listener)
+
+ repo.addActiveTask(1)
+ assertThat(listener.activeTaskChangedCalls).isEqualTo(1)
+ assertThat(repo.isActiveTask(1)).isTrue()
+ }
+
+ @Test
+ fun addActiveTask_sameTaskDoesNotNotify() {
+ val listener = TestListener()
+ repo.addListener(listener)
+
+ repo.addActiveTask(1)
+ repo.addActiveTask(1)
+ assertThat(listener.activeTaskChangedCalls).isEqualTo(1)
+ }
+
+ @Test
+ fun addActiveTask_multipleTasksAddedNotifiesForEach() {
+ val listener = TestListener()
+ repo.addListener(listener)
+
+ repo.addActiveTask(1)
+ repo.addActiveTask(2)
+ assertThat(listener.activeTaskChangedCalls).isEqualTo(2)
+ }
+
+ @Test
+ fun removeActiveTask_listenerNotifiedAndTaskNotActive() {
+ val listener = TestListener()
+ repo.addListener(listener)
+
+ repo.addActiveTask(1)
+ repo.removeActiveTask(1)
+ // Notify once for add and once for remove
+ assertThat(listener.activeTaskChangedCalls).isEqualTo(2)
+ assertThat(repo.isActiveTask(1)).isFalse()
+ }
+
+ @Test
+ fun removeActiveTask_removeNotExistingTaskDoesNotNotify() {
+ val listener = TestListener()
+ repo.addListener(listener)
+ repo.removeActiveTask(99)
+ assertThat(listener.activeTaskChangedCalls).isEqualTo(0)
+ }
+
+ @Test
+ fun isActiveTask_notExistingTaskReturnsFalse() {
+ assertThat(repo.isActiveTask(99)).isFalse()
+ }
+
+ class TestListener : DesktopModeTaskRepository.Listener {
+ var activeTaskChangedCalls = 0
+ override fun onActiveTasksChanged() {
+ activeTaskChangedCalls++
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
index aaeebef03d0f..b6dbcf204364 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
@@ -21,6 +21,7 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.DragEvent.ACTION_DRAG_STARTED;
import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -44,9 +45,12 @@ import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
@@ -54,35 +58,50 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.Optional;
-
/**
* Tests for the drag and drop controller.
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class DragAndDropControllerTest {
+public class DragAndDropControllerTest extends ShellTestCase {
@Mock
private Context mContext;
-
+ @Mock
+ private ShellInit mShellInit;
+ @Mock
+ private ShellController mShellController;
@Mock
private DisplayController mDisplayController;
-
@Mock
private UiEventLogger mUiEventLogger;
-
@Mock
private DragAndDropController.DragAndDropListener mDragAndDropListener;
+ @Mock
+ private IconProvider mIconProvider;
+ @Mock
+ private ShellExecutor mMainExecutor;
+ @Mock
+ private SplitScreenController mSplitScreenController;
private DragAndDropController mController;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
- mController = new DragAndDropController(mContext, mDisplayController, mUiEventLogger,
- mock(IconProvider.class), mock(ShellExecutor.class));
- mController.initialize(Optional.of(mock(SplitScreenController.class)));
+ mController = new DragAndDropController(mContext, mShellInit, mShellController,
+ mDisplayController, mUiEventLogger, mIconProvider, mMainExecutor);
+ mController.onInit();
+ }
+
+ @Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void instantiateController_registerConfigChangeListener() {
+ verify(mShellController, times(1)).addConfigurationChangeListener(any());
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
index bb6026c36c97..9e988e8e8726 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
@@ -34,7 +34,6 @@ import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPL
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP;
-import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
@@ -57,7 +56,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
-import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
@@ -68,6 +66,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.InstanceId;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -87,7 +86,7 @@ import java.util.HashSet;
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class DragAndDropPolicyTest {
+public class DragAndDropPolicyTest extends ShellTestCase {
@Mock
private Context mContext;
@@ -182,8 +181,10 @@ public class DragAndDropPolicyTest {
info.configuration.windowConfiguration.setActivityType(actType);
info.configuration.windowConfiguration.setWindowingMode(winMode);
info.isResizeable = true;
- info.baseActivity = new ComponentName(getInstrumentation().getContext().getPackageName(),
+ info.baseActivity = new ComponentName(getInstrumentation().getContext(),
".ActivityWithMode" + winMode);
+ info.baseIntent = new Intent();
+ info.baseIntent.setComponent(info.baseActivity);
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.packageName = info.baseActivity.getPackageName();
activityInfo.name = info.baseActivity.getClassName();
@@ -263,62 +264,6 @@ public class DragAndDropPolicyTest {
}
}
- @Test
- public void testLaunchMultipleTask_differentActivity() {
- setRunningTask(mFullscreenAppTask);
- mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
- Intent fillInIntent = mPolicy.getStartIntentFillInIntent(mock(PendingIntent.class), 0);
- assertNull(fillInIntent);
- }
-
- @Test
- public void testLaunchMultipleTask_differentActivity_inSplitscreen() {
- setRunningTask(mFullscreenAppTask);
- doReturn(true).when(mSplitScreenStarter).isSplitScreenVisible();
- doReturn(mFullscreenAppTask).when(mSplitScreenStarter).getTaskInfo(anyInt());
- mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
- Intent fillInIntent = mPolicy.getStartIntentFillInIntent(mock(PendingIntent.class), 0);
- assertNull(fillInIntent);
- }
-
- @Test
- public void testLaunchMultipleTask_sameActivity() {
- setRunningTask(mFullscreenAppTask);
-
- // Replace the mocked drag pending intent and ensure it resolves to the same activity
- PendingIntent launchIntent = mock(PendingIntent.class);
- ResolveInfo launchInfo = new ResolveInfo();
- launchInfo.activityInfo = mFullscreenAppTask.topActivityInfo;
- doReturn(Collections.singletonList(launchInfo))
- .when(launchIntent).queryIntentComponents(anyInt());
- mActivityClipData.getItemAt(0).getIntent().putExtra(ClipDescription.EXTRA_PENDING_INTENT,
- launchIntent);
-
- mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
- Intent fillInIntent = mPolicy.getStartIntentFillInIntent(launchIntent, 0);
- assertTrue((fillInIntent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0);
- }
-
- @Test
- public void testLaunchMultipleTask_sameActivity_inSplitScreen() {
- setRunningTask(mFullscreenAppTask);
-
- // Replace the mocked drag pending intent and ensure it resolves to the same activity
- PendingIntent launchIntent = mock(PendingIntent.class);
- ResolveInfo launchInfo = new ResolveInfo();
- launchInfo.activityInfo = mFullscreenAppTask.topActivityInfo;
- doReturn(Collections.singletonList(launchInfo))
- .when(launchIntent).queryIntentComponents(anyInt());
- mActivityClipData.getItemAt(0).getIntent().putExtra(ClipDescription.EXTRA_PENDING_INTENT,
- launchIntent);
-
- doReturn(true).when(mSplitScreenStarter).isSplitScreenVisible();
- doReturn(mFullscreenAppTask).when(mSplitScreenStarter).getTaskInfo(anyInt());
- mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
- Intent fillInIntent = mPolicy.getStartIntentFillInIntent(launchIntent, 0);
- assertTrue((fillInIntent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0);
- }
-
private Target filterTargetByType(ArrayList<Target> targets, int type) {
for (Target t : targets) {
if (type == t.type) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
new file mode 100644
index 000000000000..a88c83779f25
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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.floating;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.wm.shell.floating.FloatingTasksController.SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TaskViewTransitions;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.floating.views.FloatingTaskLayer;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+/**
+ * Tests for the floating tasks controller.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class FloatingTasksControllerTest extends ShellTestCase {
+ // Some behavior in the controller constructor is dependent on this so we can only
+ // validate if it's working for the real value for those things.
+ private static final boolean FLOATING_TASKS_ACTUALLY_ENABLED =
+ SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
+
+ @Mock private ShellInit mShellInit;
+ @Mock private ShellController mShellController;
+ @Mock private WindowManager mWindowManager;
+ @Mock private ShellTaskOrganizer mTaskOrganizer;
+ @Captor private ArgumentCaptor<FloatingTaskLayer> mFloatingTaskLayerCaptor;
+
+ private FloatingTasksController mController;
+
+ @Before
+ public void setUp() throws RemoteException {
+ MockitoAnnotations.initMocks(this);
+
+ WindowMetrics windowMetrics = mock(WindowMetrics.class);
+ WindowInsets windowInsets = mock(WindowInsets.class);
+ Insets insets = Insets.of(0, 0, 0, 0);
+ when(mWindowManager.getCurrentWindowMetrics()).thenReturn(windowMetrics);
+ when(windowMetrics.getWindowInsets()).thenReturn(windowInsets);
+ when(windowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1000, 1000));
+ when(windowInsets.getInsetsIgnoringVisibility(anyInt())).thenReturn(insets);
+
+ // For the purposes of this test, just run everything synchronously
+ ShellExecutor shellExecutor = new TestShellExecutor();
+ when(mTaskOrganizer.getExecutor()).thenReturn(shellExecutor);
+ }
+
+ @After
+ public void tearDown() {
+ if (mController != null) {
+ mController.removeTask();
+ mController = null;
+ }
+ }
+
+ private void setUpTabletConfig() {
+ Configuration config = mock(Configuration.class);
+ config.smallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET;
+ mController.setConfig(config);
+ }
+
+ private void setUpPhoneConfig() {
+ Configuration config = mock(Configuration.class);
+ config.smallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET - 1;
+ mController.setConfig(config);
+ }
+
+ private void createController() {
+ mController = new FloatingTasksController(mContext,
+ mShellInit,
+ mShellController,
+ mock(ShellCommandHandler.class),
+ Optional.empty(),
+ mWindowManager,
+ mTaskOrganizer,
+ mock(TaskViewTransitions.class),
+ mock(ShellExecutor.class),
+ mock(ShellExecutor.class),
+ mock(SyncTransactionQueue.class));
+ spyOn(mController);
+ }
+
+ //
+ // Shell specific
+ //
+ @Test
+ public void instantiateController_addInitCallback() {
+ if (FLOATING_TASKS_ACTUALLY_ENABLED) {
+ createController();
+ setUpTabletConfig();
+
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+ }
+
+ @Test
+ public void instantiateController_doesntAddInitCallback() {
+ if (!FLOATING_TASKS_ACTUALLY_ENABLED) {
+ createController();
+
+ verify(mShellInit, never()).addInitCallback(any(), any());
+ }
+ }
+
+ @Test
+ public void onInit_registerConfigChangeListener() {
+ if (FLOATING_TASKS_ACTUALLY_ENABLED) {
+ createController();
+ setUpTabletConfig();
+ mController.onInit();
+
+ verify(mShellController, times(1)).addConfigurationChangeListener(any());
+ }
+ }
+
+ //
+ // Tests for floating layer, which is only available for tablets.
+ //
+
+ @Test
+ public void testIsFloatingLayerAvailable_true() {
+ createController();
+ setUpTabletConfig();
+ assertThat(mController.isFloatingLayerAvailable()).isTrue();
+ }
+
+ @Test
+ public void testIsFloatingLayerAvailable_false() {
+ createController();
+ setUpPhoneConfig();
+ assertThat(mController.isFloatingLayerAvailable()).isFalse();
+ }
+
+ //
+ // Tests for floating tasks being enabled, guarded by sysprop flag.
+ //
+
+ @Test
+ public void testIsFloatingTasksEnabled_true() {
+ createController();
+ mController.setFloatingTasksEnabled(true);
+ setUpTabletConfig();
+ assertThat(mController.isFloatingTasksEnabled()).isTrue();
+ }
+
+ @Test
+ public void testIsFloatingTasksEnabled_false() {
+ createController();
+ mController.setFloatingTasksEnabled(false);
+ setUpTabletConfig();
+ assertThat(mController.isFloatingTasksEnabled()).isFalse();
+ }
+
+ //
+ // Tests for behavior depending on flags
+ //
+
+ @Test
+ public void testShowTaskIntent_enabled() {
+ createController();
+ mController.setFloatingTasksEnabled(true);
+ setUpTabletConfig();
+
+ mController.showTask(mock(Intent.class));
+ verify(mWindowManager).addView(mFloatingTaskLayerCaptor.capture(), any());
+ assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testShowTaskIntent_notEnabled() {
+ createController();
+ mController.setFloatingTasksEnabled(false);
+ setUpTabletConfig();
+
+ mController.showTask(mock(Intent.class));
+ verify(mWindowManager, never()).addView(any(), any());
+ }
+
+ @Test
+ public void testRemoveTask() {
+ createController();
+ mController.setFloatingTasksEnabled(true);
+ setUpTabletConfig();
+
+ mController.showTask(mock(Intent.class));
+ verify(mWindowManager).addView(mFloatingTaskLayerCaptor.capture(), any());
+ assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(1);
+
+ mController.removeTask();
+ verify(mWindowManager).removeView(mFloatingTaskLayerCaptor.capture());
+ assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(0);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
new file mode 100644
index 000000000000..0fd5cb081ea9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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.freeform;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+
+import static com.android.wm.shell.transition.Transitions.TRANSIT_MAXIMIZE;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.IWindowContainerToken;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.fullscreen.FullscreenTaskListener;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests of {@link FreeformTaskTransitionObserver}
+ */
+@SmallTest
+public class FreeformTaskTransitionObserverTest {
+
+ @Mock
+ private ShellInit mShellInit;
+ @Mock
+ private Transitions mTransitions;
+ @Mock
+ private FullscreenTaskListener<?> mFullscreenTaskListener;
+ @Mock
+ private FreeformTaskListener<?> mFreeformTaskListener;
+
+ private FreeformTaskTransitionObserver mTransitionObserver;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ PackageManager pm = mock(PackageManager.class);
+ doReturn(true).when(pm).hasSystemFeature(
+ PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT);
+ final Context context = mock(Context.class);
+ doReturn(pm).when(context).getPackageManager();
+
+ mTransitionObserver = new FreeformTaskTransitionObserver(
+ context, mShellInit, mTransitions, mFullscreenTaskListener, mFreeformTaskListener);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(
+ Runnable.class);
+ verify(mShellInit).addInitCallback(initRunnableCaptor.capture(),
+ same(mTransitionObserver));
+ initRunnableCaptor.getValue().run();
+ } else {
+ mTransitionObserver.onInit();
+ }
+ }
+
+ @Test
+ public void testRegistersObserverAtInit() {
+ verify(mTransitions).registerObserver(same(mTransitionObserver));
+ }
+
+ @Test
+ public void testCreatesWindowDecorOnOpenTransition_freeform() {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ info.addChange(change);
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+
+ verify(mFreeformTaskListener).createWindowDecoration(change, startT, finishT);
+ }
+
+ @Test
+ public void testObtainsWindowDecorOnCloseTransition_freeform() {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
+ info.addChange(change);
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+
+ verify(mFreeformTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT);
+ }
+
+ @Test
+ public void testDoesntCloseWindowDecorDuringCloseTransition() throws Exception {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
+ info.addChange(change);
+
+ final AutoCloseable windowDecor = mock(AutoCloseable.class);
+ doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration(
+ eq(change.getTaskInfo()), any(), any());
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+
+ verify(windowDecor, never()).close();
+ }
+
+ @Test
+ public void testClosesWindowDecorAfterCloseTransition() throws Exception {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
+ info.addChange(change);
+
+ final AutoCloseable windowDecor = mock(AutoCloseable.class);
+ doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration(
+ eq(change.getTaskInfo()), any(), any());
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+ mTransitionObserver.onTransitionFinished(transition, false);
+
+ verify(windowDecor).close();
+ }
+
+ @Test
+ public void testClosesMergedWindowDecorationAfterTransitionFinishes() throws Exception {
+ // The playing transition
+ final TransitionInfo.Change change1 =
+ createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info1 = new TransitionInfo(TRANSIT_OPEN, 0);
+ info1.addChange(change1);
+
+ final IBinder transition1 = mock(IBinder.class);
+ final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition1, info1, startT1, finishT1);
+ mTransitionObserver.onTransitionStarting(transition1);
+
+ // The merged transition
+ final TransitionInfo.Change change2 =
+ createChange(TRANSIT_CLOSE, 2, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0);
+ info2.addChange(change2);
+
+ final AutoCloseable windowDecor2 = mock(AutoCloseable.class);
+ doReturn(windowDecor2).when(mFreeformTaskListener).giveWindowDecoration(
+ eq(change2.getTaskInfo()), any(), any());
+
+ final IBinder transition2 = mock(IBinder.class);
+ final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition2, info2, startT2, finishT2);
+ mTransitionObserver.onTransitionMerged(transition2, transition1);
+
+ mTransitionObserver.onTransitionFinished(transition1, false);
+
+ verify(windowDecor2).close();
+ }
+
+ @Test
+ public void testClosesAllWindowDecorsOnTransitionMergeAfterCloseTransitions() throws Exception {
+ // The playing transition
+ final TransitionInfo.Change change1 =
+ createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info1 = new TransitionInfo(TRANSIT_CLOSE, 0);
+ info1.addChange(change1);
+
+ final AutoCloseable windowDecor1 = mock(AutoCloseable.class);
+ doReturn(windowDecor1).when(mFreeformTaskListener).giveWindowDecoration(
+ eq(change1.getTaskInfo()), any(), any());
+
+ final IBinder transition1 = mock(IBinder.class);
+ final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition1, info1, startT1, finishT1);
+ mTransitionObserver.onTransitionStarting(transition1);
+
+ // The merged transition
+ final TransitionInfo.Change change2 =
+ createChange(TRANSIT_CLOSE, 2, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0);
+ info2.addChange(change2);
+
+ final AutoCloseable windowDecor2 = mock(AutoCloseable.class);
+ doReturn(windowDecor2).when(mFreeformTaskListener).giveWindowDecoration(
+ eq(change2.getTaskInfo()), any(), any());
+
+ final IBinder transition2 = mock(IBinder.class);
+ final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition2, info2, startT2, finishT2);
+ mTransitionObserver.onTransitionMerged(transition2, transition1);
+
+ mTransitionObserver.onTransitionFinished(transition1, false);
+
+ verify(windowDecor1).close();
+ verify(windowDecor2).close();
+ }
+
+ @Test
+ public void testTransfersWindowDecorOnMaximize() {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN);
+ final TransitionInfo info = new TransitionInfo(TRANSIT_MAXIMIZE, 0);
+ info.addChange(change);
+
+ final AutoCloseable windowDecor = mock(AutoCloseable.class);
+ doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration(
+ eq(change.getTaskInfo()), any(), any());
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+
+ verify(mFreeformTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT);
+ verify(mFullscreenTaskListener).adoptWindowDecoration(
+ eq(change), same(startT), same(finishT), any());
+ }
+
+ @Test
+ public void testTransfersWindowDecorOnRestoreFromMaximize() {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info = new TransitionInfo(TRANSIT_RESTORE_FROM_MAXIMIZE, 0);
+ info.addChange(change);
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+
+ verify(mFullscreenTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT);
+ verify(mFreeformTaskListener).adoptWindowDecoration(
+ eq(change), same(startT), same(finishT), any());
+ }
+
+ private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
+ final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+ taskInfo.taskId = taskId;
+ taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
+
+ final TransitionInfo.Change change = new TransitionInfo.Change(
+ new WindowContainerToken(mock(IWindowContainerToken.class)),
+ mock(SurfaceControl.class));
+ change.setMode(mode);
+ change.setTaskInfo(taskInfo);
+ return change;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
deleted file mode 100644
index 4523e2c9cba5..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.fullscreen;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-
-import static org.junit.Assume.assumeFalse;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager.RunningTaskInfo;
-import android.app.WindowConfiguration;
-import android.content.res.Configuration;
-import android.graphics.Point;
-import android.os.SystemProperties;
-import android.view.SurfaceControl;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.recents.RecentTasksController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Optional;
-
-@SmallTest
-public class FullscreenTaskListenerTest {
- private static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
-
- @Mock
- private SyncTransactionQueue mSyncQueue;
- @Mock
- private FullscreenUnfoldController mUnfoldController;
- @Mock
- private RecentTasksController mRecentTasksController;
- @Mock
- private SurfaceControl mSurfaceControl;
-
- private Optional<FullscreenUnfoldController> mFullscreenUnfoldController;
-
- private FullscreenTaskListener mListener;
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- mFullscreenUnfoldController = Optional.of(mUnfoldController);
- mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController,
- Optional.empty());
- }
-
- @Test
- public void testAnimatableTaskAppeared_notifiesUnfoldController() {
- assumeFalse(ENABLE_SHELL_TRANSITIONS);
- RunningTaskInfo info = createTaskInfo(/* visible */ true, /* taskId */ 0);
-
- mListener.onTaskAppeared(info, mSurfaceControl);
-
- verify(mUnfoldController).onTaskAppeared(eq(info), any());
- }
-
- @Test
- public void testMultipleAnimatableTasksAppeared_notifiesUnfoldController() {
- assumeFalse(ENABLE_SHELL_TRANSITIONS);
- RunningTaskInfo animatable1 = createTaskInfo(/* visible */ true, /* taskId */ 0);
- RunningTaskInfo animatable2 = createTaskInfo(/* visible */ true, /* taskId */ 1);
-
- mListener.onTaskAppeared(animatable1, mSurfaceControl);
- mListener.onTaskAppeared(animatable2, mSurfaceControl);
-
- InOrder order = inOrder(mUnfoldController);
- order.verify(mUnfoldController).onTaskAppeared(eq(animatable1), any());
- order.verify(mUnfoldController).onTaskAppeared(eq(animatable2), any());
- }
-
- @Test
- public void testNonAnimatableTaskAppeared_doesNotNotifyUnfoldController() {
- assumeFalse(ENABLE_SHELL_TRANSITIONS);
- RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0);
-
- mListener.onTaskAppeared(info, mSurfaceControl);
-
- verifyNoMoreInteractions(mUnfoldController);
- }
-
- @Test
- public void testNonAnimatableTaskChanged_doesNotNotifyUnfoldController() {
- assumeFalse(ENABLE_SHELL_TRANSITIONS);
- RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0);
- mListener.onTaskAppeared(info, mSurfaceControl);
-
- mListener.onTaskInfoChanged(info);
-
- verifyNoMoreInteractions(mUnfoldController);
- }
-
- @Test
- public void testNonAnimatableTaskVanished_doesNotNotifyUnfoldController() {
- assumeFalse(ENABLE_SHELL_TRANSITIONS);
- RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0);
- mListener.onTaskAppeared(info, mSurfaceControl);
-
- mListener.onTaskVanished(info);
-
- verifyNoMoreInteractions(mUnfoldController);
- }
-
- @Test
- public void testAnimatableTaskBecameInactive_notifiesUnfoldController() {
- assumeFalse(ENABLE_SHELL_TRANSITIONS);
- RunningTaskInfo animatableTask = createTaskInfo(/* visible */ true, /* taskId */ 0);
- mListener.onTaskAppeared(animatableTask, mSurfaceControl);
- RunningTaskInfo notAnimatableTask = createTaskInfo(/* visible */ false, /* taskId */ 0);
-
- mListener.onTaskInfoChanged(notAnimatableTask);
-
- verify(mUnfoldController).onTaskVanished(eq(notAnimatableTask));
- }
-
- @Test
- public void testAnimatableTaskVanished_notifiesUnfoldController() {
- assumeFalse(ENABLE_SHELL_TRANSITIONS);
- RunningTaskInfo taskInfo = createTaskInfo(/* visible */ true, /* taskId */ 0);
- mListener.onTaskAppeared(taskInfo, mSurfaceControl);
-
- mListener.onTaskVanished(taskInfo);
-
- verify(mUnfoldController).onTaskVanished(eq(taskInfo));
- }
-
- private RunningTaskInfo createTaskInfo(boolean visible, int taskId) {
- final RunningTaskInfo info = spy(new RunningTaskInfo());
- info.isVisible = visible;
- info.positionInParent = new Point();
- when(info.getWindowingMode()).thenReturn(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
- final Configuration configuration = new Configuration();
- configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
- when(info.getConfiguration()).thenReturn(configuration);
- info.taskId = taskId;
- return info;
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java
index b976c1287aca..6c301bbbc7f1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java
@@ -16,10 +16,13 @@
package com.android.wm.shell.hidedisplaycutout;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import android.platform.test.annotations.Presubmit;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
@@ -27,7 +30,11 @@ import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
@@ -35,25 +42,45 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-@Presubmit
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
-public class HideDisplayCutoutControllerTest {
+public class HideDisplayCutoutControllerTest extends ShellTestCase {
private TestableContext mContext = new TestableContext(
InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
- private HideDisplayCutoutController mHideDisplayCutoutController;
@Mock
- private HideDisplayCutoutOrganizer mMockDisplayAreaOrganizer;
+ private ShellCommandHandler mShellCommandHandler;
@Mock
- private ShellExecutor mMockMainExecutor;
+ private ShellController mShellController;
+ @Mock
+ private HideDisplayCutoutOrganizer mMockDisplayAreaOrganizer;
+
+ private HideDisplayCutoutController mHideDisplayCutoutController;
+ private ShellInit mShellInit;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mHideDisplayCutoutController = new HideDisplayCutoutController(
- mContext, mMockDisplayAreaOrganizer, mMockMainExecutor);
+ mShellInit = spy(new ShellInit(mock(ShellExecutor.class)));
+ mHideDisplayCutoutController = new HideDisplayCutoutController(mContext, mShellInit,
+ mShellCommandHandler, mShellController, mMockDisplayAreaOrganizer);
+ mShellInit.init();
+ }
+
+ @Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void instantiateController_registerDumpCallback() {
+ verify(mShellCommandHandler, times(1)).addDumpCallback(any(), any());
+ }
+
+ @Test
+ public void instantiateController_registerConfigChangeListener() {
+ verify(mShellController, times(1)).addConfigurationChangeListener(any());
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java
index 16e92395c85e..49521cfbb34d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java
@@ -32,7 +32,6 @@ import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Binder;
-import android.platform.test.annotations.Presubmit;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
@@ -48,6 +47,7 @@ import android.window.WindowContainerToken;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
@@ -61,11 +61,10 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
-@Presubmit
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
-public class HideDisplayCutoutOrganizerTest {
+public class HideDisplayCutoutOrganizerTest extends ShellTestCase {
private TestableContext mContext = new TestableContext(
InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
index 440a6f8fb59a..ecfb427dbced 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
@@ -44,11 +44,13 @@ import android.window.WindowContainerTransaction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.startingsurface.StartingWindowController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
@@ -61,7 +63,7 @@ import java.util.Optional;
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class KidsModeTaskOrganizerTest {
+public class KidsModeTaskOrganizerTest extends ShellTestCase {
@Mock private ITaskOrganizerController mTaskOrganizerController;
@Mock private Context mContext;
@Mock private Handler mHandler;
@@ -72,7 +74,8 @@ public class KidsModeTaskOrganizerTest {
@Mock private WindowContainerToken mToken;
@Mock private WindowContainerTransaction mTransaction;
@Mock private KidsModeSettingsObserver mObserver;
- @Mock private StartingWindowController mStartingWindowController;
+ @Mock private ShellInit mShellInit;
+ @Mock private ShellCommandHandler mShellCommandHandler;
@Mock private DisplayInsetsController mDisplayInsetsController;
KidsModeTaskOrganizer mOrganizer;
@@ -86,15 +89,20 @@ public class KidsModeTaskOrganizerTest {
} catch (RemoteException e) {
}
// NOTE: KidsModeTaskOrganizer should have a null CompatUIController.
- mOrganizer = spy(new KidsModeTaskOrganizer(mTaskOrganizerController, mTestExecutor,
- mHandler, mContext, mSyncTransactionQueue, mDisplayController,
- mDisplayInsetsController, Optional.empty(), mObserver));
- mOrganizer.initialize(mStartingWindowController);
+ mOrganizer = spy(new KidsModeTaskOrganizer(mContext, mShellInit, mShellCommandHandler,
+ mTaskOrganizerController, mSyncTransactionQueue, mDisplayController,
+ mDisplayInsetsController, Optional.empty(), Optional.empty(), mObserver,
+ mTestExecutor, mHandler));
doReturn(mTransaction).when(mOrganizer).getWindowContainerTransaction();
doReturn(new InsetsState()).when(mDisplayController).getInsetsState(DEFAULT_DISPLAY);
}
@Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
public void testKidsModeOn() {
doReturn(true).when(mObserver).isEnabled();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
index ecf1c5d41864..cf8297eec061 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
@@ -30,27 +30,27 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.content.om.IOverlayManager;
import android.graphics.Rect;
import android.os.Handler;
-import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import android.util.ArrayMap;
import android.view.Display;
import android.view.Surface;
-import android.view.SurfaceControl;
import android.window.WindowContainerTransaction;
import androidx.test.filters.SmallTest;
-import com.android.internal.jank.InteractionJankMonitor;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
@@ -62,16 +62,20 @@ import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class OneHandedControllerTest extends OneHandedTestCase {
- private int mCurrentUser = UserHandle.myUserId();
Display mDisplay;
OneHandedAccessibilityUtil mOneHandedAccessibilityUtil;
OneHandedController mSpiedOneHandedController;
OneHandedTimeoutHandler mSpiedTimeoutHandler;
OneHandedState mSpiedTransitionState;
+ ShellInit mShellInit;
@Mock
- DisplayLayout mDisplayLayout;
+ ShellCommandHandler mMockShellCommandHandler;
+ @Mock
+ ShellController mMockShellController;
+ @Mock
+ DisplayLayout mMockDisplayLayout;
@Mock
DisplayController mMockDisplayController;
@Mock
@@ -87,16 +91,10 @@ public class OneHandedControllerTest extends OneHandedTestCase {
@Mock
OneHandedUiEventLogger mMockUiEventLogger;
@Mock
- InteractionJankMonitor mMockJankMonitor;
- @Mock
- IOverlayManager mMockOverlayManager;
- @Mock
TaskStackListenerImpl mMockTaskStackListener;
@Mock
ShellExecutor mMockShellMainExecutor;
@Mock
- SurfaceControl mMockLeash;
- @Mock
Handler mMockShellMainHandler;
final boolean mDefaultEnabled = true;
@@ -107,7 +105,7 @@ public class OneHandedControllerTest extends OneHandedTestCase {
public void setUp() {
MockitoAnnotations.initMocks(this);
mDisplay = mContext.getDisplay();
- mDisplayLayout = Mockito.mock(DisplayLayout.class);
+ mMockDisplayLayout = Mockito.mock(DisplayLayout.class);
mSpiedTimeoutHandler = spy(new OneHandedTimeoutHandler(mMockShellMainExecutor));
mSpiedTransitionState = spy(new OneHandedState());
@@ -127,11 +125,15 @@ public class OneHandedControllerTest extends OneHandedTestCase {
when(mMockDisplayAreaOrganizer.getLastDisplayBounds()).thenReturn(
new Rect(0, 0, 1080, 2400));
- when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(mDisplayLayout);
+ when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(mMockDisplayLayout);
+ mShellInit = spy(new ShellInit(mMockShellMainExecutor));
mOneHandedAccessibilityUtil = new OneHandedAccessibilityUtil(mContext);
mSpiedOneHandedController = spy(new OneHandedController(
mContext,
+ mShellInit,
+ mMockShellCommandHandler,
+ mMockShellController,
mMockDisplayController,
mMockDisplayAreaOrganizer,
mMockTouchHandler,
@@ -140,13 +142,37 @@ public class OneHandedControllerTest extends OneHandedTestCase {
mOneHandedAccessibilityUtil,
mSpiedTimeoutHandler,
mSpiedTransitionState,
- mMockJankMonitor,
mMockUiEventLogger,
- mMockOverlayManager,
mMockTaskStackListener,
mMockShellMainExecutor,
mMockShellMainHandler)
);
+ mShellInit.init();
+ }
+
+ @Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void instantiateController_registerDumpCallback() {
+ verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), any());
+ }
+
+ @Test
+ public void testControllerRegistersConfigChangeListener() {
+ verify(mMockShellController, times(1)).addConfigurationChangeListener(any());
+ }
+
+ @Test
+ public void testControllerRegistersKeyguardChangeListener() {
+ verify(mMockShellController, times(1)).addKeyguardChangeListener(any());
+ }
+
+ @Test
+ public void testControllerRegistersUserChangeListener() {
+ verify(mMockShellController, times(1)).addUserChangeListener(any());
}
@Test
@@ -304,9 +330,9 @@ public class OneHandedControllerTest extends OneHandedTestCase {
@Test
public void testRotation90CanNotStartOneHanded() {
- mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_90);
+ mMockDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_90);
mSpiedTransitionState.setState(STATE_NONE);
- when(mDisplayLayout.isLandscape()).thenReturn(true);
+ when(mMockDisplayLayout.isLandscape()).thenReturn(true);
mSpiedOneHandedController.setOneHandedEnabled(true);
mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */);
mSpiedOneHandedController.startOneHanded();
@@ -316,10 +342,10 @@ public class OneHandedControllerTest extends OneHandedTestCase {
@Test
public void testRotation180CanStartOneHanded() {
- mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_180);
+ mMockDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_180);
mSpiedTransitionState.setState(STATE_NONE);
when(mMockDisplayAreaOrganizer.isReady()).thenReturn(true);
- when(mDisplayLayout.isLandscape()).thenReturn(false);
+ when(mMockDisplayLayout.isLandscape()).thenReturn(false);
mSpiedOneHandedController.setOneHandedEnabled(true);
mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */);
mSpiedOneHandedController.startOneHanded();
@@ -329,9 +355,9 @@ public class OneHandedControllerTest extends OneHandedTestCase {
@Test
public void testRotation270CanNotStartOneHanded() {
- mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_270);
+ mMockDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_270);
mSpiedTransitionState.setState(STATE_NONE);
- when(mDisplayLayout.isLandscape()).thenReturn(true);
+ when(mMockDisplayLayout.isLandscape()).thenReturn(true);
mSpiedOneHandedController.setOneHandedEnabled(true);
mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */);
mSpiedOneHandedController.startOneHanded();
@@ -345,8 +371,8 @@ public class OneHandedControllerTest extends OneHandedTestCase {
when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn(
false);
final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
- mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0,
- Surface.ROTATION_90, handlerWCT);
+ mSpiedOneHandedController.onDisplayChange(mDisplay.getDisplayId(), Surface.ROTATION_0,
+ Surface.ROTATION_90, null /* newDisplayAreaInfo */, handlerWCT);
verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext),
eq(Surface.ROTATION_90), any(WindowContainerTransaction.class));
@@ -358,8 +384,8 @@ public class OneHandedControllerTest extends OneHandedTestCase {
when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn(
false);
final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
- mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0,
- Surface.ROTATION_90, handlerWCT);
+ mSpiedOneHandedController.onDisplayChange(mDisplay.getDisplayId(), Surface.ROTATION_0,
+ Surface.ROTATION_90, null /* newDisplayAreaInfo */, handlerWCT);
verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext),
eq(Surface.ROTATION_90), any(WindowContainerTransaction.class));
@@ -371,8 +397,8 @@ public class OneHandedControllerTest extends OneHandedTestCase {
when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn(
true);
final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
- mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0,
- Surface.ROTATION_90, handlerWCT);
+ mSpiedOneHandedController.onDisplayChange(mDisplay.getDisplayId(), Surface.ROTATION_0,
+ Surface.ROTATION_90, null /* newDisplayAreaInfo */, handlerWCT);
verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext),
eq(Surface.ROTATION_90), any(WindowContainerTransaction.class));
@@ -384,8 +410,8 @@ public class OneHandedControllerTest extends OneHandedTestCase {
when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn(
false);
final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
- mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0,
- Surface.ROTATION_90, handlerWCT);
+ mSpiedOneHandedController.onDisplayChange(mDisplay.getDisplayId(), Surface.ROTATION_0,
+ Surface.ROTATION_90, null /* newDisplayAreaInfo */, handlerWCT);
verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext),
eq(Surface.ROTATION_90), any(WindowContainerTransaction.class));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java
index dba1b8b86261..a39bdf04bf56 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java
@@ -29,22 +29,21 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.content.om.IOverlayManager;
import android.graphics.Rect;
import android.os.Handler;
-import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import android.util.ArrayMap;
import android.view.Display;
-import android.view.SurfaceControl;
import androidx.test.filters.SmallTest;
-import com.android.internal.jank.InteractionJankMonitor;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
@@ -55,7 +54,6 @@ import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class OneHandedStateTest extends OneHandedTestCase {
- private int mCurrentUser = UserHandle.myUserId();
Display mDisplay;
DisplayLayout mDisplayLayout;
@@ -65,6 +63,12 @@ public class OneHandedStateTest extends OneHandedTestCase {
OneHandedState mSpiedState;
@Mock
+ ShellInit mMockShellInit;
+ @Mock
+ ShellCommandHandler mMockShellCommandHandler;
+ @Mock
+ ShellController mMockShellController;
+ @Mock
DisplayController mMockDisplayController;
@Mock
OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer;
@@ -77,16 +81,10 @@ public class OneHandedStateTest extends OneHandedTestCase {
@Mock
OneHandedUiEventLogger mMockUiEventLogger;
@Mock
- InteractionJankMonitor mMockJankMonitor;
- @Mock
- IOverlayManager mMockOverlayManager;
- @Mock
TaskStackListenerImpl mMockTaskStackListener;
@Mock
ShellExecutor mMockShellMainExecutor;
@Mock
- SurfaceControl mMockLeash;
- @Mock
Handler mMockShellMainHandler;
final boolean mDefaultEnabled = true;
@@ -119,6 +117,9 @@ public class OneHandedStateTest extends OneHandedTestCase {
mOneHandedAccessibilityUtil = new OneHandedAccessibilityUtil(mContext);
mSpiedOneHandedController = spy(new OneHandedController(
mContext,
+ mMockShellInit,
+ mMockShellCommandHandler,
+ mMockShellController,
mMockDisplayController,
mMockDisplayAreaOrganizer,
mMockTouchHandler,
@@ -127,9 +128,7 @@ public class OneHandedStateTest extends OneHandedTestCase {
mOneHandedAccessibilityUtil,
mSpiedTimeoutHandler,
mSpiedState,
- mMockJankMonitor,
mMockUiEventLogger,
- mMockOverlayManager,
mMockTaskStackListener,
mMockShellMainExecutor,
mMockShellMainHandler)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java
index 8b03dc58c3bf..808ab2167dc7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java
@@ -30,6 +30,8 @@ import android.view.WindowManager;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.wm.shell.ShellTestCase;
+
import org.junit.Before;
import org.junit.Rule;
import org.mockito.Answers;
@@ -38,7 +40,7 @@ import org.mockito.Mock;
/**
* Base class that does One Handed specific setup.
*/
-public abstract class OneHandedTestCase {
+public abstract class OneHandedTestCase extends ShellTestCase {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
protected Context mContext;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
index c685fdc1f09c..5880ffb0dce2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
@@ -21,6 +21,7 @@ import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
+import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
@@ -36,7 +37,9 @@ import android.testing.TestableLooper;
import android.view.SurfaceControl;
import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.wm.shell.MockSurfaceControlHelper;
import com.android.wm.shell.ShellTestCase;
import org.junit.Before;
@@ -60,19 +63,18 @@ public class PipAnimationControllerTest extends ShellTestCase {
@Mock
private TaskInfo mTaskInfo;
-
@Mock
private PipAnimationController.PipAnimationCallback mPipAnimationCallback;
@Before
public void setUp() throws Exception {
- mPipAnimationController = new PipAnimationController(
- new PipSurfaceTransactionHelper());
+ MockitoAnnotations.initMocks(this);
+ mPipAnimationController = new PipAnimationController(new PipSurfaceTransactionHelper(
+ InstrumentationRegistry.getInstrumentation().getTargetContext()));
mLeash = new SurfaceControl.Builder()
.setContainerLayer()
.setName("FakeLeash")
.build();
- MockitoAnnotations.initMocks(this);
}
@Test
@@ -103,7 +105,8 @@ public class PipAnimationControllerTest extends ShellTestCase {
final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController
.getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null,
TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
- oldAnimator.setSurfaceControlTransactionFactory(PipDummySurfaceControlTx::new);
+ oldAnimator.setSurfaceControlTransactionFactory(
+ MockSurfaceControlHelper::createMockSurfaceControlTransaction);
oldAnimator.start();
final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController
@@ -133,7 +136,7 @@ public class PipAnimationControllerTest extends ShellTestCase {
@Test
public void pipTransitionAnimator_rotatedEndValue() {
- final PipDummySurfaceControlTx tx = new PipDummySurfaceControlTx();
+ final SurfaceControl.Transaction tx = createMockSurfaceControlTransaction();
final Rect startBounds = new Rect(200, 700, 400, 800);
final Rect endBounds = new Rect(0, 0, 500, 1000);
// Fullscreen to PiP.
@@ -183,7 +186,8 @@ public class PipAnimationControllerTest extends ShellTestCase {
final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
.getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
- animator.setSurfaceControlTransactionFactory(PipDummySurfaceControlTx::new);
+ animator.setSurfaceControlTransactionFactory(
+ MockSurfaceControlHelper::createMockSurfaceControlTransaction);
animator.setPipAnimationCallback(mPipAnimationCallback);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
index 0059846c6055..262e4290ef44 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
@@ -64,7 +64,7 @@ public class PipBoundsAlgorithmTest extends ShellTestCase {
initializeMockResources();
mPipBoundsState = new PipBoundsState(mContext);
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
- new PipSnapAlgorithm());
+ new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
mPipBoundsState.setDisplayLayout(
new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipDummySurfaceControlTx.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipDummySurfaceControlTx.java
deleted file mode 100644
index ccf8f6e03844..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipDummySurfaceControlTx.java
+++ /dev/null
@@ -1,66 +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.pip;
-
-import android.graphics.Matrix;
-import android.view.SurfaceControl;
-
-/**
- * A dummy {@link SurfaceControl.Transaction} class for testing purpose and supports
- * method chaining.
- */
-public class PipDummySurfaceControlTx extends SurfaceControl.Transaction {
- @Override
- public SurfaceControl.Transaction setAlpha(SurfaceControl leash, float alpha) {
- return this;
- }
-
- @Override
- public SurfaceControl.Transaction setPosition(SurfaceControl leash, float x, float y) {
- return this;
- }
-
- @Override
- public SurfaceControl.Transaction setWindowCrop(SurfaceControl leash, int w, int h) {
- return this;
- }
-
- @Override
- public SurfaceControl.Transaction setCornerRadius(SurfaceControl leash, float radius) {
- return this;
- }
-
- @Override
- public SurfaceControl.Transaction setShadowRadius(SurfaceControl leash, float radius) {
- return this;
- }
-
- @Override
- public SurfaceControl.Transaction setMatrix(SurfaceControl leash, Matrix matrix,
- float[] float9) {
- return this;
- }
-
- @Override
- public SurfaceControl.Transaction setFrameTimelineVsync(long frameTimelineVsyncId) {
- return this;
- }
-
- @Override
- public void apply() {}
-}
-
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index e8e6254697c2..90880772b25d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -21,13 +21,13 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
@@ -42,8 +42,10 @@ import android.testing.TestableLooper;
import android.util.Rational;
import android.util.Size;
import android.view.DisplayInfo;
+import android.view.SurfaceControl;
import android.window.WindowContainerToken;
+import com.android.wm.shell.MockSurfaceControlHelper;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
@@ -68,7 +70,7 @@ import java.util.Optional;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class PipTaskOrganizerTest extends ShellTestCase {
- private PipTaskOrganizer mSpiedPipTaskOrganizer;
+ private PipTaskOrganizer mPipTaskOrganizer;
@Mock private DisplayController mMockDisplayController;
@Mock private SyncTransactionQueue mMockSyncTransactionQueue;
@@ -96,16 +98,17 @@ public class PipTaskOrganizerTest extends ShellTestCase {
mPipBoundsState = new PipBoundsState(mContext);
mPipTransitionState = new PipTransitionState();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
- new PipSnapAlgorithm());
+ new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
mMainExecutor = new TestShellExecutor();
- mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext,
+ mPipTaskOrganizer = new PipTaskOrganizer(mContext,
mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState,
mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController,
mMockPipSurfaceTransactionHelper, mMockPipTransitionController,
mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, mMockDisplayController,
- mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor));
+ mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor);
mMainExecutor.flushAll();
preparePipTaskOrg();
+ preparePipSurfaceTransactionHelper();
}
@Test
@@ -122,14 +125,14 @@ public class PipTaskOrganizerTest extends ShellTestCase {
public void startSwipePipToHome_updatesAspectRatio() {
final Rational aspectRatio = new Rational(2, 1);
- mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(aspectRatio));
+ mPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(aspectRatio));
assertEquals(aspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f);
}
@Test
public void startSwipePipToHome_updatesLastPipComponentName() {
- mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(null));
+ mPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(null));
assertEquals(mComponent1, mPipBoundsState.getLastPipComponentName());
}
@@ -138,7 +141,7 @@ public class PipTaskOrganizerTest extends ShellTestCase {
public void startSwipePipToHome_updatesOverrideMinSize() {
final Size minSize = new Size(400, 320);
- mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, createActivityInfo(minSize),
+ mPipTaskOrganizer.startSwipePipToHome(mComponent1, createActivityInfo(minSize),
createPipParams(null));
assertEquals(minSize, mPipBoundsState.getOverrideMinSize());
@@ -148,16 +151,16 @@ public class PipTaskOrganizerTest extends ShellTestCase {
public void onTaskAppeared_updatesAspectRatio() {
final Rational aspectRatio = new Rational(2, 1);
- mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
- createPipParams(aspectRatio)), null /* leash */);
+ mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
+ createPipParams(aspectRatio)), mock(SurfaceControl.class));
assertEquals(aspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f);
}
@Test
public void onTaskAppeared_updatesLastPipComponentName() {
- mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, createPipParams(null)),
- null /* leash */);
+ mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, createPipParams(null)),
+ mock(SurfaceControl.class));
assertEquals(mComponent1, mPipBoundsState.getLastPipComponentName());
}
@@ -166,9 +169,9 @@ public class PipTaskOrganizerTest extends ShellTestCase {
public void onTaskAppeared_updatesOverrideMinSize() {
final Size minSize = new Size(400, 320);
- mSpiedPipTaskOrganizer.onTaskAppeared(
+ mPipTaskOrganizer.onTaskAppeared(
createTaskInfo(mComponent1, createPipParams(null), minSize),
- null /* leash */);
+ mock(SurfaceControl.class));
assertEquals(minSize, mPipBoundsState.getOverrideMinSize());
}
@@ -177,16 +180,16 @@ public class PipTaskOrganizerTest extends ShellTestCase {
public void onTaskInfoChanged_notInPip_deferUpdatesAspectRatio() {
final Rational startAspectRatio = new Rational(2, 1);
final Rational newAspectRatio = new Rational(1, 2);
- mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
- createPipParams(startAspectRatio)), null /* leash */);
+ mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
+ createPipParams(startAspectRatio)), mock(SurfaceControl.class));
// It is in entering transition, should defer onTaskInfoChanged callback in this case.
- mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1,
+ mPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1,
createPipParams(newAspectRatio)));
verify(mMockPipParamsChangedForwarder, never()).notifyAspectRatioChanged(anyFloat());
// Once the entering transition finishes, the new aspect ratio applies in a deferred manner
- mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
verify(mMockPipParamsChangedForwarder)
.notifyAspectRatioChanged(newAspectRatio.floatValue());
}
@@ -195,11 +198,11 @@ public class PipTaskOrganizerTest extends ShellTestCase {
public void onTaskInfoChanged_inPip_updatesAspectRatioIfChanged() {
final Rational startAspectRatio = new Rational(2, 1);
final Rational newAspectRatio = new Rational(1, 2);
- mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
- createPipParams(startAspectRatio)), null /* leash */);
- mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+ mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
+ createPipParams(startAspectRatio)), mock(SurfaceControl.class));
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
- mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1,
+ mPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1,
createPipParams(newAspectRatio)));
verify(mMockPipParamsChangedForwarder)
@@ -208,11 +211,11 @@ public class PipTaskOrganizerTest extends ShellTestCase {
@Test
public void onTaskInfoChanged_inPip_updatesLastPipComponentName() {
- mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
- createPipParams(null)), null /* leash */);
- mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+ mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
+ createPipParams(null)), mock(SurfaceControl.class));
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
- mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2,
+ mPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2,
createPipParams(null)));
assertEquals(mComponent2, mPipBoundsState.getLastPipComponentName());
@@ -220,12 +223,12 @@ public class PipTaskOrganizerTest extends ShellTestCase {
@Test
public void onTaskInfoChanged_inPip_updatesOverrideMinSize() {
- mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
- createPipParams(null)), null /* leash */);
- mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+ mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
+ createPipParams(null)), mock(SurfaceControl.class));
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
final Size minSize = new Size(400, 320);
- mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2,
+ mPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2,
createPipParams(null), minSize));
assertEquals(minSize, mPipBoundsState.getOverrideMinSize());
@@ -233,22 +236,42 @@ public class PipTaskOrganizerTest extends ShellTestCase {
@Test
public void onTaskVanished_clearsPipBounds() {
- mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
- createPipParams(null)), null /* leash */);
+ mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
+ createPipParams(null)), mock(SurfaceControl.class));
mPipBoundsState.setBounds(new Rect(100, 100, 200, 150));
- mSpiedPipTaskOrganizer.onTaskVanished(createTaskInfo(mComponent1, createPipParams(null)));
+ mPipTaskOrganizer.onTaskVanished(createTaskInfo(mComponent1, createPipParams(null)));
assertTrue(mPipBoundsState.getBounds().isEmpty());
}
+ private void sendOnPipTransitionFinished(
+ @PipAnimationController.TransitionDirection int direction) {
+ mPipTaskOrganizer.sendOnPipTransitionFinished(direction);
+ // PipTransitionController will call back into PipTaskOrganizer.
+ mPipTaskOrganizer.mPipTransitionCallback.onPipTransitionFinished(direction);
+ }
+
private void preparePipTaskOrg() {
final DisplayInfo info = new DisplayInfo();
mPipBoundsState.setDisplayLayout(new DisplayLayout(info,
mContext.getResources(), true, true));
- mSpiedPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA);
- mSpiedPipTaskOrganizer.setSurfaceControlTransactionFactory(PipDummySurfaceControlTx::new);
- doNothing().when(mSpiedPipTaskOrganizer).enterPipWithAlphaAnimation(any(), anyLong());
- doNothing().when(mSpiedPipTaskOrganizer).scheduleAnimateResizePip(any(), anyInt(), any());
+ mPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA);
+ mPipTaskOrganizer.setSurfaceControlTransactionFactory(
+ MockSurfaceControlHelper::createMockSurfaceControlTransaction);
+ }
+
+ private void preparePipSurfaceTransactionHelper() {
+ doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
+ .crop(any(), any(), any());
+ doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
+ .resetScale(any(), any(), any());
+ doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
+ .round(any(), any(), anyBoolean());
+ doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
+ .scale(any(), any(), any(), any(), anyFloat());
+ doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
+ .alpha(any(), any(), anyFloat());
+ doNothing().when(mMockPipSurfaceTransactionHelper).onDensityOrFontScaleChanged(any());
}
private static ActivityManager.RunningTaskInfo createTaskInfo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java
new file mode 100644
index 000000000000..4d7e9e450ceb
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.pip.phone;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+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 java.util.Set;
+
+/**
+ * Unit tests against {@link PhonePipKeepClearAlgorithm}.
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class PhonePipKeepClearAlgorithmTest extends ShellTestCase {
+
+ private PhonePipKeepClearAlgorithm mPipKeepClearAlgorithm;
+ private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 1000, 1000);
+
+ @Before
+ public void setUp() throws Exception {
+ mPipKeepClearAlgorithm = new PhonePipKeepClearAlgorithm(mContext);
+ }
+
+ @Test
+ public void findUnoccludedPosition_withCollidingRestrictedKeepClearArea_movesBounds() {
+ final Rect inBounds = new Rect(0, 0, 100, 100);
+ final Rect keepClearRect = new Rect(50, 50, 150, 150);
+
+ final Rect outBounds = mPipKeepClearAlgorithm.findUnoccludedPosition(inBounds,
+ Set.of(keepClearRect), Set.of(), DISPLAY_BOUNDS);
+
+ assertFalse(outBounds.contains(keepClearRect));
+ }
+
+ @Test
+ public void findUnoccludedPosition_withNonCollidingRestrictedKeepClearArea_boundsUnchanged() {
+ final Rect inBounds = new Rect(0, 0, 100, 100);
+ final Rect keepClearRect = new Rect(100, 100, 150, 150);
+
+ final Rect outBounds = mPipKeepClearAlgorithm.findUnoccludedPosition(inBounds,
+ Set.of(keepClearRect), Set.of(), DISPLAY_BOUNDS);
+
+ assertEquals(inBounds, outBounds);
+ }
+
+ @Test
+ public void findUnoccludedPosition_withCollidingUnrestrictedKeepClearArea_moveBounds() {
+ // TODO(b/183746978): update this test to accommodate for the updated algorithm
+ final Rect inBounds = new Rect(0, 0, 100, 100);
+ final Rect keepClearRect = new Rect(50, 50, 150, 150);
+
+ final Rect outBounds = mPipKeepClearAlgorithm.findUnoccludedPosition(inBounds, Set.of(),
+ Set.of(keepClearRect), DISPLAY_BOUNDS);
+
+ assertFalse(outBounds.contains(keepClearRect));
+ }
+
+ @Test
+ public void findUnoccludedPosition_withNonCollidingUnrestrictedKeepClearArea_boundsUnchanged() {
+ final Rect inBounds = new Rect(0, 0, 100, 100);
+ final Rect keepClearRect = new Rect(100, 100, 150, 150);
+
+ final Rect outBounds = mPipKeepClearAlgorithm.findUnoccludedPosition(inBounds, Set.of(),
+ Set.of(keepClearRect), DISPLAY_BOUNDS);
+
+ assertEquals(inBounds, outBounds);
+ }
+}
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 df18133adcfb..1e08f1e55797 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
@@ -23,7 +23,9 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
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;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -41,10 +43,12 @@ import android.util.Size;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.WindowManagerShellWrapper;
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.ShellExecutor;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.onehanded.OneHandedController;
+import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipAppOpsListener;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
@@ -53,11 +57,16 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.PipTransitionState;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.Optional;
@@ -71,14 +80,20 @@ import java.util.Set;
@TestableLooper.RunWithLooper
public class PipControllerTest extends ShellTestCase {
private PipController mPipController;
+ private ShellInit mShellInit;
+ private ShellController mShellController;
+ @Mock private ShellCommandHandler mMockShellCommandHandler;
@Mock private DisplayController mMockDisplayController;
@Mock private PhonePipMenuController mMockPhonePipMenuController;
+ @Mock private PipAnimationController mMockPipAnimationController;
@Mock private PipAppOpsListener mMockPipAppOpsListener;
@Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm;
+ @Mock private PhonePipKeepClearAlgorithm mMockPipKeepClearAlgorithm;
@Mock private PipSnapAlgorithm mMockPipSnapAlgorithm;
@Mock private PipMediaController mMockPipMediaController;
@Mock private PipTaskOrganizer mMockPipTaskOrganizer;
+ @Mock private PipTransitionState mMockPipTransitionState;
@Mock private PipTransitionController mMockPipTransitionController;
@Mock private PipTouchHandler mMockPipTouchHandler;
@Mock private PipMotionHelper mMockPipMotionHelper;
@@ -87,7 +102,8 @@ public class PipControllerTest extends ShellTestCase {
@Mock private TaskStackListenerImpl mMockTaskStackListener;
@Mock private ShellExecutor mMockExecutor;
@Mock private Optional<OneHandedController> mMockOneHandedController;
- @Mock private PipParamsChangedForwarder mPipParamsChangedForwarder;
+ @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder;
+ @Mock private DisplayInsetsController mMockDisplayInsetsController;
@Mock private DisplayLayout mMockDisplayLayout1;
@Mock private DisplayLayout mMockDisplayLayout2;
@@ -99,18 +115,53 @@ public class PipControllerTest extends ShellTestCase {
((Runnable) invocation.getArgument(0)).run();
return null;
}).when(mMockExecutor).execute(any());
- mPipController = new PipController(mContext, mMockDisplayController,
- mMockPipAppOpsListener, mMockPipBoundsAlgorithm,
- mMockPipBoundsState, mMockPipMediaController,
- mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler,
- mMockPipTransitionController, mMockWindowManagerShellWrapper,
- mMockTaskStackListener, mPipParamsChangedForwarder,
- mMockOneHandedController, mMockExecutor);
+ mShellInit = spy(new ShellInit(mMockExecutor));
+ mShellController = spy(new ShellController(mShellInit, mMockShellCommandHandler,
+ mMockExecutor));
+ mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
+ mShellController, mMockDisplayController, mMockPipAnimationController,
+ mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
+ mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
+ mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
+ mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
+ mMockTaskStackListener, mMockPipParamsChangedForwarder,
+ mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor);
+ mShellInit.init();
when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm);
when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper);
}
@Test
+ public void instantiatePipController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void instantiateController_registerDumpCallback() {
+ verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), any());
+ }
+
+ @Test
+ public void instantiatePipController_registerConfigChangeListener() {
+ verify(mShellController, times(1)).addConfigurationChangeListener(any());
+ }
+
+ @Test
+ public void instantiatePipController_registerKeyguardChangeListener() {
+ verify(mShellController, times(1)).addKeyguardChangeListener(any());
+ }
+
+ @Test
+ public void instantiatePipController_registerUserChangeListener() {
+ verify(mShellController, times(1)).addUserChangeListener(any());
+ }
+
+ @Test
+ public void instantiatePipController_registerMediaListener() {
+ verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser();
+ }
+
+ @Test
public void instantiatePipController_registersPipTransitionCallback() {
verify(mMockPipTransitionController).registerPipTransitionCallback(any());
}
@@ -132,13 +183,15 @@ public class PipControllerTest extends ShellTestCase {
when(mockPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)).thenReturn(false);
when(spyContext.getPackageManager()).thenReturn(mockPackageManager);
- assertNull(PipController.create(spyContext, mMockDisplayController,
- mMockPipAppOpsListener, mMockPipBoundsAlgorithm,
- mMockPipBoundsState, mMockPipMediaController,
- mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler,
- mMockPipTransitionController, mMockWindowManagerShellWrapper,
- mMockTaskStackListener, mPipParamsChangedForwarder,
- mMockOneHandedController, mMockExecutor));
+ ShellInit shellInit = new ShellInit(mMockExecutor);
+ assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler,
+ mShellController, mMockDisplayController, mMockPipAnimationController,
+ mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
+ mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
+ mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
+ mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
+ mMockTaskStackListener, mMockPipParamsChangedForwarder,
+ mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor));
}
@Test
@@ -199,7 +252,7 @@ public class PipControllerTest extends ShellTestCase {
mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged(
displayId, new Configuration());
- verify(mMockPipMotionHelper).movePip(any(Rect.class));
+ verify(mMockPipTaskOrganizer).scheduleFinishResizePip(any(Rect.class));
}
@Test
@@ -215,11 +268,24 @@ public class PipControllerTest extends ShellTestCase {
mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged(
displayId, new Configuration());
- verify(mMockPipMotionHelper, never()).movePip(any(Rect.class));
+ verify(mMockPipTaskOrganizer, never()).scheduleFinishResizePip(any(Rect.class));
}
@Test
- public void onKeepClearAreasChanged_updatesPipBoundsState() {
+ public void onKeepClearAreasChanged_featureDisabled_pipBoundsStateDoesntChange() {
+ final int displayId = 1;
+ final Rect keepClearArea = new Rect(0, 0, 10, 10);
+ when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId);
+
+ mPipController.mDisplaysChangedListener.onKeepClearAreasChanged(
+ displayId, Set.of(keepClearArea), Set.of());
+
+ verify(mMockPipBoundsState, never()).setKeepClearAreas(Mockito.anySet(), Mockito.anySet());
+ }
+
+ @Test
+ public void onKeepClearAreasChanged_featureEnabled_updatesPipBoundsState() {
+ mPipController.setEnablePipKeepClearAlgorithm(true);
final int displayId = 1;
final Rect keepClearArea = new Rect(0, 0, 10, 10);
when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId);
@@ -229,4 +295,11 @@ public class PipControllerTest extends ShellTestCase {
verify(mMockPipBoundsState).setKeepClearAreas(Set.of(keepClearArea), Set.of());
}
+
+ @Test
+ public void onUserChangeRegisterMediaListener() {
+ reset(mMockPipMediaController);
+ mShellController.asShell().onUserChanged(100, mContext);
+ verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser();
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java
new file mode 100644
index 000000000000..8ce3ca4bdc00
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.pip.phone;
+
+import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_CUSTOM;
+import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_DEFAULT;
+import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_MAX;
+import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.nextSizeSpec;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.pip.PipBoundsState;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Unit test against {@link PipDoubleTapHelper}.
+ */
+@RunWith(AndroidTestingRunner.class)
+public class PipDoubleTapHelperTest extends ShellTestCase {
+ // represents the current pip window state and has information on current
+ // max, min, and normal sizes
+ @Mock private PipBoundsState mBoundStateMock;
+ // tied to boundsStateMock.getBounds() in setUp()
+ @Mock private Rect mBoundsMock;
+
+ // represents the most recent manually resized bounds
+ // i.e. dimensions from the most recent pinch in/out
+ @Mock private Rect mUserResizeBoundsMock;
+
+ // actual dimensions of the pip screen bounds
+ private static final int MAX_WIDTH = 100;
+ private static final int DEFAULT_WIDTH = 40;
+ private static final int MIN_WIDTH = 10;
+
+ private static final int AVERAGE_WIDTH = (MAX_WIDTH + MIN_WIDTH) / 2;
+
+ /**
+ * Initializes mocks and assigns values for different pip screen bounds.
+ */
+ @Before
+ public void setUp() {
+ // define pip bounds
+ when(mBoundStateMock.getMaxSize()).thenReturn(new Point(MAX_WIDTH, 20));
+ when(mBoundStateMock.getMinSize()).thenReturn(new Point(MIN_WIDTH, 2));
+
+ Rect rectMock = mock(Rect.class);
+ when(rectMock.width()).thenReturn(DEFAULT_WIDTH);
+ when(mBoundStateMock.getNormalBounds()).thenReturn(rectMock);
+
+ when(mBoundsMock.width()).thenReturn(DEFAULT_WIDTH);
+ when(mBoundStateMock.getBounds()).thenReturn(mBoundsMock);
+ }
+
+ /**
+ * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}.
+ *
+ * <p>when the user resizes the screen to a larger than the average but not the maximum width,
+ * then we toggle between {@code PipSizeSpec.CUSTOM} and {@code PipSizeSpec.DEFAULT}
+ */
+ @Test
+ public void testNextScreenSize_resizedWiderThanAverage_returnDefaultThenCustom() {
+ // make the user resize width in between MAX and average
+ when(mUserResizeBoundsMock.width()).thenReturn((MAX_WIDTH + AVERAGE_WIDTH) / 2);
+ // make current bounds same as resized bound since no double tap yet
+ when(mBoundsMock.width()).thenReturn((MAX_WIDTH + AVERAGE_WIDTH) / 2);
+
+ // then nextScreenSize() i.e. double tapping should
+ // toggle to DEFAULT state
+ Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
+ SIZE_SPEC_DEFAULT);
+
+ // once we toggle to DEFAULT our screen size gets updated
+ // but not the user resize bounds
+ when(mBoundsMock.width()).thenReturn(DEFAULT_WIDTH);
+
+ // then nextScreenSize() i.e. double tapping should
+ // toggle to CUSTOM state
+ Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
+ SIZE_SPEC_CUSTOM);
+ }
+
+ /**
+ * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}.
+ *
+ * <p>when the user resizes the screen to a smaller than the average but not the default width,
+ * then we toggle between {@code PipSizeSpec.CUSTOM} and {@code PipSizeSpec.MAX}
+ */
+ @Test
+ public void testNextScreenSize_resizedNarrowerThanAverage_returnMaxThenCustom() {
+ // make the user resize width in between MIN and average
+ when(mUserResizeBoundsMock.width()).thenReturn((MIN_WIDTH + AVERAGE_WIDTH) / 2);
+ // make current bounds same as resized bound since no double tap yet
+ when(mBoundsMock.width()).thenReturn((MIN_WIDTH + AVERAGE_WIDTH) / 2);
+
+ // then nextScreenSize() i.e. double tapping should
+ // toggle to MAX state
+ Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
+ SIZE_SPEC_MAX);
+
+ // once we toggle to MAX our screen size gets updated
+ // but not the user resize bounds
+ when(mBoundsMock.width()).thenReturn(MAX_WIDTH);
+
+ // then nextScreenSize() i.e. double tapping should
+ // toggle to CUSTOM state
+ Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
+ SIZE_SPEC_CUSTOM);
+ }
+
+ /**
+ * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}.
+ *
+ * <p>when the user resizes the screen to exactly the maximum width
+ * then we toggle to {@code PipSizeSpec.DEFAULT}
+ */
+ @Test
+ public void testNextScreenSize_resizedToMax_returnDefault() {
+ // the resized width is the same as MAX_WIDTH
+ when(mUserResizeBoundsMock.width()).thenReturn(MAX_WIDTH);
+ // the current bounds are also at MAX_WIDTH
+ when(mBoundsMock.width()).thenReturn(MAX_WIDTH);
+
+ // then nextScreenSize() i.e. double tapping should
+ // toggle to DEFAULT state
+ Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
+ SIZE_SPEC_DEFAULT);
+ }
+
+ /**
+ * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}.
+ *
+ * <p>when the user resizes the screen to exactly the default width
+ * then we toggle to {@code PipSizeSpec.MAX}
+ */
+ @Test
+ public void testNextScreenSize_resizedToDefault_returnMax() {
+ // the resized width is the same as DEFAULT_WIDTH
+ when(mUserResizeBoundsMock.width()).thenReturn(DEFAULT_WIDTH);
+ // the current bounds are also at DEFAULT_WIDTH
+ when(mBoundsMock.width()).thenReturn(DEFAULT_WIDTH);
+
+ // then nextScreenSize() i.e. double tapping should
+ // toggle to MAX state
+ Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
+ SIZE_SPEC_MAX);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index dd10aa7752f5..dba037db72eb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -36,6 +36,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipKeepClearAlgorithm;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
@@ -87,8 +88,10 @@ public class PipResizeGestureHandlerTest extends ShellTestCase {
MockitoAnnotations.initMocks(this);
mPipBoundsState = new PipBoundsState(mContext);
final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm();
+ final PipKeepClearAlgorithm pipKeepClearAlgorithm =
+ new PipKeepClearAlgorithm() {};
final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext,
- mPipBoundsState, pipSnapAlgorithm);
+ mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm);
final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState,
mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm,
mMockPipTransitionController, mFloatingContentCoordinator);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 74519eaf3ebf..474d6aaf4623 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -34,10 +34,12 @@ import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipKeepClearAlgorithm;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipUiEventLogger;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
@@ -78,6 +80,9 @@ public class PipTouchHandlerTest extends ShellTestCase {
private PipUiEventLogger mPipUiEventLogger;
@Mock
+ private ShellInit mShellInit;
+
+ @Mock
private ShellExecutor mMainExecutor;
private PipBoundsState mPipBoundsState;
@@ -100,15 +105,16 @@ public class PipTouchHandlerTest extends ShellTestCase {
MockitoAnnotations.initMocks(this);
mPipBoundsState = new PipBoundsState(mContext);
mPipSnapAlgorithm = new PipSnapAlgorithm();
- mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm);
+ mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm,
+ new PipKeepClearAlgorithm() {});
PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState,
mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
mMockPipTransitionController, mFloatingContentCoordinator);
- mPipTouchHandler = new PipTouchHandler(mContext, mPhonePipMenuController,
- mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer,
- pipMotionHelper, mFloatingContentCoordinator, mPipUiEventLogger,
- mMainExecutor);
- mPipTouchHandler.init();
+ mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController,
+ mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer, pipMotionHelper,
+ mFloatingContentCoordinator, mPipUiEventLogger, mMainExecutor);
+ // We aren't actually using ShellInit, so just call init directly
+ mPipTouchHandler.onInit();
mMotionHelper = Mockito.spy(mPipTouchHandler.getMotionHelper());
mPipResizeGestureHandler = Mockito.spy(mPipTouchHandler.getPipResizeGestureHandler());
mPipTouchHandler.setPipMotionHelper(mMotionHelper);
@@ -133,6 +139,11 @@ public class PipTouchHandlerTest extends ShellTestCase {
}
@Test
+ public void instantiate_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
public void updateMovementBounds_minMaxBounds() {
final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(),
mPipBoundsState.getDisplayBounds().height());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
new file mode 100644
index 000000000000..baa06f2f0c45
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.recents
+
+import android.app.ActivityManager
+import android.graphics.Rect
+import android.os.Parcel
+import android.testing.AndroidTestingRunner
+import android.window.IWindowContainerToken
+import android.window.WindowContainerToken
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.util.GroupedRecentTaskInfo
+import com.android.wm.shell.util.GroupedRecentTaskInfo.CREATOR
+import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM
+import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SINGLE
+import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SPLIT
+import com.android.wm.shell.util.SplitBounds
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+/**
+ * Tests for [GroupedRecentTaskInfo]
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class GroupedRecentTaskInfoTest : ShellTestCase() {
+
+ @Test
+ fun testSingleTask_hasCorrectType() {
+ assertThat(singleTaskGroupInfo().type).isEqualTo(TYPE_SINGLE)
+ }
+
+ @Test
+ fun testSingleTask_task1Set_task2Null() {
+ val group = singleTaskGroupInfo()
+ assertThat(group.taskInfo1.taskId).isEqualTo(1)
+ assertThat(group.taskInfo2).isNull()
+ }
+
+ @Test
+ fun testSingleTask_taskInfoList_hasOneTask() {
+ val list = singleTaskGroupInfo().taskInfoList
+ assertThat(list).hasSize(1)
+ assertThat(list[0].taskId).isEqualTo(1)
+ }
+
+ @Test
+ fun testSplitTasks_hasCorrectType() {
+ assertThat(splitTasksGroupInfo().type).isEqualTo(TYPE_SPLIT)
+ }
+
+ @Test
+ fun testSplitTasks_task1Set_task2Set_boundsSet() {
+ val group = splitTasksGroupInfo()
+ assertThat(group.taskInfo1.taskId).isEqualTo(1)
+ assertThat(group.taskInfo2?.taskId).isEqualTo(2)
+ assertThat(group.splitBounds).isNotNull()
+ }
+
+ @Test
+ fun testSplitTasks_taskInfoList_hasTwoTasks() {
+ val list = splitTasksGroupInfo().taskInfoList
+ assertThat(list).hasSize(2)
+ assertThat(list[0].taskId).isEqualTo(1)
+ assertThat(list[1].taskId).isEqualTo(2)
+ }
+
+ @Test
+ fun testFreeformTasks_hasCorrectType() {
+ assertThat(freeformTasksGroupInfo().type).isEqualTo(TYPE_FREEFORM)
+ }
+
+ @Test
+ fun testSplitTasks_taskInfoList_hasThreeTasks() {
+ val list = freeformTasksGroupInfo().taskInfoList
+ assertThat(list).hasSize(3)
+ assertThat(list[0].taskId).isEqualTo(1)
+ assertThat(list[1].taskId).isEqualTo(2)
+ assertThat(list[2].taskId).isEqualTo(3)
+ }
+
+ @Test
+ fun testParcelling_singleTask() {
+ val recentTaskInfo = singleTaskGroupInfo()
+ val parcel = Parcel.obtain()
+ recentTaskInfo.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+ // Read the object back from the parcel
+ val recentTaskInfoParcel = CREATOR.createFromParcel(parcel)
+ assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_SINGLE)
+ assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1)
+ assertThat(recentTaskInfoParcel.taskInfo2).isNull()
+ }
+
+ @Test
+ fun testParcelling_splitTasks() {
+ val recentTaskInfo = splitTasksGroupInfo()
+ val parcel = Parcel.obtain()
+ recentTaskInfo.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+ // Read the object back from the parcel
+ val recentTaskInfoParcel = CREATOR.createFromParcel(parcel)
+ assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_SPLIT)
+ assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1)
+ assertThat(recentTaskInfoParcel.taskInfo2).isNotNull()
+ assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2)
+ assertThat(recentTaskInfoParcel.splitBounds).isNotNull()
+ }
+
+ @Test
+ fun testParcelling_freeformTasks() {
+ val recentTaskInfo = freeformTasksGroupInfo()
+ val parcel = Parcel.obtain()
+ recentTaskInfo.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+ // Read the object back from the parcel
+ val recentTaskInfoParcel = CREATOR.createFromParcel(parcel)
+ assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FREEFORM)
+ assertThat(recentTaskInfoParcel.taskInfoList).hasSize(3)
+ // Only compare task ids
+ val taskIdComparator = Correspondence.transforming<ActivityManager.RecentTaskInfo, Int>(
+ { it?.taskId }, "has taskId of"
+ )
+ assertThat(recentTaskInfoParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
+ .containsExactly(1, 2, 3)
+ }
+
+ private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply {
+ taskId = id
+ token = WindowContainerToken(mock(IWindowContainerToken::class.java))
+ }
+
+ private fun singleTaskGroupInfo(): GroupedRecentTaskInfo {
+ val task = createTaskInfo(id = 1)
+ return GroupedRecentTaskInfo.forSingleTask(task)
+ }
+
+ private fun splitTasksGroupInfo(): GroupedRecentTaskInfo {
+ val task1 = createTaskInfo(id = 1)
+ val task2 = createTaskInfo(id = 2)
+ val splitBounds = SplitBounds(Rect(), Rect(), 1, 2)
+ return GroupedRecentTaskInfo.forSplitTasks(task1, task2, splitBounds)
+ }
+
+ private fun freeformTasksGroupInfo(): GroupedRecentTaskInfo {
+ val task1 = createTaskInfo(id = 1)
+ val task2 = createTaskInfo(id = 2)
+ val task3 = createTaskInfo(id = 3)
+ return GroupedRecentTaskInfo.forFreeformTasks(task1, task2, task3)
+ }
+}
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 50f6bd7b4927..b8aaaa76e3c7 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
@@ -20,34 +20,46 @@ import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static java.lang.Integer.MAX_VALUE;
import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.view.SurfaceControl;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+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.TestShellExecutor;
-import com.android.wm.shell.common.ShellExecutor;
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.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
-import com.android.wm.shell.util.StagedSplitBounds;
+import com.android.wm.shell.util.SplitBounds;
import org.junit.Before;
import org.junit.Test;
@@ -56,7 +68,9 @@ import org.mockito.Mock;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
+import java.util.function.Consumer;
/**
* Tests for {@link RecentTasksController}.
@@ -69,18 +83,41 @@ public class RecentTasksControllerTest extends ShellTestCase {
private Context mContext;
@Mock
private TaskStackListenerImpl mTaskStackListener;
+ @Mock
+ private ShellCommandHandler mShellCommandHandler;
+ @Mock
+ private DesktopModeTaskRepository mDesktopModeTaskRepository;
+ @Mock
+ private ActivityTaskManager mActivityTaskManager;
private ShellTaskOrganizer mShellTaskOrganizer;
private RecentTasksController mRecentTasksController;
- private ShellExecutor mMainExecutor;
+ private ShellInit mShellInit;
+ private TestShellExecutor mMainExecutor;
@Before
public void setUp() {
mMainExecutor = new TestShellExecutor();
- mRecentTasksController = spy(new RecentTasksController(mContext, mTaskStackListener,
- mMainExecutor));
- mShellTaskOrganizer = new ShellTaskOrganizer(mMainExecutor, mContext,
- null /* sizeCompatUI */, Optional.of(mRecentTasksController));
+ when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
+ mShellInit = spy(new ShellInit(mMainExecutor));
+ mRecentTasksController = spy(new RecentTasksController(mContext, mShellInit,
+ mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
+ Optional.of(mDesktopModeTaskRepository), mMainExecutor));
+ mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
+ null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
+ mMainExecutor);
+ mShellInit.init();
+ }
+
+ @Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), isA(RecentTasksController.class));
+ }
+
+ @Test
+ public void instantiateController_addDumpCallback() {
+ verify(mShellCommandHandler, times(1)).addDumpCallback(any(),
+ isA(RecentTasksController.class));
}
@Test
@@ -89,7 +126,7 @@ public class RecentTasksControllerTest extends ShellTestCase {
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
setRawList(t1, t2);
- mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, mock(StagedSplitBounds.class));
+ mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, mock(SplitBounds.class));
verify(mRecentTasksController).notifyRecentTasksChanged();
reset(mRecentTasksController);
@@ -104,10 +141,10 @@ public class RecentTasksControllerTest extends ShellTestCase {
setRawList(t1, t2);
// Verify only one update if the split info is the same
- StagedSplitBounds bounds1 = new StagedSplitBounds(new Rect(0, 0, 50, 50),
+ SplitBounds bounds1 = new SplitBounds(new Rect(0, 0, 50, 50),
new Rect(50, 50, 100, 100), t1.taskId, t2.taskId);
mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds1);
- StagedSplitBounds bounds2 = new StagedSplitBounds(new Rect(0, 0, 50, 50),
+ SplitBounds bounds2 = new SplitBounds(new Rect(0, 0, 50, 50),
new Rect(50, 50, 100, 100), t1.taskId, t2.taskId);
mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds2);
verify(mRecentTasksController, times(1)).notifyRecentTasksChanged();
@@ -139,8 +176,8 @@ public class RecentTasksControllerTest extends ShellTestCase {
setRawList(t1, t2, t3, t4, t5, t6);
// Mark a couple pairs [t2, t4], [t3, t5]
- StagedSplitBounds pair1Bounds = new StagedSplitBounds(new Rect(), new Rect(), 2, 4);
- StagedSplitBounds pair2Bounds = new StagedSplitBounds(new Rect(), new Rect(), 3, 5);
+ SplitBounds pair1Bounds = new SplitBounds(new Rect(), new Rect(), 2, 4);
+ SplitBounds pair2Bounds = new SplitBounds(new Rect(), new Rect(), 3, 5);
mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds);
mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds);
@@ -155,6 +192,77 @@ public class RecentTasksControllerTest extends ShellTestCase {
}
@Test
+ public void testGetRecentTasks_ReturnsRecentTasksAsynchronously() {
+ @SuppressWarnings("unchecked")
+ final List<GroupedRecentTaskInfo>[] recentTasks = new List[1];
+ Consumer<List<GroupedRecentTaskInfo>> consumer = argument -> recentTasks[0] = argument;
+ 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);
+ ActivityManager.RecentTaskInfo t6 = makeTaskInfo(6);
+ setRawList(t1, t2, t3, t4, t5, t6);
+
+ // Mark a couple pairs [t2, t4], [t3, t5]
+ SplitBounds pair1Bounds = new SplitBounds(new Rect(), new Rect(), 2, 4);
+ SplitBounds pair2Bounds = new SplitBounds(new Rect(), new Rect(), 3, 5);
+
+ mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds);
+ mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds);
+
+ mRecentTasksController.asRecentTasks()
+ .getRecentTasks(MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0, Runnable::run, consumer);
+ mMainExecutor.flushAll();
+
+ assertGroupedTasksListEquals(recentTasks[0],
+ t1.taskId, -1,
+ t2.taskId, t4.taskId,
+ t3.taskId, t5.taskId,
+ t6.taskId, -1);
+ }
+
+ @Test
+ public void testGetRecentTasks_groupActiveFreeformTasks() {
+ StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
+ DesktopModeStatus.class).startMocking();
+ when(DesktopModeStatus.isActive(any())).thenReturn(true);
+
+ ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+ ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
+ ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
+ ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4);
+ setRawList(t1, t2, t3, t4);
+
+ when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true);
+ when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
+
+ ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
+ MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
+
+ // 2 freeform tasks should be grouped into one, 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(t1, freeformGroup.getTaskInfoList().get(0));
+ assertEquals(t3, freeformGroup.getTaskInfoList().get(1));
+
+ // Check single entries
+ assertEquals(t2, singleGroup1.getTaskInfo1());
+ assertEquals(t4, singleGroup2.getTaskInfo1());
+
+ mockitoSession.finishMocking();
+ }
+
+ @Test
public void testRemovedTaskRemovesSplit() {
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
@@ -162,7 +270,7 @@ public class RecentTasksControllerTest extends ShellTestCase {
setRawList(t1, t2, t3);
// Add a pair
- StagedSplitBounds pair1Bounds = new StagedSplitBounds(new Rect(), new Rect(), 2, 3);
+ SplitBounds pair1Bounds = new SplitBounds(new Rect(), new Rect(), 2, 3);
mRecentTasksController.addSplitPair(t2.taskId, t3.taskId, pair1Bounds);
reset(mRecentTasksController);
@@ -223,37 +331,39 @@ public class RecentTasksControllerTest extends ShellTestCase {
for (ActivityManager.RecentTaskInfo task : tasks) {
rawList.add(task);
}
- doReturn(rawList).when(mRecentTasksController).getRawRecentTasks(anyInt(), anyInt(),
+ doReturn(rawList).when(mActivityTaskManager).getRecentTasks(anyInt(), anyInt(),
anyInt());
return rawList;
}
/**
* Asserts that the recent tasks matches the given task ids.
+ *
* @param expectedTaskIds list of task ids that map to the flattened task ids of the tasks in
* the grouped task list
*/
- private void assertGroupedTasksListEquals(ArrayList<GroupedRecentTaskInfo> recentTasks,
+ private void assertGroupedTasksListEquals(List<GroupedRecentTaskInfo> recentTasks,
int... expectedTaskIds) {
int[] flattenedTaskIds = new int[recentTasks.size() * 2];
for (int i = 0; i < recentTasks.size(); i++) {
GroupedRecentTaskInfo pair = recentTasks.get(i);
- int taskId1 = pair.mTaskInfo1.taskId;
+ int taskId1 = pair.getTaskInfo1().taskId;
flattenedTaskIds[2 * i] = taskId1;
- flattenedTaskIds[2 * i + 1] = pair.mTaskInfo2 != null
- ? pair.mTaskInfo2.taskId
+ flattenedTaskIds[2 * i + 1] = pair.getTaskInfo2() != null
+ ? pair.getTaskInfo2().taskId
: -1;
- if (pair.mTaskInfo2 != null) {
- assertNotNull(pair.mStagedSplitBounds);
- int leftTopTaskId = pair.mStagedSplitBounds.leftTopTaskId;
- int bottomRightTaskId = pair.mStagedSplitBounds.rightBottomTaskId;
+ if (pair.getTaskInfo2() != null) {
+ assertNotNull(pair.getSplitBounds());
+ int leftTopTaskId = pair.getSplitBounds().leftTopTaskId;
+ int bottomRightTaskId = pair.getSplitBounds().rightBottomTaskId;
// Unclear if pairs are ordered by split position, most likely not.
- assertTrue(leftTopTaskId == taskId1 || leftTopTaskId == pair.mTaskInfo2.taskId);
+ assertTrue(leftTopTaskId == taskId1
+ || leftTopTaskId == pair.getTaskInfo2().taskId);
assertTrue(bottomRightTaskId == taskId1
- || bottomRightTaskId == pair.mTaskInfo2.taskId);
+ || bottomRightTaskId == pair.getTaskInfo2().taskId);
} else {
- assertNull(pair.mStagedSplitBounds);
+ assertNull(pair.getSplitBounds());
}
}
assertTrue("Expected: " + Arrays.toString(expectedTaskIds)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
index ad73c56950bd..50d02ae0dccd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
@@ -9,7 +9,8 @@ import android.graphics.Rect;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.wm.shell.util.StagedSplitBounds;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.util.SplitBounds;
import org.junit.Before;
import org.junit.Test;
@@ -17,7 +18,7 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
-public class StagedSplitBoundsTest {
+public class SplitBoundsTest extends ShellTestCase {
private static final int DEVICE_WIDTH = 100;
private static final int DEVICE_LENGTH = 200;
private static final int DIVIDER_SIZE = 20;
@@ -42,21 +43,21 @@ public class StagedSplitBoundsTest {
@Test
public void testVerticalStacked() {
- StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect,
+ SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
TASK_ID_1, TASK_ID_2);
assertTrue(ssb.appsStackedVertically);
}
@Test
public void testHorizontalStacked() {
- StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect,
+ SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
TASK_ID_1, TASK_ID_2);
assertFalse(ssb.appsStackedVertically);
}
@Test
public void testHorizontalDividerBounds() {
- StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect,
+ SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
TASK_ID_1, TASK_ID_2);
Rect dividerBounds = ssb.visualDividerBounds;
assertEquals(0, dividerBounds.left);
@@ -67,7 +68,7 @@ public class StagedSplitBoundsTest {
@Test
public void testVerticalDividerBounds() {
- StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect,
+ SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
TASK_ID_1, TASK_ID_2);
Rect dividerBounds = ssb.visualDividerBounds;
assertEquals(DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, dividerBounds.left);
@@ -78,7 +79,7 @@ public class StagedSplitBoundsTest {
@Test
public void testEqualVerticalTaskPercent() {
- StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect,
+ SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
TASK_ID_1, TASK_ID_2);
float topPercentSpaceTaken = (float) (DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2) / DEVICE_LENGTH;
assertEquals(topPercentSpaceTaken, ssb.topTaskPercent, 0.01);
@@ -86,7 +87,7 @@ public class StagedSplitBoundsTest {
@Test
public void testEqualHorizontalTaskPercent() {
- StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect,
+ SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
TASK_ID_1, TASK_ID_2);
float leftPercentSpaceTaken = (float) (DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2) / DEVICE_WIDTH;
assertEquals(leftPercentSpaceTaken, ssb.leftTaskPercent, 0.01);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
index 0639ad5d0a62..68cb57c14d8c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
@@ -61,7 +61,7 @@ public class MainStageTests extends ShellTestCase {
MockitoAnnotations.initMocks(this);
mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
- mSyncQueue, mSurfaceSession, mIconProvider, null);
+ mSyncQueue, mSurfaceSession, mIconProvider);
mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
index a31aa58bdc26..3b42a48b5a40 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
@@ -66,7 +66,7 @@ public class SideStageTests extends ShellTestCase {
MockitoAnnotations.initMocks(this);
mRootTask = new TestRunningTaskInfoBuilder().build();
mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
- mSyncQueue, mSurfaceSession, mIconProvider, null);
+ mSyncQueue, mSurfaceSession, mIconProvider);
mSideStage.onTaskAppeared(mRootTask, mRootLeash);
}
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
new file mode 100644
index 000000000000..5a68361c595c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -0,0 +1,210 @@
+/*
+ * 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.splitscreen;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+
+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 org.junit.Assert.assertFalse;
+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.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
+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.TransactionPool;
+import com.android.wm.shell.draganddrop.DragAndDropController;
+import com.android.wm.shell.recents.RecentTasksController;
+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 org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+/**
+ * Tests for {@link SplitScreenController}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SplitScreenControllerTests extends ShellTestCase {
+
+ @Mock ShellInit mShellInit;
+ @Mock ShellController mShellController;
+ @Mock ShellCommandHandler mShellCommandHandler;
+ @Mock ShellTaskOrganizer mTaskOrganizer;
+ @Mock SyncTransactionQueue mSyncQueue;
+ @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
+ @Mock ShellExecutor mMainExecutor;
+ @Mock DisplayController mDisplayController;
+ @Mock DisplayImeController mDisplayImeController;
+ @Mock DisplayInsetsController mDisplayInsetsController;
+ @Mock DragAndDropController mDragAndDropController;
+ @Mock Transitions mTransitions;
+ @Mock TransactionPool mTransactionPool;
+ @Mock IconProvider mIconProvider;
+ @Mock Optional<RecentTasksController> mRecentTasks;
+
+ private SplitScreenController mSplitScreenController;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
+ mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+ mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+ mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+ mIconProvider, mRecentTasks, mMainExecutor));
+ }
+
+ @Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void instantiateController_registerDumpCallback() {
+ doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
+ when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
+ mSplitScreenController.onInit();
+ verify(mShellCommandHandler, times(1)).addDumpCallback(any(), any());
+ }
+
+ @Test
+ public void instantiateController_registerCommandCallback() {
+ doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
+ when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
+ mSplitScreenController.onInit();
+ verify(mShellCommandHandler, times(1)).addCommandCallback(eq("splitscreen"), any(), any());
+ }
+
+ @Test
+ public void testControllerRegistersKeyguardChangeListener() {
+ doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
+ when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
+ mSplitScreenController.onInit();
+ verify(mShellController, times(1)).addKeyguardChangeListener(any());
+ }
+
+ @Test
+ public void testShouldAddMultipleTaskFlag_notInSplitScreen() {
+ doReturn(false).when(mSplitScreenController).isSplitScreenVisible();
+ doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any());
+
+ // Verify launching the same activity returns true.
+ Intent startIntent = createStartIntent("startActivity");
+ ActivityManager.RunningTaskInfo focusTaskInfo =
+ createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
+ doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
+ assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
+ startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+
+ // Verify launching different activity returns false.
+ Intent diffIntent = createStartIntent("diffActivity");
+ focusTaskInfo =
+ createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent);
+ doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
+ assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
+ startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ }
+
+ @Test
+ public void testShouldAddMultipleTaskFlag_inSplitScreen() {
+ doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
+ Intent startIntent = createStartIntent("startActivity");
+ ActivityManager.RunningTaskInfo sameTaskInfo =
+ createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
+ Intent diffIntent = createStartIntent("diffActivity");
+ ActivityManager.RunningTaskInfo differentTaskInfo =
+ createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent);
+
+ // Verify launching the same activity return false.
+ doReturn(sameTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+ assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
+ startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+
+ // Verify launching the same activity as adjacent returns true.
+ doReturn(differentTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+ doReturn(sameTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
+ startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+
+ // Verify launching different activity from adjacent returns false.
+ doReturn(differentTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+ doReturn(differentTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
+ startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ }
+
+ private Intent createStartIntent(String activityName) {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(mContext, activityName));
+ return intent;
+ }
+
+ private ActivityManager.RunningTaskInfo createTaskInfo(int winMode, int actType,
+ Intent strIntent) {
+ ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
+ info.configuration.windowConfiguration.setActivityType(actType);
+ info.configuration.windowConfiguration.setWindowingMode(winMode);
+ info.supportsMultiWindow = true;
+ info.baseIntent = strIntent;
+ info.baseActivity = strIntent.getComponent();
+ ActivityInfo activityInfo = new ActivityInfo();
+ activityInfo.packageName = info.baseActivity.getPackageName();
+ activityInfo.name = info.baseActivity.getClassName();
+ info.topActivityInfo = activityInfo;
+ return info;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index eb9d3a11d285..ae69b3ddd042 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -40,8 +40,6 @@ import com.android.wm.shell.transition.Transitions;
import java.util.Optional;
-import javax.inject.Provider;
-
public class SplitTestUtils {
static SplitLayout createMockSplitLayout() {
@@ -73,13 +71,11 @@ public class SplitTestUtils {
DisplayController displayController, DisplayImeController imeController,
DisplayInsetsController insetsController, SplitLayout splitLayout,
Transitions transitions, TransactionPool transactionPool,
- SplitscreenEventLogger logger, ShellExecutor mainExecutor,
- Optional<RecentTasksController> recentTasks,
- Provider<Optional<StageTaskUnfoldController>> unfoldController) {
+ ShellExecutor mainExecutor,
+ Optional<RecentTasksController> recentTasks) {
super(context, displayId, syncQueue, taskOrganizer, mainStage,
sideStage, displayController, imeController, insetsController, splitLayout,
- transitions, transactionPool, logger, mainExecutor, recentTasks,
- unfoldController);
+ transitions, transactionPool, mainExecutor, recentTasks);
// Prepare root task for testing.
mRootTask = new TestRunningTaskInfoBuilder().build();
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 ffaab652aa99..ea0033ba4bbb 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
@@ -95,7 +95,6 @@ public class SplitTransitionTests extends ShellTestCase {
@Mock private TransactionPool mTransactionPool;
@Mock private Transitions mTransitions;
@Mock private SurfaceSession mSurfaceSession;
- @Mock private SplitscreenEventLogger mLogger;
@Mock private IconProvider mIconProvider;
@Mock private ShellExecutor mMainExecutor;
private SplitLayout mSplitLayout;
@@ -118,16 +117,16 @@ public class SplitTransitionTests extends ShellTestCase {
mSplitLayout = SplitTestUtils.createMockSplitLayout();
mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
- mIconProvider, null);
+ mIconProvider);
mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
- mIconProvider, null);
+ mIconProvider);
mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
- mTransactionPool, mLogger, mMainExecutor, Optional.empty(), Optional::empty);
+ mTransactionPool, mMainExecutor, Optional.empty());
mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
.when(mTransitions).startTransition(anyInt(), any(), any());
@@ -182,7 +181,7 @@ public class SplitTransitionTests extends ShellTestCase {
IBinder transition = mSplitScreenTransitions.startEnterTransition(
TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
- new RemoteTransition(testRemote), mStageCoordinator);
+ new RemoteTransition(testRemote), mStageCoordinator, null);
mMainStage.onTaskAppeared(mMainChild, createMockSurface());
mSideStage.onTaskAppeared(mSideChild, createMockSurface());
boolean accepted = mStageCoordinator.startAnimation(transition, info,
@@ -340,7 +339,7 @@ public class SplitTransitionTests extends ShellTestCase {
TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0);
info.addChange(mainChange);
info.addChange(sideChange);
- IBinder transition = mSplitScreenTransitions.startDismissTransition(null,
+ IBinder transition = mSplitScreenTransitions.startDismissTransition(
new WindowContainerTransaction(), mStageCoordinator,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW, STAGE_TYPE_SIDE);
boolean accepted = mStageCoordinator.startAnimation(transition, info,
@@ -363,7 +362,7 @@ public class SplitTransitionTests extends ShellTestCase {
TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0);
info.addChange(mainChange);
info.addChange(sideChange);
- IBinder transition = mSplitScreenTransitions.startDismissTransition(null,
+ IBinder transition = mSplitScreenTransitions.startDismissTransition(
new WindowContainerTransaction(), mStageCoordinator, EXIT_REASON_DRAG_DIVIDER,
STAGE_TYPE_SIDE);
mMainStage.onTaskVanished(mMainChild);
@@ -422,7 +421,7 @@ public class SplitTransitionTests extends ShellTestCase {
TransitionInfo enterInfo = createEnterPairInfo();
IBinder enterTransit = mSplitScreenTransitions.startEnterTransition(
TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
- new RemoteTransition(new TestRemoteTransition()), mStageCoordinator);
+ new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null);
mMainStage.onTaskAppeared(mMainChild, createMockSurface());
mSideStage.onTaskAppeared(mSideChild, createMockSurface());
mStageCoordinator.startAnimation(enterTransit, enterInfo,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 42d998f6b0ee..835087007b30 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -16,7 +16,10 @@
package com.android.wm.shell.splitscreen;
+import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED;
+import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -28,12 +31,12 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
import static org.junit.Assert.assertEquals;
+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.clearInvocations;
-import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -41,8 +44,10 @@ import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.content.res.Configuration;
import android.graphics.Rect;
+import android.os.Bundle;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
+import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -58,6 +63,7 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
@@ -68,8 +74,6 @@ import org.mockito.MockitoAnnotations;
import java.util.Optional;
-import javax.inject.Provider;
-
/**
* Tests for {@link StageCoordinator}
*/
@@ -85,10 +89,6 @@ public class StageCoordinatorTests extends ShellTestCase {
@Mock
private SideStage mSideStage;
@Mock
- private StageTaskUnfoldController mMainUnfoldController;
- @Mock
- private StageTaskUnfoldController mSideUnfoldController;
- @Mock
private SplitLayout mSplitLayout;
@Mock
private DisplayController mDisplayController;
@@ -101,12 +101,11 @@ public class StageCoordinatorTests extends ShellTestCase {
@Mock
private TransactionPool mTransactionPool;
@Mock
- private SplitscreenEventLogger mLogger;
- @Mock
private ShellExecutor mMainExecutor;
private final Rect mBounds1 = new Rect(10, 20, 30, 40);
private final Rect mBounds2 = new Rect(5, 10, 15, 20);
+ private final Rect mRootBounds = new Rect(0, 0, 45, 60);
private SurfaceSession mSurfaceSession = new SurfaceSession();
private SurfaceControl mRootLeash;
@@ -118,17 +117,20 @@ public class StageCoordinatorTests extends ShellTestCase {
MockitoAnnotations.initMocks(this);
mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController,
- mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, mLogger,
- mMainExecutor, Optional.empty(), new UnfoldControllerProvider()));
- doNothing().when(mStageCoordinator).updateActivityOptions(any(), anyInt());
+ mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
+ mMainExecutor, Optional.empty()));
when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
+ when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds);
when(mSplitLayout.isLandscape()).thenReturn(false);
mRootTask = new TestRunningTaskInfoBuilder().build();
mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash);
+
+ mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
+ mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
}
@Test
@@ -168,13 +170,6 @@ public class StageCoordinatorTests extends ShellTestCase {
}
@Test
- public void testRootTaskAppeared_initializesUnfoldControllers() {
- verify(mMainUnfoldController).init();
- verify(mSideUnfoldController).init();
- verify(mStageCoordinator).onRootTaskAppeared();
- }
-
- @Test
public void testRootTaskInfoChanged_updatesSplitLayout() {
mStageCoordinator.onTaskInfoChanged(mRootTask);
@@ -184,26 +179,25 @@ public class StageCoordinatorTests extends ShellTestCase {
@Test
public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() {
mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null);
- clearInvocations(mMainUnfoldController, mSideUnfoldController);
+ final SplitScreenListener listener = mock(SplitScreenListener.class);
+ mStageCoordinator.registerSplitScreenListener(listener);
+ clearInvocations(listener);
mStageCoordinator.onLayoutSizeChanged(mSplitLayout);
- verify(mMainUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT,
- false);
- verify(mSideUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT, false);
+ verify(listener).onSplitBoundsChanged(mRootBounds, mBounds2, mBounds1);
}
@Test
public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() {
mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null);
- clearInvocations(mMainUnfoldController, mSideUnfoldController);
+ final SplitScreenListener listener = mock(SplitScreenListener.class);
+ mStageCoordinator.registerSplitScreenListener(listener);
+ clearInvocations(listener);
mStageCoordinator.onLayoutSizeChanged(mSplitLayout);
- verify(mMainUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT,
- false);
- verify(mSideUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT,
- false);
+ verify(listener).onSplitBoundsChanged(mRootBounds, mBounds1, mBounds2);
}
@Test
@@ -234,8 +228,7 @@ public class StageCoordinatorTests extends ShellTestCase {
mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME);
verify(mMainStage).reorderChild(eq(testTaskId), eq(true),
any(WindowContainerTransaction.class));
- verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false));
- verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(true));
+ verify(mMainStage).resetBounds(any(WindowContainerTransaction.class));
}
@Test
@@ -247,8 +240,7 @@ public class StageCoordinatorTests extends ShellTestCase {
mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME);
verify(mSideStage).reorderChild(eq(testTaskId), eq(true),
any(WindowContainerTransaction.class));
- verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(true));
- verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
+ verify(mSideStage).resetBounds(any(WindowContainerTransaction.class));
}
@Test
@@ -315,19 +307,15 @@ public class StageCoordinatorTests extends ShellTestCase {
verify(mSplitLayout).applySurfaceChanges(any(), any(), any(), any(), any(), eq(false));
}
- private class UnfoldControllerProvider implements
- Provider<Optional<StageTaskUnfoldController>> {
-
- private boolean isMain = true;
-
- @Override
- public Optional<StageTaskUnfoldController> get() {
- if (isMain) {
- isMain = false;
- return Optional.of(mMainUnfoldController);
- } else {
- return Optional.of(mSideUnfoldController);
- }
- }
+ @Test
+ public void testAddActivityOptions_addsBackgroundActivitiesFlags() {
+ Bundle options = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN,
+ SPLIT_POSITION_UNDEFINED, null /* options */, null /* wct */);
+
+ assertEquals(options.getParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, WindowContainerToken.class),
+ mMainStage.mRootTaskInfo.token);
+ assertTrue(options.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED));
+ assertTrue(options.getBoolean(
+ KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION));
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 157c30bcb6c7..5ee8bf3006a3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -25,7 +25,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -72,8 +71,6 @@ public final class StageTaskListenerTests extends ShellTestCase {
private SyncTransactionQueue mSyncQueue;
@Mock
private IconProvider mIconProvider;
- @Mock
- private StageTaskUnfoldController mStageTaskUnfoldController;
@Captor
private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
private SurfaceSession mSurfaceSession = new SurfaceSession();
@@ -92,8 +89,7 @@ public final class StageTaskListenerTests extends ShellTestCase {
mCallbacks,
mSyncQueue,
mSurfaceSession,
- mIconProvider,
- mStageTaskUnfoldController);
+ mIconProvider);
mRootTask = new TestRunningTaskInfoBuilder().build();
mRootTask.parentTaskId = INVALID_TASK_ID;
mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
@@ -130,30 +126,6 @@ public final class StageTaskListenerTests extends ShellTestCase {
verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true));
}
- @Test
- public void testTaskAppeared_notifiesUnfoldListener() {
- assumeFalse(ENABLE_SHELL_TRANSITIONS);
- final ActivityManager.RunningTaskInfo task =
- new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
-
- mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
-
- verify(mStageTaskUnfoldController).onTaskAppeared(eq(task), eq(mSurfaceControl));
- }
-
- @Test
- public void testTaskVanished_notifiesUnfoldListener() {
- assumeFalse(ENABLE_SHELL_TRANSITIONS);
- final ActivityManager.RunningTaskInfo task =
- new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
- mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
- clearInvocations(mStageTaskUnfoldController);
-
- mStageTaskListener.onTaskVanished(task);
-
- verify(mStageTaskUnfoldController).onTaskVanished(eq(task));
- }
-
@Test(expected = IllegalArgumentException.class)
public void testUnknownTaskVanished() {
final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index 630d0d2c827c..e5ae2962e6e4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -73,6 +73,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.HandlerExecutor;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
@@ -91,7 +92,7 @@ import java.util.function.IntSupplier;
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class StartingSurfaceDrawerTests {
+public class StartingSurfaceDrawerTests extends ShellTestCase {
@Mock
private IBinder mBinder;
@Mock
@@ -249,7 +250,8 @@ public class StartingSurfaceDrawerTests {
any() /* window */, any() /* attrs */,
anyInt() /* viewVisibility */, anyInt() /* displayId */,
any() /* requestedVisibility */, any() /* outInputChannel */,
- any() /* outInsetsState */, any() /* outActiveControls */);
+ any() /* outInsetsState */, any() /* outActiveControls */,
+ any() /* outAttachedFrame */, any() /* outSizeCompatScale */);
TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo,
mBinder,
snapshot, mTestExecutor, () -> {
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
new file mode 100644
index 000000000000..35515e3bb6e8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
@@ -0,0 +1,80 @@
+/*
+ * 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.startingsurface;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the starting window controller.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:StartingWindowControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StartingWindowControllerTests extends ShellTestCase {
+
+ private @Mock Context mContext;
+ private @Mock DisplayManager mDisplayManager;
+ private @Mock ShellInit mShellInit;
+ private @Mock ShellTaskOrganizer mTaskOrganizer;
+ private @Mock ShellExecutor mMainExecutor;
+ private @Mock StartingWindowTypeAlgorithm mTypeAlgorithm;
+ private @Mock IconProvider mIconProvider;
+ private @Mock TransactionPool mTransactionPool;
+ private StartingWindowController mController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ doReturn(mock(Display.class)).when(mDisplayManager).getDisplay(anyInt());
+ doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
+ mController = new StartingWindowController(mContext, mShellInit, mTaskOrganizer,
+ mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool);
+ }
+
+ @Test
+ public void instantiate_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
index 78e27c956807..3de50bb60470 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
@@ -47,6 +47,7 @@ import android.window.TaskSnapshot;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import org.junit.Test;
@@ -58,7 +59,7 @@ import org.junit.runner.RunWith;
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class TaskSnapshotWindowTest {
+public class TaskSnapshotWindowTest extends ShellTestCase {
private TaskSnapshotWindow mWindow;
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
new file mode 100644
index 000000000000..d6ddba9e927d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
@@ -0,0 +1,411 @@
+/*
+ * 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.sysui;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ShellExecutor;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class ShellControllerTest extends ShellTestCase {
+
+ private static final int TEST_USER_ID = 100;
+
+ @Mock
+ private ShellInit mShellInit;
+ @Mock
+ private ShellCommandHandler mShellCommandHandler;
+ @Mock
+ private ShellExecutor mExecutor;
+ @Mock
+ private Context mTestUserContext;
+
+ private ShellController mController;
+ private TestConfigurationChangeListener mConfigChangeListener;
+ private TestKeyguardChangeListener mKeyguardChangeListener;
+ private TestUserChangeListener mUserChangeListener;
+
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mKeyguardChangeListener = new TestKeyguardChangeListener();
+ mConfigChangeListener = new TestConfigurationChangeListener();
+ mUserChangeListener = new TestUserChangeListener();
+ mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor);
+ mController.onConfigurationChanged(getConfigurationCopy());
+ }
+
+ @After
+ public void tearDown() {
+ // Do nothing
+ }
+
+ @Test
+ public void testAddUserChangeListener_ensureCallback() {
+ mController.addUserChangeListener(mUserChangeListener);
+
+ mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+ assertTrue(mUserChangeListener.userChanged == 1);
+ assertTrue(mUserChangeListener.lastUserContext == mTestUserContext);
+ }
+
+ @Test
+ public void testDoubleAddUserChangeListener_ensureSingleCallback() {
+ mController.addUserChangeListener(mUserChangeListener);
+ mController.addUserChangeListener(mUserChangeListener);
+
+ mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+ assertTrue(mUserChangeListener.userChanged == 1);
+ assertTrue(mUserChangeListener.lastUserContext == mTestUserContext);
+ }
+
+ @Test
+ public void testAddRemoveUserChangeListener_ensureNoCallback() {
+ mController.addUserChangeListener(mUserChangeListener);
+ mController.removeUserChangeListener(mUserChangeListener);
+
+ mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+ assertTrue(mUserChangeListener.userChanged == 0);
+ assertTrue(mUserChangeListener.lastUserContext == null);
+ }
+
+ @Test
+ public void testUserProfilesChanged() {
+ mController.addUserChangeListener(mUserChangeListener);
+
+ ArrayList<UserInfo> profiles = new ArrayList<>();
+ profiles.add(mock(UserInfo.class));
+ profiles.add(mock(UserInfo.class));
+ mController.onUserProfilesChanged(profiles);
+ assertTrue(mUserChangeListener.lastUserProfiles.equals(profiles));
+ }
+
+ @Test
+ public void testAddKeyguardChangeListener_ensureCallback() {
+ mController.addKeyguardChangeListener(mKeyguardChangeListener);
+
+ mController.onKeyguardVisibilityChanged(true, false, false);
+ assertTrue(mKeyguardChangeListener.visibilityChanged == 1);
+ assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 0);
+ }
+
+ @Test
+ public void testDoubleAddKeyguardChangeListener_ensureSingleCallback() {
+ mController.addKeyguardChangeListener(mKeyguardChangeListener);
+ mController.addKeyguardChangeListener(mKeyguardChangeListener);
+
+ mController.onKeyguardVisibilityChanged(true, false, false);
+ assertTrue(mKeyguardChangeListener.visibilityChanged == 1);
+ assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 0);
+ }
+
+ @Test
+ public void testAddRemoveKeyguardChangeListener_ensureNoCallback() {
+ mController.addKeyguardChangeListener(mKeyguardChangeListener);
+ mController.removeKeyguardChangeListener(mKeyguardChangeListener);
+
+ mController.onKeyguardVisibilityChanged(true, false, false);
+ assertTrue(mKeyguardChangeListener.visibilityChanged == 0);
+ assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 0);
+ }
+
+ @Test
+ public void testKeyguardVisibilityChanged() {
+ mController.addKeyguardChangeListener(mKeyguardChangeListener);
+
+ mController.onKeyguardVisibilityChanged(true, true, true);
+ assertTrue(mKeyguardChangeListener.visibilityChanged == 1);
+ assertTrue(mKeyguardChangeListener.lastAnimatingDismiss);
+ assertTrue(mKeyguardChangeListener.lastOccluded);
+ assertTrue(mKeyguardChangeListener.lastAnimatingDismiss);
+ assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 0);
+ }
+
+ @Test
+ public void testKeyguardDismissAnimationFinished() {
+ mController.addKeyguardChangeListener(mKeyguardChangeListener);
+
+ mController.onKeyguardDismissAnimationFinished();
+ assertTrue(mKeyguardChangeListener.visibilityChanged == 0);
+ assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 1);
+ }
+
+ @Test
+ public void testAddConfigurationChangeListener_ensureCallback() {
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.densityDpi = 200;
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 1);
+ }
+
+ @Test
+ public void testDoubleAddConfigurationChangeListener_ensureSingleCallback() {
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.densityDpi = 200;
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 1);
+ }
+
+ @Test
+ public void testAddRemoveConfigurationChangeListener_ensureNoCallback() {
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+ mController.removeConfigurationChangeListener(mConfigChangeListener);
+
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.densityDpi = 200;
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 0);
+ }
+
+ @Test
+ public void testMultipleConfigurationChangeListeners() {
+ TestConfigurationChangeListener listener2 = new TestConfigurationChangeListener();
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+ mController.addConfigurationChangeListener(listener2);
+
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.densityDpi = 200;
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 1);
+ assertTrue(listener2.configChanges == 1);
+ }
+
+ @Test
+ public void testRemoveListenerDuringCallback() {
+ TestConfigurationChangeListener badListener = new TestConfigurationChangeListener() {
+ @Override
+ public void onConfigurationChanged(Configuration newConfiguration) {
+ mController.removeConfigurationChangeListener(this);
+ }
+ };
+ mController.addConfigurationChangeListener(badListener);
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+
+ // Ensure we don't fail just because a listener was removed mid-callback
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.densityDpi = 200;
+ mController.onConfigurationChanged(newConfig);
+ }
+
+ @Test
+ public void testDensityChangeCallback() {
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.densityDpi = 200;
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 1);
+ assertTrue(mConfigChangeListener.densityChanges == 1);
+ assertTrue(mConfigChangeListener.smallestWidthChanges == 0);
+ assertTrue(mConfigChangeListener.themeChanges == 0);
+ assertTrue(mConfigChangeListener.localeChanges == 0);
+ }
+
+ @Test
+ public void testFontScaleChangeCallback() {
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.fontScale = 2;
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 1);
+ assertTrue(mConfigChangeListener.densityChanges == 1);
+ assertTrue(mConfigChangeListener.smallestWidthChanges == 0);
+ assertTrue(mConfigChangeListener.themeChanges == 0);
+ assertTrue(mConfigChangeListener.localeChanges == 0);
+ }
+
+ @Test
+ public void testSmallestWidthChangeCallback() {
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.smallestScreenWidthDp = 100;
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 1);
+ assertTrue(mConfigChangeListener.densityChanges == 0);
+ assertTrue(mConfigChangeListener.smallestWidthChanges == 1);
+ assertTrue(mConfigChangeListener.themeChanges == 0);
+ assertTrue(mConfigChangeListener.localeChanges == 0);
+ }
+
+ @Test
+ public void testThemeChangeCallback() {
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.assetsSeq++;
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 1);
+ assertTrue(mConfigChangeListener.densityChanges == 0);
+ assertTrue(mConfigChangeListener.smallestWidthChanges == 0);
+ assertTrue(mConfigChangeListener.themeChanges == 1);
+ assertTrue(mConfigChangeListener.localeChanges == 0);
+ }
+
+ @Test
+ public void testNightModeChangeCallback() {
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+
+ Configuration newConfig = getConfigurationCopy();
+ newConfig.uiMode = Configuration.UI_MODE_NIGHT_YES;
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 1);
+ assertTrue(mConfigChangeListener.densityChanges == 0);
+ assertTrue(mConfigChangeListener.smallestWidthChanges == 0);
+ assertTrue(mConfigChangeListener.themeChanges == 1);
+ assertTrue(mConfigChangeListener.localeChanges == 0);
+ }
+
+ @Test
+ public void testLocaleChangeCallback() {
+ mController.addConfigurationChangeListener(mConfigChangeListener);
+
+ Configuration newConfig = getConfigurationCopy();
+ // Just change the locales to be different
+ if (newConfig.locale == Locale.CANADA) {
+ newConfig.locale = Locale.US;
+ } else {
+ newConfig.locale = Locale.CANADA;
+ }
+ mController.onConfigurationChanged(newConfig);
+ assertTrue(mConfigChangeListener.configChanges == 1);
+ assertTrue(mConfigChangeListener.densityChanges == 0);
+ assertTrue(mConfigChangeListener.smallestWidthChanges == 0);
+ assertTrue(mConfigChangeListener.themeChanges == 0);
+ assertTrue(mConfigChangeListener.localeChanges == 1);
+ }
+
+ private Configuration getConfigurationCopy() {
+ final Configuration c = new Configuration(InstrumentationRegistry.getInstrumentation()
+ .getTargetContext().getResources().getConfiguration());
+ // In tests this might be undefined so make sure it's valid
+ c.assetsSeq = 1;
+ return c;
+ }
+
+ private class TestConfigurationChangeListener implements ConfigurationChangeListener {
+ // Counts of number of times each of the callbacks are called
+ public int configChanges;
+ public int densityChanges;
+ public int smallestWidthChanges;
+ public int themeChanges;
+ public int localeChanges;
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfiguration) {
+ configChanges++;
+ }
+
+ @Override
+ public void onDensityOrFontScaleChanged() {
+ densityChanges++;
+ }
+
+ @Override
+ public void onSmallestScreenWidthChanged() {
+ smallestWidthChanges++;
+ }
+
+ @Override
+ public void onThemeChanged() {
+ themeChanges++;
+ }
+
+ @Override
+ public void onLocaleOrLayoutDirectionChanged() {
+ localeChanges++;
+ }
+ }
+
+ private class TestKeyguardChangeListener implements KeyguardChangeListener {
+ // Counts of number of times each of the callbacks are called
+ public int visibilityChanged;
+ public boolean lastVisibility;
+ public boolean lastOccluded;
+ public boolean lastAnimatingDismiss;
+ public int dismissAnimationFinished;
+
+ @Override
+ public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
+ boolean animatingDismiss) {
+ lastVisibility = visible;
+ lastOccluded = occluded;
+ lastAnimatingDismiss = animatingDismiss;
+ visibilityChanged++;
+ }
+
+ @Override
+ public void onKeyguardDismissAnimationFinished() {
+ dismissAnimationFinished++;
+ }
+ }
+
+ private class TestUserChangeListener implements UserChangeListener {
+ // Counts of number of times each of the callbacks are called
+ public int userChanged;
+ public int lastUserId;
+ public Context lastUserContext;
+ public int userProfilesChanged;
+ public List<? extends UserInfo> lastUserProfiles;
+
+
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ userChanged++;
+ lastUserId = newUserId;
+ lastUserContext = userContext;
+ }
+
+ @Override
+ public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+ userProfilesChanged++;
+ lastUserProfiles = profiles;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperControllerTest.java
deleted file mode 100644
index d6142753b48a..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperControllerTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.tasksurfacehelper;
-
-import static org.mockito.Mockito.verify;
-
-import android.platform.test.annotations.Presubmit;
-import android.testing.AndroidTestingRunner;
-import android.view.SurfaceControl;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.ShellExecutor;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@Presubmit
-@RunWith(AndroidTestingRunner.class)
-@SmallTest
-public class TaskSurfaceHelperControllerTest {
- private TaskSurfaceHelperController mTaskSurfaceHelperController;
- @Mock
- private ShellTaskOrganizer mMockTaskOrganizer;
- @Mock
- private ShellExecutor mMockShellExecutor;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- mTaskSurfaceHelperController = new TaskSurfaceHelperController(
- mMockTaskOrganizer, mMockShellExecutor);
- }
-
- @Test
- public void testSetGameModeForTask() {
- mTaskSurfaceHelperController.setGameModeForTask(/*taskId*/1, /*gameMode*/3);
- verify(mMockTaskOrganizer).setSurfaceMetadata(1, SurfaceControl.METADATA_GAME_MODE, 3);
- }
-}
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 a0b12976b467..c6492bee040e 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
@@ -22,6 +22,7 @@ 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.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
@@ -43,11 +44,13 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -59,8 +62,6 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
-import android.view.IDisplayWindowListener;
-import android.view.IWindowManager;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
@@ -79,14 +80,18 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.wm.shell.ShellTestCase;
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.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.InOrder;
import java.util.ArrayList;
@@ -98,7 +103,7 @@ import java.util.ArrayList;
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class ShellTransitionTests {
+public class ShellTransitionTests extends ShellTestCase {
private final WindowOrganizer mOrganizer = mock(WindowOrganizer.class);
private final TransactionPool mTransactionPool = mock(TransactionPool.class);
@@ -116,6 +121,14 @@ public class ShellTransitionTests {
}
@Test
+ public void instantiate_addInitCallback() {
+ ShellInit shellInit = mock(ShellInit.class);
+ final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool,
+ createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor);
+ verify(shellInit, times(1)).addInitCallback(any(), eq(t));
+ }
+
+ @Test
public void testBasicTransitionFlow() {
Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
@@ -541,64 +554,77 @@ public class ShellTransitionTests {
final @Surface.Rotation int upsideDown = displays
.getDisplayLayout(DEFAULT_DISPLAY).getUpsideDownRotation();
+ TransitionInfo.Change displayChange = new ChangeBuilder(TRANSIT_CHANGE)
+ .setFlags(FLAG_IS_DISPLAY).setRotate().build();
+ // Set non-square display so nav bar won't be allowed to move.
+ displayChange.getStartAbsBounds().set(0, 0, 1000, 2000);
final TransitionInfo normalDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE)
- .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
- .build())
+ .addChange(displayChange)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo).setRotate().build())
.build();
- assertFalse(DefaultTransitionHandler.isRotationSeamless(normalDispRotate, displays));
+ assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint(
+ displayChange, normalDispRotate, displays));
// Seamless if all tasks are seamless
final TransitionInfo rotateSeamless = new TransitionInfoBuilder(TRANSIT_CHANGE)
- .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
- .build())
+ .addChange(displayChange)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
.setRotate(ROTATION_ANIMATION_SEAMLESS).build())
.build();
- assertTrue(DefaultTransitionHandler.isRotationSeamless(rotateSeamless, displays));
+ assertEquals(ROTATION_ANIMATION_SEAMLESS, DefaultTransitionHandler.getRotationAnimationHint(
+ displayChange, rotateSeamless, displays));
// Not seamless if there is PiP (or any other non-seamless task)
final TransitionInfo pipDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE)
- .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
- .build())
+ .addChange(displayChange)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
.setRotate(ROTATION_ANIMATION_SEAMLESS).build())
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfoPip)
.setRotate().build())
.build();
- assertFalse(DefaultTransitionHandler.isRotationSeamless(pipDispRotate, displays));
+ assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint(
+ displayChange, pipDispRotate, displays));
+
+ // Not seamless if there is no changed task.
+ final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE)
+ .addChange(displayChange)
+ .build();
+ assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint(
+ displayChange, noTask, displays));
// Not seamless if one of rotations is upside-down
+ displayChange = new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
+ .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build();
final TransitionInfo seamlessUpsideDown = new TransitionInfoBuilder(TRANSIT_CHANGE)
- .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
- .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build())
+ .addChange(displayChange)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
.setRotate(upsideDown, ROTATION_ANIMATION_SEAMLESS).build())
.build();
- assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessUpsideDown, displays));
+ assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint(
+ displayChange, seamlessUpsideDown, displays));
// Not seamless if system alert windows
+ displayChange = new ChangeBuilder(TRANSIT_CHANGE)
+ .setFlags(FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build();
final TransitionInfo seamlessButAlert = new TransitionInfoBuilder(TRANSIT_CHANGE)
- .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(
- FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build())
+ .addChange(displayChange)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
.setRotate(ROTATION_ANIMATION_SEAMLESS).build())
.build();
- assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessButAlert, displays));
-
- // Not seamless if there is no changed task.
- final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE)
- .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
- .setRotate().build())
- .build();
- assertFalse(DefaultTransitionHandler.isRotationSeamless(noTask, displays));
+ assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint(
+ displayChange, seamlessButAlert, displays));
// Seamless if display is explicitly seamless.
+ displayChange = new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
+ .setRotate(ROTATION_ANIMATION_SEAMLESS).build();
final TransitionInfo seamlessDisplay = new TransitionInfoBuilder(TRANSIT_CHANGE)
- .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
- .setRotate(ROTATION_ANIMATION_SEAMLESS).build())
+ .addChange(displayChange)
+ // The animation hint of task will be ignored.
+ .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
+ .setRotate(ROTATION_ANIMATION_ROTATE).build())
.build();
- assertTrue(DefaultTransitionHandler.isRotationSeamless(seamlessDisplay, displays));
+ assertEquals(ROTATION_ANIMATION_SEAMLESS, DefaultTransitionHandler.getRotationAnimationHint(
+ displayChange, seamlessDisplay, displays));
}
@Test
@@ -678,6 +704,204 @@ public class ShellTransitionTests {
verify(runnable4, times(1)).run();
}
+ @Test
+ public void testObserverLifecycle_basicTransitionFlow() {
+ Transitions transitions = createTestTransitions();
+ Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class);
+ transitions.registerObserver(observer);
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+
+ IBinder transitToken = new Binder();
+ transitions.requestStartTransition(transitToken,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ transitions.onTransitionReady(transitToken, info, startT, finishT);
+
+ InOrder observerOrder = inOrder(observer);
+ observerOrder.verify(observer).onTransitionReady(transitToken, info, startT, finishT);
+ observerOrder.verify(observer).onTransitionStarting(transitToken);
+ verify(observer, times(0)).onTransitionFinished(eq(transitToken), anyBoolean());
+ mDefaultHandler.finishAll();
+ mMainExecutor.flushAll();
+ verify(observer).onTransitionFinished(transitToken, false);
+ }
+
+ @Test
+ public void testObserverLifecycle_queueing() {
+ Transitions transitions = createTestTransitions();
+ Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class);
+ transitions.registerObserver(observer);
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+
+ IBinder transitToken1 = new Binder();
+ transitions.requestStartTransition(transitToken1,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class);
+ transitions.onTransitionReady(transitToken1, info1, startT1, finishT1);
+ verify(observer).onTransitionReady(transitToken1, info1, startT1, finishT1);
+
+ IBinder transitToken2 = new Binder();
+ transitions.requestStartTransition(transitToken2,
+ new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */));
+ TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
+ transitions.onTransitionReady(transitToken2, info2, startT2, finishT2);
+ verify(observer, times(1)).onTransitionReady(transitToken2, info2, startT2, finishT2);
+ verify(observer, times(0)).onTransitionStarting(transitToken2);
+ verify(observer, times(0)).onTransitionFinished(eq(transitToken1), anyBoolean());
+ verify(observer, times(0)).onTransitionFinished(eq(transitToken2), anyBoolean());
+
+ mDefaultHandler.finishAll();
+ mMainExecutor.flushAll();
+ // first transition finished
+ verify(observer, times(1)).onTransitionFinished(transitToken1, false);
+ verify(observer, times(1)).onTransitionStarting(transitToken2);
+ verify(observer, times(0)).onTransitionFinished(eq(transitToken2), anyBoolean());
+
+ mDefaultHandler.finishAll();
+ mMainExecutor.flushAll();
+ verify(observer, times(1)).onTransitionFinished(transitToken2, false);
+ }
+
+
+ @Test
+ public void testObserverLifecycle_merging() {
+ Transitions transitions = createTestTransitions();
+ Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class);
+ transitions.registerObserver(observer);
+ mDefaultHandler.setSimulateMerge(true);
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+
+ IBinder transitToken1 = new Binder();
+ transitions.requestStartTransition(transitToken1,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class);
+ transitions.onTransitionReady(transitToken1, info1, startT1, finishT1);
+
+ IBinder transitToken2 = new Binder();
+ transitions.requestStartTransition(transitToken2,
+ new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */));
+ TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
+ transitions.onTransitionReady(transitToken2, info2, startT2, finishT2);
+
+ InOrder observerOrder = inOrder(observer);
+ observerOrder.verify(observer).onTransitionReady(transitToken2, info2, startT2, finishT2);
+ observerOrder.verify(observer).onTransitionMerged(transitToken2, transitToken1);
+ verify(observer, times(0)).onTransitionFinished(eq(transitToken1), anyBoolean());
+
+ mDefaultHandler.finishAll();
+ mMainExecutor.flushAll();
+ // transition + merged all finished.
+ verify(observer, times(1)).onTransitionFinished(transitToken1, false);
+ // Merged transition won't receive any lifecycle calls beyond ready
+ verify(observer, times(0)).onTransitionStarting(transitToken2);
+ verify(observer, times(0)).onTransitionFinished(eq(transitToken2), anyBoolean());
+ }
+
+ @Test
+ public void testObserverLifecycle_mergingAfterQueueing() {
+ Transitions transitions = createTestTransitions();
+ Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class);
+ transitions.registerObserver(observer);
+ mDefaultHandler.setSimulateMerge(true);
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+
+ // Make a test handler that only responds to multi-window triggers AND only animates
+ // Change transitions.
+ final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
+ TestTransitionHandler testHandler = new TestTransitionHandler() {
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ for (TransitionInfo.Change chg : info.getChanges()) {
+ if (chg.getMode() == TRANSIT_CHANGE) {
+ return super.startAnimation(transition, info, startTransaction,
+ finishTransaction, finishCallback);
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ final RunningTaskInfo task = request.getTriggerTask();
+ return (task != null && task.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW)
+ ? handlerWCT : null;
+ }
+ };
+ transitions.addHandler(testHandler);
+
+ // Use test handler to play an animation
+ IBinder transitToken1 = new Binder();
+ RunningTaskInfo mwTaskInfo =
+ createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
+ transitions.requestStartTransition(transitToken1,
+ new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */));
+ TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE)
+ .addChange(TRANSIT_CHANGE).build();
+ SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class);
+ transitions.onTransitionReady(transitToken1, change, startT1, finishT1);
+
+ // Request the second transition that should be handled by the default handler
+ IBinder transitToken2 = new Binder();
+ TransitionInfo open = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ transitions.requestStartTransition(transitToken2,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
+ transitions.onTransitionReady(transitToken2, open, startT2, finishT2);
+ verify(observer).onTransitionReady(transitToken2, open, startT2, finishT2);
+ verify(observer, times(0)).onTransitionStarting(transitToken2);
+
+ // Request the third transition that should be merged into the second one
+ IBinder transitToken3 = new Binder();
+ transitions.requestStartTransition(transitToken3,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ SurfaceControl.Transaction startT3 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT3 = mock(SurfaceControl.Transaction.class);
+ transitions.onTransitionReady(transitToken3, open, startT3, finishT3);
+ verify(observer, times(0)).onTransitionStarting(transitToken2);
+ verify(observer).onTransitionReady(transitToken3, open, startT3, finishT3);
+ verify(observer, times(0)).onTransitionStarting(transitToken3);
+
+ testHandler.finishAll();
+ mMainExecutor.flushAll();
+
+ verify(observer).onTransitionFinished(transitToken1, false);
+
+ mDefaultHandler.finishAll();
+ mMainExecutor.flushAll();
+
+ InOrder observerOrder = inOrder(observer);
+ observerOrder.verify(observer).onTransitionStarting(transitToken2);
+ observerOrder.verify(observer).onTransitionMerged(transitToken3, transitToken2);
+ observerOrder.verify(observer).onTransitionFinished(transitToken2, false);
+
+ // Merged transition won't receive any lifecycle calls beyond ready
+ verify(observer, times(0)).onTransitionStarting(transitToken3);
+ verify(observer, times(0)).onTransitionFinished(eq(transitToken3), anyBoolean());
+ }
+
class TransitionInfoBuilder {
final TransitionInfo mInfo;
@@ -824,33 +1048,21 @@ public class ShellTransitionTests {
}
private DisplayController createTestDisplayController() {
- IWindowManager mockWM = mock(IWindowManager.class);
- final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1];
- try {
- doReturn(new int[]{DEFAULT_DISPLAY}).when(mockWM).registerDisplayWindowListener(any());
- } catch (RemoteException e) {
- // No remote stuff happening, so this can't be hit
- }
- DisplayController out = new DisplayController(mContext, mockWM, mMainExecutor);
- out.initialize();
+ DisplayLayout displayLayout = mock(DisplayLayout.class);
+ doReturn(Surface.ROTATION_180).when(displayLayout).getUpsideDownRotation();
+ // By default we ignore nav bar in deciding if a seamless rotation is allowed.
+ doReturn(true).when(displayLayout).allowSeamlessRotationDespiteNavBarMoving();
+
+ DisplayController out = mock(DisplayController.class);
+ doReturn(displayLayout).when(out).getDisplayLayout(DEFAULT_DISPLAY);
return out;
}
private Transitions createTestTransitions() {
- return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(),
- mContext, mMainExecutor, mMainHandler, mAnimExecutor);
+ ShellInit shellInit = new ShellInit(mMainExecutor);
+ final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool,
+ createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor);
+ shellInit.init();
+ return t;
}
-//
-// private class TestDisplayController extends DisplayController {
-// private final DisplayLayout mTestDisplayLayout;
-// TestDisplayController() {
-// super(mContext, mock(IWindowManager.class), mMainExecutor);
-// mTestDisplayLayout = new DisplayLayout();
-// mTestDisplayLayout.
-// }
-//
-// @Override
-// DisplayLayout
-// }
-
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java
new file mode 100644
index 000000000000..81eefe25704e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java
@@ -0,0 +1,367 @@
+/*
+ * 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.unfold;
+
+import static com.android.wm.shell.unfold.UnfoldAnimationControllerTest.TestUnfoldTaskAnimator.UNSET;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.TaskInfo;
+import android.testing.AndroidTestingRunner;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Predicate;
+
+/**
+ * Tests for {@link UnfoldAnimationController}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:UnfoldAnimationControllerTest
+ */
+@RunWith(AndroidTestingRunner.class)
+public class UnfoldAnimationControllerTest extends ShellTestCase {
+
+ @Mock
+ private TransactionPool mTransactionPool;
+ @Mock
+ private UnfoldTransitionHandler mUnfoldTransitionHandler;
+ @Mock
+ private ShellInit mShellInit;
+ @Mock
+ private SurfaceControl mLeash;
+
+ private UnfoldAnimationController mUnfoldAnimationController;
+
+ private final TestShellUnfoldProgressProvider mProgressProvider =
+ new TestShellUnfoldProgressProvider();
+ private final TestShellExecutor mShellExecutor = new TestShellExecutor();
+
+ private final TestUnfoldTaskAnimator mTaskAnimator1 = new TestUnfoldTaskAnimator();
+ private final TestUnfoldTaskAnimator mTaskAnimator2 = new TestUnfoldTaskAnimator();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mTransactionPool.acquire()).thenReturn(mock(SurfaceControl.Transaction.class));
+
+ final List<UnfoldTaskAnimator> animators = new ArrayList<>();
+ animators.add(mTaskAnimator1);
+ animators.add(mTaskAnimator2);
+ mUnfoldAnimationController = new UnfoldAnimationController(
+ mShellInit,
+ mTransactionPool,
+ mProgressProvider,
+ animators,
+ () -> Optional.of(mUnfoldTransitionHandler),
+ mShellExecutor
+ );
+ }
+
+ @Test
+ public void instantiateController_addInitCallback() {
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void testAppearedMatchingTask_appliesUnfoldProgress() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(2).build();
+
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+
+ mUnfoldAnimationController.onStateChangeProgress(0.5f);
+ assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+ }
+
+ @Test
+ public void testAppearedMatchingTaskTwoDifferentAnimators_appliesUnfoldProgressToBoth() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 1);
+ mTaskAnimator2.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(1).build();
+ RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(2).build();
+
+ mUnfoldAnimationController.onTaskAppeared(taskInfo1, mLeash);
+ mUnfoldAnimationController.onTaskAppeared(taskInfo2, mLeash);
+
+ mUnfoldAnimationController.onStateChangeProgress(0.5f);
+ assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+ assertThat(mTaskAnimator2.mLastAppliedProgress).isEqualTo(0.5f);
+ }
+
+ @Test
+ public void testAppearedNonMatchingTask_doesNotApplyUnfoldProgress() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(0).build();
+
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+
+ mUnfoldAnimationController.onStateChangeProgress(0.5f);
+ assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(UNSET);
+ }
+
+ @Test
+ public void testAppearedAndChangedToNonMatchingTask_doesNotApplyUnfoldProgress() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(2).build();
+
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(0);
+ mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+
+ mUnfoldAnimationController.onStateChangeProgress(0.5f);
+ assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(UNSET);
+ }
+
+ @Test
+ public void testAppearedAndChangedToNonMatchingTaskAndBack_appliesUnfoldProgress() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(2).build();
+
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(0);
+ mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(2);
+ mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+
+ mUnfoldAnimationController.onStateChangeProgress(0.5f);
+ assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+ }
+
+ @Test
+ public void testAppearedNonMatchingTaskAndChangedToMatching_appliesUnfoldProgress() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(0).build();
+
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(2);
+ mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+
+ mUnfoldAnimationController.onStateChangeProgress(0.5f);
+ assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+ }
+
+ @Test
+ public void testAppearedMatchingTaskAndChanged_appliesUnfoldProgress() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(2).build();
+
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+ mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+
+ mUnfoldAnimationController.onStateChangeProgress(0.5f);
+ assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+ }
+
+ @Test
+ public void testShellTransitionRunning_doesNotApplyUnfoldProgress() {
+ when(mUnfoldTransitionHandler.willHandleTransition()).thenReturn(true);
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(2).build();
+
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+
+ mUnfoldAnimationController.onStateChangeProgress(0.5f);
+ assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(UNSET);
+ }
+
+ @Test
+ public void testApplicableTaskDisappeared_resetsSurface() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 0);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(0).build();
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+ assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId);
+
+ mUnfoldAnimationController.onTaskVanished(taskInfo);
+
+ assertThat(mTaskAnimator1.mResetTasks).contains(taskInfo.taskId);
+ }
+
+ @Test
+ public void testApplicablePinnedTaskDisappeared_doesNotResetSurface() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(2).build();
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+ assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId);
+
+ mUnfoldAnimationController.onTaskVanished(taskInfo);
+
+ assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId);
+ }
+
+ @Test
+ public void testNonApplicableTaskAppearedDisappeared_doesNotResetSurface() {
+ mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+ RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(0).build();
+
+ mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+ mUnfoldAnimationController.onTaskVanished(taskInfo);
+
+ assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId);
+ }
+
+ @Test
+ public void testInit_initsAndStartsAnimators() {
+ mUnfoldAnimationController.onInit();
+ mShellExecutor.flushAll();
+
+ assertThat(mTaskAnimator1.mInitialized).isTrue();
+ assertThat(mTaskAnimator1.mStarted).isTrue();
+ }
+
+ private static class TestShellUnfoldProgressProvider implements ShellUnfoldProgressProvider,
+ ShellUnfoldProgressProvider.UnfoldListener {
+
+ private final List<UnfoldListener> mListeners = new ArrayList<>();
+
+ @Override
+ public void addListener(Executor executor, UnfoldListener listener) {
+ mListeners.add(listener);
+ }
+
+ @Override
+ public void onStateChangeStarted() {
+ mListeners.forEach(UnfoldListener::onStateChangeStarted);
+ }
+
+ @Override
+ public void onStateChangeProgress(float progress) {
+ mListeners.forEach(unfoldListener -> unfoldListener.onStateChangeProgress(progress));
+ }
+
+ @Override
+ public void onStateChangeFinished() {
+ mListeners.forEach(UnfoldListener::onStateChangeFinished);
+ }
+ }
+
+ public static class TestUnfoldTaskAnimator implements UnfoldTaskAnimator {
+
+ public static final float UNSET = -1f;
+ private Predicate<TaskInfo> mTaskMatcher = (info) -> false;
+
+ Map<Integer, TaskInfo> mTasksMap = new HashMap<>();
+ Set<Integer> mResetTasks = new HashSet<>();
+
+ boolean mInitialized = false;
+ boolean mStarted = false;
+ float mLastAppliedProgress = UNSET;
+
+ @Override
+ public void init() {
+ mInitialized = true;
+ }
+
+ @Override
+ public void start() {
+ mStarted = true;
+ }
+
+ @Override
+ public void stop() {
+ mStarted = false;
+ }
+
+ @Override
+ public boolean isApplicableTask(TaskInfo taskInfo) {
+ return mTaskMatcher.test(taskInfo);
+ }
+
+ @Override
+ public void applyAnimationProgress(float progress, Transaction transaction) {
+ mLastAppliedProgress = progress;
+ }
+
+ public void setTaskMatcher(Predicate<TaskInfo> taskMatcher) {
+ mTaskMatcher = taskMatcher;
+ }
+
+ @Override
+ public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {
+ mTasksMap.put(taskInfo.taskId, taskInfo);
+ }
+
+ @Override
+ public void onTaskVanished(TaskInfo taskInfo) {
+ mTasksMap.remove(taskInfo.taskId);
+ }
+
+ @Override
+ public void onTaskChanged(TaskInfo taskInfo) {
+ mTasksMap.put(taskInfo.taskId, taskInfo);
+ }
+
+ @Override
+ public void resetSurface(TaskInfo taskInfo, Transaction transaction) {
+ mResetTasks.add(taskInfo.taskId);
+ }
+
+ @Override
+ public void resetAllSurfaces(Transaction transaction) {
+ mTasksMap.values().forEach((t) -> mResetTasks.add(t.taskId));
+ }
+
+ @Override
+ public boolean hasActiveTasks() {
+ return mTasksMap.size() > 0;
+ }
+
+ public List<TaskInfo> getCurrentTasks() {
+ return new ArrayList<>(mTasksMap.values());
+ }
+ }
+}
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
new file mode 100644
index 000000000000..ab6ac949d4a3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -0,0 +1,417 @@
+/*
+ * 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.windowdecor;
+
+import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlBuilder;
+import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+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.verify;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManager.LayoutParams;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+
+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 org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Tests for {@link WindowDecoration}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:WindowDecorationTests
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class WindowDecorationTests extends ShellTestCase {
+ private static final int CAPTION_HEIGHT_DP = 32;
+ private static final int SHADOW_RADIUS_DP = 5;
+ private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400);
+ private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60);
+
+ private final Rect mOutsetsDp = new Rect();
+ private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult =
+ new WindowDecoration.RelayoutResult<>();
+
+ @Mock
+ private DisplayController mMockDisplayController;
+ @Mock
+ private ShellTaskOrganizer mMockShellTaskOrganizer;
+ @Mock
+ private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory;
+ @Mock
+ private SurfaceControlViewHost mMockSurfaceControlViewHost;
+ @Mock
+ private TestView mMockView;
+ @Mock
+ private WindowContainerTransaction mMockWindowContainerTransaction;
+
+ private final List<SurfaceControl.Transaction> mMockSurfaceControlTransactions =
+ new ArrayList<>();
+ private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>();
+ private SurfaceControl.Transaction mMockSurfaceControlStartT;
+ private SurfaceControl.Transaction mMockSurfaceControlFinishT;
+
+ @Before
+ public void setUp() {
+ mMockSurfaceControlStartT = createMockSurfaceControlTransaction();
+ mMockSurfaceControlFinishT = createMockSurfaceControlTransaction();
+
+ doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
+ .create(any(), any(), any());
+ }
+
+ @Test
+ public void testLayoutResultCalculation_invisibleTask() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final SurfaceControl decorContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder decorContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(decorContainerSurface);
+ mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
+ final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
+ createMockSurfaceControlBuilder(taskBackgroundSurface);
+ mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
+ final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder captionContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(captionContainerSurface);
+ mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder);
+
+ final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
+ new ActivityManager.TaskDescription.Builder()
+ .setBackgroundColor(Color.YELLOW);
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setTaskDescriptionBuilder(taskDescriptionBuilder)
+ .setBounds(TASK_BOUNDS)
+ .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+ .setVisible(false)
+ .build();
+ taskInfo.isFocused = false;
+ // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
+ // 64px.
+ taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+ mOutsetsDp.set(10, 20, 30, 40);
+
+ final SurfaceControl taskSurface = mock(SurfaceControl.class);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+
+ windowDecor.relayout(taskInfo);
+
+ verify(decorContainerSurfaceBuilder, never()).build();
+ verify(taskBackgroundSurfaceBuilder, never()).build();
+ verify(captionContainerSurfaceBuilder, never()).build();
+ verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
+
+ verify(mMockSurfaceControlFinishT).hide(taskSurface);
+
+ assertNull(mRelayoutResult.mRootView);
+ }
+
+ @Test
+ public void testLayoutResultCalculation_visibleFocusedTask() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final SurfaceControl decorContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder decorContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(decorContainerSurface);
+ mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
+ final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
+ createMockSurfaceControlBuilder(taskBackgroundSurface);
+ mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
+ final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder captionContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(captionContainerSurface);
+ mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder);
+
+ final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
+ new ActivityManager.TaskDescription.Builder()
+ .setBackgroundColor(Color.YELLOW);
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setTaskDescriptionBuilder(taskDescriptionBuilder)
+ .setBounds(TASK_BOUNDS)
+ .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+ .setVisible(true)
+ .build();
+ taskInfo.isFocused = true;
+ // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
+ // 64px.
+ taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+ mOutsetsDp.set(10, 20, 30, 40);
+
+ final SurfaceControl taskSurface = mock(SurfaceControl.class);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+
+ windowDecor.relayout(taskInfo);
+
+ verify(decorContainerSurfaceBuilder).setParent(taskSurface);
+ verify(decorContainerSurfaceBuilder).setContainerLayer();
+ verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true);
+ verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40);
+ verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220);
+
+ verify(taskBackgroundSurfaceBuilder).setParent(taskSurface);
+ verify(taskBackgroundSurfaceBuilder).setEffectLayer();
+ verify(mMockSurfaceControlStartT).setWindowCrop(taskBackgroundSurface, 300, 100);
+ verify(mMockSurfaceControlStartT)
+ .setColor(taskBackgroundSurface, new float[] {1.f, 1.f, 0.f});
+ verify(mMockSurfaceControlStartT).setShadowRadius(taskBackgroundSurface, 10);
+ verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface, -1);
+ verify(mMockSurfaceControlStartT).show(taskBackgroundSurface);
+
+ verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
+ verify(captionContainerSurfaceBuilder).setContainerLayer();
+ verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40);
+ verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
+ verify(mMockSurfaceControlStartT).show(captionContainerSurface);
+
+ verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
+ verify(mMockSurfaceControlViewHost)
+ .setView(same(mMockView),
+ 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)
+ .addRectInsetsProvider(taskInfo.token,
+ new Rect(100, 300, 400, 364),
+ new int[] { InsetsState.ITYPE_CAPTION_BAR });
+ }
+
+ verify(mMockSurfaceControlFinishT)
+ .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y);
+ verify(mMockSurfaceControlFinishT)
+ .setCrop(taskSurface, new Rect(-20, -40, 360, 180));
+ verify(mMockSurfaceControlStartT)
+ .show(taskSurface);
+
+ assertEquals(380, mRelayoutResult.mWidth);
+ assertEquals(220, mRelayoutResult.mHeight);
+ assertEquals(2, mRelayoutResult.mDensity, 0.f);
+ }
+
+ @Test
+ public void testLayoutResultCalculation_visibleFocusedTaskToInvisible() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final SurfaceControl decorContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder decorContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(decorContainerSurface);
+ mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
+ final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
+ createMockSurfaceControlBuilder(taskBackgroundSurface);
+ mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
+ final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder captionContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(captionContainerSurface);
+ mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder);
+
+ final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+ mMockSurfaceControlTransactions.add(t);
+
+ final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
+ new ActivityManager.TaskDescription.Builder()
+ .setBackgroundColor(Color.YELLOW);
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setTaskDescriptionBuilder(taskDescriptionBuilder)
+ .setBounds(TASK_BOUNDS)
+ .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+ .setVisible(true)
+ .build();
+ taskInfo.isFocused = true;
+ // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
+ // 64px.
+ taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+ mOutsetsDp.set(10, 20, 30, 40);
+
+ final SurfaceControl taskSurface = mock(SurfaceControl.class);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+
+ windowDecor.relayout(taskInfo);
+
+ verify(mMockSurfaceControlViewHost, never()).release();
+ verify(t, never()).apply();
+ verify(mMockWindowContainerTransaction, never())
+ .removeInsetsProvider(eq(taskInfo.token), any());
+
+ taskInfo.isVisible = false;
+ windowDecor.relayout(taskInfo);
+
+ final InOrder releaseOrder = inOrder(t, mMockSurfaceControlViewHost);
+ releaseOrder.verify(mMockSurfaceControlViewHost).release();
+ releaseOrder.verify(t).remove(captionContainerSurface);
+ releaseOrder.verify(t).remove(decorContainerSurface);
+ releaseOrder.verify(t).remove(taskBackgroundSurface);
+ releaseOrder.verify(t).apply();
+ verify(mMockWindowContainerTransaction).removeInsetsProvider(eq(taskInfo.token), any());
+ }
+
+ @Test
+ public void testNotCrashWhenDisplayAppearsAfterTask() {
+ doReturn(mock(Display.class)).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final int displayId = Display.DEFAULT_DISPLAY + 1;
+ final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
+ new ActivityManager.TaskDescription.Builder()
+ .setBackgroundColor(Color.BLACK);
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(displayId)
+ .setTaskDescriptionBuilder(taskDescriptionBuilder)
+ .setVisible(true)
+ .build();
+
+ final TestWindowDecoration windowDecor =
+ createWindowDecoration(taskInfo, new SurfaceControl());
+ windowDecor.relayout(taskInfo);
+
+ // It shouldn't show the window decoration when it can't obtain the display instance.
+ assertThat(mRelayoutResult.mRootView).isNull();
+
+ final ArgumentCaptor<DisplayController.OnDisplaysChangedListener> listenerArgumentCaptor =
+ ArgumentCaptor.forClass(DisplayController.OnDisplaysChangedListener.class);
+ verify(mMockDisplayController).addDisplayWindowListener(listenerArgumentCaptor.capture());
+ final DisplayController.OnDisplaysChangedListener listener =
+ listenerArgumentCaptor.getValue();
+
+ // Adding an irrelevant display shouldn't change the result.
+ listener.onDisplayAdded(Display.DEFAULT_DISPLAY);
+ assertThat(mRelayoutResult.mRootView).isNull();
+
+ final Display mockDisplay = mock(Display.class);
+ doReturn(mockDisplay).when(mMockDisplayController).getDisplay(displayId);
+
+ listener.onDisplayAdded(displayId);
+
+ // The listener should be removed when the display shows up.
+ verify(mMockDisplayController).removeDisplayWindowListener(same(listener));
+
+ assertThat(mRelayoutResult.mRootView).isSameInstanceAs(mMockView);
+ verify(mMockSurfaceControlViewHostFactory).create(any(), eq(mockDisplay), any());
+ verify(mMockSurfaceControlViewHost).setView(same(mMockView), any());
+ }
+
+ private TestWindowDecoration createWindowDecoration(
+ ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
+ return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
+ taskInfo, testSurface,
+ new MockObjectSupplier<>(mMockSurfaceControlBuilders,
+ () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))),
+ new MockObjectSupplier<>(mMockSurfaceControlTransactions,
+ () -> mock(SurfaceControl.Transaction.class)),
+ () -> mMockWindowContainerTransaction, mMockSurfaceControlViewHostFactory);
+ }
+
+ private class MockObjectSupplier<T> implements Supplier<T> {
+ private final List<T> mObjects;
+ private final Supplier<T> mDefaultSupplier;
+ private int mNumOfCalls = 0;
+
+ private MockObjectSupplier(List<T> objects, Supplier<T> defaultSupplier) {
+ mObjects = objects;
+ mDefaultSupplier = defaultSupplier;
+ }
+
+ @Override
+ public T get() {
+ final T mock = mNumOfCalls < mObjects.size()
+ ? mObjects.get(mNumOfCalls) : mDefaultSupplier.get();
+ ++mNumOfCalls;
+ return mock;
+ }
+ }
+
+ private static class TestView extends View implements TaskFocusStateConsumer {
+ private TestView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setTaskFocusState(boolean focused) {}
+ }
+
+ private class TestWindowDecoration extends WindowDecoration<TestView> {
+ TestWindowDecoration(Context context, DisplayController displayController,
+ ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
+ Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
+ Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
+ SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
+ super(context, displayController, taskOrganizer, taskInfo, taskSurface,
+ surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
+ windowContainerTransactionSupplier, surfaceControlViewHostFactory);
+ }
+
+ @Override
+ void relayout(ActivityManager.RunningTaskInfo taskInfo) {
+ relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP,
+ mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT,
+ mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult);
+ }
+ }
+}
diff --git a/libs/dream/lowlight/Android.bp b/libs/dream/lowlight/Android.bp
new file mode 100644
index 000000000000..5b5b0f07cabd
--- /dev/null
+++ b/libs/dream/lowlight/Android.bp
@@ -0,0 +1,47 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "low_light_dream_lib-sources",
+ srcs: [
+ "src/**/*.java",
+ ],
+ path: "src",
+}
+
+android_library {
+ name: "LowLightDreamLib",
+ srcs: [
+ ":low_light_dream_lib-sources",
+ ],
+ resource_dirs: [
+ "res",
+ ],
+ static_libs: [
+ "androidx.arch.core_core-runtime",
+ "dagger2",
+ "jsr330",
+ ],
+ manifest: "AndroidManifest.xml",
+ plugins: ["dagger2-compiler"],
+}
diff --git a/libs/WindowManager/Shell/res/color/unfold_transition_background.xml b/libs/dream/lowlight/AndroidManifest.xml
index 63289a3f75d9..a8d952699943 100644
--- a/libs/WindowManager/Shell/res/color/unfold_transition_background.xml
+++ b/libs/dream/lowlight/AndroidManifest.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 The Android Open Source Project
+<!--
+ 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.
@@ -13,7 +14,5 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <!-- Matches taskbar color -->
- <item android:color="@android:color/system_neutral2_500" android:lStar="35" />
-</selector>
+
+<manifest package="com.android.dream.lowlight" />
diff --git a/libs/dream/lowlight/res/values/config.xml b/libs/dream/lowlight/res/values/config.xml
new file mode 100644
index 000000000000..70fe0738a6f4
--- /dev/null
+++ b/libs/dream/lowlight/res/values/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<resources>
+ <!-- The dream component used when the device is low light environment. -->
+ <string translatable="false" name="config_lowLightDreamComponent"/>
+</resources>
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java
new file mode 100644
index 000000000000..5ecec4ddd1ad
--- /dev/null
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java
@@ -0,0 +1,117 @@
+/*
+ * 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.dream.lowlight;
+
+import static com.android.dream.lowlight.dagger.LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT;
+
+import android.annotation.IntDef;
+import android.annotation.RequiresPermission;
+import android.app.DreamManager;
+import android.content.ComponentName;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * Maintains the ambient light mode of the environment the device is in, and sets a low light dream
+ * component, if present, as the system dream when the ambient light mode is low light.
+ *
+ * @hide
+ */
+public final class LowLightDreamManager {
+ private static final String TAG = "LowLightDreamManager";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "AMBIENT_LIGHT_MODE_" }, value = {
+ AMBIENT_LIGHT_MODE_UNKNOWN,
+ AMBIENT_LIGHT_MODE_REGULAR,
+ AMBIENT_LIGHT_MODE_LOW_LIGHT
+ })
+ public @interface AmbientLightMode {}
+
+ /**
+ * Constant for ambient light mode being unknown.
+ * @hide
+ */
+ public static final int AMBIENT_LIGHT_MODE_UNKNOWN = 0;
+
+ /**
+ * Constant for ambient light mode being regular / bright.
+ * @hide
+ */
+ public static final int AMBIENT_LIGHT_MODE_REGULAR = 1;
+
+ /**
+ * Constant for ambient light mode being low light / dim.
+ * @hide
+ */
+ public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2;
+
+ private final DreamManager mDreamManager;
+
+ @Nullable
+ private final ComponentName mLowLightDreamComponent;
+
+ private int mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN;
+
+ @Inject
+ public LowLightDreamManager(
+ DreamManager dreamManager,
+ @Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) {
+ mDreamManager = dreamManager;
+ mLowLightDreamComponent = lowLightDreamComponent;
+ }
+
+ /**
+ * Sets the current ambient light mode.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE)
+ public void setAmbientLightMode(@AmbientLightMode int ambientLightMode) {
+ if (mLowLightDreamComponent == null) {
+ if (DEBUG) {
+ Log.d(TAG, "ignore ambient light mode change because low light dream component "
+ + "is empty");
+ }
+ return;
+ }
+
+ if (mAmbientLightMode == ambientLightMode) {
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "ambient light mode changed from " + mAmbientLightMode + " to "
+ + ambientLightMode);
+ }
+
+ mAmbientLightMode = ambientLightMode;
+
+ mDreamManager.setSystemDreamComponent(mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT
+ ? mLowLightDreamComponent : null);
+ }
+}
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java
new file mode 100644
index 000000000000..c183a04cb2f9
--- /dev/null
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java
@@ -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.
+ */
+
+package com.android.dream.lowlight.dagger;
+
+import android.app.DreamManager;
+import android.content.ComponentName;
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+
+import com.android.dream.lowlight.R;
+
+import javax.inject.Named;
+
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Dagger module for low light dream.
+ *
+ * @hide
+ */
+@Module
+public interface LowLightDreamModule {
+ String LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component";
+
+ /**
+ * Provides dream manager.
+ */
+ @Provides
+ static DreamManager providesDreamManager(Context context) {
+ return context.getSystemService(DreamManager.class);
+ }
+
+ /**
+ * Provides the component name of the low light dream, or null if not configured.
+ */
+ @Provides
+ @Named(LOW_LIGHT_DREAM_COMPONENT)
+ @Nullable
+ static ComponentName providesLowLightDreamComponent(Context context) {
+ final String lowLightDreamComponent = context.getResources().getString(
+ R.string.config_lowLightDreamComponent);
+ return lowLightDreamComponent.isEmpty() ? null
+ : ComponentName.unflattenFromString(lowLightDreamComponent);
+ }
+}
diff --git a/libs/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp
new file mode 100644
index 000000000000..bd6f05eabac5
--- /dev/null
+++ b/libs/dream/lowlight/tests/Android.bp
@@ -0,0 +1,45 @@
+// 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 {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "LowLightDreamTests",
+ srcs: [
+ "**/*.java",
+ ],
+ static_libs: [
+ "LowLightDreamLib",
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "frameworks-base-testutils",
+ "junit",
+ "mockito-target-extended-minus-junit4",
+ "platform-test-annotations",
+ "testables",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.mock",
+ "android.test.base",
+ "android.test.runner",
+ ],
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+}
diff --git a/libs/dream/lowlight/tests/AndroidManifest.xml b/libs/dream/lowlight/tests/AndroidManifest.xml
new file mode 100644
index 000000000000..abb71fb53b49
--- /dev/null
+++ b/libs/dream/lowlight/tests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.dream.lowlight.tests">
+
+ <application android:debuggable="true" android:largeHeap="true">
+ <uses-library android:name="android.test.mock" />
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:label="Tests for LowLightDreamLib"
+ android:targetPackage="com.android.dream.lowlight.tests">
+ </instrumentation>
+
+</manifest>
diff --git a/libs/dream/lowlight/tests/AndroidTest.xml b/libs/dream/lowlight/tests/AndroidTest.xml
new file mode 100644
index 000000000000..10800333add2
--- /dev/null
+++ b/libs/dream/lowlight/tests/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Tests for LowLightDreamLib">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="LowLightDreamTests.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="framework-base-presubmit" />
+ <option name="test-tag" value="LowLightDreamLibTests" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.dream.lowlight.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java
new file mode 100644
index 000000000000..91a170f7ae14
--- /dev/null
+++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.dream.lowlight;
+
+import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT;
+import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR;
+import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.app.DreamManager;
+import android.content.ComponentName;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class LowLightDreamManagerTest {
+ @Mock
+ private DreamManager mDreamManager;
+
+ @Mock
+ private ComponentName mDreamComponent;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void setAmbientLightMode_lowLight_setSystemDream() {
+ final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
+ mDreamComponent);
+
+ lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
+
+ verify(mDreamManager).setSystemDreamComponent(mDreamComponent);
+ }
+
+ @Test
+ public void setAmbientLightMode_regularLight_clearSystemDream() {
+ final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
+ mDreamComponent);
+
+ lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
+
+ verify(mDreamManager).setSystemDreamComponent(null);
+ }
+
+ @Test
+ public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() {
+ final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
+ mDreamComponent);
+
+ // Set to low light first.
+ lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
+ clearInvocations(mDreamManager);
+
+ // Return to default unknown mode.
+ lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN);
+
+ verify(mDreamManager).setSystemDreamComponent(null);
+ }
+
+ @Test
+ public void setAmbientLightMode_dreamComponentNotSet_doNothing() {
+ final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
+ null /*dream component*/);
+
+ lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
+
+ verify(mDreamManager, never()).setSystemDreamComponent(any());
+ }
+}
diff --git a/libs/hwui/FrameInfoVisualizer.cpp b/libs/hwui/FrameInfoVisualizer.cpp
index 3a8e559f6d7e..687e4dd324d3 100644
--- a/libs/hwui/FrameInfoVisualizer.cpp
+++ b/libs/hwui/FrameInfoVisualizer.cpp
@@ -179,7 +179,7 @@ void FrameInfoVisualizer::initializeRects(const int baseline, const int width) {
void FrameInfoVisualizer::nextBarSegment(FrameInfoIndex start, FrameInfoIndex end) {
int fast_i = (mNumFastRects - 1) * 4;
int janky_i = (mNumJankyRects - 1) * 4;
- ;
+
for (size_t fi = 0; fi < mFrameSource.size(); fi++) {
if (mFrameSource[fi][FrameInfoIndex::Flags] & FrameInfoFlags::SkippedFrame) {
continue;
diff --git a/libs/hwui/JankTracker.cpp b/libs/hwui/JankTracker.cpp
index 1e5be6c3eed7..4b0ddd2fa2ef 100644
--- a/libs/hwui/JankTracker.cpp
+++ b/libs/hwui/JankTracker.cpp
@@ -201,8 +201,9 @@ void JankTracker::finishFrame(FrameInfo& frame, std::unique_ptr<FrameMetricsRepo
// If we are in triple buffering, we have enough buffers in queue to sustain a single frame
// drop without jank, so adjust the frame interval to the deadline.
if (isTripleBuffered) {
- deadline += frameInterval;
- frame.set(FrameInfoIndex::FrameDeadline) += frameInterval;
+ int64_t originalDeadlineDuration = deadline - frame[FrameInfoIndex::IntendedVsync];
+ deadline = mNextFrameStartUnstuffed + originalDeadlineDuration;
+ frame.set(FrameInfoIndex::FrameDeadline) = deadline;
}
// If we hit the deadline, cool!
diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp
index 0ef80ee10708..132234b38003 100644
--- a/libs/hwui/jni/android_graphics_Canvas.cpp
+++ b/libs/hwui/jni/android_graphics_Canvas.cpp
@@ -407,14 +407,28 @@ static void drawVertices(JNIEnv* env, jobject, jlong canvasHandle,
indices = (const uint16_t*)(indexA.ptr() + indexIndex);
}
- SkVertices::VertexMode mode = static_cast<SkVertices::VertexMode>(modeHandle);
+ SkVertices::VertexMode vertexMode = static_cast<SkVertices::VertexMode>(modeHandle);
const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
- get_canvas(canvasHandle)->drawVertices(SkVertices::MakeCopy(mode, vertexCount,
- reinterpret_cast<const SkPoint*>(verts),
- reinterpret_cast<const SkPoint*>(texs),
- reinterpret_cast<const SkColor*>(colors),
- indexCount, indices).get(),
- SkBlendMode::kModulate, *paint);
+
+ // Preserve legacy Skia behavior: ignore the shader if there are no texs set.
+ Paint noShaderPaint;
+ if (jtexs == NULL) {
+ noShaderPaint = Paint(*paint);
+ noShaderPaint.setShader(nullptr);
+ paint = &noShaderPaint;
+ }
+ // Since https://skia-review.googlesource.com/c/skia/+/473676, Skia will blend paint and vertex
+ // colors when no shader is provided. This ternary uses kDst to mimic the old behavior of
+ // ignoring the paint and using the vertex colors directly when no shader is provided.
+ SkBlendMode blendMode = paint->getShader() ? SkBlendMode::kModulate : SkBlendMode::kDst;
+
+ get_canvas(canvasHandle)
+ ->drawVertices(SkVertices::MakeCopy(
+ vertexMode, vertexCount, reinterpret_cast<const SkPoint*>(verts),
+ reinterpret_cast<const SkPoint*>(texs),
+ reinterpret_cast<const SkColor*>(colors), indexCount, indices)
+ .get(),
+ blendMode, *paint);
}
static void drawNinePatch(JNIEnv* env, jobject, jlong canvasHandle, jlong bitmapHandle,
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
index 2aca41e41905..8e350d5012a5 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
@@ -53,8 +53,12 @@ SkiaOpenGLPipeline::~SkiaOpenGLPipeline() {
}
MakeCurrentResult SkiaOpenGLPipeline::makeCurrent() {
- // TODO: Figure out why this workaround is needed, see b/13913604
- // In the meantime this matches the behavior of GLRenderer, so it is not a regression
+ // In case the surface was destroyed (e.g. a previous trimMemory call) we
+ // need to recreate it here.
+ if (!isSurfaceReady() && mNativeWindow) {
+ setSurface(mNativeWindow.get(), mSwapBehavior);
+ }
+
EGLint error = 0;
if (!mEglManager.makeCurrent(mEglSurface, &error)) {
return MakeCurrentResult::AlreadyCurrent;
@@ -166,6 +170,9 @@ void SkiaOpenGLPipeline::onStop() {
}
bool SkiaOpenGLPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) {
+ mNativeWindow = surface;
+ mSwapBehavior = swapBehavior;
+
if (mEglSurface != EGL_NO_SURFACE) {
mEglManager.destroySurface(mEglSurface);
mEglSurface = EGL_NO_SURFACE;
@@ -182,7 +189,8 @@ bool SkiaOpenGLPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBeh
if (mEglSurface != EGL_NO_SURFACE) {
const bool preserveBuffer = (swapBehavior != SwapBehavior::kSwap_discardBuffer);
- mBufferPreserved = mEglManager.setPreserveBuffer(mEglSurface, preserveBuffer);
+ const bool isPreserved = mEglManager.setPreserveBuffer(mEglSurface, preserveBuffer);
+ ALOGE_IF(preserveBuffer != isPreserved, "Unable to match the desired swap behavior.");
return true;
}
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
index 186998a01745..a80c613697f2 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
@@ -61,7 +61,8 @@ protected:
private:
renderthread::EglManager& mEglManager;
EGLSurface mEglSurface = EGL_NO_SURFACE;
- bool mBufferPreserved = false;
+ sp<ANativeWindow> mNativeWindow;
+ renderthread::SwapBehavior mSwapBehavior = renderthread::SwapBehavior::kSwap_discardBuffer;
};
} /* namespace skiapipeline */
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
index 905d46e58014..cc2565d88d5e 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
@@ -55,7 +55,12 @@ VulkanManager& SkiaVulkanPipeline::vulkanManager() {
}
MakeCurrentResult SkiaVulkanPipeline::makeCurrent() {
- return MakeCurrentResult::AlreadyCurrent;
+ // In case the surface was destroyed (e.g. a previous trimMemory call) we
+ // need to recreate it here.
+ if (!isSurfaceReady() && mNativeWindow) {
+ setSurface(mNativeWindow.get(), SwapBehavior::kSwap_default);
+ }
+ return isContextReady() ? MakeCurrentResult::AlreadyCurrent : MakeCurrentResult::Failed;
}
Frame SkiaVulkanPipeline::getFrame() {
@@ -130,7 +135,11 @@ DeferredLayerUpdater* SkiaVulkanPipeline::createTextureLayer() {
void SkiaVulkanPipeline::onStop() {}
-bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) {
+// We can safely ignore the swap behavior because VkManager will always operate
+// in a mode equivalent to EGLManager::SwapBehavior::kBufferAge
+bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior /*swapBehavior*/) {
+ mNativeWindow = surface;
+
if (mVkSurface) {
vulkanManager().destroySurface(mVkSurface);
mVkSurface = nullptr;
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
index ada6af67d4a0..a6e685d08aeb 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
@@ -61,6 +61,7 @@ private:
renderthread::VulkanManager& vulkanManager();
renderthread::VulkanSurface* mVkSurface = nullptr;
+ sp<ANativeWindow> mNativeWindow;
};
} /* namespace skiapipeline */
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 976117b9bbd4..75d3ff7753cb 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -512,9 +512,19 @@ nsecs_t CanvasContext::draw() {
ATRACE_FORMAT("Drawing " RECT_STRING, SK_RECT_ARGS(dirty));
- const auto drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry,
- &mLayerUpdateQueue, mContentDrawBounds, mOpaque,
- mLightInfo, mRenderNodes, &(profiler()));
+ IRenderPipeline::DrawResult drawResult;
+ {
+ // FrameInfoVisualizer accesses the frame events, which cannot be mutated mid-draw
+ // or it can lead to memory corruption.
+ // This lock is overly broad, but it's the quickest fix since this mutex is otherwise
+ // not visible to IRenderPipeline much less FrameInfoVisualizer. And since this is
+ // the thread we're primarily concerned about being responsive, this being too broad
+ // shouldn't pose a performance issue.
+ std::scoped_lock lock(mFrameMetricsReporterMutex);
+ drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry,
+ &mLayerUpdateQueue, mContentDrawBounds, mOpaque,
+ mLightInfo, mRenderNodes, &(profiler()));
+ }
uint64_t frameCompleteNr = getFrameNumber();
@@ -754,11 +764,11 @@ void CanvasContext::onSurfaceStatsAvailable(void* context, int32_t surfaceContro
FrameInfo* frameInfo = instance->getFrameInfoFromLast4(frameNumber, surfaceControlId);
if (frameInfo != nullptr) {
+ std::scoped_lock lock(instance->mFrameMetricsReporterMutex);
frameInfo->set(FrameInfoIndex::FrameCompleted) = std::max(gpuCompleteTime,
frameInfo->get(FrameInfoIndex::SwapBuffersCompleted));
frameInfo->set(FrameInfoIndex::GpuCompleted) = std::max(
gpuCompleteTime, frameInfo->get(FrameInfoIndex::CommandSubmissionCompleted));
- std::scoped_lock lock(instance->mFrameMetricsReporterMutex);
instance->mJankTracker.finishFrame(*frameInfo, instance->mFrameMetricsReporter, frameNumber,
surfaceControlId);
}
diff --git a/libs/hwui/tests/unit/JankTrackerTests.cpp b/libs/hwui/tests/unit/JankTrackerTests.cpp
index 5b397de36a86..b67e419e7d4a 100644
--- a/libs/hwui/tests/unit/JankTrackerTests.cpp
+++ b/libs/hwui/tests/unit/JankTrackerTests.cpp
@@ -195,3 +195,68 @@ TEST(JankTracker, doubleStuffedThenPauseThenJank) {
ASSERT_EQ(3, container.get()->totalFrameCount());
ASSERT_EQ(2, container.get()->jankFrameCount());
}
+
+TEST(JankTracker, doubleStuffedTwoIntervalBehind) {
+ std::mutex mutex;
+ ProfileDataContainer container(mutex);
+ JankTracker jankTracker(&container);
+ std::unique_ptr<FrameMetricsReporter> reporter = std::make_unique<FrameMetricsReporter>();
+
+ uint64_t frameNumber = 0;
+ uint32_t surfaceId = 0;
+
+ // First frame janks
+ FrameInfo* info = jankTracker.startFrame();
+ info->set(FrameInfoIndex::IntendedVsync) = 100_ms;
+ info->set(FrameInfoIndex::Vsync) = 101_ms;
+ info->set(FrameInfoIndex::SwapBuffersCompleted) = 107_ms;
+ info->set(FrameInfoIndex::GpuCompleted) = 117_ms;
+ info->set(FrameInfoIndex::FrameCompleted) = 117_ms;
+ info->set(FrameInfoIndex::FrameInterval) = 16_ms;
+ info->set(FrameInfoIndex::FrameDeadline) = 116_ms;
+ jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId);
+
+ ASSERT_EQ(1, container.get()->jankFrameCount());
+
+ // Second frame is long, but doesn't jank because double-stuffed.
+ // Second frame duration is between 1*interval ~ 2*interval
+ info = jankTracker.startFrame();
+ info->set(FrameInfoIndex::IntendedVsync) = 116_ms;
+ info->set(FrameInfoIndex::Vsync) = 116_ms;
+ info->set(FrameInfoIndex::SwapBuffersCompleted) = 129_ms;
+ info->set(FrameInfoIndex::GpuCompleted) = 133_ms;
+ info->set(FrameInfoIndex::FrameCompleted) = 133_ms;
+ info->set(FrameInfoIndex::FrameInterval) = 16_ms;
+ info->set(FrameInfoIndex::FrameDeadline) = 132_ms;
+ jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId);
+
+ ASSERT_EQ(1, container.get()->jankFrameCount());
+
+ // Third frame is even longer, cause a jank
+ // Third frame duration is between 2*interval ~ 3*interval
+ info = jankTracker.startFrame();
+ info->set(FrameInfoIndex::IntendedVsync) = 132_ms;
+ info->set(FrameInfoIndex::Vsync) = 132_ms;
+ info->set(FrameInfoIndex::SwapBuffersCompleted) = 160_ms;
+ info->set(FrameInfoIndex::GpuCompleted) = 165_ms;
+ info->set(FrameInfoIndex::FrameCompleted) = 165_ms;
+ info->set(FrameInfoIndex::FrameInterval) = 16_ms;
+ info->set(FrameInfoIndex::FrameDeadline) = 148_ms;
+ jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId);
+
+ ASSERT_EQ(2, container.get()->jankFrameCount());
+
+ // 4th frame is double-stuffed with a 2 * interval latency
+ // 4th frame duration is between 2*interval ~ 3*interval
+ info = jankTracker.startFrame();
+ info->set(FrameInfoIndex::IntendedVsync) = 148_ms;
+ info->set(FrameInfoIndex::Vsync) = 148_ms;
+ info->set(FrameInfoIndex::SwapBuffersCompleted) = 170_ms;
+ info->set(FrameInfoIndex::GpuCompleted) = 181_ms;
+ info->set(FrameInfoIndex::FrameCompleted) = 181_ms;
+ info->set(FrameInfoIndex::FrameInterval) = 16_ms;
+ info->set(FrameInfoIndex::FrameDeadline) = 164_ms;
+ jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId);
+
+ ASSERT_EQ(2, container.get()->jankFrameCount());
+}
diff --git a/libs/hwui/tests/unit/SkiaPipelineTests.cpp b/libs/hwui/tests/unit/SkiaPipelineTests.cpp
index 60ae6044cd5b..7419f8fd89f1 100644
--- a/libs/hwui/tests/unit/SkiaPipelineTests.cpp
+++ b/libs/hwui/tests/unit/SkiaPipelineTests.cpp
@@ -404,7 +404,9 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaPipeline, context_lost) {
EXPECT_TRUE(pipeline->isSurfaceReady());
renderThread.destroyRenderingContext();
EXPECT_FALSE(pipeline->isSurfaceReady());
- LOG_ALWAYS_FATAL_IF(pipeline->isSurfaceReady());
+
+ pipeline->makeCurrent();
+ EXPECT_TRUE(pipeline->isSurfaceReady());
}
RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaPipeline, pictureCallback) {