summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java10
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java48
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java305
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java82
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java774
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java353
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationController.java61
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationRunner.java93
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java246
-rw-r--r--libs/WindowManager/Jetpack/window-extensions-release.aarbin7613 -> 17281 bytes
-rw-r--r--libs/WindowManager/Shell/res/color/split_divider_background.xml19
-rw-r--r--libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml2
-rw-r--r--libs/WindowManager/Shell/res/drawable/split_rounded_bottom.xml30
-rw-r--r--libs/WindowManager/Shell/res/drawable/split_rounded_left.xml30
-rw-r--r--libs/WindowManager/Shell/res/drawable/split_rounded_right.xml30
-rw-r--r--libs/WindowManager/Shell/res/drawable/split_rounded_top.xml26
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_manage_button.xml10
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml30
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml1
-rw-r--r--libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml19
-rw-r--r--libs/WindowManager/Shell/res/layout/docked_stack_divider.xml2
-rw-r--r--libs/WindowManager/Shell/res/layout/split_divider.xml30
-rw-r--r--libs/WindowManager/Shell/res/layout/split_outline.xml26
-rw-r--r--libs/WindowManager/Shell/res/values-land/dimens.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-land/styles.xml15
-rw-r--r--libs/WindowManager/Shell/res/values/colors.xml1
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml31
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java67
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java285
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java254
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt81
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt60
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java165
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java83
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java92
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java265
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java86
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java69
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java80
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java418
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java132
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java147
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java167
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java177
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java97
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java57
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java127
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java62
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java76
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java41
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java273
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java324
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java489
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java491
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java124
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java52
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java485
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java89
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java71
-rw-r--r--libs/WindowManager/Shell/tests/flicker/Android.bp8
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt63
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt8
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt17
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt25
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt15
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt36
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt24
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt43
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt45
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt18
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt13
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt8
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt44
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt27
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt56
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt22
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt24
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt58
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt53
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt111
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt64
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt80
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt64
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt62
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt26
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt53
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt48
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt39
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt37
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt60
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt52
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt26
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt16
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt16
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt29
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt18
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt8
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt10
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt52
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt26
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt28
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt27
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt41
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt9
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java15
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java32
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java21
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java193
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java59
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java85
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java23
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java275
-rw-r--r--libs/hwui/apex/android_matrix.cpp7
-rw-r--r--libs/hwui/apex/include/android/graphics/matrix.h10
-rw-r--r--libs/hwui/jni/android_graphics_Matrix.cpp7
-rw-r--r--libs/hwui/jni/android_graphics_Matrix.h3
-rw-r--r--libs/hwui/libhwui.map.txt1
-rw-r--r--libs/input/SpriteController.cpp3
179 files changed, 9064 insertions, 1911 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java
index b7a60392c512..ce4e10364ba2 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java
@@ -18,6 +18,10 @@ package androidx.window.extensions;
import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
+import androidx.window.extensions.organizer.EmbeddingExtensionImpl;
+
/**
* Provider class that will instantiate the library implementation. It must be included in the
* vendor library, and the vendor implementation must match the signature of this class.
@@ -31,6 +35,12 @@ public class ExtensionProvider {
return new SampleExtensionImpl(context);
}
+ /** Provides a reference implementation of {@link ActivityEmbeddingComponent}. */
+ public static ActivityEmbeddingComponent getActivityEmbeddingExtensionImpl(
+ @NonNull Context context) {
+ return new EmbeddingExtensionImpl();
+ }
+
/**
* The support library will use this method to check API version compatibility.
* @return API version string in MAJOR.MINOR.PATCH-description format.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java
new file mode 100644
index 000000000000..9a8961f1d460
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java
@@ -0,0 +1,48 @@
+/*
+ * 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 androidx.window.extensions.organizer;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
+import androidx.window.extensions.embedding.EmbeddingRule;
+import androidx.window.extensions.embedding.SplitInfo;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/**
+ * Reference implementation of the activity embedding interface defined in WM Jetpack.
+ */
+public class EmbeddingExtensionImpl implements ActivityEmbeddingComponent {
+
+ private final SplitController mSplitController;
+
+ public EmbeddingExtensionImpl() {
+ mSplitController = new SplitController();
+ }
+
+ @Override
+ public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) {
+ mSplitController.setEmbeddingRules(rules);
+ }
+
+ @Override
+ public void setEmbeddingCallback(@NonNull Consumer<List<SplitInfo>> consumer) {
+ mSplitController.setEmbeddingCallback(consumer);
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java
new file mode 100644
index 000000000000..9212a0f5e6b9
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java
@@ -0,0 +1,305 @@
+/*
+ * 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 androidx.window.extensions.organizer;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+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;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.window.TaskFragmentAppearedInfo;
+import android.window.TaskFragmentCreationParams;
+import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentOrganizer;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.embedding.SplitRule;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Platform default Extensions implementation of {@link TaskFragmentOrganizer} to organize
+ * task fragments.
+ *
+ * All calls into methods of this class are expected to be on the UI thread.
+ */
+class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
+
+ /** Mapping from the client assigned unique token to the {@link TaskFragmentInfo}. */
+ private final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>();
+
+ /** Mapping from the client assigned unique token to the TaskFragment {@link SurfaceControl}. */
+ private final Map<IBinder, SurfaceControl> mFragmentLeashes = new ArrayMap<>();
+
+ /**
+ * Mapping from the client assigned unique token to the TaskFragment parent
+ * {@link Configuration}.
+ */
+ final Map<IBinder, Configuration> mFragmentParentConfigs = new ArrayMap<>();
+
+ private final TaskFragmentCallback mCallback;
+ private TaskFragmentAnimationController mAnimationController;
+
+ /**
+ * Callback that notifies the controller about changes to task fragments.
+ */
+ interface TaskFragmentCallback {
+ void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo);
+ void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo);
+ void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo);
+ void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken,
+ @NonNull Configuration parentConfig);
+ }
+
+ /**
+ * @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) {
+ super(executor);
+ mCallback = callback;
+ }
+
+ @Override
+ public void registerOrganizer() {
+ if (mAnimationController != null) {
+ throw new IllegalStateException("Must unregister the organizer before re-register.");
+ }
+ super.registerOrganizer();
+ mAnimationController = new TaskFragmentAnimationController(this);
+ mAnimationController.registerRemoteAnimations();
+ }
+
+ @Override
+ public void unregisterOrganizer() {
+ if (mAnimationController != null) {
+ mAnimationController.unregisterRemoteAnimations();
+ mAnimationController = null;
+ }
+ super.unregisterOrganizer();
+ }
+
+ /**
+ * Starts a new Activity and puts it into split with an existing Activity side-by-side.
+ * @param launchingFragmentToken token for the launching TaskFragment. If it exists, it will
+ * be resized based on {@param launchingFragmentBounds}.
+ * Otherwise, we will create a new TaskFragment with the given
+ * token for the {@param launchingActivity}.
+ * @param launchingFragmentBounds the initial bounds for the launching TaskFragment.
+ * @param launchingActivity the Activity to put on the left hand side of the split as the
+ * primary.
+ * @param secondaryFragmentToken token to create the secondary TaskFragment with.
+ * @param secondaryFragmentBounds the initial bounds for the secondary TaskFragment
+ * @param activityIntent Intent to start the secondary Activity with.
+ * @param activityOptions ActivityOptions to start the secondary Activity with.
+ */
+ void startActivityToSide(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingFragmentBounds,
+ @NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken,
+ @NonNull Rect secondaryFragmentBounds, @NonNull Intent activityIntent,
+ @Nullable Bundle activityOptions, @NonNull SplitRule rule) {
+ final IBinder ownerToken = launchingActivity.getActivityToken();
+
+ // Create or resize the launching TaskFragment.
+ if (mFragmentInfos.containsKey(launchingFragmentToken)) {
+ resizeTaskFragment(wct, launchingFragmentToken, launchingFragmentBounds);
+ } else {
+ createTaskFragmentAndReparentActivity(wct, launchingFragmentToken, ownerToken,
+ launchingFragmentBounds, WINDOWING_MODE_MULTI_WINDOW, launchingActivity);
+ }
+
+ // Create a TaskFragment for the secondary activity.
+ createTaskFragmentAndStartActivity(wct, secondaryFragmentToken, ownerToken,
+ secondaryFragmentBounds, WINDOWING_MODE_MULTI_WINDOW, activityIntent,
+ activityOptions);
+
+ // Set adjacent to each other so that the containers below will be invisible.
+ setAdjacentTaskFragments(wct, launchingFragmentToken, secondaryFragmentToken, rule);
+ }
+
+ /**
+ * Expands an existing TaskFragment to fill parent.
+ * @param wct WindowContainerTransaction in which the task fragment should be resized.
+ * @param fragmentToken token of an existing TaskFragment.
+ */
+ void expandTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+ resizeTaskFragment(wct, fragmentToken, new Rect());
+ setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */);
+ }
+
+ /**
+ * 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();
+ 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) {
+ final TaskFragmentCreationParams fragmentOptions =
+ createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode);
+ wct.createTaskFragment(fragmentOptions);
+ }
+
+ /**
+ * @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) {
+ createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
+ wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
+ }
+
+ /**
+ * @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,
+ @Nullable Bundle activityOptions) {
+ createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
+ wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions);
+ }
+
+ void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) {
+ WindowContainerTransaction.TaskFragmentAdjacentOptions adjacentOptions = null;
+ final boolean finishSecondaryWithPrimary =
+ splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
+ final boolean finishPrimaryWithSecondary =
+ splitRule != null && SplitContainer.shouldFinishPrimaryWithSecondary(splitRule);
+ if (finishSecondaryWithPrimary || finishPrimaryWithSecondary) {
+ adjacentOptions = new WindowContainerTransaction.TaskFragmentAdjacentOptions();
+ adjacentOptions.setDelayPrimaryLastActivityRemoval(finishSecondaryWithPrimary);
+ adjacentOptions.setDelaySecondaryLastActivityRemoval(finishPrimaryWithSecondary);
+ }
+ wct.setAdjacentTaskFragments(primary, secondary, adjacentOptions);
+ }
+
+ TaskFragmentCreationParams createFragmentOptions(IBinder fragmentToken, IBinder ownerToken,
+ Rect bounds, @WindowingMode int windowingMode) {
+ if (mFragmentInfos.containsKey(fragmentToken)) {
+ throw new IllegalArgumentException(
+ "There is an existing TaskFragment with fragmentToken=" + fragmentToken);
+ }
+
+ return new TaskFragmentCreationParams.Builder(
+ getOrganizerToken(),
+ fragmentToken,
+ ownerToken)
+ .setInitialBounds(bounds)
+ .setWindowingMode(windowingMode)
+ .build();
+ }
+
+ void resizeTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
+ @Nullable Rect bounds) {
+ if (!mFragmentInfos.containsKey(fragmentToken)) {
+ throw new IllegalArgumentException(
+ "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
+ }
+ if (bounds == null) {
+ bounds = new Rect();
+ }
+ wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds);
+ }
+
+ void deleteTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+ if (!mFragmentInfos.containsKey(fragmentToken)) {
+ throw new IllegalArgumentException(
+ "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
+ }
+ wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken());
+ }
+
+ @Override
+ public void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo) {
+ final TaskFragmentInfo info = taskFragmentAppearedInfo.getTaskFragmentInfo();
+ final IBinder fragmentToken = info.getFragmentToken();
+ final SurfaceControl leash = taskFragmentAppearedInfo.getLeash();
+ mFragmentInfos.put(fragmentToken, info);
+ mFragmentLeashes.put(fragmentToken, leash);
+
+ if (mCallback != null) {
+ mCallback.onTaskFragmentAppeared(taskFragmentAppearedInfo);
+ }
+ }
+
+ @Override
+ public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
+ final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
+ mFragmentInfos.put(fragmentToken, taskFragmentInfo);
+
+ if (mCallback != null) {
+ mCallback.onTaskFragmentInfoChanged(taskFragmentInfo);
+ }
+ }
+
+ @Override
+ public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+ mFragmentInfos.remove(taskFragmentInfo.getFragmentToken());
+ mFragmentLeashes.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);
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java
new file mode 100644
index 000000000000..4fd2126dfa27
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java
@@ -0,0 +1,82 @@
+/*
+ * 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 androidx.window.extensions.organizer;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+
+import androidx.window.extensions.embedding.SplitPairRule;
+import androidx.window.extensions.embedding.SplitPlaceholderRule;
+import androidx.window.extensions.embedding.SplitRule;
+
+/**
+ * Client-side descriptor of a split that holds two containers.
+ */
+class SplitContainer {
+ private final TaskFragmentContainer mPrimaryContainer;
+ private final TaskFragmentContainer mSecondaryContainer;
+ private final SplitRule mSplitRule;
+
+ SplitContainer(@NonNull TaskFragmentContainer primaryContainer,
+ @NonNull Activity primaryActivity,
+ @NonNull TaskFragmentContainer secondaryContainer,
+ @NonNull SplitRule splitRule) {
+ mPrimaryContainer = primaryContainer;
+ mSecondaryContainer = secondaryContainer;
+ mSplitRule = splitRule;
+
+ if (shouldFinishPrimaryWithSecondary(splitRule)) {
+ mSecondaryContainer.addActivityToFinishOnExit(primaryActivity);
+ }
+ if (shouldFinishSecondaryWithPrimary(splitRule)) {
+ mPrimaryContainer.addContainerToFinishOnExit(mSecondaryContainer);
+ }
+ }
+
+ @NonNull
+ TaskFragmentContainer getPrimaryContainer() {
+ return mPrimaryContainer;
+ }
+
+ @NonNull
+ TaskFragmentContainer getSecondaryContainer() {
+ return mSecondaryContainer;
+ }
+
+ @NonNull
+ SplitRule getSplitRule() {
+ return mSplitRule;
+ }
+
+ boolean isPlaceholderContainer() {
+ return (mSplitRule instanceof SplitPlaceholderRule);
+ }
+
+ static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
+ final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule;
+ final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule)
+ && ((SplitPairRule) splitRule).shouldFinishPrimaryWithSecondary();
+ return shouldFinishPrimaryWithSecondary || isPlaceholderContainer;
+ }
+
+ static boolean shouldFinishSecondaryWithPrimary(@NonNull SplitRule splitRule) {
+ final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule;
+ final boolean shouldFinishSecondaryWithPrimary = (splitRule instanceof SplitPairRule)
+ && ((SplitPairRule) splitRule).shouldFinishSecondaryWithPrimary();
+ return shouldFinishSecondaryWithPrimary || isPlaceholderContainer;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java
new file mode 100644
index 000000000000..05c6792a3fc7
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java
@@ -0,0 +1,774 @@
+/*
+ * 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 androidx.window.extensions.organizer;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityClient;
+import android.app.ActivityOptions;
+import android.app.ActivityThread;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.util.Pair;
+import android.window.TaskFragmentAppearedInfo;
+import android.window.TaskFragmentInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.window.extensions.embedding.ActivityRule;
+import androidx.window.extensions.embedding.EmbeddingRule;
+import androidx.window.extensions.embedding.SplitInfo;
+import androidx.window.extensions.embedding.SplitPairRule;
+import androidx.window.extensions.embedding.SplitPlaceholderRule;
+import androidx.window.extensions.embedding.SplitRule;
+import androidx.window.extensions.embedding.TaskFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Main controller class that manages split states and presentation.
+ */
+public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback {
+
+ private final SplitPresenter mPresenter;
+
+ // Currently applied split configuration.
+ private final List<EmbeddingRule> mSplitRules = new ArrayList<>();
+ private final List<TaskFragmentContainer> mContainers = new ArrayList<>();
+ private final List<SplitContainer> mSplitContainers = new ArrayList<>();
+
+ // Callback to Jetpack to notify about changes to split states.
+ private @NonNull Consumer<List<SplitInfo>> mEmbeddingCallback;
+
+ public SplitController() {
+ mPresenter = new SplitPresenter(new MainThreadExecutor(), this);
+ ActivityThread activityThread = ActivityThread.currentActivityThread();
+ // Register a callback to be notified about activities being created.
+ activityThread.getApplication().registerActivityLifecycleCallbacks(
+ new LifecycleCallbacks());
+ // Intercept activity starts to route activities to new containers if necessary.
+ Instrumentation instrumentation = activityThread.getInstrumentation();
+ instrumentation.addMonitor(new ActivityStartMonitor());
+ }
+
+ /** Updates the embedding rules applied to future activity launches. */
+ public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) {
+ mSplitRules.clear();
+ mSplitRules.addAll(rules);
+ }
+
+ @NonNull
+ public List<EmbeddingRule> getSplitRules() {
+ return mSplitRules;
+ }
+
+ /**
+ * Starts an activity to side of the launchingActivity with the provided split config.
+ */
+ public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
+ @Nullable Bundle options, @NonNull SplitRule sideRule,
+ @NonNull Consumer<Exception> failureCallback) {
+ try {
+ mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule);
+ } catch (Exception e) {
+ failureCallback.accept(e);
+ }
+ }
+
+ /**
+ * Registers the split organizer callback to notify about changes to active splits.
+ */
+ public void setEmbeddingCallback(@NonNull Consumer<List<SplitInfo>> callback) {
+ mEmbeddingCallback = callback;
+ updateCallbackIfNecessary();
+ }
+
+ @Override
+ public void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo) {
+ TaskFragmentContainer container = getContainer(
+ taskFragmentAppearedInfo.getTaskFragmentInfo().getFragmentToken());
+ if (container == null) {
+ return;
+ }
+
+ container.setInfo(taskFragmentAppearedInfo.getTaskFragmentInfo());
+ }
+
+ @Override
+ public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
+ TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
+ if (container == null) {
+ return;
+ }
+
+ container.setInfo(taskFragmentInfo);
+ // Check if there are no running activities - consider the container empty if there are no
+ // non-finishing activities left.
+ if (!taskFragmentInfo.hasRunningActivity()) {
+ mPresenter.cleanupContainer(container, true /* shouldFinishDependent */);
+ updateCallbackIfNecessary();
+ }
+ }
+
+ @Override
+ public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+ TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
+ if (container == null) {
+ return;
+ }
+
+ mPresenter.cleanupContainer(container, true /* shouldFinishDependent */);
+ updateCallbackIfNecessary();
+ }
+
+ @Override
+ public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken,
+ @NonNull Configuration parentConfig) {
+ TaskFragmentContainer container = getContainer(fragmentToken);
+ if (container != null) {
+ mPresenter.updateContainer(container);
+ updateCallbackIfNecessary();
+ }
+ }
+
+ /**
+ * Checks if the activity start should be routed to a particular container. It can create a new
+ * container for the activity and a new split container if necessary.
+ */
+ // TODO(b/190433398): Break down into smaller functions.
+ void onActivityCreated(@NonNull Activity launchedActivity) {
+ final List<EmbeddingRule> splitRules = getSplitRules();
+ final TaskFragmentContainer currentContainer = getContainerWithActivity(
+ launchedActivity.getActivityToken(), launchedActivity);
+
+ // Check if the activity is configured to always be expanded.
+ if (shouldExpand(launchedActivity, splitRules)) {
+ if (shouldContainerBeExpanded(currentContainer)) {
+ // Make sure that the existing container is expanded
+ mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken());
+ } else {
+ // Put activity into a new expanded container
+ final TaskFragmentContainer newContainer = newContainer(launchedActivity);
+ mPresenter.expandActivity(newContainer.getTaskFragmentToken(),
+ launchedActivity);
+ }
+ return;
+ }
+
+ // Check if activity requires a placeholder
+ if (launchPlaceholderIfNecessary(launchedActivity)) {
+ return;
+ }
+
+ // TODO(b/190433398): Check if it is a placeholder and there is already another split
+ // created by the primary activity. This is necessary for the case when the primary activity
+ // launched another secondary in the split, but the placeholder was still launched by the
+ // logic above. We didn't prevent the placeholder launcher because we didn't know that
+ // another secondary activity is coming up.
+
+ // Check if the activity should form a split with the activity below in the same task
+ // fragment.
+ Activity activityBelow = null;
+ if (currentContainer != null) {
+ final List<Activity> containerActivities = currentContainer.collectActivities();
+ final int index = containerActivities.indexOf(launchedActivity);
+ if (index > 0) {
+ activityBelow = containerActivities.get(index - 1);
+ }
+ }
+ if (activityBelow == null) {
+ IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow(
+ launchedActivity.getActivityToken());
+ if (belowToken != null) {
+ activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken);
+ }
+ }
+ if (activityBelow == null) {
+ return;
+ }
+
+ // Check if the split is already set.
+ final TaskFragmentContainer activityBelowContainer = getContainerWithActivity(
+ activityBelow.getActivityToken());
+ if (currentContainer != null && activityBelowContainer != null) {
+ final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer,
+ activityBelowContainer);
+ if (existingSplit != null) {
+ // There is already an active split with the activity below.
+ return;
+ }
+ }
+
+ final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity,
+ splitRules);
+ if (splitPairRule == null) {
+ return;
+ }
+
+ mPresenter.createNewSplitContainer(activityBelow, launchedActivity,
+ splitPairRule);
+
+ updateCallbackIfNecessary();
+ }
+
+ private void onActivityConfigurationChanged(@NonNull Activity activity) {
+ final TaskFragmentContainer currentContainer = getContainerWithActivity(
+ activity.getActivityToken());
+
+ if (currentContainer != null) {
+ // Changes to activities in controllers are handled in
+ // onTaskFragmentParentInfoChanged
+ return;
+ }
+
+ // Check if activity requires a placeholder
+ launchPlaceholderIfNecessary(activity);
+ }
+
+ /**
+ * Returns a container that this activity is registered with. An activity can only belong to one
+ * container, or no container at all.
+ */
+ @Nullable
+ TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
+ return getContainerWithActivity(activityToken, null /* activityToAdd */);
+ }
+
+ /**
+ * This method can only be called from {@link #onActivityCreated(Activity)}, use
+ * {@link #getContainerWithActivity(IBinder) } otherwise.
+ *
+ * Returns a container that this activity is registered with. The activity could be created
+ * before the container appeared, adding the activity to the container if so.
+ */
+ @Nullable
+ private TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken,
+ Activity activityToAdd) {
+ final IBinder taskFragmentToken = ActivityThread.currentActivityThread().getActivityClient(
+ activityToken).mInitialTaskFragmentToken;
+ for (TaskFragmentContainer container : mContainers) {
+ if (container.hasActivity(activityToken)) {
+ return container;
+ } else if (container.getTaskFragmentToken().equals(taskFragmentToken)) {
+ if (activityToAdd != null) {
+ container.addPendingAppearedActivity(activityToAdd);
+ }
+ return container;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates and registers a new organized container with an optional activity that will be
+ * re-parented to it in a WCT.
+ */
+ TaskFragmentContainer newContainer(@Nullable Activity activity) {
+ TaskFragmentContainer container = new TaskFragmentContainer(activity);
+ mContainers.add(container);
+ return container;
+ }
+
+ /**
+ * Creates and registers a new split with the provided containers and configuration. Finishes
+ * existing secondary containers if found for the given primary container.
+ */
+ void registerSplit(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity,
+ @NonNull TaskFragmentContainer secondaryContainer,
+ @NonNull SplitRule splitRule) {
+ if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) {
+ removeExistingSecondaryContainers(wct, primaryContainer);
+ }
+ SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity,
+ secondaryContainer, splitRule);
+ mSplitContainers.add(splitContainer);
+ }
+
+ /**
+ * Removes the container from bookkeeping records.
+ */
+ void removeContainer(@NonNull TaskFragmentContainer container) {
+ // Remove all split containers that included this one
+ mContainers.remove(container);
+ List<SplitContainer> containersToRemove = new ArrayList<>();
+ for (SplitContainer splitContainer : mSplitContainers) {
+ if (container.equals(splitContainer.getSecondaryContainer())
+ || container.equals(splitContainer.getPrimaryContainer())) {
+ containersToRemove.add(splitContainer);
+ }
+ }
+ mSplitContainers.removeAll(containersToRemove);
+ }
+
+ /**
+ * Removes a secondary container for the given primary container if an existing split is
+ * already registered.
+ */
+ void removeExistingSecondaryContainers(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer primaryContainer) {
+ // If the primary container was already in a split - remove the secondary container that
+ // is now covered by the new one that replaced it.
+ final SplitContainer existingSplitContainer = getActiveSplitForContainer(
+ primaryContainer);
+ if (existingSplitContainer == null
+ || primaryContainer == existingSplitContainer.getSecondaryContainer()) {
+ return;
+ }
+
+ existingSplitContainer.getSecondaryContainer().finish(
+ false /* shouldFinishDependent */, mPresenter, wct, this);
+ }
+
+ /**
+ * Returns the topmost not finished container.
+ */
+ @Nullable
+ TaskFragmentContainer getTopActiveContainer() {
+ for (int i = mContainers.size() - 1; i >= 0; i--) {
+ TaskFragmentContainer container = mContainers.get(i);
+ if (!container.isFinished()) {
+ return container;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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.
+ */
+ void updateContainer(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer container) {
+ if (launchPlaceholderIfNecessary(container)) {
+ // Placeholder was launched, the positions will be updated when the activity is added
+ // to the secondary container.
+ return;
+ }
+ if (shouldContainerBeExpanded(container)) {
+ if (container.getInfo() != null) {
+ mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
+ }
+ // If the info is not available yet the task fragment will be expanded when it's ready
+ return;
+ }
+ SplitContainer splitContainer = getActiveSplitForContainer(container);
+ if (splitContainer == null) {
+ return;
+ }
+ if (splitContainer != mSplitContainers.get(mSplitContainers.size() - 1)) {
+ // Skip position update - it isn't the topmost split.
+ return;
+ }
+ if (splitContainer.getPrimaryContainer().isEmpty()
+ || splitContainer.getSecondaryContainer().isEmpty()) {
+ // Skip position update - one or both containers are empty.
+ return;
+ }
+ if (dismissPlaceholderIfNecessary(splitContainer)) {
+ // Placeholder was finished, the positions will be updated when its container is emptied
+ return;
+ }
+ mPresenter.updateSplitContainer(splitContainer, container, wct);
+ }
+
+ /**
+ * Returns the top active split container that has the provided container, if available.
+ */
+ @Nullable
+ private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) {
+ for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
+ SplitContainer splitContainer = mSplitContainers.get(i);
+ if (container.equals(splitContainer.getSecondaryContainer())
+ || container.equals(splitContainer.getPrimaryContainer())) {
+ return splitContainer;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the active split that has the provided containers as primary and secondary or as
+ * secondary and primary, if available.
+ */
+ @Nullable
+ private SplitContainer getActiveSplitForContainers(
+ @NonNull TaskFragmentContainer firstContainer,
+ @NonNull TaskFragmentContainer secondContainer) {
+ for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
+ SplitContainer splitContainer = mSplitContainers.get(i);
+ final TaskFragmentContainer primary = splitContainer.getPrimaryContainer();
+ final TaskFragmentContainer secondary = splitContainer.getSecondaryContainer();
+ if ((firstContainer == secondary && secondContainer == primary)
+ || (firstContainer == primary && secondContainer == secondary)) {
+ return splitContainer;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Checks if the container requires a placeholder and launches it if necessary.
+ */
+ private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) {
+ final Activity topActivity = container.getTopNonFinishingActivity();
+ if (topActivity == null) {
+ return false;
+ }
+
+ return launchPlaceholderIfNecessary(topActivity);
+ }
+
+ boolean launchPlaceholderIfNecessary(@NonNull Activity activity) {
+ final TaskFragmentContainer container = getContainerWithActivity(
+ activity.getActivityToken());
+
+ SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container)
+ : null;
+ if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
+ // Don't launch placeholder in primary split container
+ return false;
+ }
+
+ // Check if there is enough space for launch
+ final SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity);
+ if (placeholderRule == null || !mPresenter.shouldShowSideBySide(
+ mPresenter.getParentContainerBounds(activity), placeholderRule)) {
+ return false;
+ }
+
+ // TODO(b/190433398): Handle failed request
+ startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null,
+ placeholderRule, null);
+ return true;
+ }
+
+ private boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) {
+ if (!splitContainer.isPlaceholderContainer()) {
+ return false;
+ }
+
+ if (mPresenter.shouldShowSideBySide(splitContainer)) {
+ return false;
+ }
+
+ mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(),
+ false /* shouldFinishDependent */);
+ return true;
+ }
+
+ /**
+ * Returns the rule to launch a placeholder for the activity with the provided component name
+ * if it is configured in the split config.
+ */
+ private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) {
+ for (EmbeddingRule rule : mSplitRules) {
+ if (!(rule instanceof SplitPlaceholderRule)) {
+ continue;
+ }
+ SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) rule;
+ if (placeholderRule.getActivityPredicate().test(activity)) {
+ return placeholderRule;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Notifies listeners about changes to split states if necessary.
+ */
+ private void updateCallbackIfNecessary() {
+ if (mEmbeddingCallback == null) {
+ return;
+ }
+ // TODO(b/190433398): Check if something actually changed
+ mEmbeddingCallback.accept(getActiveSplitStates());
+ }
+
+ /**
+ * Returns a list of descriptors for currently active split states.
+ */
+ private List<SplitInfo> getActiveSplitStates() {
+ List<SplitInfo> splitStates = new ArrayList<>();
+ for (SplitContainer container : mSplitContainers) {
+ TaskFragment primaryContainer =
+ new TaskFragment(
+ container.getPrimaryContainer().collectActivities());
+ TaskFragment secondaryContainer =
+ new TaskFragment(
+ container.getSecondaryContainer().collectActivities());
+ SplitInfo splitState = new SplitInfo(primaryContainer,
+ secondaryContainer, container.getSplitRule().getSplitRatio());
+ splitStates.add(splitState);
+ }
+ return splitStates;
+ }
+
+ /**
+ * Returns {@code true} if the container is expanded to occupy full task size.
+ * Returns {@code false} if the container is included in an active split.
+ */
+ boolean shouldContainerBeExpanded(@Nullable TaskFragmentContainer container) {
+ if (container == null) {
+ return false;
+ }
+ for (SplitContainer splitContainer : mSplitContainers) {
+ if (container.equals(splitContainer.getPrimaryContainer())
+ || container.equals(splitContainer.getSecondaryContainer())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns a split rule for the provided pair of primary activity and secondary activity intent
+ * if available.
+ */
+ @Nullable
+ private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
+ @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) {
+ for (EmbeddingRule rule : splitRules) {
+ if (!(rule instanceof SplitPairRule)) {
+ continue;
+ }
+ SplitPairRule pairRule = (SplitPairRule) rule;
+ if (pairRule.getActivityIntentPredicate().test(
+ new Pair(primaryActivity, secondaryActivityIntent))) {
+ return pairRule;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a split rule for the provided pair of primary and secondary activities if available.
+ */
+ @Nullable
+ private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
+ @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) {
+ for (EmbeddingRule rule : splitRules) {
+ if (!(rule instanceof SplitPairRule)) {
+ continue;
+ }
+ SplitPairRule pairRule = (SplitPairRule) rule;
+ final Intent intent = secondaryActivity.getIntent();
+ if (pairRule.getActivityPairPredicate().test(
+ new Pair(primaryActivity, secondaryActivity))
+ && (intent == null || pairRule.getActivityIntentPredicate().test(
+ new Pair(primaryActivity, intent)))) {
+ return pairRule;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) {
+ for (TaskFragmentContainer container : mContainers) {
+ if (container.getTaskFragmentToken().equals(fragmentToken)) {
+ return container;
+ }
+ }
+ return 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.
+ */
+ private static boolean shouldExpand(@NonNull Activity activity,
+ List<EmbeddingRule> splitRules) {
+ if (splitRules == null) {
+ return false;
+ }
+ for (EmbeddingRule rule : splitRules) {
+ if (!(rule instanceof ActivityRule)) {
+ continue;
+ }
+ ActivityRule activityRule = (ActivityRule) rule;
+ if (!activityRule.shouldAlwaysExpand()) {
+ continue;
+ }
+ if (activityRule.getActivityPredicate().test(activity)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private final class LifecycleCallbacks implements ActivityLifecycleCallbacks {
+
+ @Override
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ }
+
+ @Override
+ public void onActivityPostCreated(Activity activity, 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.
+ SplitController.this.onActivityCreated(activity);
+ }
+
+ @Override
+ public void onActivityStarted(Activity activity) {
+ }
+
+ @Override
+ public void onActivityResumed(Activity activity) {
+ }
+
+ @Override
+ public void onActivityPaused(Activity activity) {
+ }
+
+ @Override
+ public void onActivityStopped(Activity activity) {
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+ }
+
+ @Override
+ public void onActivityDestroyed(Activity activity) {
+ }
+
+ @Override
+ public void onActivityConfigurationChanged(Activity activity) {
+ SplitController.this.onActivityConfigurationChanged(activity);
+ }
+ }
+
+ /** Executor that posts on the main application thread. */
+ private static class MainThreadExecutor implements Executor {
+ private final Handler handler = new Handler(Looper.getMainLooper());
+
+ @Override
+ public void execute(Runnable r) {
+ handler.post(r);
+ }
+ }
+
+ /**
+ * 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 {
+
+ @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 (!setLaunchingToSideContainer(launchingActivity, intent, options)) {
+ setLaunchingInSameContainer(launchingActivity, intent, options);
+ }
+
+ return super.onStartActivity(who, intent, options);
+ }
+
+ /**
+ * Returns {@code true} if the activity that is going to be started via the
+ * {@code intent} should be paired with the {@code launchingActivity} and is set to be
+ * launched in an empty side container.
+ */
+ private boolean setLaunchingToSideContainer(Activity launchingActivity, Intent intent,
+ Bundle options) {
+ final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent,
+ getSplitRules());
+ if (splitPairRule == null) {
+ return false;
+ }
+
+ // Create a new split with an empty side container
+ final TaskFragmentContainer secondaryContainer = mPresenter
+ .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule);
+
+ // 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,
+ secondaryContainer.getTaskFragmentToken());
+ return true;
+ }
+
+ /**
+ * Checks if the activity that is going to be started via the {@code intent} should be
+ * paired with the existing top activity which is currently paired with the
+ * {@code launchingActivity}. If so, set the activity to be launched in the same
+ * container of the {@code launchingActivity}.
+ */
+ private void setLaunchingInSameContainer(Activity launchingActivity, Intent intent,
+ Bundle options) {
+ final TaskFragmentContainer launchingContainer = getContainerWithActivity(
+ launchingActivity.getActivityToken());
+ if (launchingContainer == null) {
+ return;
+ }
+
+ final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer);
+ if (splitContainer == null) {
+ return;
+ }
+
+ if (splitContainer.getSecondaryContainer() != launchingContainer) {
+ return;
+ }
+
+ // The launching activity is on the secondary container. Retrieve the primary
+ // activity from the other container.
+ Activity primaryActivity =
+ splitContainer.getPrimaryContainer().getTopNonFinishingActivity();
+ if (primaryActivity == null) {
+ return;
+ }
+
+ final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent,
+ getSplitRules());
+ if (splitPairRule == null) {
+ return;
+ }
+
+ // Amend the request to let the WM know that the activity should be placed in the
+ // dedicated container. This is necessary for the case that the activity is started
+ // into a new Task, or new Task will be escaped from the current host Task and be
+ // displayed in fullscreen.
+ options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+ launchingContainer.getTaskFragmentToken());
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java
new file mode 100644
index 000000000000..ac85ac8cbc34
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java
@@ -0,0 +1,353 @@
+/*
+ * 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 androidx.window.extensions.organizer;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.WindowInsets;
+import android.view.WindowMetrics;
+import android.window.TaskFragmentCreationParams;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.embedding.SplitPairRule;
+import androidx.window.extensions.embedding.SplitRule;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Controls the visual presentation of the splits according to the containers formed by
+ * {@link SplitController}.
+ */
+class SplitPresenter extends JetpackTaskFragmentOrganizer {
+ private static final int POSITION_LEFT = 0;
+ private static final int POSITION_RIGHT = 1;
+ private static final int POSITION_FILL = 2;
+
+ @IntDef(value = {
+ POSITION_LEFT,
+ POSITION_RIGHT,
+ POSITION_FILL,
+ })
+ private @interface Position {}
+
+ private final SplitController mController;
+
+ SplitPresenter(@NonNull Executor executor, SplitController controller) {
+ super(executor, controller);
+ mController = controller;
+ registerOrganizer();
+ }
+
+ /**
+ * Updates the presentation of the provided container.
+ */
+ void updateContainer(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();
+
+ container.finish(shouldFinishDependent, this, wct, mController);
+
+ final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer();
+ if (newTopContainer != null) {
+ mController.updateContainer(wct, newTopContainer);
+ }
+
+ applyTransaction(wct);
+ }
+
+ /**
+ * Creates a new split with the primary activity and an empty secondary container.
+ * @return The newly created secondary container.
+ */
+ TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity,
+ @NonNull SplitPairRule rule) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+ final Rect parentBounds = getParentContainerBounds(primaryActivity);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
+ final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
+ primaryActivity, primaryRectBounds, null);
+
+ // Create new empty task fragment
+ TaskFragmentContainer secondaryContainer = mController.newContainer(null);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
+ createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
+ primaryActivity.getActivityToken(), secondaryRectBounds,
+ WINDOWING_MODE_MULTI_WINDOW);
+ secondaryContainer.setLastRequestedBounds(secondaryRectBounds);
+
+ // Set adjacent to each other so that the containers below will be invisible.
+ setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+ secondaryContainer.getTaskFragmentToken(), rule);
+
+ mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
+
+ applyTransaction(wct);
+
+ return secondaryContainer;
+ }
+
+ /**
+ * Creates a new split container with the two provided activities.
+ * @param primaryActivity An activity that should be in the primary container. If it is not
+ * currently in an existing container, a new one will be created and the
+ * activity will be re-parented to it.
+ * @param secondaryActivity An activity that should be in the secondary container. If it is not
+ * currently in an existing container, or if it is currently in the
+ * same container as the primary activity, a new container will be
+ * 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);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
+ final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
+ primaryActivity, primaryRectBounds, null);
+
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
+ final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
+ secondaryActivity, secondaryRectBounds, primaryContainer);
+
+ // Set adjacent to each other so that the containers below will be invisible.
+ setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+ secondaryContainer.getTaskFragmentToken(), rule);
+
+ mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
+
+ applyTransaction(wct);
+ }
+
+ /**
+ * Creates a new container or resizes an existing container for activity to the provided bounds.
+ * @param activity The activity to be re-parented to the container if necessary.
+ * @param containerToAvoid Re-parent from this container if an activity is already in it.
+ */
+ private TaskFragmentContainer prepareContainerForActivity(
+ @NonNull WindowContainerTransaction wct, @NonNull Activity activity,
+ @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) {
+ TaskFragmentContainer container = mController.getContainerWithActivity(
+ activity.getActivityToken());
+ if (container == null || container == containerToAvoid) {
+ container = mController.newContainer(activity);
+
+ final TaskFragmentCreationParams fragmentOptions =
+ createFragmentOptions(
+ container.getTaskFragmentToken(),
+ activity.getActivityToken(),
+ bounds,
+ WINDOWING_MODE_MULTI_WINDOW);
+ wct.createTaskFragment(fragmentOptions);
+
+ wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
+ activity.getActivityToken());
+
+ container.setLastRequestedBounds(bounds);
+ } else {
+ resizeTaskFragmentIfRegistered(wct, container, bounds);
+ }
+
+ return container;
+ }
+
+ /**
+ * Starts a new activity to the side, creating a new split container. A new container will be
+ * created for the activity that will be started.
+ * @param launchingActivity An activity that should be in the primary container. If it is not
+ * currently in an existing container, a new one will be created and
+ * the activity will be re-parented to it.
+ * @param activityIntent The intent to start the new activity.
+ * @param activityOptions The options to apply to new activity start.
+ * @param rule The split rule to be applied to the container.
+ */
+ void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
+ @Nullable Bundle activityOptions, @NonNull SplitRule rule) {
+ final Rect parentBounds = getParentContainerBounds(launchingActivity);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
+
+ TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
+ launchingActivity.getActivityToken());
+ if (primaryContainer == null) {
+ primaryContainer = mController.newContainer(launchingActivity);
+ }
+
+ TaskFragmentContainer secondaryContainer = mController.newContainer(null);
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
+ rule);
+ startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds,
+ launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds,
+ activityIntent, activityOptions, rule);
+ applyTransaction(wct);
+
+ primaryContainer.setLastRequestedBounds(primaryRectBounds);
+ secondaryContainer.setLastRequestedBounds(secondaryRectBounds);
+ }
+
+ /**
+ * Updates the positions of containers in an existing split.
+ * @param splitContainer The split container to be updated.
+ * @param updatedContainer The task fragment that was updated and caused this split update.
+ * @param wct WindowContainerTransaction that this update should be performed with.
+ */
+ 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);
+ final SplitRule rule = splitContainer.getSplitRule();
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
+
+ // If the task fragments are not registered yet, the positions will be updated after they
+ // are created again.
+ resizeTaskFragmentIfRegistered(wct, splitContainer.getPrimaryContainer(),
+ primaryRectBounds);
+ resizeTaskFragmentIfRegistered(wct, splitContainer.getSecondaryContainer(),
+ secondaryRectBounds);
+ }
+
+ /**
+ * Resizes the task fragment if it was already registered. Skips the operation if the container
+ * creation has not been reported from the server yet.
+ */
+ // TODO(b/190433398): Handle resize if the fragment hasn't appeared yet.
+ void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer container,
+ @Nullable Rect bounds) {
+ if (container.getInfo() == null) {
+ return;
+ }
+ resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds);
+ }
+
+ @Override
+ void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+ @Nullable Rect bounds) {
+ TaskFragmentContainer container = mController.getContainer(fragmentToken);
+ if (container == null) {
+ throw new IllegalStateException(
+ "Resizing a task fragment that is not registered with controller.");
+ }
+
+ if (container.areLastRequestedBoundsEqual(bounds)) {
+ // Return early if the provided bounds were already requested
+ return;
+ }
+
+ container.setLastRequestedBounds(bounds);
+ super.resizeTaskFragment(wct, fragmentToken, bounds);
+ }
+
+ boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) {
+ final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer());
+ return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule());
+ }
+
+ boolean shouldShowSideBySide(@Nullable Rect parentBounds, @NonNull SplitRule rule) {
+ // TODO(b/190433398): Supply correct insets.
+ final WindowMetrics parentMetrics = new WindowMetrics(parentBounds,
+ new WindowInsets(new Rect()));
+ return rule.getParentWindowMetricsPredicate().test(parentMetrics);
+ }
+
+ @NonNull
+ private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds,
+ @NonNull SplitRule rule) {
+ if (!shouldShowSideBySide(parentBounds, rule)) {
+ return new Rect();
+ }
+
+ float splitRatio = rule.getSplitRatio();
+ switch (position) {
+ case POSITION_LEFT:
+ return new Rect(
+ parentBounds.left,
+ parentBounds.top,
+ (int) (parentBounds.left + parentBounds.width() * splitRatio),
+ parentBounds.bottom);
+ case POSITION_RIGHT:
+ return new Rect(
+ (int) (parentBounds.left + parentBounds.width() * splitRatio),
+ parentBounds.top,
+ parentBounds.right,
+ parentBounds.bottom);
+ case POSITION_FILL:
+ return parentBounds;
+ }
+ return parentBounds;
+ }
+
+ @NonNull
+ Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
+ final Configuration parentConfig = mFragmentParentConfigs.get(
+ container.getTaskFragmentToken());
+ if (parentConfig != null) {
+ return parentConfig.windowConfiguration.getBounds();
+ }
+
+ // If there is no parent yet - then assuming that activities are running in full task bounds
+ final Activity topActivity = container.getTopNonFinishingActivity();
+ final Rect bounds = topActivity != null ? getParentContainerBounds(topActivity) : null;
+
+ if (bounds == null) {
+ throw new IllegalStateException("Unknown parent bounds");
+ }
+ return bounds;
+ }
+
+ @NonNull
+ Rect getParentContainerBounds(@NonNull Activity activity) {
+ final TaskFragmentContainer container = mController.getContainerWithActivity(
+ activity.getActivityToken());
+ if (container != null) {
+ final Configuration parentConfig = mFragmentParentConfigs.get(
+ container.getTaskFragmentToken());
+ if (parentConfig != null) {
+ return parentConfig.windowConfiguration.getBounds();
+ }
+ }
+
+ // TODO(b/190433398): Check if the client-side available info about parent bounds is enough.
+ if (!activity.isInMultiWindowMode()) {
+ // In fullscreen mode the max bounds should correspond to the task bounds.
+ return activity.getResources().getConfiguration().windowConfiguration.getMaxBounds();
+ }
+ return activity.getResources().getConfiguration().windowConfiguration.getBounds();
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationController.java
new file mode 100644
index 000000000000..b85287d8a919
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationController.java
@@ -0,0 +1,61 @@
+/*
+ * 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 androidx.window.extensions.organizer;
+
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
+
+import android.util.Log;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationDefinition;
+import android.window.TaskFragmentOrganizer;
+
+/** Controls the TaskFragment remote animations. */
+class TaskFragmentAnimationController {
+
+ private static final String TAG = "TaskFragAnimationCtrl";
+ // TODO(b/196173550) turn off when finalize
+ static final boolean DEBUG = false;
+
+ private final TaskFragmentOrganizer mOrganizer;
+ private final TaskFragmentAnimationRunner mRemoteRunner = new TaskFragmentAnimationRunner();
+
+ TaskFragmentAnimationController(TaskFragmentOrganizer organizer) {
+ mOrganizer = organizer;
+ }
+
+ void registerRemoteAnimations() {
+ if (DEBUG) {
+ Log.v(TAG, "registerRemoteAnimations");
+ }
+ final RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
+ final RemoteAnimationAdapter animationAdapter =
+ new RemoteAnimationAdapter(mRemoteRunner, 0, 0);
+ definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, animationAdapter);
+ definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, animationAdapter);
+ definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, animationAdapter);
+ mOrganizer.registerRemoteAnimations(definition);
+ }
+
+ void unregisterRemoteAnimations() {
+ if (DEBUG) {
+ Log.v(TAG, "unregisterRemoteAnimations");
+ }
+ mOrganizer.unregisterRemoteAnimations();
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationRunner.java
new file mode 100644
index 000000000000..9ee60d8c6bd3
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationRunner.java
@@ -0,0 +1,93 @@
+/*
+ * 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 androidx.window.extensions.organizer;
+
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+
+/** To run the TaskFragment animations. */
+class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
+
+ private static final String TAG = "TaskFragAnimationRunner";
+ private final Handler mHandler = new Handler(Looper.myLooper());
+
+ @Nullable
+ private IRemoteAnimationFinishedCallback mFinishedCallback;
+
+ @Override
+ public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+ RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps,
+ IRemoteAnimationFinishedCallback finishedCallback) {
+ if (wallpapers.length != 0 || nonApps.length != 0) {
+ throw new IllegalArgumentException("TaskFragment shouldn't handle animation with"
+ + "wallpaper or non-app windows.");
+ }
+ if (TaskFragmentAnimationController.DEBUG) {
+ Log.v(TAG, "onAnimationStart transit=" + transit);
+ }
+ mHandler.post(() -> startAnimation(apps, finishedCallback));
+ }
+
+ @Override
+ public void onAnimationCancelled() {
+ if (TaskFragmentAnimationController.DEBUG) {
+ Log.v(TAG, "onAnimationCancelled");
+ }
+ mHandler.post(this::onAnimationFinished);
+ }
+
+ private void startAnimation(RemoteAnimationTarget[] targets,
+ IRemoteAnimationFinishedCallback finishedCallback) {
+ // TODO(b/196173550) replace with actual animations
+ mFinishedCallback = finishedCallback;
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (RemoteAnimationTarget target : targets) {
+ if (target.mode == MODE_OPENING) {
+ t.show(target.leash);
+ t.setAlpha(target.leash, 1);
+ }
+ t.setPosition(target.leash, target.localBounds.left, target.localBounds.top);
+ }
+ t.apply();
+ onAnimationFinished();
+ }
+
+ private void onAnimationFinished() {
+ if (mFinishedCallback == null) {
+ return;
+ }
+ try {
+ mFinishedCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ mFinishedCallback = null;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java
new file mode 100644
index 000000000000..a4f5c75276f5
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java
@@ -0,0 +1,246 @@
+/*
+ * 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 androidx.window.extensions.organizer;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityThread;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IBinder;
+import android.window.TaskFragmentInfo;
+import android.window.WindowContainerTransaction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Client-side container for a stack of activities. Corresponds to an instance of TaskFragment
+ * on the server side.
+ */
+class TaskFragmentContainer {
+ /**
+ * Client-created token that uniquely identifies the task fragment container instance.
+ */
+ @NonNull
+ private final IBinder mToken;
+
+ /**
+ * Server-provided task fragment information.
+ */
+ private TaskFragmentInfo mInfo;
+
+ /**
+ * Activities that are being reparented or being started to this container, but haven't been
+ * added to {@link #mInfo} yet.
+ */
+ private final ArrayList<Activity> mPendingAppearedActivities = new ArrayList<>();
+
+ /** Containers that are dependent on this one and should be completely destroyed on exit. */
+ private final List<TaskFragmentContainer> mContainersToFinishOnExit =
+ new ArrayList<>();
+
+ /** Individual associated activities in different containers that should be finished on exit. */
+ private final List<Activity> mActivitiesToFinishOnExit = new ArrayList<>();
+
+ /** Indicates whether the container was cleaned up after the last activity was removed. */
+ private boolean mIsFinished;
+
+ /**
+ * Bounds that were requested last via {@link android.window.WindowContainerTransaction}.
+ */
+ private final Rect mLastRequestedBounds = new Rect();
+
+ /**
+ * Creates a container with an existing activity that will be re-parented to it in a window
+ * container transaction.
+ */
+ TaskFragmentContainer(@Nullable Activity activity) {
+ mToken = new Binder("TaskFragmentContainer");
+ if (activity != null) {
+ addPendingAppearedActivity(activity);
+ }
+ }
+
+ /**
+ * Returns the client-created token that uniquely identifies this container.
+ */
+ @NonNull
+ IBinder getTaskFragmentToken() {
+ return mToken;
+ }
+
+ /** List of activities that belong to this container and live in this process. */
+ @NonNull
+ List<Activity> collectActivities() {
+ // Add the re-parenting activity, in case the server has not yet reported the task
+ // fragment info update with it placed in this container. We still want to apply rules
+ // in this intermediate state.
+ List<Activity> allActivities = new ArrayList<>();
+ if (!mPendingAppearedActivities.isEmpty()) {
+ allActivities.addAll(mPendingAppearedActivities);
+ }
+ // Add activities reported from the server.
+ if (mInfo == null) {
+ return allActivities;
+ }
+ ActivityThread activityThread = ActivityThread.currentActivityThread();
+ for (IBinder token : mInfo.getActivities()) {
+ Activity activity = activityThread.getActivity(token);
+ if (activity != null && !allActivities.contains(activity)) {
+ allActivities.add(activity);
+ }
+ }
+ return allActivities;
+ }
+
+ void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) {
+ mPendingAppearedActivities.add(pendingAppearedActivity);
+ }
+
+ boolean hasActivity(@NonNull IBinder token) {
+ if (mInfo != null && mInfo.getActivities().contains(token)) {
+ return true;
+ }
+ for (Activity activity : mPendingAppearedActivities) {
+ if (activity.getActivityToken().equals(token)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ TaskFragmentInfo getInfo() {
+ return mInfo;
+ }
+
+ void setInfo(@Nullable TaskFragmentInfo info) {
+ mInfo = info;
+ if (mInfo == null || mPendingAppearedActivities.isEmpty()) {
+ return;
+ }
+ // Cleanup activities that were being re-parented
+ List<IBinder> infoActivities = mInfo.getActivities();
+ for (int i = mPendingAppearedActivities.size() - 1; i >= 0; --i) {
+ final Activity activity = mPendingAppearedActivities.get(i);
+ if (infoActivities.contains(activity.getActivityToken())) {
+ mPendingAppearedActivities.remove(i);
+ }
+ }
+ }
+
+ @Nullable
+ Activity getTopNonFinishingActivity() {
+ List<Activity> activities = collectActivities();
+ if (activities.isEmpty()) {
+ return null;
+ }
+ int i = activities.size() - 1;
+ while (i >= 0 && activities.get(i).isFinishing()) {
+ i--;
+ }
+ return i >= 0 ? activities.get(i) : null;
+ }
+
+ boolean isEmpty() {
+ return mPendingAppearedActivities.isEmpty() && (mInfo == null || mInfo.isEmpty());
+ }
+
+ /**
+ * Adds a container that should be finished when this container is finished.
+ */
+ void addContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToFinish) {
+ mContainersToFinishOnExit.add(containerToFinish);
+ }
+
+ /**
+ * Adds an activity that should be finished when this container is finished.
+ */
+ void addActivityToFinishOnExit(@NonNull Activity activityToFinish) {
+ mActivitiesToFinishOnExit.add(activityToFinish);
+ }
+
+ /**
+ * Removes all activities that belong to this process and finishes other containers/activities
+ * configured to finish together.
+ */
+ void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter,
+ @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
+ if (mIsFinished) {
+ return;
+ }
+ mIsFinished = true;
+
+ // Finish own activities
+ for (Activity activity : collectActivities()) {
+ activity.finish();
+ }
+
+ // Cleanup the visuals
+ presenter.deleteTaskFragment(wct, getTaskFragmentToken());
+ // Cleanup the records
+ controller.removeContainer(this);
+
+ if (!shouldFinishDependent) {
+ return;
+ }
+
+ // Finish dependent containers
+ for (TaskFragmentContainer container : mContainersToFinishOnExit) {
+ container.finish(true /* shouldFinishDependent */, presenter,
+ wct, controller);
+ }
+ mContainersToFinishOnExit.clear();
+
+ // Finish associated activities
+ for (Activity activity : mActivitiesToFinishOnExit) {
+ activity.finish();
+ }
+ mActivitiesToFinishOnExit.clear();
+
+ // Finish activities that were being re-parented to this container.
+ for (Activity activity : mPendingAppearedActivities) {
+ activity.finish();
+ }
+ mPendingAppearedActivities.clear();
+ }
+
+ boolean isFinished() {
+ return mIsFinished;
+ }
+
+ /**
+ * Checks if last requested bounds are equal to the provided value.
+ */
+ boolean areLastRequestedBoundsEqual(@Nullable Rect bounds) {
+ return (bounds == null && mLastRequestedBounds.isEmpty())
+ || mLastRequestedBounds.equals(bounds);
+ }
+
+ /**
+ * Updates the last requested bounds.
+ */
+ void setLastRequestedBounds(@Nullable Rect bounds) {
+ if (bounds == null) {
+ mLastRequestedBounds.setEmpty();
+ } else {
+ mLastRequestedBounds.set(bounds);
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index be6652d43fb2..097febf9770a 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/split_divider_background.xml b/libs/WindowManager/Shell/res/color/split_divider_background.xml
new file mode 100644
index 000000000000..84f4fdff4e1a
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/split_divider_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral2_500" android:lStar="35" />
+</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
index 8710fb8ac69b..96d2d7c954d8 100644
--- a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
+++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
@@ -18,7 +18,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
- android:color="@android:color/system_neutral1_900"
+ android:color="@android:color/system_neutral1_800"
/>
<corners android:radius="20dp" />
diff --git a/libs/WindowManager/Shell/res/drawable/split_rounded_bottom.xml b/libs/WindowManager/Shell/res/drawable/split_rounded_bottom.xml
new file mode 100644
index 000000000000..18dc909ae955
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/split_rounded_bottom.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/split_divider_corner_size"
+ android:height="@dimen/split_divider_corner_size"
+ android:viewportWidth="42"
+ android:viewportHeight="42">
+
+ <group android:pivotX="21"
+ android:pivotY="21"
+ android:rotation="180">
+ <path
+ android:fillColor="@color/split_divider_background"
+ android:pathData="m 0 0 c 8 0 16 8 16 16 h 10 c 0 -8 8 -16 16 -16 z" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/split_rounded_left.xml b/libs/WindowManager/Shell/res/drawable/split_rounded_left.xml
new file mode 100644
index 000000000000..931cacf887cd
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/split_rounded_left.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/split_divider_corner_size"
+ android:height="@dimen/split_divider_corner_size"
+ android:viewportWidth="42"
+ android:viewportHeight="42">
+
+ <group android:pivotX="21"
+ android:pivotY="21"
+ android:rotation="-90">
+ <path
+ android:fillColor="@color/split_divider_background"
+ android:pathData="m 0 0 c 8 0 16 8 16 16 h 10 c 0 -8 8 -16 16 -16 z" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/split_rounded_right.xml b/libs/WindowManager/Shell/res/drawable/split_rounded_right.xml
new file mode 100644
index 000000000000..54e47612faa8
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/split_rounded_right.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/split_divider_corner_size"
+ android:height="@dimen/split_divider_corner_size"
+ android:viewportWidth="42"
+ android:viewportHeight="42">
+
+ <group android:pivotX="21"
+ android:pivotY="21"
+ android:rotation="90">
+ <path
+ android:fillColor="@color/split_divider_background"
+ android:pathData="m 0 0 c 8 0 16 8 16 16 h 10 c 0 -8 8 -16 16 -16 z" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/split_rounded_top.xml b/libs/WindowManager/Shell/res/drawable/split_rounded_top.xml
new file mode 100644
index 000000000000..9115b5a2352e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/split_rounded_top.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/split_divider_corner_size"
+ android:height="@dimen/split_divider_corner_size"
+ android:viewportWidth="42"
+ android:viewportHeight="42">
+
+ <path
+ android:fillColor="@color/split_divider_background"
+ android:pathData="m 0 0 c 8 0 16 8 16 16 h 10 c 0 -8 8 -16 16 -16 z" />
+
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml
index c09ae53746da..0cf6d73162d2 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml
@@ -17,13 +17,13 @@
<com.android.wm.shell.common.AlphaOptimizedButton
xmlns:android="http://schemas.android.com/apk/res/android"
style="@android:style/Widget.DeviceDefault.Button.Borderless"
- android:id="@+id/settings_button"
+ android:id="@+id/manage_button"
android:layout_gravity="start"
android:layout_width="wrap_content"
- android:layout_height="40dp"
- android:layout_marginTop="8dp"
- android:layout_marginLeft="16dp"
- android:layout_marginBottom="8dp"
+ android:layout_height="@dimen/bubble_manage_button_height"
+ android:layout_marginStart="@dimen/bubble_manage_button_margin"
+ android:layout_marginTop="@dimen/bubble_manage_button_margin"
+ android:layout_marginBottom="@dimen/bubble_manage_button_margin"
android:focusable="true"
android:text="@string/manage_bubbles_text"
android:textSize="@*android:dimen/text_size_body_2_material"
diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml
index f4b3aca33dd7..298ad3025b00 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml
@@ -25,15 +25,15 @@
android:id="@+id/bubble_manage_menu_dismiss_container"
android:background="@drawable/bubble_manage_menu_row"
android:layout_width="match_parent"
- android:layout_height="48dp"
+ android:layout_height="@dimen/bubble_menu_item_height"
android:gravity="center_vertical"
- android:paddingStart="16dp"
- android:paddingEnd="16dp"
+ android:paddingStart="@dimen/bubble_menu_padding"
+ android:paddingEnd="@dimen/bubble_menu_padding"
android:orientation="horizontal">
<ImageView
- android:layout_width="24dp"
- android:layout_height="24dp"
+ android:layout_width="@dimen/bubble_menu_icon_size"
+ android:layout_height="@dimen/bubble_menu_icon_size"
android:src="@drawable/ic_remove_no_shadow"
android:tint="@color/bubbles_icon_tint"/>
@@ -50,15 +50,15 @@
android:id="@+id/bubble_manage_menu_dont_bubble_container"
android:background="@drawable/bubble_manage_menu_row"
android:layout_width="match_parent"
- android:layout_height="48dp"
+ android:layout_height="@dimen/bubble_menu_item_height"
android:gravity="center_vertical"
- android:paddingStart="16dp"
- android:paddingEnd="16dp"
+ android:paddingStart="@dimen/bubble_menu_padding"
+ android:paddingEnd="@dimen/bubble_menu_padding"
android:orientation="horizontal">
<ImageView
- android:layout_width="24dp"
- android:layout_height="24dp"
+ android:layout_width="@dimen/bubble_menu_icon_size"
+ android:layout_height="@dimen/bubble_menu_icon_size"
android:src="@drawable/bubble_ic_stop_bubble"
android:tint="@color/bubbles_icon_tint"/>
@@ -75,16 +75,16 @@
android:id="@+id/bubble_manage_menu_settings_container"
android:background="@drawable/bubble_manage_menu_row"
android:layout_width="match_parent"
- android:layout_height="48dp"
+ android:layout_height="@dimen/bubble_menu_item_height"
android:gravity="center_vertical"
- android:paddingStart="16dp"
- android:paddingEnd="16dp"
+ android:paddingStart="@dimen/bubble_menu_padding"
+ android:paddingEnd="@dimen/bubble_menu_padding"
android:orientation="horizontal">
<ImageView
android:id="@+id/bubble_manage_menu_settings_icon"
- android:layout_width="24dp"
- android:layout_height="24dp"
+ android:layout_width="@dimen/bubble_menu_icon_size"
+ android:layout_height="@dimen/bubble_menu_icon_size"
android:src="@drawable/ic_remove_no_shadow"/>
<TextView
diff --git a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml
index fd4c3ba87026..87deb8b5a1fd 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml
@@ -21,7 +21,6 @@
android:layout_width="wrap_content"
android:paddingTop="48dp"
android:paddingBottom="48dp"
- android:paddingStart="@dimen/bubble_stack_user_education_side_inset"
android:paddingEnd="16dp"
android:layout_marginEnd="24dp"
android:orientation="vertical"
diff --git a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml
index c5c42fca323d..fafe40e924f5 100644
--- a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml
+++ b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml
@@ -23,7 +23,6 @@
android:clickable="true"
android:paddingTop="28dp"
android:paddingBottom="16dp"
- android:paddingStart="@dimen/bubble_expanded_view_padding"
android:paddingEnd="48dp"
android:layout_marginEnd="24dp"
android:orientation="vertical"
@@ -66,27 +65,21 @@
android:id="@+id/button_layout"
android:orientation="horizontal" >
- <com.android.wm.shell.common.AlphaOptimizedButton
- style="@android:style/Widget.Material.Button.Borderless"
- android:id="@+id/manage"
- android:layout_gravity="start"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:focusable="true"
- android:clickable="false"
- android:text="@string/manage_bubbles_text"
- android:textColor="@android:color/system_neutral1_900"
+ <include
+ layout="@layout/bubble_manage_button"
/>
<com.android.wm.shell.common.AlphaOptimizedButton
- style="@android:style/Widget.Material.Button.Borderless"
+ style="@android:style/Widget.DeviceDefault.Button.Borderless"
android:id="@+id/got_it"
android:layout_gravity="start"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/bubble_manage_button_height"
android:focusable="true"
android:text="@string/bubbles_user_education_got_it"
+ android:textSize="@*android:dimen/text_size_body_2_material"
android:textColor="@android:color/system_neutral1_900"
+ android:background="@drawable/bubble_manage_btn_bg"
/>
</LinearLayout>
</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml
index ed5d2e1b49f5..d732b01ce106 100644
--- a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml
+++ b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml
@@ -22,7 +22,7 @@
<View
style="@style/DockedDividerBackground"
android:id="@+id/docked_divider_background"
- android:background="@color/docked_divider_background"/>
+ android:background="@color/split_divider_background"/>
<com.android.wm.shell.legacysplitscreen.MinimizedDockShadow
style="@style/DockedDividerMinimizedShadow"
diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml
index 7f583f3e6bac..94182cdba0dd 100644
--- a/libs/WindowManager/Shell/res/layout/split_divider.xml
+++ b/libs/WindowManager/Shell/res/layout/split_divider.xml
@@ -19,15 +19,25 @@
android:layout_height="match_parent"
android:layout_width="match_parent">
- <View
- style="@style/DockedDividerBackground"
- android:id="@+id/docked_divider_background"
- android:background="@color/docked_divider_background"/>
-
- <com.android.wm.shell.common.split.DividerHandleView
- style="@style/DockedDividerHandle"
- android:id="@+id/docked_divider_handle"
- android:contentDescription="@string/accessibility_divider"
- android:background="@null"/>
+ <FrameLayout
+ android:id="@+id/divider_bar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <View style="@style/DockedDividerTopLeftRoundCorner"/>
+
+ <View
+ style="@style/DockedDividerBackground"
+ android:id="@+id/docked_divider_background"/>
+
+ <View style="@style/DockedDividerBottomRightRoundCorner"/>
+
+ <com.android.wm.shell.common.split.DividerHandleView
+ style="@style/DockedDividerHandle"
+ android:id="@+id/docked_divider_handle"
+ android:contentDescription="@string/accessibility_divider"
+ android:background="@null"/>
+
+ </FrameLayout>
</com.android.wm.shell.common.split.DividerView>
diff --git a/libs/WindowManager/Shell/res/layout/split_outline.xml b/libs/WindowManager/Shell/res/layout/split_outline.xml
new file mode 100644
index 000000000000..4e2a77f213a0
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/split_outline.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.wm.shell.splitscreen.OutlineRoot
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.wm.shell.splitscreen.OutlineView
+ android:id="@+id/split_outline"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent" />
+
+</com.android.wm.shell.splitscreen.OutlineRoot>
diff --git a/libs/WindowManager/Shell/res/values-land/dimens.xml b/libs/WindowManager/Shell/res/values-land/dimens.xml
index aafba58cef59..a95323fd4801 100644
--- a/libs/WindowManager/Shell/res/values-land/dimens.xml
+++ b/libs/WindowManager/Shell/res/values-land/dimens.xml
@@ -16,8 +16,12 @@
*/
-->
<resources>
+ <!-- Divider handle size for legacy split screen -->
<dimen name="docked_divider_handle_width">2dp</dimen>
<dimen name="docked_divider_handle_height">16dp</dimen>
+ <!-- Divider handle size for split screen -->
+ <dimen name="split_divider_handle_width">3dp</dimen>
+ <dimen name="split_divider_handle_height">72dp</dimen>
<!-- Padding between status bar and bubbles when displayed in expanded state, smaller
value in landscape since we have limited vertical space-->
diff --git a/libs/WindowManager/Shell/res/values-land/styles.xml b/libs/WindowManager/Shell/res/values-land/styles.xml
index 863bb69d4034..e5707f3170d8 100644
--- a/libs/WindowManager/Shell/res/values-land/styles.xml
+++ b/libs/WindowManager/Shell/res/values-land/styles.xml
@@ -19,6 +19,7 @@
<item name="android:layout_width">10dp</item>
<item name="android:layout_height">match_parent</item>
<item name="android:layout_gravity">center_horizontal</item>
+ <item name="android:background">@color/split_divider_background</item>
</style>
<style name="DockedDividerHandle">
@@ -27,6 +28,20 @@
<item name="android:layout_height">96dp</item>
</style>
+ <style name="DockedDividerTopLeftRoundCorner">
+ <item name="android:layout_gravity">center_horizontal|top</item>
+ <item name="android:background">@drawable/split_rounded_top</item>
+ <item name="android:layout_width">@dimen/split_divider_corner_size</item>
+ <item name="android:layout_height">@dimen/split_divider_corner_size</item>
+ </style>
+
+ <style name="DockedDividerBottomRightRoundCorner">
+ <item name="android:layout_gravity">center_horizontal|bottom</item>
+ <item name="android:background">@drawable/split_rounded_bottom</item>
+ <item name="android:layout_width">@dimen/split_divider_corner_size</item>
+ <item name="android:layout_height">@dimen/split_divider_corner_size</item>
+ </style>
+
<style name="DockedDividerMinimizedShadow">
<item name="android:layout_width">8dp</item>
<item name="android:layout_height">match_parent</item>
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 350beafae961..93c0352a2ad3 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -17,7 +17,6 @@
*/
-->
<resources>
- <color name="docked_divider_background">#ff000000</color>
<color name="docked_divider_handle">#ffffff</color>
<drawable name="forced_resizable_background">#59000000</drawable>
<color name="minimize_dock_shadow_start">#60000000</color>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index f28ee820eb35..f85766437b44 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -76,8 +76,15 @@
<!-- How high we lift the divider when touching -->
<dimen name="docked_stack_divider_lift_elevation">4dp</dimen>
+ <!-- Divider handle size for legacy split screen -->
<dimen name="docked_divider_handle_width">16dp</dimen>
<dimen name="docked_divider_handle_height">2dp</dimen>
+ <!-- Divider handle size for split screen -->
+ <dimen name="split_divider_handle_width">72dp</dimen>
+ <dimen name="split_divider_handle_height">3dp</dimen>
+
+ <dimen name="split_divider_bar_width">10dp</dimen>
+ <dimen name="split_divider_corner_size">42dp</dimen>
<!-- One-Handed Mode -->
<!-- Threshold for dragging distance to enable one-handed mode -->
@@ -100,6 +107,8 @@
<dimen name="bubble_flyout_space_from_bubble">8dp</dimen>
<!-- How much space to leave between the flyout text and the avatar displayed in the flyout. -->
<dimen name="bubble_flyout_avatar_message_space">6dp</dimen>
+ <!-- If the screen percentage is smaller than this, we'll use this value instead. -->
+ <dimen name="bubbles_flyout_min_width_large_screen">200dp</dimen>
<!-- Padding between status bar and bubbles when displayed in expanded state -->
<dimen name="bubble_padding_top">16dp</dimen>
<!-- Space between bubbles when expanded. -->
@@ -122,7 +131,7 @@
should also be updated. -->
<dimen name="bubble_expanded_default_height">180dp</dimen>
<!-- On large screens the width of the expanded view is restricted to this size. -->
- <dimen name="bubble_expanded_view_tablet_width">412dp</dimen>
+ <dimen name="bubble_expanded_view_phone_landscape_overflow_width">412dp</dimen>
<!-- Inset to apply to the icon in the overflow button. -->
<dimen name="bubble_overflow_icon_inset">30dp</dimen>
<!-- Default (and minimum) height of bubble overflow -->
@@ -149,9 +158,17 @@
<!-- Extra padding around the dismiss target for bubbles -->
<dimen name="bubble_dismiss_slop">16dp</dimen>
<!-- Height of button allowing users to adjust settings for bubbles. -->
- <dimen name="bubble_manage_button_height">56dp</dimen>
+ <dimen name="bubble_manage_button_height">36dp</dimen>
+ <!-- Height of manage button including margins. -->
+ <dimen name="bubble_manage_button_total_height">68dp</dimen>
+ <!-- The margin around the outside of the manage button. -->
+ <dimen name="bubble_manage_button_margin">16dp</dimen>
<!-- Height of an item in the bubble manage menu. -->
<dimen name="bubble_menu_item_height">60dp</dimen>
+ <!-- Padding applied to the bubble manage menu. -->
+ <dimen name="bubble_menu_padding">16dp</dimen>
+ <!-- Size of the icons in the manage menu. -->
+ <dimen name="bubble_menu_icon_size">24dp</dimen>
<!-- Max width of the message bubble-->
<dimen name="bubble_message_max_width">144dp</dimen>
<!-- Min width of the message bubble -->
@@ -174,14 +191,8 @@
<dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
<dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
<dimen name="bubble_manage_menu_elevation">4dp</dimen>
-
- <!-- Bubbles user education views -->
- <dimen name="bubbles_manage_education_width">160dp</dimen>
- <!-- The inset from the top bound of the manage button to place the user education. -->
- <dimen name="bubbles_manage_education_top_inset">65dp</dimen>
- <!-- Size of padding for the user education cling, this should at minimum be larger than
- individual_bubble_size + some padding. -->
- <dimen name="bubble_stack_user_education_side_inset">72dp</dimen>
+ <!-- Size of user education views on large screens (phone is just match parent). -->
+ <dimen name="bubbles_user_education_width_large_screen">400dp</dimen>
<!-- The width/height of the size compat restart button. -->
<dimen name="size_compat_button_size">48dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index fffcd33f7992..28ff25ae0fbe 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -32,8 +32,23 @@
<style name="DockedDividerBackground">
<item name="android:layout_width">match_parent</item>
- <item name="android:layout_height">10dp</item>
+ <item name="android:layout_height">@dimen/split_divider_bar_width</item>
<item name="android:layout_gravity">center_vertical</item>
+ <item name="android:background">@color/split_divider_background</item>
+ </style>
+
+ <style name="DockedDividerTopLeftRoundCorner">
+ <item name="android:layout_gravity">center_vertical|left</item>
+ <item name="android:background">@drawable/split_rounded_left</item>
+ <item name="android:layout_width">@dimen/split_divider_corner_size</item>
+ <item name="android:layout_height">@dimen/split_divider_corner_size</item>
+ </style>
+
+ <style name="DockedDividerBottomRightRoundCorner">
+ <item name="android:layout_gravity">center_vertical|right</item>
+ <item name="android:background">@drawable/split_rounded_right</item>
+ <item name="android:layout_width">@dimen/split_divider_corner_size</item>
+ <item name="android:layout_height">@dimen/split_divider_corner_size</item>
</style>
<style name="DockedDividerMinimizedShadow">
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 34c66a4f4b82..bf074b0337ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
@@ -97,6 +97,14 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer {
b.setParent(sc);
}
+ public void setPosition(@NonNull SurfaceControl.Transaction tx, int displayId, int x, int y) {
+ final SurfaceControl sc = mLeashes.get(displayId);
+ if (sc == null) {
+ throw new IllegalArgumentException("can't find display" + displayId);
+ }
+ tx.setPosition(sc, x, y);
+ }
+
@Override
public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
@NonNull SurfaceControl leash) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
index 0b941b59b3db..9113c79d40f8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
@@ -103,6 +103,8 @@ public final class ShellCommandHandlerImpl {
return runMoveToSideStage(args, pw);
case "removeFromSideStage":
return runRemoveFromSideStage(args, pw);
+ case "setSideStageOutline":
+ return runSetSideStageOutline(args, pw);
case "setSideStagePosition":
return runSetSideStagePosition(args, pw);
case "setSideStageVisibility":
@@ -161,6 +163,18 @@ public final class ShellCommandHandlerImpl {
return true;
}
+ private boolean runSetSideStageOutline(String[] args, PrintWriter pw) {
+ if (args.length < 3) {
+ // First arguments are "WMShell" and command name.
+ pw.println("Error: whether to enable or disable side stage outline border should be"
+ + " provided as arguments");
+ return false;
+ }
+ final boolean enable = new Boolean(args[2]);
+ mSplitScreenOptional.ifPresent(split -> split.setSideStageOutline(enable));
+ return true;
+ }
+
private boolean runSetSideStagePosition(String[] args, PrintWriter pw) {
if (args.length < 3) {
// First arguments are "WMShell" and command name.
@@ -175,7 +189,7 @@ public final class ShellCommandHandlerImpl {
private boolean runSetSideStageVisibility(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");
+ pw.println("Error: side stage visibility should be provided as arguments");
return false;
}
final Boolean visible = new Boolean(args[2]);
@@ -197,6 +211,8 @@ public final class ShellCommandHandlerImpl {
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.");
pw.println(" setSideStageVisibility <true/false>");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
index d1fbf31e2b99..df4f2383c062 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
@@ -20,10 +20,13 @@ import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCR
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.legacysplitscreen.LegacySplitScreenController;
import com.android.wm.shell.pip.phone.PipTouchHandler;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -38,7 +41,9 @@ import java.util.Optional;
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 Optional<BubbleController> mBubblesOptional;
@@ -47,13 +52,17 @@ public class ShellInitImpl {
private final Optional<AppPairsController> mAppPairsOptional;
private final Optional<PipTouchHandler> mPipTouchHandlerOptional;
private final FullscreenTaskListener mFullscreenTaskListener;
+ private final Optional<FreeformTaskListener> mFreeformTaskListenerOptional;
private final ShellExecutor mMainExecutor;
private final Transitions mTransitions;
private final StartingWindowController mStartingWindow;
private final InitImpl mImpl = new InitImpl();
- public ShellInitImpl(DisplayImeController displayImeController,
+ public ShellInitImpl(
+ DisplayController displayController,
+ DisplayImeController displayImeController,
+ DisplayInsetsController displayInsetsController,
DragAndDropController dragAndDropController,
ShellTaskOrganizer shellTaskOrganizer,
Optional<BubbleController> bubblesOptional,
@@ -62,10 +71,13 @@ public class ShellInitImpl {
Optional<AppPairsController> appPairsOptional,
Optional<PipTouchHandler> pipTouchHandlerOptional,
FullscreenTaskListener fullscreenTaskListener,
+ Optional<Optional<FreeformTaskListener>> freeformTaskListenerOptional,
Transitions transitions,
StartingWindowController startingWindow,
ShellExecutor mainExecutor) {
+ mDisplayController = displayController;
mDisplayImeController = displayImeController;
+ mDisplayInsetsController = displayInsetsController;
mDragAndDropController = dragAndDropController;
mShellTaskOrganizer = shellTaskOrganizer;
mBubblesOptional = bubblesOptional;
@@ -74,6 +86,7 @@ public class ShellInitImpl {
mAppPairsOptional = appPairsOptional;
mFullscreenTaskListener = fullscreenTaskListener;
mPipTouchHandlerOptional = pipTouchHandlerOptional;
+ mFreeformTaskListenerOptional = freeformTaskListenerOptional.flatMap(f -> f);
mTransitions = transitions;
mMainExecutor = mainExecutor;
mStartingWindow = startingWindow;
@@ -84,7 +97,9 @@ public class ShellInitImpl {
}
private void init() {
- // Start listening for display changes
+ // Start listening for display and insets changes
+ mDisplayController.initialize();
+ mDisplayInsetsController.initialize();
mDisplayImeController.startMonitorDisplays();
// Setup the shell organizer
@@ -108,6 +123,11 @@ public class ShellInitImpl {
// 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));
}
@ExternalThread
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 ba0ab6db1003..b5dffba7a0f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -31,6 +31,7 @@ import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
import android.content.Context;
import android.content.LocusId;
+import android.content.pm.ActivityInfo;
import android.graphics.Rect;
import android.os.Binder;
import android.os.IBinder;
@@ -46,6 +47,7 @@ import android.window.TaskOrganizer;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.sizecompatui.SizeCompatUIController;
@@ -71,12 +73,14 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
public static final int TASK_LISTENER_TYPE_FULLSCREEN = -2;
public static final int TASK_LISTENER_TYPE_MULTI_WINDOW = -3;
public static final int TASK_LISTENER_TYPE_PIP = -4;
+ public static final int TASK_LISTENER_TYPE_FREEFORM = -5;
@IntDef(prefix = {"TASK_LISTENER_TYPE_"}, value = {
TASK_LISTENER_TYPE_UNDEFINED,
TASK_LISTENER_TYPE_FULLSCREEN,
TASK_LISTENER_TYPE_MULTI_WINDOW,
TASK_LISTENER_TYPE_PIP,
+ TASK_LISTENER_TYPE_FREEFORM,
})
public @interface TaskListenerType {}
@@ -486,14 +490,40 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
}
@Override
+ public void onSizeCompatRestartButtonAppeared(int taskId) {
+ final TaskAppearedInfo info;
+ synchronized (mLock) {
+ info = mTasks.get(taskId);
+ }
+ if (info == null) {
+ return;
+ }
+ logSizeCompatRestartButtonEventReported(info,
+ FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__APPEARED);
+ }
+
+ @Override
public void onSizeCompatRestartButtonClicked(int taskId) {
final TaskAppearedInfo info;
synchronized (mLock) {
info = mTasks.get(taskId);
}
- if (info != null) {
- restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token);
+ if (info == null) {
+ return;
+ }
+ logSizeCompatRestartButtonEventReported(info,
+ FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__CLICKED);
+ restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token);
+ }
+
+ private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
+ int event) {
+ ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
+ if (topActivityInfo == null) {
+ return;
}
+ FrameworkStatsLog.write(FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED,
+ topActivityInfo.applicationInfo.uid, event);
}
/**
@@ -572,6 +602,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
case WINDOWING_MODE_PINNED:
return TASK_LISTENER_TYPE_PIP;
case WINDOWING_MODE_FREEFORM:
+ return TASK_LISTENER_TYPE_FREEFORM;
case WINDOWING_MODE_UNDEFINED:
default:
return TASK_LISTENER_TYPE_UNDEFINED;
@@ -586,6 +617,8 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
return "TASK_LISTENER_TYPE_MULTI_WINDOW";
case TASK_LISTENER_TYPE_PIP:
return "TASK_LISTENER_TYPE_PIP";
+ case TASK_LISTENER_TYPE_FREEFORM:
+ return "TASK_LISTENER_TYPE_FREEFORM";
case TASK_LISTENER_TYPE_UNDEFINED:
return "TASK_LISTENER_TYPE_UNDEFINED";
default:
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 1861e48482b8..2f3214d1d1ab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
@@ -40,6 +40,8 @@ import android.view.ViewTreeObserver;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
import java.io.PrintWriter;
import java.util.concurrent.Executor;
@@ -74,6 +76,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
private final ShellTaskOrganizer mTaskOrganizer;
private final Executor mShellExecutor;
+ private final SyncTransactionQueue mSyncQueue;
private ActivityManager.RunningTaskInfo mTaskInfo;
private WindowContainerToken mTaskToken;
@@ -89,11 +92,12 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
private final Rect mTmpRootRect = new Rect();
private final int[] mTmpLocation = new int[2];
- public TaskView(Context context, ShellTaskOrganizer organizer) {
+ public TaskView(Context context, ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) {
super(context, null, 0, 0, true /* disableBackgroundLayer */);
mTaskOrganizer = organizer;
mShellExecutor = organizer.getExecutor();
+ mSyncQueue = syncQueue;
setUseAlpha();
getHolder().addCallback(this);
mGuard.open("release");
@@ -189,8 +193,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setBounds(mTaskToken, mTmpRect);
- // TODO(b/151449487): Enable synchronization
- mTaskOrganizer.applyTransaction(wct);
+ mSyncQueue.queue(wct);
}
/**
@@ -236,14 +239,16 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
private void updateTaskVisibility() {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */);
- mTaskOrganizer.applyTransaction(wct);
- // TODO(b/151449487): Only call callback once we enable synchronization
- if (mListener != null) {
- final int taskId = mTaskInfo.taskId;
+ mSyncQueue.queue(wct);
+ if (mListener == null) {
+ return;
+ }
+ int taskId = mTaskInfo.taskId;
+ mSyncQueue.runInSync((t) -> {
mListenerExecutor.execute(() -> {
mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated);
});
- }
+ });
}
@Override
@@ -264,10 +269,12 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
updateTaskVisibility();
}
mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true);
- // TODO: Synchronize show with the resize
onLocationChanged();
if (taskInfo.taskDescription != null) {
- setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor());
+ int backgroundColor = taskInfo.taskDescription.getBackgroundColor();
+ mSyncQueue.runInSync((t) -> {
+ setResizeBackgroundColor(t, backgroundColor);
+ });
}
if (mListener != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java
index 58ca1fbaba24..8286d102791e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java
@@ -20,8 +20,8 @@ import android.annotation.UiContext;
import android.content.Context;
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.ShellMainThread;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -30,12 +30,14 @@ import java.util.function.Consumer;
public class TaskViewFactoryController {
private final ShellTaskOrganizer mTaskOrganizer;
private final ShellExecutor mShellExecutor;
+ private final SyncTransactionQueue mSyncQueue;
private final TaskViewFactory mImpl = new TaskViewFactoryImpl();
public TaskViewFactoryController(ShellTaskOrganizer taskOrganizer,
- ShellExecutor shellExecutor) {
+ ShellExecutor shellExecutor, SyncTransactionQueue syncQueue) {
mTaskOrganizer = taskOrganizer;
mShellExecutor = shellExecutor;
+ mSyncQueue = syncQueue;
}
public TaskViewFactory asTaskViewFactory() {
@@ -44,7 +46,7 @@ public class TaskViewFactoryController {
/** Creates an {@link TaskView} */
public void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate) {
- TaskView taskView = new TaskView(context, mTaskOrganizer);
+ TaskView taskView = new TaskView(context, mTaskOrganizer, mSyncQueue);
executor.execute(() -> {
onCreate.accept(taskView);
});
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 8aca01d2467b..2aead9392e59 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
@@ -62,4 +62,10 @@ public class Interpolators {
*/
public static final Interpolator PANEL_CLOSE_ACCELERATED =
new PathInterpolator(0.3f, 0, 0.5f, 1);
+
+ public static final PathInterpolator SLOWDOWN_INTERPOLATOR =
+ new PathInterpolator(0.5f, 1f, 0.5f, 1f);
+
+ public static final PathInterpolator DIM_INTERPOLATOR =
+ new PathInterpolator(.23f, .87f, .52f, -0.11f);
}
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
index e6d088e6537d..3800b8d234f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
@@ -19,6 +19,7 @@ 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 android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
@@ -181,12 +182,13 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou
// TODO: Is there more we need to do here?
mSyncQueue.runInSync(t -> {
- t.setLayer(dividerLeash, Integer.MAX_VALUE)
+ t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER)
.setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x,
mTaskInfo1.positionInParent.y)
.setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x,
mTaskInfo2.positionInParent.y)
.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
+ .show(dividerLeash)
.show(mRootTaskLeash)
.show(mTaskLeash1)
.show(mTaskLeash2);
@@ -212,9 +214,12 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou
}
mRootTaskInfo = taskInfo;
- if (mSplitLayout != null
- && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) {
- onBoundsChanged(mSplitLayout);
+ if (mSplitLayout != null) {
+ if (mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) {
+ onLayoutChanged(mSplitLayout);
+ }
+ // updateConfiguration re-inits the dividerbar, so show it now
+ mSyncQueue.runInSync(t -> t.show(mSplitLayout.getDividerLeash()));
}
} else if (taskInfo.taskId == getTaskId1()) {
mTaskInfo1 = taskInfo;
@@ -295,17 +300,24 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou
}
@Override
- public void onBoundsChanging(SplitLayout layout) {
+ public void onLayoutChanging(SplitLayout layout) {
mSyncQueue.runInSync(t ->
layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2));
}
@Override
- public void onBoundsChanged(SplitLayout layout) {
+ public void onLayoutChanged(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));
}
+
+ @Override
+ public void onLayoutShifted(int offsetX, int offsetY, SplitLayout layout) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ layout.applyLayoutShifted(wct, offsetX, offsetY, mTaskInfo1, mTaskInfo2);
+ mController.getTaskOrganizer().applyTransaction(wct);
+ }
}
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 09fcb86e56de..95b80df7fcbd 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
@@ -56,7 +56,6 @@ import android.graphics.Rect;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
-import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
@@ -85,6 +84,7 @@ 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;
+import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
@@ -97,7 +97,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
-import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
@@ -137,6 +136,7 @@ public class BubbleController {
private final TaskStackListenerImpl mTaskStackListener;
private final ShellTaskOrganizer mTaskOrganizer;
private final DisplayController mDisplayController;
+ private final SyncTransactionQueue mSyncQueue;
// Used to post to main UI thread
private final ShellExecutor mMainExecutor;
@@ -209,7 +209,8 @@ public class BubbleController {
ShellTaskOrganizer organizer,
DisplayController displayController,
ShellExecutor mainExecutor,
- Handler mainHandler) {
+ Handler mainHandler,
+ SyncTransactionQueue syncQueue) {
BubbleLogger logger = new BubbleLogger(uiEventLogger);
BubblePositioner positioner = new BubblePositioner(context, windowManager);
BubbleData data = new BubbleData(context, logger, positioner, mainExecutor);
@@ -217,7 +218,7 @@ public class BubbleController {
new BubbleDataRepository(context, launcherApps, mainExecutor),
statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
logger, taskStackListener, organizer, positioner, displayController, mainExecutor,
- mainHandler);
+ mainHandler, syncQueue);
}
/**
@@ -239,7 +240,8 @@ public class BubbleController {
BubblePositioner positioner,
DisplayController displayController,
ShellExecutor mainExecutor,
- Handler mainHandler) {
+ Handler mainHandler,
+ SyncTransactionQueue syncQueue) {
mContext = context;
mLauncherApps = launcherApps;
mBarService = statusBarService == null
@@ -262,6 +264,7 @@ public class BubbleController {
mSavedBubbleKeysPerUser = new SparseSetArray<>();
mBubbleIconFactory = new BubbleIconFactory(context);
mDisplayController = displayController;
+ mSyncQueue = syncQueue;
}
public void initialize() {
@@ -561,6 +564,10 @@ public class BubbleController {
return mTaskOrganizer;
}
+ SyncTransactionQueue getSyncTransactionQueue() {
+ return mSyncQueue;
+ }
+
/** Contains information to help position things on the screen. */
BubblePositioner getPositioner() {
return mBubblePositioner;
@@ -572,7 +579,7 @@ public class BubbleController {
/**
* BubbleStackView is lazily created by this method the first time a Bubble is added. This
- * method initializes the stack view and adds it to the StatusBar just above the scrim.
+ * method initializes the stack view and adds it to window manager.
*/
private void ensureStackViewCreated() {
if (mStackView == null) {
@@ -620,7 +627,6 @@ public class BubbleController {
try {
mAddedToWindowManager = true;
mBubbleData.getOverflow().initialize(this);
- mStackView.addView(mBubbleScrim);
mWindowManager.addView(mStackView, mWmLayoutParams);
// Position info is dependent on us being attached to a window
mBubblePositioner.update();
@@ -630,10 +636,16 @@ public class BubbleController {
}
}
- /** For the overflow to be focusable & receive key events the flags must be update. **/
- void updateWindowFlagsForOverflow(boolean showingOverflow) {
+ /**
+ * In some situations bubble's should be able to receive key events for back:
+ * - when the bubble overflow is showing
+ * - when the user education for the stack is showing.
+ *
+ * @param interceptBack whether back should be intercepted or not.
+ */
+ void updateWindowFlagsForBackpress(boolean interceptBack) {
if (mStackView != null && mAddedToWindowManager) {
- mWmLayoutParams.flags = showingOverflow
+ mWmLayoutParams.flags = interceptBack
? 0
: WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
@@ -652,7 +664,6 @@ public class BubbleController {
mAddedToWindowManager = false;
if (mStackView != null) {
mWindowManager.removeView(mStackView);
- mStackView.removeView(mBubbleScrim);
mBubbleData.getOverflow().cleanUpExpandedState();
} else {
Log.w(TAG, "StackView added to WindowManager, but was null when removing!");
@@ -754,13 +765,6 @@ public class BubbleController {
}
}
- private void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) {
- mBubbleScrim = view;
- callback.accept(mMainExecutor, mMainExecutor.executeBlockingForResult(() -> {
- return Looper.myLooper();
- }, Looper.class));
- }
-
private void setSysuiProxy(Bubbles.SysuiProxy proxy) {
mSysuiProxy = proxy;
}
@@ -897,8 +901,7 @@ public class BubbleController {
* Fills the overflow bubbles by loading them from disk.
*/
void loadOverflowBubblesFromDisk() {
- if (!mBubbleData.getOverflowBubbles().isEmpty() && !mOverflowDataLoadNeeded) {
- // we don't need to load overflow bubbles from disk if it is already in memory
+ if (!mOverflowDataLoadNeeded) {
return;
}
mOverflowDataLoadNeeded = false;
@@ -1566,13 +1569,6 @@ public class BubbleController {
}
@Override
- public void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) {
- mMainExecutor.execute(() -> {
- BubbleController.this.setBubbleScrim(view, callback);
- });
- }
-
- @Override
public void setExpandListener(BubbleExpandListener listener) {
mMainExecutor.execute(() -> {
BubbleController.this.setExpandListener(listener);
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 d73ce6951e6d..b48bda3a6e48 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
@@ -699,10 +699,9 @@ public class BubbleData {
if (DEBUG_BUBBLE_DATA) {
Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
}
- if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) {
+ if (Objects.equals(bubble, mSelectedBubble)) {
return;
}
- // Otherwise, if we are showing the overflow menu, return to the previously selected bubble.
boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey());
if (bubble != null
&& !mBubbles.contains(bubble)
@@ -771,6 +770,10 @@ public class BubbleData {
Log.e(TAG, "Attempt to expand stack without selected bubble!");
return;
}
+ if (mSelectedBubble.getKey().equals(mOverflow.getKey()) && !mBubbles.isEmpty()) {
+ // Show previously selected bubble instead of overflow menu when expanding.
+ setSelectedBubbleInternal(mBubbles.get(0));
+ }
if (mSelectedBubble instanceof Bubble) {
((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
}
@@ -779,16 +782,6 @@ public class BubbleData {
// Apply ordering and grouping rules from expanded -> collapsed, then save
// the result.
mStateChange.orderChanged |= repackAll();
- // Save the state which should be returned to when expanded (with no other changes)
-
- if (mShowingOverflow) {
- // Show previously selected bubble instead of overflow menu on next expansion.
- if (!mSelectedBubble.getKey().equals(mOverflow.getKey())) {
- setSelectedBubbleInternal(mSelectedBubble);
- } else {
- setSelectedBubbleInternal(mBubbles.get(0));
- }
- }
if (mBubbles.indexOf(mSelectedBubble) > 0) {
// Move the selected bubble to the top while collapsed.
int index = mBubbles.indexOf(mSelectedBubble);
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 9687ec6a8168..7d7bfb2a92a3 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
@@ -25,6 +25,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;
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.MAX_HEIGHT;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
@@ -60,7 +61,6 @@ import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.R;
import com.android.wm.shell.TaskView;
import com.android.wm.shell.common.AlphaOptimizedButton;
@@ -77,7 +77,6 @@ public class BubbleExpandedView extends LinearLayout {
// The triangle pointing to the expanded view
private View mPointerView;
- private int mPointerMargin;
@Nullable private int[] mExpandedViewContainerLocation;
private AlphaOptimizedButton mManageButton;
@@ -102,9 +101,6 @@ public class BubbleExpandedView extends LinearLayout {
*/
private boolean mIsAlphaAnimating = false;
- private int mMinHeight;
- private int mOverflowHeight;
- private int mManageButtonHeight;
private int mPointerWidth;
private int mPointerHeight;
private float mPointerRadius;
@@ -232,7 +228,7 @@ public class BubbleExpandedView extends LinearLayout {
@Override
public void onBackPressedOnTaskRoot(int taskId) {
if (mTaskId == taskId && mStackView.isExpanded()) {
- mController.collapseStack();
+ mStackView.onBackPressed();
}
}
};
@@ -338,7 +334,8 @@ public class BubbleExpandedView extends LinearLayout {
bringChildToFront(mOverflowView);
mManageButton.setVisibility(GONE);
} else {
- mTaskView = new TaskView(mContext, mController.getTaskOrganizer());
+ mTaskView = new TaskView(mContext, mController.getTaskOrganizer(),
+ mController.getSyncTransactionQueue());
mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener);
mExpandedViewContainer.addView(mTaskView);
bringChildToFront(mTaskView);
@@ -347,12 +344,8 @@ public class BubbleExpandedView extends LinearLayout {
void updateDimensions() {
Resources res = getResources();
- mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
- mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
-
updateFontSize();
- mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
mPointerRadius = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_radius);
@@ -368,7 +361,6 @@ public class BubbleExpandedView extends LinearLayout {
updatePointerView();
}
- mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_height);
if (mManageButton != null) {
int visibility = mManageButton.getVisibility();
removeView(mManageButton);
@@ -632,12 +624,11 @@ public class BubbleExpandedView extends LinearLayout {
}
if ((mBubble != null && mTaskView != null) || mIsOverflow) {
- float desiredHeight = mIsOverflow
- ? mPositioner.isLargeScreen() ? getMaxExpandedHeight() : mOverflowHeight
- : mBubble.getDesiredHeight(mContext);
- desiredHeight = Math.max(desiredHeight, mMinHeight);
- float height = Math.min(desiredHeight, getMaxExpandedHeight());
- height = Math.max(height, mMinHeight);
+ float desiredHeight = mPositioner.getExpandedViewHeight(mBubble);
+ int maxHeight = mPositioner.getMaxExpandedViewHeight(mIsOverflow);
+ float height = desiredHeight == MAX_HEIGHT
+ ? maxHeight
+ : Math.min(desiredHeight, maxHeight);
FrameLayout.LayoutParams lp = mIsOverflow
? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams()
: (FrameLayout.LayoutParams) mTaskView.getLayoutParams();
@@ -661,23 +652,6 @@ public class BubbleExpandedView extends LinearLayout {
}
}
- private int getMaxExpandedHeight() {
- int expandedContainerY = mExpandedViewContainerLocation != null
- // Remove top insets back here because availableRect.height would account for that
- ? mExpandedViewContainerLocation[1] - mPositioner.getInsets().top
- : 0;
- int settingsHeight = mIsOverflow ? 0 : mManageButtonHeight;
- int pointerHeight = mPositioner.showBubblesVertically()
- ? mPointerWidth
- : (int) (mPointerHeight - mPointerOverlap + mPointerMargin);
- return mPositioner.getAvailableRect().height()
- - expandedContainerY
- - getPaddingTop()
- - getPaddingBottom()
- - settingsHeight
- - pointerHeight;
- }
-
/**
* Update appearance of the expanded view being displayed.
*
@@ -722,19 +696,18 @@ public class BubbleExpandedView extends LinearLayout {
? mPointerHeight - mPointerOverlap
: 0;
final float paddingRight = (showVertically && !onLeft)
- ? mPointerHeight - mPointerOverlap : 0;
- final float paddingTop = showVertically ? 0
+ ? mPointerHeight - mPointerOverlap
+ : 0;
+ final float paddingTop = showVertically
+ ? 0
: mPointerHeight - mPointerOverlap;
setPadding((int) paddingLeft, (int) paddingTop, (int) paddingRight, 0);
- final float expandedViewY = mPositioner.getExpandedViewY();
- // TODO: I don't understand why it works but it does - why normalized in portrait
- // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation?
- final float normalizedSize = IconNormalizer.getNormalizedCircleSize(
- mPositioner.getBubbleSize());
- final float bubbleCenter = showVertically
- ? bubblePosition + (mPositioner.getBubbleSize() / 2f) - expandedViewY
- : bubblePosition + (normalizedSize / 2f) - mPointerWidth;
+ // Subtract the expandedViewY here because the pointer is placed within the expandedView.
+ float pointerPosition = mPositioner.getPointerPosition(bubblePosition);
+ final float bubbleCenter = mPositioner.showBubblesVertically()
+ ? pointerPosition - mPositioner.getExpandedViewY(mBubble, bubblePosition)
+ : pointerPosition;
// Post because we need the width of the view
post(() -> {
float pointerY;
@@ -764,6 +737,10 @@ public class BubbleExpandedView extends LinearLayout {
mManageButton.getBoundsOnScreen(rect);
}
+ public int getManageButtonMargin() {
+ return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart();
+ }
+
/**
* Cleans up anything related to the task and {@code TaskView}. If this view should be reused
* after this method is called, then
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
index 35a4f33ecf72..9374da4c4fab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
@@ -56,9 +56,6 @@ import com.android.wm.shell.common.TriangleShape;
* transform into the 'new' dot, which is used during flyout dismiss animations/gestures.
*/
public class BubbleFlyoutView extends FrameLayout {
- /** Max width of the flyout, in terms of percent of the screen width. */
- private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f;
-
/** Translation Y of fade animation. */
private static final float FLYOUT_FADE_Y = 40f;
@@ -68,6 +65,8 @@ public class BubbleFlyoutView extends FrameLayout {
// Whether the flyout view should show a pointer to the bubble.
private static final boolean SHOW_POINTER = false;
+ private BubblePositioner mPositioner;
+
private final int mFlyoutPadding;
private final int mFlyoutSpaceFromBubble;
private final int mPointerSize;
@@ -156,10 +155,11 @@ public class BubbleFlyoutView extends FrameLayout {
/** Callback to run when the flyout is hidden. */
@Nullable private Runnable mOnHide;
- public BubbleFlyoutView(Context context) {
+ public BubbleFlyoutView(Context context, BubblePositioner positioner) {
super(context);
- LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true);
+ mPositioner = positioner;
+ LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true);
mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container);
mSenderText = findViewById(R.id.bubble_flyout_name);
mSenderAvatar = findViewById(R.id.bubble_flyout_avatar);
@@ -230,11 +230,11 @@ public class BubbleFlyoutView extends FrameLayout {
/*
* Fade animation for consecutive flyouts.
*/
- void animateUpdate(Bubble.FlyoutMessage flyoutMessage, float parentWidth, PointF stackPos,
+ void animateUpdate(Bubble.FlyoutMessage flyoutMessage, PointF stackPos,
boolean hideDot, Runnable onHide) {
mOnHide = onHide;
final Runnable afterFadeOut = () -> {
- updateFlyoutMessage(flyoutMessage, parentWidth);
+ updateFlyoutMessage(flyoutMessage);
// Wait for TextViews to layout with updated height.
post(() -> {
fade(true /* in */, stackPos, hideDot, () -> {} /* after */);
@@ -266,7 +266,7 @@ public class BubbleFlyoutView extends FrameLayout {
.withEndAction(afterFade);
}
- private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage, float parentWidth) {
+ private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage) {
final Drawable senderAvatar = flyoutMessage.senderAvatar;
if (senderAvatar != null && flyoutMessage.isGroupChat) {
mSenderAvatar.setVisibility(VISIBLE);
@@ -278,8 +278,7 @@ public class BubbleFlyoutView extends FrameLayout {
mSenderText.setTranslationX(0);
}
- final int maxTextViewWidth =
- (int) (parentWidth * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2;
+ final int maxTextViewWidth = (int) mPositioner.getMaxFlyoutSize() - mFlyoutPadding * 2;
// Name visibility
if (!TextUtils.isEmpty(flyoutMessage.senderName)) {
@@ -328,22 +327,20 @@ public class BubbleFlyoutView extends FrameLayout {
void setupFlyoutStartingAsDot(
Bubble.FlyoutMessage flyoutMessage,
PointF stackPos,
- float parentWidth,
boolean arrowPointingLeft,
int dotColor,
@Nullable Runnable onLayoutComplete,
@Nullable Runnable onHide,
float[] dotCenter,
- boolean hideDot,
- BubblePositioner positioner) {
+ boolean hideDot) {
- mBubbleSize = positioner.getBubbleSize();
+ mBubbleSize = mPositioner.getBubbleSize();
mOriginalDotSize = SIZE_PERCENTAGE * mBubbleSize;
mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f;
mNewDotSize = mNewDotRadius * 2f;
- updateFlyoutMessage(flyoutMessage, parentWidth);
+ updateFlyoutMessage(flyoutMessage);
mArrowPointingLeft = arrowPointingLeft;
mDotColor = dotColor;
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 ede42285d9cd..5e9d97f23c57 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
@@ -142,7 +142,7 @@ public class BubbleOverflowContainerView extends LinearLayout {
super.onAttachedToWindow();
if (mController != null) {
// For the overflow to get key events (e.g. back press) we need to adjust the flags
- mController.updateWindowFlagsForOverflow(true);
+ mController.updateWindowFlagsForBackpress(true);
}
setOnKeyListener(mKeyListener);
}
@@ -151,7 +151,7 @@ public class BubbleOverflowContainerView extends LinearLayout {
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mController != null) {
- mController.updateWindowFlagsForOverflow(false);
+ mController.updateWindowFlagsForBackpress(false);
}
setOnKeyListener(null);
}
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 c600f56ba0c5..306224bd316c 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
@@ -34,6 +34,7 @@ import android.view.WindowMetrics;
import androidx.annotation.VisibleForTesting;
+import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.R;
import java.lang.annotation.Retention;
@@ -58,23 +59,40 @@ public class BubblePositioner {
/** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/
public static final int NUM_VISIBLE_WHEN_RESTING = 2;
+ /** Indicates a bubble's height should be the maximum available space. **/
+ public static final int MAX_HEIGHT = -1;
+ /** The max percent of screen width to use for the flyout on large screens. */
+ public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f;
+ /** The max percent of screen width to use for the flyout on phone. */
+ public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f;
+ /** The percent of screen width that should be used for the expanded view on a large screen. **/
+ public static final float EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT = 0.72f;
private Context mContext;
private WindowManager mWindowManager;
private Rect mPositionRect;
+ private Rect mScreenRect;
private @Surface.Rotation int mRotation = Surface.ROTATION_0;
private Insets mInsets;
private int mDefaultMaxBubbles;
private int mMaxBubbles;
private int mBubbleSize;
- private int mBubbleBadgeSize;
private int mSpacingBetweenBubbles;
+
+ private int mExpandedViewMinHeight;
private int mExpandedViewLargeScreenWidth;
+ private int mExpandedViewLargeScreenInset;
+
+ private int mOverflowWidth;
private int mExpandedViewPadding;
private int mPointerMargin;
- private float mPointerWidth;
- private float mPointerHeight;
+ private int mPointerWidth;
+ private int mPointerHeight;
+ private int mPointerOverlap;
+ private int mManageButtonHeight;
+ private int mOverflowHeight;
+ private int mMinimumFlyoutWidthLargeScreen;
private PointF mPinLocation;
private PointF mRestingStackPosition;
@@ -143,6 +161,7 @@ public class BubblePositioner {
mRotation = rotation;
mInsets = insets;
+ mScreenRect = new Rect(bounds);
mPositionRect = new Rect(bounds);
mPositionRect.left += mInsets.left;
mPositionRect.top += mInsets.top;
@@ -151,16 +170,27 @@ public class BubblePositioner {
Resources res = mContext.getResources();
mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
- mBubbleBadgeSize = res.getDimensionPixelSize(R.dimen.bubble_badge_size);
mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
-
- mExpandedViewLargeScreenWidth = res.getDimensionPixelSize(
- R.dimen.bubble_expanded_view_tablet_width);
mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
+ mExpandedViewLargeScreenWidth = (int) (bounds.width()
+ * EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT);
+ mExpandedViewLargeScreenInset = mIsLargeScreen
+ ? (bounds.width() - mExpandedViewLargeScreenWidth) / 2
+ : mExpandedViewPadding;
+ mOverflowWidth = mIsLargeScreen
+ ? mExpandedViewLargeScreenWidth
+ : res.getDimensionPixelSize(
+ R.dimen.bubble_expanded_view_phone_landscape_overflow_width);
mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
+ mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap);
+ mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_total_height);
+ mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
+ mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
+ mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize(
+ R.dimen.bubbles_flyout_min_width_large_screen);
mMaxBubbles = calculateMaxBubbles();
@@ -225,6 +255,13 @@ public class BubblePositioner {
}
/**
+ * @return a rect of the screen size.
+ */
+ public Rect getScreenRect() {
+ return mScreenRect;
+ }
+
+ /**
* @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its
* inset is not included here.
*/
@@ -266,46 +303,226 @@ public class BubblePositioner {
}
/**
- * Calculates the left & right padding for the bubble expanded view.
+ * Calculates the padding for the bubble expanded view.
*
- * On larger screens the width of the expanded view is restricted via this padding.
- * On landscape the bubble overflow expanded view is also restricted via this padding.
+ * Some specifics:
+ * On large screens the width of the expanded view is restricted via this padding.
+ * On phone landscape the bubble overflow expanded view is also restricted via this padding.
+ * On large screens & landscape no top padding is set, the top position is set via translation.
+ * On phone portrait top padding is set as the space between the tip of the pointer and the
+ * bubble.
+ * When the overflow is shown it doesn't have the manage button to pad out the bottom so
+ * padding is added.
*/
- public int[] getExpandedViewPadding(boolean onLeft, boolean isOverflow) {
- int leftPadding = mInsets.left + mExpandedViewPadding;
- int rightPadding = mInsets.right + mExpandedViewPadding;
- final boolean isLargeOrOverflow = mIsLargeScreen || isOverflow;
- if (showBubblesVertically()) {
- if (!onLeft) {
- rightPadding += mBubbleSize - mPointerHeight;
- leftPadding += isLargeOrOverflow
- ? (mPositionRect.width() - rightPadding - mExpandedViewLargeScreenWidth)
- : 0;
- } else {
- leftPadding += mBubbleSize - mPointerHeight;
- rightPadding += isLargeOrOverflow
- ? (mPositionRect.width() - leftPadding - mExpandedViewLargeScreenWidth)
- : 0;
+ public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) {
+ final int pointerTotalHeight = mPointerHeight - mPointerOverlap;
+ if (mIsLargeScreen) {
+ // [left, top, right, bottom]
+ mPaddings[0] = onLeft
+ ? mExpandedViewLargeScreenInset - pointerTotalHeight
+ : mExpandedViewLargeScreenInset;
+ mPaddings[1] = 0;
+ mPaddings[2] = onLeft
+ ? mExpandedViewLargeScreenInset
+ : mExpandedViewLargeScreenInset - pointerTotalHeight;
+ // Overflow doesn't show manage button / get padding from it so add padding here for it
+ mPaddings[3] = isOverflow ? mExpandedViewPadding : 0;
+ return mPaddings;
+ } else {
+ int leftPadding = mInsets.left + mExpandedViewPadding;
+ int rightPadding = mInsets.right + mExpandedViewPadding;
+ final float expandedViewWidth = isOverflow
+ ? mOverflowWidth
+ : mExpandedViewLargeScreenWidth;
+ if (showBubblesVertically()) {
+ if (!onLeft) {
+ rightPadding += mBubbleSize - pointerTotalHeight;
+ leftPadding += isOverflow
+ ? (mPositionRect.width() - rightPadding - expandedViewWidth)
+ : 0;
+ } else {
+ leftPadding += mBubbleSize - pointerTotalHeight;
+ rightPadding += isOverflow
+ ? (mPositionRect.width() - leftPadding - expandedViewWidth)
+ : 0;
+ }
}
+ // [left, top, right, bottom]
+ mPaddings[0] = leftPadding;
+ mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
+ mPaddings[2] = rightPadding;
+ mPaddings[3] = 0;
+ return mPaddings;
}
- // [left, top, right, bottom]
- mPaddings[0] = leftPadding;
- mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
- mPaddings[2] = rightPadding;
- mPaddings[3] = 0;
- return mPaddings;
}
- /** Calculates the y position of the expanded view when it is expanded. */
- public float getExpandedViewY() {
+ /** Gets the y position of the expanded view if it was top-aligned. */
+ private float getExpandedViewYTopAligned() {
final int top = getAvailableRect().top;
if (showBubblesVertically()) {
- return top - mPointerWidth;
+ return top - mPointerWidth + mExpandedViewPadding;
} else {
return top + mBubbleSize + mPointerMargin;
}
}
+ public float getExpandedBubblesY() {
+ return getAvailableRect().top + mExpandedViewPadding;
+ }
+
+ /**
+ * Calculate the maximum height the expanded view can be depending on where it's placed on
+ * the screen and the size of the elements around it (e.g. padding, pointer, manage button).
+ */
+ public int getMaxExpandedViewHeight(boolean isOverflow) {
+ // Subtract top insets because availableRect.height would account for that
+ int expandedContainerY = (int) getExpandedViewYTopAligned() - getInsets().top;
+ int paddingTop = showBubblesVertically()
+ ? 0
+ : mPointerHeight;
+ // Subtract pointer size because it's laid out in LinearLayout with the expanded view.
+ int pointerSize = showBubblesVertically()
+ ? mPointerWidth
+ : (mPointerHeight + mPointerMargin);
+ int bottomPadding = isOverflow ? mExpandedViewPadding : mManageButtonHeight;
+ return getAvailableRect().height()
+ - expandedContainerY
+ - paddingTop
+ - pointerSize
+ - bottomPadding;
+ }
+
+ /**
+ * Determines the height for the bubble, ensuring a minimum height. If the height should be as
+ * big as available, returns {@link #MAX_HEIGHT}.
+ */
+ public float getExpandedViewHeight(BubbleViewProvider bubble) {
+ boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
+ if (isOverflow && showBubblesVertically() && !mIsLargeScreen) {
+ // overflow in landscape on phone is max
+ return MAX_HEIGHT;
+ }
+ float desiredHeight = isOverflow
+ ? mOverflowHeight
+ : ((Bubble) bubble).getDesiredHeight(mContext);
+ desiredHeight = Math.max(desiredHeight, mExpandedViewMinHeight);
+ if (desiredHeight > getMaxExpandedViewHeight(isOverflow)) {
+ return MAX_HEIGHT;
+ }
+ return desiredHeight;
+ }
+
+ /**
+ * Gets the y position for the expanded view. This is the position on screen of the top
+ * horizontal line of the expanded view.
+ *
+ * @param bubble the bubble being positioned.
+ * @param bubblePosition the x position of the bubble if showing on top, the y position of the
+ * bubble if showing vertically.
+ * @return the y position for the expanded view.
+ */
+ public float getExpandedViewY(BubbleViewProvider bubble, float bubblePosition) {
+ boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
+ float expandedViewHeight = getExpandedViewHeight(bubble);
+ float topAlignment = getExpandedViewYTopAligned();
+ if (!showBubblesVertically() || expandedViewHeight == MAX_HEIGHT) {
+ // Top-align when bubbles are shown at the top or are max size.
+ return topAlignment;
+ }
+ // If we're here, we're showing vertically & developer has made height less than maximum.
+ int manageButtonHeight = isOverflow ? mExpandedViewPadding : mManageButtonHeight;
+ float pointerPosition = getPointerPosition(bubblePosition);
+ float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight;
+ float topIfCentered = pointerPosition - (expandedViewHeight / 2);
+ if (topIfCentered > mPositionRect.top && mPositionRect.bottom > bottomIfCentered) {
+ // Center it
+ return pointerPosition - mPointerWidth - (expandedViewHeight / 2f);
+ } else if (topIfCentered <= mPositionRect.top) {
+ // Top align
+ return topAlignment;
+ } else {
+ // Bottom align
+ return mPositionRect.bottom - manageButtonHeight - expandedViewHeight - mPointerWidth;
+ }
+ }
+
+ /**
+ * The position the pointer points to, the center of the bubble.
+ *
+ * @param bubblePosition the x position of the bubble if showing on top, the y position of the
+ * bubble if showing vertically.
+ * @return the position the tip of the pointer points to. The x position if showing on top, the
+ * y position if showing vertically.
+ */
+ public float getPointerPosition(float bubblePosition) {
+ // TODO: I don't understand why it works but it does - why normalized in portrait
+ // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation?
+ final float normalizedSize = IconNormalizer.getNormalizedCircleSize(
+ getBubbleSize());
+ return showBubblesVertically()
+ ? bubblePosition + (getBubbleSize() / 2f)
+ : bubblePosition + (normalizedSize / 2f) - mPointerWidth;
+ }
+
+ /**
+ * Returns the position of the bubble on-screen when the stack is expanded.
+ *
+ * @param index the index of the bubble in the stack.
+ * @param numberOfBubbles the total number of bubbles in the stack.
+ * @param onLeftEdge whether the stack would rest on the left edge of the screen when collapsed.
+ * @return the x, y position of the bubble on-screen when the stack is expanded.
+ */
+ public PointF getExpandedBubbleXY(int index, int numberOfBubbles, boolean onLeftEdge) {
+ final float positionInRow = index * (mBubbleSize + mSpacingBetweenBubbles);
+ final float expandedStackSize = (numberOfBubbles * mBubbleSize)
+ + ((numberOfBubbles - 1) * mSpacingBetweenBubbles);
+ 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()) {
+ y = rowStart + positionInRow;
+ int left = mIsLargeScreen
+ ? mExpandedViewLargeScreenInset - mExpandedViewPadding - mBubbleSize
+ : mPositionRect.left;
+ int right = mIsLargeScreen
+ ? mPositionRect.right - mExpandedViewLargeScreenInset + mExpandedViewPadding
+ : mPositionRect.right - mBubbleSize;
+ x = onLeftEdge
+ ? left
+ : right;
+ } else {
+ y = mPositionRect.top + mExpandedViewPadding;
+ x = rowStart + positionInRow;
+ }
+ return new PointF(x, y);
+ }
+
+ /**
+ * @return the width of the bubble flyout (message originating from the bubble).
+ */
+ public float getMaxFlyoutSize() {
+ if (isLargeScreen()) {
+ return Math.max(mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN,
+ mMinimumFlyoutWidthLargeScreen);
+ }
+ return mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT;
+ }
+
+ /**
+ * @return whether the stack is considered on the left side of the screen.
+ */
+ public boolean isStackOnLeft(PointF currentStackPosition) {
+ if (currentStackPosition == null) {
+ currentStackPosition = getRestingPosition();
+ }
+ final int stackCenter = (int) currentStackPosition.x + mBubbleSize / 2;
+ return stackCenter < mScreenRect.width() / 2;
+ }
+
/**
* Sets the stack's most recent position along the edge of the screen. This is saved when the
* last bubble is removed, so that the stack can be restored in its previous position.
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 ac97c8f80617..5a51eed04e1a 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
@@ -19,6 +19,8 @@ package com.android.wm.shell.bubbles;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
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_STACK_VIEW;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
@@ -33,11 +35,11 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
-import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
@@ -106,14 +108,8 @@ public class BubbleStackView extends FrameLayout
*/
private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
- /** Duration of the flyout alpha animations. */
- private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
-
private static final int FADE_IN_DURATION = 320;
- /** Percent to darken the bubbles when they're in the dismiss target. */
- private static final float DARKEN_PERCENT = 0.3f;
-
/** How long to wait, in milliseconds, before hiding the flyout. */
@VisibleForTesting
static final int FLYOUT_HIDE_AFTER = 5000;
@@ -122,6 +118,10 @@ public class BubbleStackView extends FrameLayout
private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
+ private static final int MANAGE_MENU_SCRIM_ANIM_DURATION = 150;
+
+ private static final float SCRIM_ALPHA = 0.6f;
+
/**
* How long to wait to animate the stack temporarily invisible after a drag/flyout hide
* animation ends, if we are in fact temporarily invisible.
@@ -195,7 +195,8 @@ public class BubbleStackView extends FrameLayout
private StackAnimationController mStackAnimationController;
private ExpandedAnimationController mExpandedAnimationController;
- private View mTaskbarScrim;
+ private View mScrim;
+ private View mManageMenuScrim;
private FrameLayout mExpandedViewContainer;
/** Matrix used to scale the expanded view container with a given pivot point. */
@@ -555,7 +556,7 @@ public class BubbleStackView extends FrameLayout
if (mBubbleData.isExpanded()) {
if (mManageEduView != null) {
- mManageEduView.hide(false /* show */);
+ mManageEduView.hide();
}
// If we're expanded, tell the animation controller to prepare to drag this bubble,
@@ -777,8 +778,8 @@ public class BubbleStackView extends FrameLayout
floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut,
this::animateShadows /* onStackAnimationFinished */, mPositioner);
- mExpandedAnimationController = new ExpandedAnimationController(
- mPositioner, mExpandedViewPadding, onBubbleAnimatedOut);
+ mExpandedAnimationController = new ExpandedAnimationController(mPositioner,
+ onBubbleAnimatedOut);
mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
// Force LTR by default since most of the Bubbles UI is positioned manually by the user, or
@@ -793,8 +794,6 @@ public class BubbleStackView extends FrameLayout
mBubbleContainer.setClipChildren(false);
addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
- updateUserEdu();
-
mExpandedViewContainer = new FrameLayout(context);
mExpandedViewContainer.setElevation(elevation);
mExpandedViewContainer.setClipChildren(false);
@@ -858,11 +857,20 @@ public class BubbleStackView extends FrameLayout
mBubbleData.setExpanded(true);
});
- mTaskbarScrim = new View(getContext());
- mTaskbarScrim.setBackgroundColor(Color.BLACK);
- addView(mTaskbarScrim);
- mTaskbarScrim.setAlpha(0f);
- mTaskbarScrim.setVisibility(GONE);
+ mScrim = new View(getContext());
+ mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mScrim.setBackgroundDrawable(new ColorDrawable(
+ getResources().getColor(android.R.color.system_neutral1_1000)));
+ addView(mScrim);
+ mScrim.setAlpha(0f);
+
+ mManageMenuScrim = new View(getContext());
+ mManageMenuScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mManageMenuScrim.setBackgroundDrawable(new ColorDrawable(
+ getResources().getColor(android.R.color.system_neutral1_1000)));
+ addView(mManageMenuScrim, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ mManageMenuScrim.setAlpha(0f);
+ mManageMenuScrim.setVisibility(INVISIBLE);
mOrientationChangedListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -886,8 +894,10 @@ public class BubbleStackView extends FrameLayout
mExpandedAnimationController.expandFromStack(() -> {
afterExpandedViewAnimation();
} /* after */);
+ final float translationY = mPositioner.getExpandedViewY(mExpandedBubble,
+ getBubbleIndex(mExpandedBubble));
mExpandedViewContainer.setTranslationX(0f);
- mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY());
+ mExpandedViewContainer.setTranslationY(translationY);
mExpandedViewContainer.setAlpha(1f);
}
removeOnLayoutChangeListener(mOrientationChangedListener);
@@ -917,8 +927,10 @@ public class BubbleStackView extends FrameLayout
setOnClickListener(view -> {
if (mShowingManage) {
showManageMenu(false /* show */);
+ } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+ mManageEduView.hide();
} else if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) {
- mStackEduView.hide(false);
+ mStackEduView.hide(false /* isExpanding */);
} else if (mBubbleData.isExpanded()) {
mBubbleData.setExpanded(false);
}
@@ -1117,10 +1129,10 @@ public class BubbleStackView extends FrameLayout
return;
}
if (mManageEduView == null) {
- mManageEduView = new ManageEducationView(mContext);
+ mManageEduView = new ManageEducationView(mContext, mPositioner);
addView(mManageEduView);
}
- mManageEduView.show(mExpandedBubble.getExpandedView(), mTempRect);
+ mManageEduView.show(mExpandedBubble.getExpandedView());
}
/**
@@ -1148,21 +1160,27 @@ public class BubbleStackView extends FrameLayout
return false;
}
if (mStackEduView == null) {
- mStackEduView = new StackEducationView(mContext);
+ mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
addView(mStackEduView);
}
mBubbleContainer.bringToFront();
return mStackEduView.show(mPositioner.getDefaultStartPosition());
}
+ // Recreates & shows the education views. Call when a theme/config change happens.
private void updateUserEdu() {
- maybeShowStackEdu();
- if (mManageEduView != null) {
- mManageEduView.invalidate();
+ if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) {
+ removeView(mStackEduView);
+ mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
+ addView(mStackEduView);
+ mBubbleContainer.bringToFront(); // Stack appears on top of the stack education
+ mStackEduView.show(mPositioner.getDefaultStartPosition());
}
- maybeShowManageEdu();
- if (mStackEduView != null) {
- mStackEduView.invalidate();
+ if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+ removeView(mManageEduView);
+ mManageEduView = new ManageEducationView(mContext, mPositioner);
+ addView(mManageEduView);
+ mManageEduView.show(mExpandedBubble.getExpandedView());
}
}
@@ -1171,7 +1189,7 @@ public class BubbleStackView extends FrameLayout
if (mFlyout != null) {
removeView(mFlyout);
}
- mFlyout = new BubbleFlyoutView(getContext());
+ mFlyout = new BubbleFlyoutView(getContext(), mPositioner);
mFlyout.setVisibility(GONE);
mFlyout.setOnClickListener(mFlyoutClickListener);
mFlyout.setOnTouchListener(mFlyoutTouchListener);
@@ -1218,6 +1236,10 @@ public class BubbleStackView extends FrameLayout
updateOverflow();
updateUserEdu();
updateExpandedViewTheme();
+ mScrim.setBackgroundDrawable(new ColorDrawable(
+ getResources().getColor(android.R.color.system_neutral1_1000)));
+ mManageMenuScrim.setBackgroundDrawable(new ColorDrawable(
+ getResources().getColor(android.R.color.system_neutral1_1000)));
}
/**
@@ -1255,6 +1277,7 @@ public class BubbleStackView extends FrameLayout
setUpManageMenu();
setUpFlyout();
setUpDismissView();
+ updateUserEdu();
mBubbleSize = mPositioner.getBubbleSize();
for (Bubble b : mBubbleData.getBubbles()) {
if (b.getIconView() == null) {
@@ -1535,6 +1558,7 @@ public class BubbleStackView extends FrameLayout
bubble.cleanupViews();
}
updatePointerPosition();
+ updateExpandedView();
logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
return;
}
@@ -1710,6 +1734,21 @@ public class BubbleStackView extends FrameLayout
notifyExpansionChanged(mExpandedBubble, mIsExpanded);
}
+ /**
+ * Called when back press occurs while bubbles are expanded.
+ */
+ public void onBackPressed() {
+ if (mIsExpanded) {
+ if (mShowingManage) {
+ showManageMenu(false);
+ } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+ mManageEduView.hide();
+ } else {
+ setExpanded(false);
+ }
+ }
+ }
+
void setBubbleVisibility(Bubble b, boolean visible) {
if (b.getIconView() != null) {
b.getIconView().setVisibility(visible ? VISIBLE : GONE);
@@ -1796,6 +1835,20 @@ public class BubbleStackView extends FrameLayout
mExpandedViewAlphaAnimator.start();
}
+ private void showScrim(boolean show) {
+ if (show) {
+ mScrim.animate()
+ .setInterpolator(ALPHA_IN)
+ .alpha(SCRIM_ALPHA)
+ .start();
+ } else {
+ mScrim.animate()
+ .alpha(0f)
+ .setInterpolator(ALPHA_OUT)
+ .start();
+ }
+ }
+
private void animateExpansion() {
cancelDelayedExpandCollapseSwitchAnimations();
final boolean showVertically = mPositioner.showBubblesVertically();
@@ -1805,6 +1858,7 @@ public class BubbleStackView extends FrameLayout
}
beforeExpandedViewAnimation();
+ showScrim(true);
updateZOrder();
updateBadges(false /* setBadgeForCollapsedStack */);
mBubbleContainer.setActiveController(mExpandedAnimationController);
@@ -1815,37 +1869,28 @@ public class BubbleStackView extends FrameLayout
maybeShowManageEdu();
}
} /* after */);
-
- if (mPositioner.showingInTaskbar()
- // Don't need the scrim when the bar is at the bottom
- && mPositioner.getTaskbarPosition() != BubblePositioner.TASKBAR_POSITION_BOTTOM) {
- mTaskbarScrim.getLayoutParams().width = mPositioner.getTaskbarSize();
- mTaskbarScrim.setTranslationX(mStackOnLeftOrWillBe
- ? 0f
- : mPositioner.getAvailableRect().right - mPositioner.getTaskbarSize());
- mTaskbarScrim.setVisibility(VISIBLE);
- mTaskbarScrim.animate().alpha(1f).start();
- }
-
- mExpandedViewContainer.setTranslationX(0f);
- mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY());
- mExpandedViewContainer.setAlpha(1f);
-
int index;
if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) {
index = mBubbleData.getBubbles().size();
} else {
index = getBubbleIndex(mExpandedBubble);
}
- // Position of the bubble we're expanding, once it's settled in its row.
- final float bubbleWillBeAt =
- mExpandedAnimationController.getBubbleXOrYForOrientation(index);
+ PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleContainer.getChildCount(),
+ mStackOnLeftOrWillBe);
+ final float translationY = mPositioner.getExpandedViewY(mExpandedBubble,
+ mPositioner.showBubblesVertically() ? p.y : p.x);
+ mExpandedViewContainer.setTranslationX(0f);
+ mExpandedViewContainer.setTranslationY(translationY);
+ mExpandedViewContainer.setAlpha(1f);
// How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles
// that are animating farther, so that the expanded view doesn't move as much.
final float relevantStackPosition = showVertically
? mStackAnimationController.getStackPosition().y
: mStackAnimationController.getStackPosition().x;
+ final float bubbleWillBeAt = showVertically
+ ? p.y
+ : p.x;
final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition);
// Wait for the path animation target to reach its end, and add a small amount of extra time
@@ -1862,22 +1907,22 @@ public class BubbleStackView extends FrameLayout
// Set the pivot point for the scale, so the expanded view animates out from the bubble.
if (showVertically) {
float pivotX;
- float pivotY = bubbleWillBeAt + mBubbleSize / 2f;
if (mStackOnLeftOrWillBe) {
- pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding;
+ pivotX = p.x + mBubbleSize + mExpandedViewPadding;
} else {
- pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding;
+ pivotX = p.x - mExpandedViewPadding;
}
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
- pivotX, pivotY);
+ pivotX,
+ p.y + mBubbleSize / 2f);
} else {
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
- bubbleWillBeAt + mBubbleSize / 2f,
- mPositioner.getExpandedViewY());
+ p.x + mBubbleSize / 2f,
+ p.y + mBubbleSize + mExpandedViewPadding);
}
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
@@ -1914,6 +1959,7 @@ public class BubbleStackView extends FrameLayout
mExpandedViewContainerMatrix);
})
.withEndActions(() -> {
+ mExpandedViewContainer.setAnimationMatrix(null);
afterExpandedViewAnimation();
if (mExpandedBubble != null
&& mExpandedBubble.getExpandedView() != null) {
@@ -1929,12 +1975,17 @@ public class BubbleStackView extends FrameLayout
private void animateCollapse() {
cancelDelayedExpandCollapseSwitchAnimations();
+ if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+ mManageEduView.hide();
+ }
// Hide the menu if it's visible.
showManageMenu(false);
mIsExpanded = false;
mIsExpansionAnimating = true;
+ showScrim(false);
+
mBubbleContainer.cancelAllAnimations();
// If we were in the middle of swapping, the animating-out surface would have been scaling
@@ -1952,10 +2003,6 @@ public class BubbleStackView extends FrameLayout
/* collapseTo */,
() -> mBubbleContainer.setActiveController(mStackAnimationController));
- if (mTaskbarScrim.getVisibility() == VISIBLE) {
- mTaskbarScrim.animate().alpha(0f).start();
- }
-
int index;
if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) {
index = mBubbleData.getBubbles().size();
@@ -1963,12 +2010,11 @@ public class BubbleStackView extends FrameLayout
index = mBubbleData.getBubbles().indexOf(mExpandedBubble);
}
// Value the bubble is animating from (back into the stack).
- final float expandingFromBubbleAt =
- mExpandedAnimationController.getBubbleXOrYForOrientation(index);
- final boolean showVertically = mPositioner.showBubblesVertically();
+ final PointF p = mPositioner.getExpandedBubbleXY(index,
+ mBubbleContainer.getChildCount(), mStackOnLeftOrWillBe);
if (mPositioner.showBubblesVertically()) {
float pivotX;
- float pivotY = expandingFromBubbleAt + mBubbleSize / 2f;
+ float pivotY = p.y + mBubbleSize / 2f;
if (mStackOnLeftOrWillBe) {
pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding;
} else {
@@ -1980,8 +2026,8 @@ public class BubbleStackView extends FrameLayout
} else {
mExpandedViewContainerMatrix.setScale(
1f, 1f,
- expandingFromBubbleAt + mBubbleSize / 2f,
- mPositioner.getExpandedViewY());
+ p.x + mBubbleSize / 2f,
+ p.y + mBubbleSize + mExpandedViewPadding);
}
mExpandedViewAlphaAnimator.reverse();
@@ -2008,7 +2054,7 @@ public class BubbleStackView extends FrameLayout
final BubbleViewProvider previouslySelected = mExpandedBubble;
beforeExpandedViewAnimation();
if (mManageEduView != null) {
- mManageEduView.hide(false /* fromExpansion */);
+ mManageEduView.hide();
}
if (DEBUG_BUBBLE_STACK_VIEW) {
@@ -2023,10 +2069,6 @@ public class BubbleStackView extends FrameLayout
if (previouslySelected != null) {
previouslySelected.setTaskViewVisibility(false);
}
-
- if (mPositioner.showingInTaskbar()) {
- mTaskbarScrim.setVisibility(GONE);
- }
})
.start();
}
@@ -2063,32 +2105,31 @@ public class BubbleStackView extends FrameLayout
boolean isOverflow = mExpandedBubble != null
&& mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
- float expandingFromBubbleDestination =
- mExpandedAnimationController.getBubbleXOrYForOrientation(isOverflow
- ? getBubbleCount()
- : mBubbleData.getBubbles().indexOf(mExpandedBubble));
-
+ PointF p = mPositioner.getExpandedBubbleXY(isOverflow
+ ? mBubbleContainer.getChildCount() - 1
+ : mBubbleData.getBubbles().indexOf(mExpandedBubble),
+ mBubbleContainer.getChildCount(), mStackOnLeftOrWillBe);
mExpandedViewContainer.setAlpha(1f);
mExpandedViewContainer.setVisibility(View.VISIBLE);
if (mPositioner.showBubblesVertically()) {
float pivotX;
- float pivotY = expandingFromBubbleDestination + mBubbleSize / 2f;
+ float pivotY = p.y + mBubbleSize / 2f;
if (mStackOnLeftOrWillBe) {
- pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding;
+ pivotX = p.x + mBubbleSize + mExpandedViewPadding;
} else {
- pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding;
-
+ pivotX = p.x - mExpandedViewPadding;
}
mExpandedViewContainerMatrix.setScale(
- 0f, 0f,
+ 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
+ 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
pivotX, pivotY);
} else {
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
- expandingFromBubbleDestination + mBubbleSize / 2f,
- mPositioner.getExpandedViewY());
+ p.x + mBubbleSize / 2f,
+ p.y + mBubbleSize + mExpandedViewPadding);
}
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
@@ -2113,6 +2154,7 @@ public class BubbleStackView extends FrameLayout
.withEndActions(() -> {
mExpandedViewTemporarilyHidden = false;
mIsBubbleSwitchAnimating = false;
+ mExpandedViewContainer.setAnimationMatrix(null);
})
.start();
}, 25);
@@ -2403,20 +2445,19 @@ public class BubbleStackView extends FrameLayout
if (mFlyout.getVisibility() == View.VISIBLE) {
- mFlyout.animateUpdate(bubble.getFlyoutMessage(), getWidth(),
+ mFlyout.animateUpdate(bubble.getFlyoutMessage(),
mStackAnimationController.getStackPosition(), !bubble.showDot(),
mAfterFlyoutHidden /* onHide */);
} else {
mFlyout.setVisibility(INVISIBLE);
mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(),
- mStackAnimationController.getStackPosition(), getWidth(),
+ mStackAnimationController.getStackPosition(),
mStackAnimationController.isStackOnLeftSide(),
bubble.getIconView().getDotColor() /* dotColor */,
expandFlyoutAfterDelay /* onLayoutComplete */,
mAfterFlyoutHidden /* onHide */,
bubble.getIconView().getDotCenter(),
- !bubble.showDot(),
- mPositioner);
+ !bubble.showDot());
}
mFlyout.bringToFront();
});
@@ -2501,6 +2542,24 @@ public class BubbleStackView extends FrameLayout
return;
}
+ if (show) {
+ mManageMenuScrim.setVisibility(VISIBLE);
+ mManageMenuScrim.setTranslationZ(mManageMenu.getElevation() - 1f);
+ }
+ Runnable endAction = () -> {
+ if (!show) {
+ mManageMenuScrim.setVisibility(INVISIBLE);
+ mManageMenuScrim.setTranslationZ(0f);
+ }
+ };
+
+ mManageMenuScrim.animate()
+ .setDuration(MANAGE_MENU_SCRIM_ANIM_DURATION)
+ .setInterpolator(show ? ALPHA_IN : ALPHA_OUT)
+ .alpha(show ? SCRIM_ALPHA : 0f)
+ .withEndAction(endAction)
+ .start();
+
// If available, update the manage menu's settings option with the expanded bubble's app
// name and icon.
if (show && mBubbleData.hasBubbleInStackWithKey(mExpandedBubble.getKey())) {
@@ -2510,7 +2569,6 @@ public class BubbleStackView extends FrameLayout
R.string.bubbles_app_settings, bubble.getAppName()));
}
- mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
if (mExpandedBubble.getExpandedView().getTaskView() != null) {
mExpandedBubble.getExpandedView().getTaskView().setObscuredTouchRect(mShowingManage
? new Rect(0, 0, getWidth(), getHeight())
@@ -2522,7 +2580,11 @@ public class BubbleStackView extends FrameLayout
// When the menu is open, it should be at these coordinates. The menu pops out to the right
// in LTR and to the left in RTL.
- final float targetX = isLtr ? mTempRect.left : mTempRect.right - mManageMenu.getWidth();
+ mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
+ final float margin = mExpandedBubble.getExpandedView().getManageButtonMargin();
+ final float targetX = isLtr
+ ? mTempRect.left - margin
+ : mTempRect.right + margin - mManageMenu.getWidth();
final float targetY = mTempRect.bottom - mManageMenu.getHeight();
final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f;
@@ -2702,14 +2764,17 @@ public class BubbleStackView extends FrameLayout
}
boolean isOverflowExpanded = mExpandedBubble != null
&& BubbleOverflow.KEY.equals(mExpandedBubble.getKey());
- int[] paddings = mPositioner.getExpandedViewPadding(
+ int[] paddings = mPositioner.getExpandedViewContainerPadding(
mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded);
mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);
if (mIsExpansionAnimating) {
mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
}
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
- mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY());
+ PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble),
+ mBubbleContainer.getChildCount(), mStackOnLeftOrWillBe);
+ mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble,
+ mPositioner.showBubblesVertically() ? p.y : p.x));
mExpandedViewContainer.setTranslationX(0f);
mExpandedBubble.getExpandedView().updateView(
mExpandedViewContainer.getLocationOnScreen());
@@ -2792,8 +2857,13 @@ public class BubbleStackView extends FrameLayout
if (index == -1) {
return;
}
- float bubblePosition = mExpandedAnimationController.getBubbleXOrYForOrientation(index);
- mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition, mStackOnLeftOrWillBe);
+ PointF bubblePosition = mPositioner.getExpandedBubbleXY(index,
+ mBubbleContainer.getChildCount(),
+ mStackOnLeftOrWillBe);
+ mExpandedBubble.getExpandedView().setPointerPosition(mPositioner.showBubblesVertically()
+ ? bubblePosition.y
+ : bubblePosition.x,
+ mStackOnLeftOrWillBe);
}
/**
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 c73b5eebc5c2..9b7eb2f1cfb3 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
@@ -24,12 +24,10 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.os.Bundle;
-import android.os.Looper;
import android.service.notification.NotificationListenerService.RankingMap;
import android.util.ArraySet;
import android.util.Pair;
import android.util.SparseArray;
-import android.view.View;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
@@ -43,7 +41,6 @@ import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executor;
-import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
@@ -160,14 +157,6 @@ public interface Bubbles {
/** Set the proxy to commnuicate with SysUi side components. */
void setSysuiProxy(SysuiProxy proxy);
- /**
- * Set the scrim view for bubbles.
- *
- * @param callback The callback made with the executor and the executor's looper that the view
- * will be running on.
- **/
- void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback);
-
/** Set a listener to be notified of bubble expand events. */
void setExpandListener(BubbleExpandListener listener);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
index 4cc67025fff4..eb4737ac6c63 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
@@ -18,12 +18,13 @@ package com.android.wm.shell.bubbles
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
+import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
+import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout
-import android.widget.TextView
-import com.android.internal.util.ContrastColorUtil
+import com.android.internal.R.color.system_neutral1_900
import com.android.wm.shell.R
import com.android.wm.shell.animation.Interpolators
@@ -31,21 +32,22 @@ import com.android.wm.shell.animation.Interpolators
* User education view to highlight the manage button that allows a user to configure the settings
* for the bubble. Shown only the first time a user expands a bubble.
*/
-class ManageEducationView constructor(context: Context) : LinearLayout(context) {
+class ManageEducationView constructor(context: Context, positioner: BubblePositioner)
+ : LinearLayout(context) {
- private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleManageEducationView"
+ private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "ManageEducationView"
else BubbleDebugConfig.TAG_BUBBLES
private val ANIMATE_DURATION: Long = 200
- private val ANIMATE_DURATION_SHORT: Long = 40
- private val manageView by lazy { findViewById<View>(R.id.manage_education_view) }
- private val manageButton by lazy { findViewById<Button>(R.id.manage) }
+ private val positioner: BubblePositioner = positioner
+ private val manageView by lazy { findViewById<ViewGroup>(R.id.manage_education_view) }
+ private val manageButton by lazy { findViewById<Button>(R.id.manage_button) }
private val gotItButton by lazy { findViewById<Button>(R.id.got_it) }
- private val titleTextView by lazy { findViewById<TextView>(R.id.user_education_title) }
- private val descTextView by lazy { findViewById<TextView>(R.id.user_education_description) }
private var isHiding = false
+ private var realManageButtonRect = Rect()
+ private var bubbleExpandedView: BubbleExpandedView? = null
init {
LayoutInflater.from(context).inflate(R.layout.bubbles_manage_button_education, this)
@@ -66,18 +68,17 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context)
override fun onFinishInflate() {
super.onFinishInflate()
layoutDirection = resources.configuration.layoutDirection
- setTextColor()
}
- private fun setTextColor() {
- val typedArray = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent,
- android.R.attr.textColorPrimaryInverse))
- val bgColor = typedArray.getColor(0 /* index */, Color.BLACK)
- var textColor = typedArray.getColor(1 /* index */, Color.WHITE)
+ private fun setButtonColor() {
+ val typedArray = mContext.obtainStyledAttributes(intArrayOf(
+ com.android.internal.R.attr.colorAccentPrimary))
+ val buttonColor = typedArray.getColor(0 /* index */, Color.TRANSPARENT)
typedArray.recycle()
- textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true)
- titleTextView.setTextColor(textColor)
- descTextView.setTextColor(textColor)
+
+ manageButton.setTextColor(mContext.getColor(system_neutral1_900))
+ manageButton.setBackgroundDrawable(ColorDrawable(buttonColor))
+ gotItButton.setBackgroundDrawable(ColorDrawable(buttonColor))
}
private fun setDrawableDirection() {
@@ -91,30 +92,39 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context)
* If necessary, toggles the user education view for the manage button. This is shown when the
* bubble stack is expanded for the first time.
*
- * @param show whether the user education view should show or not.
+ * @param expandedView the expandedView the user education is shown on top of.
*/
- fun show(expandedView: BubbleExpandedView, rect: Rect) {
+ fun show(expandedView: BubbleExpandedView) {
+ setButtonColor()
if (visibility == VISIBLE) return
+ bubbleExpandedView = expandedView
+ expandedView.taskView?.setObscuredTouchRect(Rect(positioner.screenRect))
+
+ layoutParams.width = if (positioner.isLargeScreen)
+ context.resources.getDimensionPixelSize(
+ R.dimen.bubbles_user_education_width_large_screen)
+ else ViewGroup.LayoutParams.MATCH_PARENT
+
alpha = 0f
visibility = View.VISIBLE
+ expandedView.getManageButtonBoundsOnScreen(realManageButtonRect)
+ manageView.setPadding(realManageButtonRect.left - expandedView.manageButtonMargin,
+ manageView.paddingTop, manageView.paddingRight, manageView.paddingBottom)
post {
- expandedView.getManageButtonBoundsOnScreen(rect)
-
manageButton
.setOnClickListener {
- expandedView.findViewById<View>(R.id.settings_button).performClick()
- hide(true /* isStackExpanding */)
+ hide()
+ expandedView.findViewById<View>(R.id.manage_button).performClick()
}
- gotItButton.setOnClickListener { hide(true /* isStackExpanding */) }
- setOnClickListener { hide(true /* isStackExpanding */) }
-
- with(manageView) {
- translationX = 0f
- val inset = resources.getDimensionPixelSize(
- R.dimen.bubbles_manage_education_top_inset)
- translationY = (rect.top - manageView.height + inset).toFloat()
- }
+ gotItButton.setOnClickListener { hide() }
+ setOnClickListener { hide() }
+
+ val offsetViewBounds = Rect()
+ manageButton.getDrawingRect(offsetViewBounds)
+ manageView.offsetDescendantRectToMyCoords(manageButton, offsetViewBounds)
+ translationX = 0f
+ translationY = (realManageButtonRect.top - offsetViewBounds.top).toFloat()
bringToFront()
animate()
.setDuration(ANIMATE_DURATION)
@@ -124,13 +134,14 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context)
setShouldShow(false)
}
- fun hide(isStackExpanding: Boolean) {
+ fun hide() {
+ bubbleExpandedView?.taskView?.setObscuredTouchRect(null)
if (visibility != VISIBLE || isHiding) return
animate()
.withStartAction { isHiding = true }
.alpha(0f)
- .setDuration(if (isStackExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
+ .setDuration(ANIMATE_DURATION)
.withEndAction {
isHiding = false
visibility = GONE
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 0a2cfc4089ed..f6a90b7a76cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -18,8 +18,11 @@ package com.android.wm.shell.bubbles
import android.content.Context
import android.graphics.Color
import android.graphics.PointF
+import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
+import android.view.View.OnKeyListener
+import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import com.android.internal.util.ContrastColorUtil
@@ -30,7 +33,12 @@ import com.android.wm.shell.animation.Interpolators
* User education view to highlight the collapsed stack of bubbles.
* Shown only the first time a user taps the stack.
*/
-class StackEducationView constructor(context: Context) : LinearLayout(context) {
+class StackEducationView constructor(
+ context: Context,
+ positioner: BubblePositioner,
+ controller: BubbleController
+)
+ : LinearLayout(context) {
private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView"
else BubbleDebugConfig.TAG_BUBBLES
@@ -38,6 +46,9 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) {
private val ANIMATE_DURATION: Long = 200
private val ANIMATE_DURATION_SHORT: Long = 40
+ private val positioner: BubblePositioner = positioner
+ private val controller: BubbleController = controller
+
private val view by lazy { findViewById<View>(R.id.stack_education_layout) }
private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) }
private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) }
@@ -67,6 +78,28 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) {
setTextColor()
}
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ setFocusableInTouchMode(true)
+ setOnKeyListener(object : OnKeyListener {
+ override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean {
+ // if the event is a key down event on the enter button
+ if (event.action == KeyEvent.ACTION_UP &&
+ keyCode == KeyEvent.KEYCODE_BACK && !isHiding) {
+ hide(false)
+ return true
+ }
+ return false
+ }
+ })
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ setOnKeyListener(null)
+ controller.updateWindowFlagsForBackpress(false /* interceptBack */)
+ }
+
private fun setTextColor() {
val ta = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent,
android.R.attr.textColorPrimaryInverse))
@@ -94,13 +127,25 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) {
fun show(stackPosition: PointF): Boolean {
if (visibility == VISIBLE) return false
+ controller.updateWindowFlagsForBackpress(true /* interceptBack */)
+ layoutParams.width = if (positioner.isLargeScreen)
+ context.resources.getDimensionPixelSize(
+ R.dimen.bubbles_user_education_width_large_screen)
+ else ViewGroup.LayoutParams.MATCH_PARENT
+
setAlpha(0f)
setVisibility(View.VISIBLE)
post {
+ requestFocus()
with(view) {
- val bubbleSize = context.resources.getDimensionPixelSize(
- R.dimen.bubble_size)
- translationY = stackPosition.y + bubbleSize / 2 - getHeight() / 2
+ if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+ setPadding(positioner.bubbleSize + paddingRight, paddingTop, paddingRight,
+ paddingBottom)
+ } else {
+ setPadding(paddingLeft, paddingTop, positioner.bubbleSize + paddingLeft,
+ paddingBottom)
+ }
+ translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2
}
animate()
.setDuration(ANIMATE_DURATION)
@@ -114,15 +159,16 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) {
/**
* If necessary, hides the stack education view.
*
- * @param fromExpansion if true this indicates the hide is happening due to the bubble being
+ * @param isExpanding if true this indicates the hide is happening due to the bubble being
* expanded, false if due to a touch outside of the bubble stack.
*/
- fun hide(fromExpansion: Boolean) {
+ fun hide(isExpanding: Boolean) {
if (visibility != VISIBLE || isHiding) return
+ controller.updateWindowFlagsForBackpress(false /* interceptBack */)
animate()
.alpha(0f)
- .setDuration(if (fromExpansion) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
+ .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
.withEndAction { visibility = GONE }
}
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 df2b440c19df..c32be98866cf 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
@@ -64,9 +64,6 @@ public class ExpandedAnimationController
/** Stiffness for the expand/collapse path-following animation. */
private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
- /** What percentage of the screen to use when centering the bubbles in landscape. */
- private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
-
/**
* Velocity required to dismiss an individual bubble without dragging it into the dismiss
* target.
@@ -79,16 +76,8 @@ public class ExpandedAnimationController
/** Horizontal offset between bubbles, which we need to know to re-stack them. */
private float mStackOffsetPx;
- /** Space between status bar and bubbles in the expanded state. */
- private float mBubblePaddingTop;
/** Size of each bubble. */
private float mBubbleSizePx;
- /** Max number of bubbles shown in row above expanded view. */
- private int mBubblesMaxRendered;
- /** Max amount of space to have between bubbles when expanded. */
- private int mBubblesMaxSpace;
- /** Amount of space between the bubbles when expanded. */
- private float mSpaceBetweenBubbles;
/** Whether the expand / collapse animation is running. */
private boolean mAnimatingExpand = false;
@@ -127,8 +116,6 @@ public class ExpandedAnimationController
/** The bubble currently being dragged out of the row (to potentially be dismissed). */
private MagnetizedObject<View> mMagnetizedBubbleDraggingOut;
- private int mExpandedViewPadding;
-
/**
* Callback to run whenever any bubble is animated out. The BubbleStackView will check if the
* end of this animation means we have no bubbles left, and notify the BubbleController.
@@ -137,11 +124,10 @@ public class ExpandedAnimationController
private BubblePositioner mPositioner;
- public ExpandedAnimationController(BubblePositioner positioner, int expandedViewPadding,
+ public ExpandedAnimationController(BubblePositioner positioner,
Runnable onBubbleAnimatedOutAction) {
mPositioner = positioner;
updateResources();
- mExpandedViewPadding = expandedViewPadding;
mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction;
mCollapsePoint = mPositioner.getDefaultStartPosition();
}
@@ -208,11 +194,8 @@ public class ExpandedAnimationController
return;
}
Resources res = mLayout.getContext().getResources();
- mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
mBubbleSizePx = mPositioner.getBubbleSize();
- mBubblesMaxRendered = mPositioner.getMaxBubbles();
- mSpaceBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
}
/**
@@ -256,31 +239,22 @@ public class ExpandedAnimationController
final Path path = new Path();
path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
- final float expandedY = mPositioner.showBubblesVertically()
- ? getBubbleXOrYForOrientation(index)
- : getExpandedY();
+ boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint);
+ final PointF p = mPositioner.getExpandedBubbleXY(index,
+ mLayout.getChildCount(),
+ onLeft);
if (expanding) {
- // If we're expanding, first draw a line from the bubble's current position to the
- // top of the screen.
- path.lineTo(bubble.getTranslationX(), expandedY);
+ // If we're expanding, first draw a line from the bubble's current position to where
+ // it'll end up
+ path.lineTo(bubble.getTranslationX(), p.y);
// Then, draw a line across the screen to the bubble's resting position.
- if (mPositioner.showBubblesVertically()) {
- Rect availableRect = mPositioner.getAvailableRect();
- boolean onLeft = mCollapsePoint != null
- && mCollapsePoint.x < (availableRect.width() / 2f);
- float translationX = onLeft
- ? availableRect.left
- : availableRect.right - mBubbleSizePx;
- path.lineTo(translationX, getBubbleXOrYForOrientation(index));
- } else {
- path.lineTo(getBubbleXOrYForOrientation(index), expandedY);
- }
+ path.lineTo(p.x, p.y);
} else {
final float stackedX = mCollapsePoint.x;
// If we're collapsing, draw a line from the bubble's current position to the side
// of the screen where the bubble will be stacked.
- path.lineTo(stackedX, expandedY);
+ path.lineTo(stackedX, p.y);
// Then, draw a line down to the stack position.
path.lineTo(stackedX, mCollapsePoint.y
@@ -390,8 +364,9 @@ public class ExpandedAnimationController
bubbleView.setTranslationY(y);
}
+ final float expandedY = mPositioner.getExpandedBubblesY();
final boolean draggedOutEnough =
- y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
+ y > expandedY + mBubbleSizePx || y < expandedY - mBubbleSizePx;
if (draggedOutEnough != mBubbleDraggedOutEnough) {
updateBubblePositions();
mBubbleDraggedOutEnough = draggedOutEnough;
@@ -435,9 +410,10 @@ public class ExpandedAnimationController
return;
}
final int index = mLayout.indexOfChild(bubbleView);
-
+ final PointF p = mPositioner.getExpandedBubbleXY(index, mLayout.getChildCount(),
+ mPositioner.isStackOnLeft(mCollapsePoint));
animationForChildAtIndex(index)
- .position(getBubbleXOrYForOrientation(index), getExpandedY())
+ .position(p.x, p.y)
.withPositionStartVelocities(velX, velY)
.start(() -> bubbleView.setTranslationZ(0f) /* after */);
@@ -454,17 +430,13 @@ public class ExpandedAnimationController
}
/**
- * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
+ * Animates the bubbles to the y position. Used in response to IME showing.
*/
public void updateYPosition(Runnable after) {
if (mLayout == null) return;
animationsForChildrenFromIndex(
- 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
- }
-
- /** The Y value of the row of expanded bubbles. */
- public float getExpandedY() {
- return mPositioner.getAvailableRect().top + mBubblePaddingTop;
+ 0, (i, anim) -> anim.translationY(mPositioner.getExpandedBubblesY()))
+ .startAll(after);
}
/** Description of current animation controller state. */
@@ -522,35 +494,36 @@ public class ExpandedAnimationController
startOrUpdatePathAnimation(true /* expanding */);
} else if (mAnimatingCollapse) {
startOrUpdatePathAnimation(false /* expanding */);
- } else if (mPositioner.showBubblesVertically()) {
- child.setTranslationY(getBubbleXOrYForOrientation(index));
- if (!mPreparingToCollapse) {
- // Only animate if we're not collapsing as that animation will handle placing the
- // new bubble in the stacked position.
- Rect availableRect = mPositioner.getAvailableRect();
- boolean onLeft = mCollapsePoint != null
- && mCollapsePoint.x < (availableRect.width() / 2f);
- float fromX = onLeft
- ? -mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR
- : availableRect.right + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
- float toX = onLeft
- ? availableRect.left + mExpandedViewPadding
- : availableRect.right - mBubbleSizePx - mExpandedViewPadding;
- animationForChild(child)
- .translationX(fromX, toX)
- .start();
- updateBubblePositions();
- }
} else {
- child.setTranslationX(getBubbleXOrYForOrientation(index));
+ boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint);
+ final PointF p = mPositioner.getExpandedBubbleXY(index,
+ mLayout.getChildCount(),
+ onLeft);
+ if (mPositioner.showBubblesVertically()) {
+ child.setTranslationY(p.y);
+ } else {
+ child.setTranslationX(p.x);
+ }
if (!mPreparingToCollapse) {
// Only animate if we're not collapsing as that animation will handle placing the
// new bubble in the stacked position.
- float toY = getExpandedY();
- float fromY = getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
- animationForChild(child)
- .translationY(fromY, toY)
- .start();
+ if (mPositioner.showBubblesVertically()) {
+ Rect availableRect = mPositioner.getAvailableRect();
+ float fromX = onLeft
+ ? -mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR
+ : availableRect.right + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
+ animationForChild(child)
+ .translationX(fromX, p.y)
+ .start();
+ } else {
+ // Only animate if we're not collapsing as that animation will handle placing
+ // the new bubble in the stacked position.
+ float fromY = mPositioner.getExpandedBubblesY() - mBubbleSizePx
+ * ANIMATE_TRANSLATION_FACTOR;
+ animationForChild(child)
+ .translationY(fromY, p.y)
+ .start();
+ }
updateBubblePositions();
}
}
@@ -599,7 +572,7 @@ public class ExpandedAnimationController
if (mAnimatingExpand || mAnimatingCollapse) {
return;
}
-
+ boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint);
for (int i = 0; i < mLayout.getChildCount(); i++) {
final View bubble = mLayout.getChildAt(i);
@@ -609,49 +582,11 @@ public class ExpandedAnimationController
return;
}
- if (mPositioner.showBubblesVertically()) {
- Rect availableRect = mPositioner.getAvailableRect();
- boolean onLeft = mCollapsePoint != null
- && mCollapsePoint.x < (availableRect.width() / 2f);
- animationForChild(bubble)
- .translationX(onLeft
- ? availableRect.left
- : availableRect.right - mBubbleSizePx)
- .translationY(getBubbleXOrYForOrientation(i))
- .start();
- } else {
- animationForChild(bubble)
- .translationX(getBubbleXOrYForOrientation(i))
- .translationY(getExpandedY())
- .start();
- }
- }
- }
-
- // TODO - could move to method on bubblePositioner if mSpaceBetweenBubbles gets moved
- /**
- * When bubbles are expanded in portrait, they display at the top of the screen in a horizontal
- * row. When in landscape or on a large screen, they show at the left or right side in a
- * vertical row. This method accounts for screen orientation and will return an x or y value
- * for the position of the bubble in the row.
- *
- * @param index Bubble index in row.
- * @return the y position of the bubble if showing vertically and the x position if showing
- * horizontally.
- */
- public float getBubbleXOrYForOrientation(int index) {
- if (mLayout == null) {
- return 0;
+ final PointF p = mPositioner.getExpandedBubbleXY(i, mLayout.getChildCount(), onLeft);
+ animationForChild(bubble)
+ .translationX(p.x)
+ .translationY(p.y)
+ .start();
}
- final float positionInBar = index * (mBubbleSizePx + mSpaceBetweenBubbles);
- Rect availableRect = mPositioner.getAvailableRect();
- final boolean isLandscape = mPositioner.showBubblesVertically();
- final float expandedStackSize = (mLayout.getChildCount() * mBubbleSizePx)
- + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles);
- final float centerPosition = isLandscape
- ? availableRect.centerY()
- : availableRect.centerX();
- final float rowStart = centerPosition - (expandedStackSize / 2f);
- return rowStart + positionInBar;
}
}
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 636e1452aa9b..9a08190675b6 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
@@ -305,10 +305,7 @@ public class StackAnimationController extends
if (mLayout == null || !isStackPositionSet()) {
return true; // Default to left, which is where it starts by default.
}
-
- float stackCenter = mStackPosition.x + mBubbleSize / 2;
- float screenCenter = mLayout.getWidth() / 2;
- return stackCenter < screenCenter;
+ return mPositioner.isStackOnLeft(mStackPosition);
}
/**
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 3a7b534f3c17..ffda1f92ec90 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
@@ -17,6 +17,7 @@
package com.android.wm.shell.common;
import android.os.RemoteException;
+import android.util.Slog;
import android.view.IDisplayWindowRotationCallback;
import android.view.IDisplayWindowRotationController;
import android.view.IWindowManager;
@@ -27,6 +28,7 @@ import androidx.annotation.BinderThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
import java.util.ArrayList;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* This module deals with display rotations coming from WM. When WM starts a rotation: after it has
@@ -35,14 +37,14 @@ import java.util.ArrayList;
* rotation.
*/
public class DisplayChangeController {
+ private static final String TAG = DisplayChangeController.class.getSimpleName();
private final ShellExecutor mMainExecutor;
private final IWindowManager mWmService;
private final IDisplayWindowRotationController mControllerImpl;
- private final ArrayList<OnDisplayChangingListener> mRotationListener =
- new ArrayList<>();
- private final ArrayList<OnDisplayChangingListener> mTmpListeners = new ArrayList<>();
+ private final CopyOnWriteArrayList<OnDisplayChangingListener> mRotationListener =
+ new CopyOnWriteArrayList<>();
public DisplayChangeController(IWindowManager wmService, ShellExecutor mainExecutor) {
mMainExecutor = mainExecutor;
@@ -59,34 +61,26 @@ public class DisplayChangeController {
* Adds a display rotation controller.
*/
public void addRotationListener(OnDisplayChangingListener listener) {
- synchronized (mRotationListener) {
- mRotationListener.add(listener);
- }
+ mRotationListener.add(listener);
}
/**
* Removes a display rotation controller.
*/
public void removeRotationListener(OnDisplayChangingListener listener) {
- synchronized (mRotationListener) {
- mRotationListener.remove(listener);
- }
+ mRotationListener.remove(listener);
}
private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation,
IDisplayWindowRotationCallback callback) {
WindowContainerTransaction t = new WindowContainerTransaction();
- synchronized (mRotationListener) {
- mTmpListeners.clear();
- // Make a local copy in case the handlers add/remove themselves.
- mTmpListeners.addAll(mRotationListener);
- }
- for (OnDisplayChangingListener c : mTmpListeners) {
+ for (OnDisplayChangingListener c : mRotationListener) {
c.onRotateDisplay(displayId, fromRotation, toRotation, t);
}
try {
callback.continueRotateDisplay(toRotation, t);
} catch (RemoteException e) {
+ Slog.e(TAG, "Failed to continue rotation", e);
}
}
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 ba9ba5e5883a..9a3bdab9f418 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
@@ -26,6 +26,7 @@ import android.util.SparseArray;
import android.view.Display;
import android.view.IDisplayWindowListener;
import android.view.IWindowManager;
+import android.view.InsetsState;
import androidx.annotation.BinderThread;
@@ -52,14 +53,6 @@ public class DisplayController {
private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>();
private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
- /**
- * Gets a display by id from DisplayManager.
- */
- public Display getDisplay(int displayId) {
- final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
- return displayManager.getDisplay(displayId);
- }
-
public DisplayController(Context context, IWindowManager wmService,
ShellExecutor mainExecutor) {
mMainExecutor = mainExecutor;
@@ -67,14 +60,28 @@ public class DisplayController {
mWmService = wmService;
mChangeController = new DisplayChangeController(mWmService, mainExecutor);
mDisplayContainerListener = new DisplayWindowListenerImpl();
+ }
+
+ /**
+ * Initializes the window listener.
+ */
+ public void initialize() {
try {
mWmService.registerDisplayWindowListener(mDisplayContainerListener);
} catch (RemoteException e) {
- throw new RuntimeException("Unable to register hierarchy listener");
+ throw new RuntimeException("Unable to register display controller");
}
}
/**
+ * Gets a display by id from DisplayManager.
+ */
+ public Display getDisplay(int displayId) {
+ final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+ return displayManager.getDisplay(displayId);
+ }
+
+ /**
* Gets the DisplayLayout associated with a display.
*/
public @Nullable DisplayLayout getDisplayLayout(int displayId) {
@@ -91,6 +98,16 @@ public class DisplayController {
}
/**
+ * Updates the insets for a given display.
+ */
+ public void updateDisplayInsets(int displayId, InsetsState state) {
+ final DisplayRecord r = mDisplays.get(displayId);
+ if (r != null) {
+ r.setInsets(state);
+ }
+ }
+
+ /**
* Add a display window-container listener. It will get notified whenever a display's
* configuration changes or when displays are added/removed from the WM hierarchy.
*/
@@ -134,17 +151,18 @@ public class DisplayController {
if (mDisplays.get(displayId) != null) {
return;
}
- Display display = getDisplay(displayId);
+ final Display display = getDisplay(displayId);
if (display == null) {
// It's likely that the display is private to some app and thus not
// accessible by system-ui.
return;
}
- DisplayRecord record = new DisplayRecord();
- record.mDisplayId = displayId;
- record.mContext = (displayId == Display.DEFAULT_DISPLAY) ? mContext
+
+ final Context context = (displayId == Display.DEFAULT_DISPLAY)
+ ? mContext
: mContext.createDisplayContext(display);
- record.mDisplayLayout = new DisplayLayout(record.mContext, display);
+ final DisplayRecord record = new DisplayRecord(displayId);
+ record.setDisplayLayout(context, new DisplayLayout(context, display));
mDisplays.put(displayId, record);
for (int i = 0; i < mDisplayChangedListeners.size(); ++i) {
mDisplayChangedListeners.get(i).onDisplayAdded(displayId);
@@ -154,24 +172,23 @@ public class DisplayController {
private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
synchronized (mDisplays) {
- DisplayRecord dr = mDisplays.get(displayId);
+ final DisplayRecord dr = mDisplays.get(displayId);
if (dr == null) {
Slog.w(TAG, "Skipping Display Configuration change on non-added"
+ " display.");
return;
}
- Display display = getDisplay(displayId);
+ final Display display = getDisplay(displayId);
if (display == null) {
Slog.w(TAG, "Skipping Display Configuration change on invalid"
+ " display. It may have been removed.");
return;
}
- Context perDisplayContext = mContext;
- if (displayId != Display.DEFAULT_DISPLAY) {
- perDisplayContext = mContext.createDisplayContext(display);
- }
- dr.mContext = perDisplayContext.createConfigurationContext(newConfig);
- dr.mDisplayLayout = new DisplayLayout(dr.mContext, display);
+ final Context perDisplayContext = (displayId == Display.DEFAULT_DISPLAY)
+ ? mContext
+ : mContext.createDisplayContext(display);
+ final Context context = perDisplayContext.createConfigurationContext(newConfig);
+ dr.setDisplayLayout(context, new DisplayLayout(context, display));
for (int i = 0; i < mDisplayChangedListeners.size(); ++i) {
mDisplayChangedListeners.get(i).onDisplayConfigurationChanged(
displayId, newConfig);
@@ -219,9 +236,25 @@ public class DisplayController {
}
private static class DisplayRecord {
- int mDisplayId;
- Context mContext;
- DisplayLayout mDisplayLayout;
+ private int mDisplayId;
+ private Context mContext;
+ private DisplayLayout mDisplayLayout;
+ private InsetsState mInsetsState = new InsetsState();
+
+ private DisplayRecord(int displayId) {
+ mDisplayId = displayId;
+ }
+
+ private void setDisplayLayout(Context context, DisplayLayout displayLayout) {
+ mContext = context;
+ mDisplayLayout = displayLayout;
+ mDisplayLayout.setInsets(mContext.getResources(), mInsetsState);
+ }
+
+ private void setInsets(InsetsState state) {
+ mInsetsState = state;
+ mDisplayLayout.setInsets(mContext.getResources(), state);
+ }
}
@BinderThread
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 a7996f056785..a7052bc49699 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
@@ -33,6 +33,7 @@ import android.view.IWindowManager;
import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
+import android.view.InsetsVisibilities;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowInsets;
@@ -68,14 +69,17 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
protected final Executor mMainExecutor;
private final TransactionPool mTransactionPool;
private final DisplayController mDisplayController;
+ private final DisplayInsetsController mDisplayInsetsController;
private final SparseArray<PerDisplay> mImePerDisplay = new SparseArray<>();
private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>();
public DisplayImeController(IWindowManager wmService, DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
Executor mainExecutor, TransactionPool transactionPool) {
mWmService = wmService;
mDisplayController = displayController;
+ mDisplayInsetsController = displayInsetsController;
mMainExecutor = mainExecutor;
mTransactionPool = transactionPool;
}
@@ -109,11 +113,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
@Override
public void onDisplayRemoved(int displayId) {
- try {
- mWmService.setDisplayWindowInsetsController(displayId, null);
- } catch (RemoteException e) {
- Slog.w(TAG, "Unable to remove insets controller on display " + displayId);
+ PerDisplay pd = mImePerDisplay.get(displayId);
+ if (pd == null) {
+ return;
}
+ pd.unregister();
mImePerDisplay.remove(displayId);
}
@@ -195,11 +199,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
/** An implementation of {@link IDisplayWindowInsetsController} for a given display id. */
- public class PerDisplay {
+ public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener {
final int mDisplayId;
final InsetsState mInsetsState = new InsetsState();
- protected final DisplayWindowInsetsControllerImpl mInsetsControllerImpl =
- new DisplayWindowInsetsControllerImpl();
+ final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
InsetsSourceControl mImeSourceControl = null;
int mAnimationDirection = DIRECTION_NONE;
ValueAnimator mAnimation = null;
@@ -214,14 +217,15 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
public void register() {
- try {
- mWmService.setDisplayWindowInsetsController(mDisplayId, mInsetsControllerImpl);
- } catch (RemoteException e) {
- Slog.w(TAG, "Unable to set insets controller on display " + mDisplayId);
- }
+ mDisplayInsetsController.addInsetsChangedListener(mDisplayId, this);
}
- protected void insetsChanged(InsetsState insetsState) {
+ public void unregister() {
+ mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, this);
+ }
+
+ @Override
+ public void insetsChanged(InsetsState insetsState) {
if (mInsetsState.equals(insetsState)) {
return;
}
@@ -239,8 +243,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
}
+ @Override
@VisibleForTesting
- protected void insetsControlChanged(InsetsState insetsState,
+ public void insetsControlChanged(InsetsState insetsState,
InsetsSourceControl[] activeControls) {
insetsChanged(insetsState);
InsetsSourceControl imeSourceControl = null;
@@ -279,9 +284,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
if (!mImeShowing) {
removeImeSurface();
}
- }
- if (mImeSourceControl != null) {
- mImeSourceControl.release(SurfaceControl::release);
+ if (mImeSourceControl != null) {
+ mImeSourceControl.release(SurfaceControl::release);
+ }
}
mImeSourceControl = imeSourceControl;
}
@@ -301,7 +306,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
}
- protected void showInsets(int types, boolean fromIme) {
+ @Override
+ public void showInsets(int types, boolean fromIme) {
if ((types & WindowInsets.Type.ime()) == 0) {
return;
}
@@ -309,8 +315,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
startAnimation(true /* show */, false /* forceRestart */);
}
-
- protected void hideInsets(int types, boolean fromIme) {
+ @Override
+ public void hideInsets(int types, boolean fromIme) {
if ((types & WindowInsets.Type.ime()) == 0) {
return;
}
@@ -318,6 +324,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
startAnimation(false /* show */, false /* forceRestart */);
}
+ @Override
public void topFocusedWindowChanged(String packageName) {
// Do nothing
}
@@ -327,8 +334,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
*/
private void setVisibleDirectly(boolean visible) {
mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible);
+ mRequestedVisibilities.setVisibility(InsetsState.ITYPE_IME, visible);
try {
- mWmService.modifyDisplayWindowInsets(mDisplayId, mInsetsState);
+ mWmService.updateDisplayWindowRequestedVisibilities(mDisplayId,
+ mRequestedVisibilities);
} catch (RemoteException e) {
}
}
@@ -489,47 +498,6 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
dispatchVisibilityChanged(mDisplayId, isShowing);
}
}
-
- @VisibleForTesting
- @BinderThread
- public class DisplayWindowInsetsControllerImpl
- extends IDisplayWindowInsetsController.Stub {
- @Override
- public void topFocusedWindowChanged(String packageName) throws RemoteException {
- mMainExecutor.execute(() -> {
- PerDisplay.this.topFocusedWindowChanged(packageName);
- });
- }
-
- @Override
- public void insetsChanged(InsetsState insetsState) throws RemoteException {
- mMainExecutor.execute(() -> {
- PerDisplay.this.insetsChanged(insetsState);
- });
- }
-
- @Override
- public void insetsControlChanged(InsetsState insetsState,
- InsetsSourceControl[] activeControls) throws RemoteException {
- mMainExecutor.execute(() -> {
- PerDisplay.this.insetsControlChanged(insetsState, activeControls);
- });
- }
-
- @Override
- public void showInsets(int types, boolean fromIme) throws RemoteException {
- mMainExecutor.execute(() -> {
- PerDisplay.this.showInsets(types, fromIme);
- });
- }
-
- @Override
- public void hideInsets(int types, boolean fromIme) throws RemoteException {
- mMainExecutor.execute(() -> {
- PerDisplay.this.hideInsets(types, fromIme);
- });
- }
- }
}
void removeImeSurface() {
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
new file mode 100644
index 000000000000..565f1481233c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common;
+
+import android.os.RemoteException;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.IDisplayWindowInsetsController;
+import android.view.IWindowManager;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+
+import androidx.annotation.BinderThread;
+
+import com.android.wm.shell.common.annotations.ShellMainThread;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Manages insets from the core.
+ */
+public class DisplayInsetsController implements DisplayController.OnDisplaysChangedListener {
+ private static final String TAG = "DisplayInsetsController";
+
+ private final IWindowManager mWmService;
+ private final ShellExecutor mMainExecutor;
+ private final DisplayController mDisplayController;
+ private final SparseArray<PerDisplay> mInsetsPerDisplay = new SparseArray<>();
+ private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners =
+ new SparseArray<>();
+
+ public DisplayInsetsController(IWindowManager wmService, DisplayController displayController,
+ ShellExecutor mainExecutor) {
+ mWmService = wmService;
+ mDisplayController = displayController;
+ mMainExecutor = mainExecutor;
+ }
+
+ /**
+ * Starts listening for insets for each display.
+ **/
+ public void initialize() {
+ mDisplayController.addDisplayWindowListener(this);
+ }
+
+ /**
+ * Adds a callback to listen for insets changes for a particular display. Note that the
+ * listener will not be updated with the existing state of the insets on that display.
+ */
+ public void addInsetsChangedListener(int displayId, OnInsetsChangedListener listener) {
+ CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(displayId);
+ if (listeners == null) {
+ listeners = new CopyOnWriteArrayList<>();
+ mListeners.put(displayId, listeners);
+ }
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes a callback listening for insets changes from a particular display.
+ */
+ public void removeInsetsChangedListener(int displayId, OnInsetsChangedListener listener) {
+ CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(displayId);
+ if (listeners == null) {
+ return;
+ }
+ listeners.remove(listener);
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+ PerDisplay pd = new PerDisplay(displayId);
+ pd.register();
+ mInsetsPerDisplay.put(displayId, pd);
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ PerDisplay pd = mInsetsPerDisplay.get(displayId);
+ if (pd == null) {
+ return;
+ }
+ pd.unregister();
+ mInsetsPerDisplay.remove(displayId);
+ }
+
+ /**
+ * An implementation of {@link IDisplayWindowInsetsController} for a given display id.
+ **/
+ public class PerDisplay {
+ private final int mDisplayId;
+ private final DisplayWindowInsetsControllerImpl mInsetsControllerImpl =
+ new DisplayWindowInsetsControllerImpl();
+
+ public PerDisplay(int displayId) {
+ mDisplayId = displayId;
+ }
+
+ public void register() {
+ try {
+ mWmService.setDisplayWindowInsetsController(mDisplayId, mInsetsControllerImpl);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Unable to set insets controller on display " + mDisplayId);
+ }
+ }
+
+ public void unregister() {
+ try {
+ mWmService.setDisplayWindowInsetsController(mDisplayId, null);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Unable to remove insets controller on display " + mDisplayId);
+ }
+ }
+
+ private void insetsChanged(InsetsState insetsState) {
+ CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+ if (listeners == null) {
+ return;
+ }
+ mDisplayController.updateDisplayInsets(mDisplayId, insetsState);
+ for (OnInsetsChangedListener listener : listeners) {
+ listener.insetsChanged(insetsState);
+ }
+ }
+
+ private void insetsControlChanged(InsetsState insetsState,
+ InsetsSourceControl[] activeControls) {
+ CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+ if (listeners == null) {
+ return;
+ }
+ for (OnInsetsChangedListener listener : listeners) {
+ listener.insetsControlChanged(insetsState, activeControls);
+ }
+ }
+
+ private void showInsets(int types, boolean fromIme) {
+ CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+ if (listeners == null) {
+ return;
+ }
+ for (OnInsetsChangedListener listener : listeners) {
+ listener.showInsets(types, fromIme);
+ }
+ }
+
+ private void hideInsets(int types, boolean fromIme) {
+ CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+ if (listeners == null) {
+ return;
+ }
+ for (OnInsetsChangedListener listener : listeners) {
+ listener.hideInsets(types, fromIme);
+ }
+ }
+
+ private void topFocusedWindowChanged(String packageName) {
+ CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+ if (listeners == null) {
+ return;
+ }
+ for (OnInsetsChangedListener listener : listeners) {
+ listener.topFocusedWindowChanged(packageName);
+ }
+ }
+
+ @BinderThread
+ private class DisplayWindowInsetsControllerImpl
+ extends IDisplayWindowInsetsController.Stub {
+ @Override
+ public void topFocusedWindowChanged(String packageName) throws RemoteException {
+ mMainExecutor.execute(() -> {
+ PerDisplay.this.topFocusedWindowChanged(packageName);
+ });
+ }
+
+ @Override
+ public void insetsChanged(InsetsState insetsState) throws RemoteException {
+ mMainExecutor.execute(() -> {
+ PerDisplay.this.insetsChanged(insetsState);
+ });
+ }
+
+ @Override
+ public void insetsControlChanged(InsetsState insetsState,
+ InsetsSourceControl[] activeControls) throws RemoteException {
+ mMainExecutor.execute(() -> {
+ PerDisplay.this.insetsControlChanged(insetsState, activeControls);
+ });
+ }
+
+ @Override
+ public void showInsets(int types, boolean fromIme) throws RemoteException {
+ mMainExecutor.execute(() -> {
+ PerDisplay.this.showInsets(types, fromIme);
+ });
+ }
+
+ @Override
+ public void hideInsets(int types, boolean fromIme) throws RemoteException {
+ mMainExecutor.execute(() -> {
+ PerDisplay.this.hideInsets(types, fromIme);
+ });
+ }
+ }
+ }
+
+ /**
+ * Gets notified whenever the insets change.
+ *
+ * @see IDisplayWindowInsetsController
+ */
+ @ShellMainThread
+ public interface OnInsetsChangedListener {
+ /**
+ * 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: Passes the top package name
+ */
+ default void topFocusedWindowChanged(String packageName) {}
+
+ /**
+ * Called when the window insets configuration has changed.
+ */
+ default void insetsChanged(InsetsState insetsState) {}
+
+ /**
+ * Called when this window retrieved control over a specified set of insets sources.
+ */
+ default void insetsControlChanged(InsetsState insetsState,
+ InsetsSourceControl[] activeControls) {}
+
+ /**
+ * Called when a set of insets source window should be shown by policy.
+ *
+ * @param types internal insets types (WindowInsets.Type.InsetsType) to show
+ * @param fromIme true if this request originated from IME (InputMethodService).
+ */
+ default void showInsets(int types, boolean fromIme) {}
+
+ /**
+ * Called when a set of insets source window should be hidden by policy.
+ *
+ * @param types internal insets types (WindowInsets.Type.InsetsType) to hide
+ * @param fromIme true if this request originated from IME (InputMethodService).
+ */
+ default void hideInsets(int types, boolean fromIme) {}
+ }
+} \ No newline at end of file
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 b7235a31af03..962aca122b4d 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
@@ -25,6 +25,7 @@ import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON
import static android.util.RotationUtils.rotateBounds;
import static android.util.RotationUtils.rotateInsets;
import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
+import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
@@ -44,7 +45,10 @@ import android.view.Display;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.Gravity;
+import android.view.InsetsSource;
+import android.view.InsetsState;
import android.view.Surface;
+import android.view.WindowInsets;
import com.android.internal.R;
@@ -82,6 +86,10 @@ public class DisplayLayout {
private boolean mHasNavigationBar = false;
private boolean mHasStatusBar = false;
private int mNavBarFrameHeight = 0;
+ private boolean mAllowSeamlessRotationDespiteNavBarMoving = false;
+ private boolean mNavigationBarCanMove = false;
+ private boolean mReverseDefaultRotation = false;
+ private InsetsState mInsetsState = new InsetsState();
@Override
public boolean equals(Object o) {
@@ -98,14 +106,20 @@ public class DisplayLayout {
&& Objects.equals(mStableInsets, other.mStableInsets)
&& mHasNavigationBar == other.mHasNavigationBar
&& mHasStatusBar == other.mHasStatusBar
- && mNavBarFrameHeight == other.mNavBarFrameHeight;
+ && mAllowSeamlessRotationDespiteNavBarMoving
+ == other.mAllowSeamlessRotationDespiteNavBarMoving
+ && mNavigationBarCanMove == other.mNavigationBarCanMove
+ && mReverseDefaultRotation == other.mReverseDefaultRotation
+ && mNavBarFrameHeight == other.mNavBarFrameHeight
+ && Objects.equals(mInsetsState, other.mInsetsState);
}
@Override
public int hashCode() {
return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi,
mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar,
- mNavBarFrameHeight);
+ mNavBarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving,
+ mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState);
}
/**
@@ -150,9 +164,13 @@ public class DisplayLayout {
mDensityDpi = dl.mDensityDpi;
mHasNavigationBar = dl.mHasNavigationBar;
mHasStatusBar = dl.mHasStatusBar;
+ mAllowSeamlessRotationDespiteNavBarMoving = dl.mAllowSeamlessRotationDespiteNavBarMoving;
+ mNavigationBarCanMove = dl.mNavigationBarCanMove;
+ mReverseDefaultRotation = dl.mReverseDefaultRotation;
mNavBarFrameHeight = dl.mNavBarFrameHeight;
mNonDecorInsets.set(dl.mNonDecorInsets);
mStableInsets.set(dl.mStableInsets);
+ mInsetsState.set(dl.mInsetsState, true /* copySources */);
}
private void init(DisplayInfo info, Resources res, boolean hasNavigationBar,
@@ -165,12 +183,24 @@ public class DisplayLayout {
mDensityDpi = info.logicalDensityDpi;
mHasNavigationBar = hasNavigationBar;
mHasStatusBar = hasStatusBar;
+ mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean(
+ R.bool.config_allowSeamlessRotationDespiteNavBarMoving);
+ mNavigationBarCanMove = res.getBoolean(R.bool.config_navBarCanMove);
+ mReverseDefaultRotation = res.getBoolean(R.bool.config_reverseDefaultRotation);
+ recalcInsets(res);
+ }
+
+ /**
+ * Updates the current insets.
+ */
+ public void setInsets(Resources res, InsetsState state) {
+ mInsetsState = state;
recalcInsets(res);
}
private void recalcInsets(Resources res) {
- computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mUiMode, mNonDecorInsets,
- mHasNavigationBar);
+ computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mInsetsState, mUiMode,
+ mNonDecorInsets, mHasNavigationBar);
mStableInsets.set(mNonDecorInsets);
if (mHasStatusBar) {
convertNonDecorInsetsToStableInsets(res, mStableInsets, mWidth, mHeight, mHasStatusBar);
@@ -244,11 +274,33 @@ public class DisplayLayout {
return mWidth > mHeight;
}
- /** Get the navbar frame height (used by ime). */
+ /** Get the navbar frame (or window) height (used by ime). */
public int navBarFrameHeight() {
return mNavBarFrameHeight;
}
+ /** @return whether we can seamlessly rotate even if nav-bar can change sides. */
+ public boolean allowSeamlessRotationDespiteNavBarMoving() {
+ return mAllowSeamlessRotationDespiteNavBarMoving;
+ }
+
+ /** @return whether the navigation bar will change sides during rotation. */
+ public boolean navigationBarCanMove() {
+ return mNavigationBarCanMove;
+ }
+
+ /** @return the rotation that would make the physical display "upside down". */
+ public int getUpsideDownRotation() {
+ boolean displayHardwareIsLandscape = mWidth > mHeight;
+ if ((mRotation % 2) != 0) {
+ displayHardwareIsLandscape = !displayHardwareIsLandscape;
+ }
+ if (displayHardwareIsLandscape) {
+ return mReverseDefaultRotation ? Surface.ROTATION_270 : Surface.ROTATION_90;
+ }
+ return Surface.ROTATION_180;
+ }
+
/** Gets the orientation of this layout */
public int getOrientation() {
return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
@@ -291,21 +343,29 @@ public class DisplayLayout {
* @param outInsets the insets to return
*/
static void computeNonDecorInsets(Resources res, int displayRotation, int displayWidth,
- int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
- boolean hasNavigationBar) {
+ int displayHeight, DisplayCutout displayCutout, InsetsState insetsState, int uiMode,
+ Rect outInsets, boolean hasNavigationBar) {
outInsets.setEmpty();
// Only navigation bar
if (hasNavigationBar) {
+ final InsetsSource extraNavBar = insetsState.getSource(ITYPE_EXTRA_NAVIGATION_BAR);
+ final boolean hasExtraNav = extraNavBar != null && extraNavBar.isVisible();
int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation);
int navBarSize =
getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode);
if (position == NAV_BAR_BOTTOM) {
- outInsets.bottom = navBarSize;
+ outInsets.bottom = hasExtraNav
+ ? Math.max(navBarSize, extraNavBar.getFrame().height())
+ : navBarSize;
} else if (position == NAV_BAR_RIGHT) {
- outInsets.right = navBarSize;
+ outInsets.right = hasExtraNav
+ ? Math.max(navBarSize, extraNavBar.getFrame().width())
+ : navBarSize;
} else if (position == NAV_BAR_LEFT) {
- outInsets.left = navBarSize;
+ outInsets.left = hasExtraNav
+ ? Math.max(navBarSize, extraNavBar.getFrame().width())
+ : navBarSize;
}
}
@@ -327,13 +387,13 @@ public class DisplayLayout {
* @param outInsets the insets to return
*/
static void computeStableInsets(Resources res, int displayRotation, int displayWidth,
- int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
- boolean hasNavigationBar, boolean hasStatusBar) {
+ int displayHeight, DisplayCutout displayCutout, InsetsState insetsState, int uiMode,
+ Rect outInsets, boolean hasNavigationBar, boolean hasStatusBar) {
outInsets.setEmpty();
// Navigation bar and status bar.
computeNonDecorInsets(res, displayRotation, displayWidth, displayHeight, displayCutout,
- uiMode, outInsets, hasNavigationBar);
+ insetsState, uiMode, outInsets, hasNavigationBar);
convertNonDecorInsetsToStableInsets(res, outInsets, displayWidth, displayHeight,
hasStatusBar);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
index 33beab5ee3f1..4c0281dcc517 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
@@ -18,13 +18,15 @@ package com.android.wm.shell.common;
import android.annotation.BinderThread;
import android.annotation.NonNull;
+import android.os.RemoteException;
import android.util.Slog;
import android.view.SurfaceControl;
+import android.view.WindowManager;
import android.window.WindowContainerTransaction;
import android.window.WindowContainerTransactionCallback;
import android.window.WindowOrganizer;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.transition.LegacyTransitions;
import java.util.ArrayList;
@@ -66,6 +68,10 @@ public final class SyncTransactionQueue {
* Queues a sync transaction to be sent serially to WM.
*/
public void queue(WindowContainerTransaction wct) {
+ if (wct.isEmpty()) {
+ if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty");
+ return;
+ }
SyncCallback cb = new SyncCallback(wct);
synchronized (mQueue) {
if (DEBUG) Slog.d(TAG, "Queueing up " + wct);
@@ -77,11 +83,34 @@ public final class SyncTransactionQueue {
}
/**
+ * Queues a legacy transition to be sent serially to WM
+ */
+ public void queue(LegacyTransitions.ILegacyTransition transition,
+ @WindowManager.TransitionType int type, WindowContainerTransaction wct) {
+ if (wct.isEmpty()) {
+ if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty");
+ return;
+ }
+ SyncCallback cb = new SyncCallback(transition, type, wct);
+ synchronized (mQueue) {
+ if (DEBUG) Slog.d(TAG, "Queueing up legacy transition " + wct);
+ mQueue.add(cb);
+ if (mQueue.size() == 1) {
+ cb.send();
+ }
+ }
+ }
+
+ /**
* Queues a sync transaction only if there are already sync transaction(s) queued or in flight.
* Otherwise just returns without queueing.
* @return {@code true} if queued, {@code false} if not.
*/
public boolean queueIfWaiting(WindowContainerTransaction wct) {
+ if (wct.isEmpty()) {
+ if (DEBUG) Slog.d(TAG, "Skip queueIfWaiting due to transaction change is empty");
+ return false;
+ }
synchronized (mQueue) {
if (mQueue.isEmpty()) {
if (DEBUG) Slog.d(TAG, "Nothing in queue, so skip queueing up " + wct);
@@ -118,12 +147,12 @@ public final class SyncTransactionQueue {
// Synchronized on mQueue
private void onTransactionReceived(@NonNull SurfaceControl.Transaction t) {
if (DEBUG) Slog.d(TAG, " Running " + mRunnables.size() + " sync runnables");
- for (int i = 0, n = mRunnables.size(); i < n; ++i) {
+ final int n = mRunnables.size();
+ for (int i = 0; i < n; ++i) {
mRunnables.get(i).runWithTransaction(t);
}
- mRunnables.clear();
- t.apply();
- t.close();
+ // More runnables may have been added, so only remove the ones that ran.
+ mRunnables.subList(0, n).clear();
}
/** Task to run with transaction. */
@@ -135,20 +164,38 @@ public final class SyncTransactionQueue {
private class SyncCallback extends WindowContainerTransactionCallback {
int mId = -1;
final WindowContainerTransaction mWCT;
+ final LegacyTransitions.LegacyTransition mLegacyTransition;
SyncCallback(WindowContainerTransaction wct) {
mWCT = wct;
+ mLegacyTransition = null;
+ }
+
+ SyncCallback(LegacyTransitions.ILegacyTransition legacyTransition,
+ @WindowManager.TransitionType int type, WindowContainerTransaction wct) {
+ mWCT = wct;
+ mLegacyTransition = new LegacyTransitions.LegacyTransition(type, legacyTransition);
}
// Must be sychronized on mQueue
void send() {
+ if (mInFlight == this) {
+ // This was probably queued up and sent during a sync runnable of the last callback.
+ // Don't queue it again.
+ return;
+ }
if (mInFlight != null) {
throw new IllegalStateException("Sync Transactions must be serialized. In Flight: "
+ mInFlight.mId + " - " + mInFlight.mWCT);
}
mInFlight = this;
if (DEBUG) Slog.d(TAG, "Sending sync transaction: " + mWCT);
- mId = new WindowOrganizer().applySyncTransaction(mWCT, this);
+ if (mLegacyTransition != null) {
+ mId = new WindowOrganizer().startLegacyTransition(mLegacyTransition.getType(),
+ mLegacyTransition.getAdapter(), this, mWCT);
+ } else {
+ mId = new WindowOrganizer().applySyncTransaction(mWCT, this);
+ }
if (DEBUG) Slog.d(TAG, " Sent sync transaction. Got id=" + mId);
mMainExecutor.executeDelayed(mOnReplyTimeout, REPLY_TIMEOUT);
}
@@ -169,6 +216,16 @@ public final class SyncTransactionQueue {
if (DEBUG) Slog.d(TAG, "onTransactionReady id=" + mId);
mQueue.remove(this);
onTransactionReceived(t);
+ if (mLegacyTransition != null) {
+ try {
+ mLegacyTransition.getSyncCallback().onTransactionReady(mId, t);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error sending callback to legacy transition: " + mId, e);
+ }
+ } else {
+ t.apply();
+ t.close();
+ }
if (!mQueue.isEmpty()) {
mQueue.get(0).send();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
index 218bf47e24aa..c76937de6669 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
@@ -70,7 +70,8 @@ public class DividerHandleView extends View {
private final Paint mPaint = new Paint();
private final int mWidth;
private final int mHeight;
- private final int mCircleDiameter;
+ private final int mTouchingWidth;
+ private final int mTouchingHeight;
private int mCurrentWidth;
private int mCurrentHeight;
private AnimatorSet mAnimator;
@@ -80,11 +81,12 @@ public class DividerHandleView extends View {
super(context, attrs);
mPaint.setColor(getResources().getColor(R.color.docked_divider_handle, null));
mPaint.setAntiAlias(true);
- mWidth = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_width);
- mHeight = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_height);
+ mWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_width);
+ mHeight = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_height);
mCurrentWidth = mWidth;
mCurrentHeight = mHeight;
- mCircleDiameter = (mWidth + mHeight) / 3;
+ mTouchingWidth = mWidth > mHeight ? mWidth / 2 : mWidth;
+ mTouchingHeight = mHeight > mWidth ? mHeight / 2 : mHeight;
}
/** Sets touching state for this handle view. */
@@ -98,16 +100,16 @@ public class DividerHandleView extends View {
}
if (!animate) {
if (touching) {
- mCurrentWidth = mCircleDiameter;
- mCurrentHeight = mCircleDiameter;
+ mCurrentWidth = mTouchingWidth;
+ mCurrentHeight = mTouchingHeight;
} else {
mCurrentWidth = mWidth;
mCurrentHeight = mHeight;
}
invalidate();
} else {
- animateToTarget(touching ? mCircleDiameter : mWidth,
- touching ? mCircleDiameter : mHeight, touching);
+ animateToTarget(touching ? mTouchingWidth : mWidth,
+ touching ? mTouchingHeight : mHeight, touching);
}
mTouching = touching;
}
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 cba019a11b28..826e2f5c2f74 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
@@ -19,15 +19,23 @@ package com.android.wm.shell.common.split;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
+import android.animation.ObjectAnimator;
import android.content.Context;
+import android.graphics.Rect;
import android.util.AttributeSet;
+import android.util.Property;
+import android.util.TypedValue;
import android.view.GestureDetector;
+import android.view.InsetsSource;
+import android.view.InsetsState;
import android.view.MotionEvent;
import android.view.SurfaceControlViewHost;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
+import android.view.ViewGroup;
import android.view.WindowManager;
+import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
@@ -44,6 +52,23 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
public static final long TOUCH_ANIMATION_DURATION = 150;
public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200;
+ // TODO(b/191269755): use the value defined in InsetsController.
+ private static final Interpolator RESIZE_INTERPOLATOR = Interpolators.LINEAR;
+
+ // TODO(b/191269755): use the value defined in InsetsController.
+ private static final int ANIMATION_DURATION_RESIZE = 300;
+
+ /**
+ * The task bar height defined in launcher. Used to determine whether to insets divider bounds
+ * or not.
+ */
+ private static final int EXPANDED_TASK_BAR_HEIGHT_IN_DP = 60;
+
+ /** The task bar expanded height. Used to determine whether to insets divider bounds or not. */
+ private final float mExpandedTaskBarHeight = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, EXPANDED_TASK_BAR_HEIGHT_IN_DP,
+ getResources().getDisplayMetrics());
+
private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
private SplitLayout mSplitLayout;
@@ -58,6 +83,31 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
private GestureDetector mDoubleTapDetector;
private boolean mInteractive;
+ /**
+ * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with
+ * insets.
+ */
+ private final Rect mDividerBounds = new Rect();
+ private final Rect mTempRect = new Rect();
+ private FrameLayout mDividerBar;
+
+
+ static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY =
+ new Property<DividerView, Integer>(Integer.class, "height") {
+ @Override
+ public Integer get(DividerView object) {
+ return object.mDividerBar.getLayoutParams().height;
+ }
+
+ @Override
+ public void set(DividerView object, Integer value) {
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
+ object.mDividerBar.getLayoutParams();
+ lp.height = value;
+ object.mDividerBar.setLayoutParams(lp);
+ }
+ };
+
public DividerView(@NonNull Context context) {
super(context);
}
@@ -79,14 +129,42 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
/** Sets up essential dependencies of the divider bar. */
public void setup(
SplitLayout layout,
- SurfaceControlViewHost viewHost) {
+ SurfaceControlViewHost viewHost,
+ InsetsState insetsState) {
mSplitLayout = layout;
mViewHost = viewHost;
+ mDividerBounds.set(layout.getDividerBounds());
+ onInsetsChanged(insetsState, false /* animate */);
+ }
+
+ void onInsetsChanged(InsetsState insetsState, boolean animate) {
+ mTempRect.set(mSplitLayout.getDividerBounds());
+ 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) {
+ mTempRect.inset(taskBarInsetsSource.calculateVisibleInsets(mTempRect));
+ }
+
+ if (!mTempRect.equals(mDividerBounds)) {
+ if (animate) {
+ ObjectAnimator animator = ObjectAnimator.ofInt(this,
+ DIVIDER_HEIGHT_PROPERTY, mDividerBounds.height(), mTempRect.height());
+ animator.setInterpolator(RESIZE_INTERPOLATOR);
+ animator.setDuration(ANIMATION_DURATION_RESIZE);
+ animator.start();
+ } else {
+ DIVIDER_HEIGHT_PROPERTY.set(this, mTempRect.height());
+ }
+ mDividerBounds.set(mTempRect);
+ }
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
+ mDividerBar = findViewById(R.id.divider_bar);
mHandle = findViewById(R.id.docked_divider_handle);
mBackground = findViewById(R.id.docked_divider_background);
mTouchElevation = getResources().getDimensionPixelSize(
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 5b158d2063ba..81cad5ac5a51 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
@@ -16,11 +16,19 @@
package com.android.wm.shell.common.split;
+import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
+import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
+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 static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
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;
+import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -30,7 +38,10 @@ import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.Point;
import android.graphics.Rect;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.WindowInsets;
import android.view.WindowManager;
@@ -39,16 +50,19 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.DividerSnapAlgorithm;
+import com.android.internal.policy.DockedDividerUtils;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
/**
* Records and handles layout of splits. Helps to calculate proper bounds when configuration or
* divide position changes.
*/
-public final class SplitLayout {
+public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener {
/**
* Split position isn't specified normally meaning to use what ever it is currently set to.
*/
@@ -78,32 +92,44 @@ public final class SplitLayout {
private final int mDividerInsets;
private final int mDividerSize;
+ private final Rect mTempRect = new Rect();
private final Rect mRootBounds = new Rect();
private final Rect mDividerBounds = new Rect();
private final Rect mBounds1 = new Rect();
private final Rect mBounds2 = new Rect();
+ private final Rect mWinBounds1 = new Rect();
+ private final Rect mWinBounds2 = new Rect();
private final SplitLayoutHandler mSplitLayoutHandler;
private final SplitWindowManager mSplitWindowManager;
private final DisplayImeController mDisplayImeController;
private final ImePositionProcessor mImePositionProcessor;
+ private final DismissingParallaxPolicy mDismissingParallaxPolicy;
private final ShellTaskOrganizer mTaskOrganizer;
+ private final InsetsState mInsetsState = new InsetsState();
private Context mContext;
private DividerSnapAlgorithm mDividerSnapAlgorithm;
+ private WindowContainerToken mWinToken1;
+ private WindowContainerToken mWinToken2;
private int mDividePosition;
private boolean mInitialized = false;
+ private int mOrientation;
+ private int mRotation;
public SplitLayout(String windowName, Context context, Configuration configuration,
SplitLayoutHandler splitLayoutHandler,
SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks,
DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer) {
mContext = context.createConfigurationContext(configuration);
+ mOrientation = configuration.orientation;
+ mRotation = configuration.windowConfiguration.getRotation();
mSplitLayoutHandler = splitLayoutHandler;
mDisplayImeController = displayImeController;
mSplitWindowManager = new SplitWindowManager(
windowName, mContext, configuration, parentContainerCallbacks);
mTaskOrganizer = taskOrganizer;
mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId());
+ mDismissingParallaxPolicy = new DismissingParallaxPolicy();
final Resources resources = context.getResources();
mDividerWindowWidth = resources.getDimensionPixelSize(
@@ -142,27 +168,61 @@ public final class SplitLayout {
return mDividePosition;
}
+ /**
+ * Returns the divider position as a fraction from 0 to 1.
+ */
+ public float getDividerPositionAsFraction() {
+ return Math.min(1f, Math.max(0f, isLandscape()
+ ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right
+ : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom));
+ }
+
/** Applies new configuration, returns {@code false} if there's no effect to the layout. */
public boolean updateConfiguration(Configuration configuration) {
- final Rect rootBounds = configuration.windowConfiguration.getBounds();
- if (mRootBounds.equals(rootBounds)) {
- return false;
+ boolean affectsLayout = false;
+
+ // Make sure to render the divider bar with proper resources that matching the screen
+ // orientation.
+ final int orientation = configuration.orientation;
+ if (orientation != mOrientation) {
+ mOrientation = orientation;
+ mContext = mContext.createConfigurationContext(configuration);
+ mSplitWindowManager.setConfiguration(configuration);
+ affectsLayout = true;
}
- mContext = mContext.createConfigurationContext(configuration);
- mSplitWindowManager.setConfiguration(configuration);
- mRootBounds.set(rootBounds);
- mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
- resetDividerPosition();
+ // 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.
+ final int rotation = configuration.windowConfiguration.getRotation();
+ final Rect rootBounds = configuration.windowConfiguration.getBounds();
+ if (rotation != mRotation || !mRootBounds.equals(rootBounds)) {
+ mTempRect.set(mRootBounds);
+ mRootBounds.set(rootBounds);
+ mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
+ initDividerPosition(mTempRect);
+ affectsLayout = true;
+ }
- // Don't inflate divider bar if it is not initialized.
- if (!mInitialized) {
- return false;
+ if (mInitialized) {
+ release();
+ init();
}
- release();
- init();
- return true;
+ return affectsLayout;
+ }
+
+ private void initDividerPosition(Rect oldBounds) {
+ final float snapRatio = (float) mDividePosition
+ / (float) (isLandscape(oldBounds) ? oldBounds.width() : oldBounds.height());
+ // Estimate position by previous ratio.
+ final float length =
+ (float) (isLandscape() ? mRootBounds.width() : mRootBounds.height());
+ final int estimatePosition = (int) (length * snapRatio);
+ // Init divider position by estimated position using current bounds snap algorithm.
+ mDividePosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
+ estimatePosition).position;
+ updateBounds(mDividePosition);
}
/** Updates recording bounds of divider window and both of the splits. */
@@ -170,7 +230,8 @@ public final class SplitLayout {
mDividerBounds.set(mRootBounds);
mBounds1.set(mRootBounds);
mBounds2.set(mRootBounds);
- if (isLandscape(mRootBounds)) {
+ final boolean isLandscape = isLandscape(mRootBounds);
+ if (isLandscape) {
position += mRootBounds.left;
mDividerBounds.left = position - mDividerInsets;
mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth;
@@ -183,13 +244,16 @@ public final class SplitLayout {
mBounds1.bottom = position;
mBounds2.top = mBounds1.bottom + mDividerSize;
}
+ DockedDividerUtils.sanitizeStackBounds(mBounds1, true /** topLeft */);
+ DockedDividerUtils.sanitizeStackBounds(mBounds2, false /** topLeft */);
+ mDismissingParallaxPolicy.applyDividerPosition(position, isLandscape);
}
/** Inflates {@link DividerView} on the root surface. */
public void init() {
if (mInitialized) return;
mInitialized = true;
- mSplitWindowManager.init(this);
+ mSplitWindowManager.init(this, mInsetsState);
mDisplayImeController.addPositionProcessor(mImePositionProcessor);
}
@@ -202,6 +266,23 @@ public final class SplitLayout {
mImePositionProcessor.reset();
}
+ @Override
+ public void insetsChanged(InsetsState insetsState) {
+ mInsetsState.set(insetsState);
+ if (!mInitialized) {
+ return;
+ }
+ mSplitWindowManager.onInsetsChanged(insetsState);
+ }
+
+ @Override
+ public void insetsControlChanged(InsetsState insetsState,
+ InsetsSourceControl[] activeControls) {
+ if (!mInsetsState.equals(insetsState)) {
+ insetsChanged(insetsState);
+ }
+ }
+
/**
* Updates bounds with the passing position. Usually used to update recording bounds while
* performing animation or dragging divider bar to resize the splits.
@@ -209,19 +290,20 @@ public final class SplitLayout {
void updateDivideBounds(int position) {
updateBounds(position);
mSplitWindowManager.setResizingSplits(true);
- mSplitLayoutHandler.onBoundsChanging(this);
+ mSplitLayoutHandler.onLayoutChanging(this);
}
void setDividePosition(int position) {
mDividePosition = position;
updateBounds(mDividePosition);
- mSplitLayoutHandler.onBoundsChanged(this);
+ mSplitLayoutHandler.onLayoutChanged(this);
mSplitWindowManager.setResizingSplits(false);
}
/** Resets divider position. */
public void resetDividerPosition() {
mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
+ mSplitWindowManager.setResizingSplits(false);
updateBounds(mDividePosition);
}
@@ -232,15 +314,15 @@ public final class SplitLayout {
public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
switch (snapTarget.flag) {
case FLAG_DISMISS_START:
- mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */);
- mSplitWindowManager.setResizingSplits(false);
+ flingDividePosition(currentPosition, snapTarget.position,
+ () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */));
break;
case FLAG_DISMISS_END:
- mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */);
- mSplitWindowManager.setResizingSplits(false);
+ flingDividePosition(currentPosition, snapTarget.position,
+ () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */));
break;
default:
- flingDividePosition(currentPosition, snapTarget.position);
+ flingDividePosition(currentPosition, snapTarget.position, null);
break;
}
}
@@ -270,8 +352,13 @@ public final class SplitLayout {
isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
}
- private void flingDividePosition(int from, int to) {
- if (from == to) return;
+ @VisibleForTesting
+ void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) {
+ if (from == to) {
+ // No animation run, it should stop resizing here.
+ mSplitWindowManager.setResizingSplits(false);
+ return;
+ }
ValueAnimator animator = ValueAnimator
.ofInt(from, to)
.setDuration(250);
@@ -282,6 +369,9 @@ public final class SplitLayout {
@Override
public void onAnimationEnd(Animator animation) {
setDividePosition(to);
+ if (flingFinishedCallback != null) {
+ flingFinishedCallback.run();
+ }
}
@Override
@@ -296,42 +386,99 @@ public final class SplitLayout {
return context.getSystemService(WindowManager.class)
.getMaximumWindowMetrics()
.getWindowInsets()
- .getInsets(WindowInsets.Type.navigationBars()
- | WindowInsets.Type.statusBars()
- | WindowInsets.Type.displayCutout()).toRect();
+ .getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout())
+ .toRect();
}
private static boolean isLandscape(Rect bounds) {
return bounds.width() > bounds.height();
}
+ /**
+ * Return if this layout is landscape.
+ */
+ public boolean isLandscape() {
+ return isLandscape(mRootBounds);
+ }
+
/** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */
public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1,
SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
- final Rect dividerBounds = mImePositionProcessor.adjustForIme(mDividerBounds);
- final Rect bounds1 = mImePositionProcessor.adjustForIme(mBounds1);
- final Rect bounds2 = mImePositionProcessor.adjustForIme(mBounds2);
final SurfaceControl dividerLeash = getDividerLeash();
if (dividerLeash != null) {
- t.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
- // Resets layer of divider bar to make sure it is always on top.
- .setLayer(dividerLeash, Integer.MAX_VALUE);
+ t.setPosition(dividerLeash, mDividerBounds.left, mDividerBounds.top);
+ // Resets layer of divider bar to make sure it is always on top.
+ t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER);
+ }
+ t.setPosition(leash1, mBounds1.left, mBounds1.top)
+ .setWindowCrop(leash1, mBounds1.width(), mBounds1.height());
+ t.setPosition(leash2, mBounds2.left, mBounds2.top)
+ .setWindowCrop(leash2, mBounds2.width(), mBounds2.height());
+
+ if (mImePositionProcessor.adjustSurfaceLayoutForIme(
+ t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) {
+ return;
}
- t.setPosition(leash1, bounds1.left, bounds1.top)
- .setWindowCrop(leash1, bounds1.width(), bounds1.height());
-
- t.setPosition(leash2, bounds2.left, bounds2.top)
- .setWindowCrop(leash2, bounds2.width(), bounds2.height());
-
- mImePositionProcessor.applySurfaceDimValues(t, dimLayer1, dimLayer2);
+ mDismissingParallaxPolicy.adjustDismissingSurface(t, leash1, leash2, dimLayer1, dimLayer2);
}
/** Apply recorded task layout to the {@link WindowContainerTransaction}. */
public void applyTaskChanges(WindowContainerTransaction wct,
ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
- wct.setBounds(task1.token, mImePositionProcessor.adjustForIme(mBounds1))
- .setBounds(task2.token, mImePositionProcessor.adjustForIme(mBounds2));
+ if (mImePositionProcessor.applyTaskLayoutForIme(wct, task1.token, task2.token)) {
+ return;
+ }
+
+ if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) {
+ wct.setBounds(task1.token, mBounds1);
+ mWinBounds1.set(mBounds1);
+ mWinToken1 = task1.token;
+ }
+ if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) {
+ wct.setBounds(task2.token, mBounds2);
+ mWinBounds2.set(mBounds2);
+ mWinToken2 = task2.token;
+ }
+ }
+
+ /**
+ * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And
+ * restore shifted configuration bounds if it's no longer shifted.
+ */
+ public void applyLayoutShifted(WindowContainerTransaction wct, int offsetX, int offsetY,
+ 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());
+ 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());
+ 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);
+ }
}
/** Handles layout change event. */
@@ -341,10 +488,18 @@ public final class SplitLayout {
void onSnappedToDismiss(boolean snappedToEnd);
/** Calls when the bounds is changing due to animation or dragging divider bar. */
- void onBoundsChanging(SplitLayout layout);
+ void onLayoutChanging(SplitLayout layout);
/** Calls when the target bounds changed. */
- void onBoundsChanged(SplitLayout layout);
+ void onLayoutChanged(SplitLayout layout);
+
+ /**
+ * Notifies when the layout shifted. So the layout handler can shift configuration
+ * bounds correspondingly to make sure client apps won't get configuration changed or
+ * relaunch. If the layout is no longer shifted, layout handler should restore shifted
+ * configuration bounds.
+ */
+ void onLayoutShifted(int offsetX, int offsetY, SplitLayout layout);
/** Calls when user double tapped on the divider bar. */
default void onDoubleTappedDivider() {
@@ -355,6 +510,106 @@ public final class SplitLayout {
int getSplitItemPosition(WindowContainerToken token);
}
+ /**
+ * Calculates and applies proper dismissing parallax offset and dimming value to hint users
+ * dismissing gesture.
+ */
+ private class DismissingParallaxPolicy {
+ // The current dismissing side.
+ int mDismissingSide = DOCKED_INVALID;
+
+ // The parallax offset to hint the dismissing side and progress.
+ final Point mDismissingParallaxOffset = new Point();
+
+ // The dimming value to hint the dismissing side and progress.
+ float mDismissingDimValue = 0.0f;
+
+ /**
+ * Applies a parallax to the task to hint dismissing progress.
+ *
+ * @param position the split position to apply dismissing parallax effect
+ * @param isLandscape indicates whether it's splitting horizontally or vertically
+ */
+ void applyDividerPosition(int position, boolean isLandscape) {
+ mDismissingSide = DOCKED_INVALID;
+ mDismissingParallaxOffset.set(0, 0);
+ mDismissingDimValue = 0;
+
+ int totalDismissingDistance = 0;
+ if (position <= mDividerSnapAlgorithm.getFirstSplitTarget().position) {
+ mDismissingSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP;
+ totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position
+ - mDividerSnapAlgorithm.getFirstSplitTarget().position;
+ } else if (position >= mDividerSnapAlgorithm.getLastSplitTarget().position) {
+ mDismissingSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM;
+ totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position
+ - mDividerSnapAlgorithm.getDismissEndTarget().position;
+ }
+
+ if (mDismissingSide != DOCKED_INVALID) {
+ float fraction = Math.max(0,
+ Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f));
+ mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction);
+ fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide);
+ if (isLandscape) {
+ mDismissingParallaxOffset.x = (int) (fraction * totalDismissingDistance);
+ } else {
+ mDismissingParallaxOffset.y = (int) (fraction * totalDismissingDistance);
+ }
+ }
+ }
+
+ /**
+ * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
+ * slowing down parallax effect
+ */
+ private 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;
+ }
+
+ /** Applies parallax offset and dimming value to the root surface at the dismissing side. */
+ boolean adjustDismissingSurface(SurfaceControl.Transaction t,
+ SurfaceControl leash1, SurfaceControl leash2,
+ SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
+ SurfaceControl targetLeash, targetDimLayer;
+ switch (mDismissingSide) {
+ case DOCKED_TOP:
+ case DOCKED_LEFT:
+ targetLeash = leash1;
+ targetDimLayer = dimLayer1;
+ mTempRect.set(mBounds1);
+ break;
+ case DOCKED_BOTTOM:
+ case DOCKED_RIGHT:
+ targetLeash = leash2;
+ targetDimLayer = dimLayer2;
+ mTempRect.set(mBounds2);
+ break;
+ case DOCKED_INVALID:
+ default:
+ t.setAlpha(dimLayer1, 0).hide(dimLayer1);
+ t.setAlpha(dimLayer2, 0).hide(dimLayer2);
+ return false;
+ }
+
+ t.setPosition(targetLeash,
+ mTempRect.left + mDismissingParallaxOffset.x,
+ mTempRect.top + mDismissingParallaxOffset.y);
+ // Transform the screen-based split bounds to surface-based crop bounds.
+ mTempRect.offsetTo(-mDismissingParallaxOffset.x, -mDismissingParallaxOffset.y);
+ t.setWindowCrop(targetLeash, mTempRect);
+ t.setAlpha(targetDimLayer, mDismissingDimValue)
+ .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f);
+ return true;
+ }
+ }
+
/** Records IME top offset changes and updates SplitLayout correspondingly. */
private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor {
/**
@@ -409,6 +664,18 @@ public final class SplitLayout {
&& !isFloating && !isLandscape(mRootBounds) && showing;
mTargetYOffset = needOffset ? getTargetYOffset() : 0;
+ if (mTargetYOffset != mLastYOffset) {
+ // Freeze the configuration size with offset to prevent app get a configuration
+ // changed or relaunch. This is required to make sure client apps will calculate
+ // insets properly after layout shifted.
+ if (mTargetYOffset == 0) {
+ mSplitLayoutHandler.onLayoutShifted(0, 0, SplitLayout.this);
+ } else {
+ mSplitLayoutHandler.onLayoutShifted(0, mTargetYOffset - mLastYOffset,
+ SplitLayout.this);
+ }
+ }
+
// Make {@link DividerView} non-interactive while IME showing in split mode. Listen to
// ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough
// because DividerView won't receive onImeVisibilityChanged callback after it being
@@ -423,7 +690,7 @@ public final class SplitLayout {
public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) {
if (displayId != mDisplayId) return;
onProgress(getProgress(imeTop));
- mSplitLayoutHandler.onBoundsChanging(SplitLayout.this);
+ mSplitLayoutHandler.onLayoutChanging(SplitLayout.this);
}
@Override
@@ -431,7 +698,7 @@ public final class SplitLayout {
SurfaceControl.Transaction t) {
if (displayId != mDisplayId || cancel) return;
onProgress(1.0f);
- mSplitLayoutHandler.onBoundsChanging(SplitLayout.this);
+ mSplitLayoutHandler.onLayoutChanging(SplitLayout.this);
}
@Override
@@ -441,7 +708,7 @@ public final class SplitLayout {
if (!controlling && mImeShown) {
reset();
mSplitWindowManager.setInteractive(true);
- mSplitLayoutHandler.onBoundsChanging(SplitLayout.this);
+ mSplitLayoutHandler.onLayoutChanging(SplitLayout.this);
}
}
@@ -473,24 +740,61 @@ public final class SplitLayout {
return start + (end - start) * progress;
}
- private void reset() {
+ void reset() {
mImeShown = false;
mYOffsetForIme = mLastYOffset = mTargetYOffset = 0;
mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f;
mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f;
}
- /* Adjust bounds with IME offset. */
- private Rect adjustForIme(Rect bounds) {
- final Rect temp = new Rect(bounds);
- if (mYOffsetForIme != 0) temp.offset(0, mYOffsetForIme);
- return temp;
+ /**
+ * Applies adjusted task layout for showing IME.
+ *
+ * @return {@code false} if there's no need to adjust, otherwise {@code true}
+ */
+ boolean applyTaskLayoutForIme(WindowContainerTransaction wct,
+ WindowContainerToken token1, WindowContainerToken token2) {
+ if (mYOffsetForIme == 0) return false;
+
+ mTempRect.set(mBounds1);
+ mTempRect.offset(0, mYOffsetForIme);
+ wct.setBounds(token1, mTempRect);
+
+ mTempRect.set(mBounds2);
+ mTempRect.offset(0, mYOffsetForIme);
+ wct.setBounds(token2, mTempRect);
+
+ return true;
}
- private void applySurfaceDimValues(SurfaceControl.Transaction t, SurfaceControl dimLayer1,
- SurfaceControl dimLayer2) {
+ /**
+ * Adjusts surface layout while showing IME.
+ *
+ * @return {@code false} if there's no need to adjust, otherwise {@code true}
+ */
+ boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t,
+ SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2,
+ SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
+ if (mYOffsetForIme == 0) return false;
+
+ if (dividerLeash != null) {
+ mTempRect.set(mDividerBounds);
+ mTempRect.offset(0, mYOffsetForIme);
+ t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
+ }
+
+ mTempRect.set(mBounds1);
+ mTempRect.offset(0, mYOffsetForIme);
+ t.setPosition(leash1, mTempRect.left, mTempRect.top);
+
+ mTempRect.set(mBounds2);
+ mTempRect.offset(0, mYOffsetForIme);
+ t.setPosition(leash2, mTempRect.left, mTempRect.top);
+
t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f);
t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f);
+
+ return true;
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
index 0cea0efc0057..fc7edfc4bceb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -36,6 +36,7 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
import android.view.IWindow;
+import android.view.InsetsState;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
@@ -95,7 +96,7 @@ public final class SplitWindowManager extends WindowlessWindowManager {
final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
.setContainerLayer()
.setName(TAG)
- .setHidden(false)
+ .setHidden(true)
.setCallsite("SplitWindowManager#attachToParentSurface");
mParentContainerCallbacks.attachToParentSurface(builder);
mLeash = builder.build();
@@ -103,7 +104,7 @@ public final class SplitWindowManager extends WindowlessWindowManager {
}
/** Inflates {@link DividerView} on to the root surface. */
- void init(SplitLayout splitLayout) {
+ void init(SplitLayout splitLayout, InsetsState insetsState) {
if (mDividerView != null || mViewHost != null) {
throw new UnsupportedOperationException(
"Try to inflate divider view again without release first");
@@ -123,7 +124,7 @@ public final class SplitWindowManager extends WindowlessWindowManager {
lp.setTitle(mWindowName);
lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
mViewHost.setView(mDividerView, lp);
- mDividerView.setup(splitLayout, mViewHost);
+ mDividerView.setup(splitLayout, mViewHost, insetsState);
}
/**
@@ -169,4 +170,10 @@ public final class SplitWindowManager extends WindowlessWindowManager {
SurfaceControl getSurfaceControl() {
return mLeash;
}
+
+ void onInsetsChanged(InsetsState insetsState) {
+ if (mDividerView != null) {
+ mDividerView.onInsetsChanged(insetsState, true /* animate */);
+ }
+ }
}
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 58bf22ad29b2..0c12d6c7bca2 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
@@ -34,6 +34,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Context;
import android.content.res.Configuration;
@@ -48,6 +49,8 @@ import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.UiEventLogger;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayController;
@@ -67,14 +70,17 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
private final Context mContext;
private final DisplayController mDisplayController;
+ private final DragAndDropEventLogger mLogger;
private SplitScreenController mSplitScreen;
private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>();
private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
- public DragAndDropController(Context context, DisplayController displayController) {
+ public DragAndDropController(Context context, DisplayController displayController,
+ UiEventLogger uiEventLogger) {
mContext = context;
mDisplayController = displayController;
+ mLogger = new DragAndDropEventLogger(uiEventLogger);
}
public void initialize(Optional<SplitScreenController> splitscreen) {
@@ -175,9 +181,10 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
Slog.w(TAG, "Unexpected drag start during an active drag");
return false;
}
+ InstanceId loggerSessionId = mLogger.logStart(event);
pd.activeDragCount++;
pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId),
- event.getClipData());
+ event.getClipData(), loggerSessionId);
setDropTargetWindowVisibility(pd, View.VISIBLE);
break;
case ACTION_DRAG_ENTERED:
@@ -198,7 +205,9 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
case ACTION_DRAG_ENDED:
// TODO(b/169894807): Ensure sure it's not possible to get ENDED without DROP
// or EXITED
- if (!pd.dragLayout.hasDropped()) {
+ if (pd.dragLayout.hasDropped()) {
+ mLogger.logDrop();
+ } else {
pd.activeDragCount--;
pd.dragLayout.hide(event, () -> {
if (pd.activeDragCount == 0) {
@@ -208,6 +217,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
}
});
}
+ mLogger.logEnd();
break;
}
return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java
new file mode 100644
index 000000000000..6e4b81563441
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java
@@ -0,0 +1,132 @@
+/*
+ * 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.draganddrop;
+
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.pm.ActivityInfo;
+import android.view.DragEvent;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+
+/**
+ * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent
+ */
+public class DragAndDropEventLogger {
+
+ private final UiEventLogger mUiEventLogger;
+ // Used to generate instance ids for this drag if one is not provided
+ private final InstanceIdSequence mIdSequence;
+
+ // Tracks the current drag session
+ private ActivityInfo mActivityInfo;
+ private InstanceId mInstanceId;
+
+ public DragAndDropEventLogger(UiEventLogger uiEventLogger) {
+ mUiEventLogger = uiEventLogger;
+ mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE);
+ }
+
+ /**
+ * Logs the start of a drag.
+ */
+ public InstanceId logStart(DragEvent event) {
+ final ClipDescription description = event.getClipDescription();
+ final ClipData data = event.getClipData();
+ final ClipData.Item item = data.getItemAt(0);
+ mInstanceId = item.getIntent().getParcelableExtra(
+ ClipDescription.EXTRA_LOGGING_INSTANCE_ID);
+ if (mInstanceId == null) {
+ mInstanceId = mIdSequence.newInstanceId();
+ }
+ mActivityInfo = item.getActivityInfo();
+ mUiEventLogger.logWithInstanceId(getStartEnum(description),
+ mActivityInfo.applicationInfo.uid,
+ mActivityInfo.applicationInfo.packageName, mInstanceId);
+ return mInstanceId;
+ }
+
+ /**
+ * Logs a successful drop.
+ */
+ public void logDrop() {
+ mUiEventLogger.logWithInstanceId(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_DROPPED,
+ mActivityInfo.applicationInfo.uid,
+ mActivityInfo.applicationInfo.packageName, mInstanceId);
+ }
+
+ /**
+ * Logs the end of a drag.
+ */
+ public void logEnd() {
+ mUiEventLogger.logWithInstanceId(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_END,
+ mActivityInfo.applicationInfo.uid,
+ mActivityInfo.applicationInfo.packageName, mInstanceId);
+ }
+
+ /**
+ * Returns the start logging enum for the given drag description.
+ */
+ private DragAndDropUiEventEnum getStartEnum(ClipDescription description) {
+ if (description.hasMimeType(MIMETYPE_APPLICATION_ACTIVITY)) {
+ return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_ACTIVITY;
+ } else if (description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT)) {
+ return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_SHORTCUT;
+ } else if (description.hasMimeType(MIMETYPE_APPLICATION_TASK)) {
+ return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_TASK;
+ }
+ throw new IllegalArgumentException("Not an app drag");
+ }
+
+ /**
+ * Enums for logging Drag & Drop UiEvents
+ */
+ public enum DragAndDropUiEventEnum implements UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "Starting a global drag and drop of an activity")
+ GLOBAL_APP_DRAG_START_ACTIVITY(884),
+
+ @UiEvent(doc = "Starting a global drag and drop of a shortcut")
+ GLOBAL_APP_DRAG_START_SHORTCUT(885),
+
+ @UiEvent(doc = "Starting a global drag and drop of a task")
+ GLOBAL_APP_DRAG_START_TASK(888),
+
+ @UiEvent(doc = "A global app drag was successfully dropped")
+ GLOBAL_APP_DRAG_DROPPED(887),
+
+ @UiEvent(doc = "Ending a global app drag and drop")
+ GLOBAL_APP_DRAG_END(886);
+
+ private final int mId;
+
+ DragAndDropUiEventEnum(int id) {
+ mId = id;
+ }
+
+ @Override
+ public int getId() {
+ return mId;
+ }
+ }
+}
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 9bcc3acf7a57..102b90ff5d3d 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
@@ -63,6 +63,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.logging.InstanceId;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
import com.android.wm.shell.splitscreen.SplitScreen.StageType;
@@ -86,6 +87,7 @@ public class DragAndDropPolicy {
private final SplitScreenController mSplitScreen;
private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>();
+ private InstanceId mLoggerSessionId;
private DragSession mSession;
public DragAndDropPolicy(Context context, SplitScreenController splitScreen) {
@@ -104,7 +106,8 @@ public class DragAndDropPolicy {
/**
* Starts a new drag session with the given initial drag data.
*/
- void start(DisplayLayout displayLayout, ClipData data) {
+ void start(DisplayLayout displayLayout, ClipData data, InstanceId loggerSessionId) {
+ mLoggerSessionId = loggerSessionId;
mSession = new DragSession(mContext, mActivityTaskManager, displayLayout, data);
// TODO(b/169894807): Also update the session data with task stack changes
mSession.update();
@@ -151,10 +154,8 @@ public class DragAndDropPolicy {
final Rect rightHitRegion = new Rect();
final Rect rightDrawRegion = bottomOrRightBounds;
- displayRegion.splitVertically(leftHitRegion, fullscreenHitRegion, rightHitRegion);
+ displayRegion.splitVertically(leftHitRegion, rightHitRegion);
- mTargets.add(
- new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion));
mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, leftDrawRegion));
mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, rightDrawRegion));
@@ -165,10 +166,8 @@ public class DragAndDropPolicy {
final Rect bottomDrawRegion = bottomOrRightBounds;
displayRegion.splitHorizontally(
- topHitRegion, fullscreenHitRegion, bottomHitRegion);
+ topHitRegion, bottomHitRegion);
- mTargets.add(
- new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion));
mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topDrawRegion));
mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomDrawRegion));
}
@@ -211,6 +210,8 @@ public class DragAndDropPolicy {
// Launch in the side stage if we are not in split-screen already.
stage = STAGE_TYPE_SIDE;
}
+ // Add some data for logging splitscreen once it is invoked
+ mSplitScreen.logOnDroppedToSplit(position, mLoggerSessionId);
}
final ClipDescription description = data.getDescription();
@@ -269,7 +270,6 @@ public class DragAndDropPolicy {
* Updates the session data based on the current state of the system.
*/
void update() {
-
List<ActivityManager.RunningTaskInfo> tasks =
mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */);
if (!tasks.isEmpty()) {
@@ -299,7 +299,12 @@ public class DragAndDropPolicy {
@StageType int stage, @SplitPosition int position,
@Nullable Bundle options);
void enterSplitScreen(int taskId, boolean leftOrTop);
- void exitSplitScreen();
+
+ /**
+ * Exits splitscreen, with an associated exit trigger from the SplitscreenUIChanged proto
+ * for logging.
+ */
+ void exitSplitScreen(int exitTrigger);
}
/**
@@ -352,7 +357,7 @@ public class DragAndDropPolicy {
}
@Override
- public void exitSplitScreen() {
+ public void exitSplitScreen(int exitTrigger) {
throw new UnsupportedOperationException("exitSplitScreen not implemented by starter");
}
}
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 b3423362347f..efc9ed0f75b2 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
@@ -38,6 +38,7 @@ import android.view.WindowInsets.Type;
import androidx.annotation.NonNull;
+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;
@@ -98,8 +99,9 @@ public class DragLayout extends View {
return mHasDropped;
}
- public void prepare(DisplayLayout displayLayout, ClipData initialData) {
- mPolicy.start(displayLayout, initialData);
+ public void prepare(DisplayLayout displayLayout, ClipData initialData,
+ InstanceId loggerSessionId) {
+ mPolicy.start(displayLayout, initialData, loggerSessionId);
mHasDropped = false;
mCurrentTarget = null;
}
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
new file mode 100644
index 000000000000..5fb3297aa6d3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -0,0 +1,147 @@
+/*
+ * 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.freeform;
+
+import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
+import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
+
+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.SparseArray;
+import android.view.SurfaceControl;
+
+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 java.io.PrintWriter;
+
+/**
+ * {@link ShellTaskOrganizer.TaskListener} for {@link
+ * ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}.
+ */
+public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
+ private static final String TAG = "FreeformTaskListener";
+
+ private final SyncTransactionQueue mSyncQueue;
+
+ private final SparseArray<State> mTasks = new SparseArray<>();
+
+ private static class State {
+ RunningTaskInfo mTaskInfo;
+ SurfaceControl mLeash;
+ }
+
+ public FreeformTaskListener(SyncTransactionQueue syncQueue) {
+ mSyncQueue = syncQueue;
+ }
+
+ @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();
+ 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);
+ });
+ }
+
+ @Override
+ public void onTaskVanished(RunningTaskInfo taskInfo) {
+ State state = mTasks.get(taskInfo.taskId);
+ if (state == null) {
+ Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
+ return;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d",
+ taskInfo.taskId);
+ mTasks.remove(taskInfo.taskId);
+ }
+
+ @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);
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d",
+ taskInfo.taskId);
+ state.mTaskInfo = taskInfo;
+
+ 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);
+ });
+ }
+
+ @Override
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + this);
+ pw.println(innerPrefix + mTasks.size() + " tasks");
+ }
+
+ @Override
+ 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;
+ }
+
+ /**
+ * Creates {@link FreeformTaskListener} if freeform is enabled.
+ */
+ public static FreeformTaskListener create(Context context,
+ SyncTransactionQueue syncQueue) {
+ if (!isFreeformEnabled(context)) {
+ return null;
+ }
+
+ return new FreeformTaskListener(syncQueue);
+ }
+}
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
index 362b40f33e89..067f80800ed5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java
@@ -20,6 +20,8 @@ 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;
@@ -100,10 +102,6 @@ public class DividerView extends FrameLayout implements OnTouchListener,
private static final float MINIMIZE_DOCK_SCALE = 0f;
private static final float ADJUSTED_FOR_IME_SCALE = 0.5f;
- private static final PathInterpolator SLOWDOWN_INTERPOLATOR =
- new PathInterpolator(0.5f, 1f, 0.5f, 1f);
- private static final PathInterpolator DIM_INTERPOLATOR =
- new PathInterpolator(.23f, .87f, .52f, -0.11f);
private static final Interpolator IME_ADJUST_INTERPOLATOR =
new PathInterpolator(0.2f, 0f, 0.1f, 1f);
@@ -460,6 +458,7 @@ public class DividerView extends FrameLayout implements OnTouchListener,
private void stopDragging() {
mHandle.setTouching(false, true /* animate */);
mWindowManager.setSlippery(true);
+ mWindowManagerProxy.setResizing(false);
releaseBackground();
}
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
index 40244fbb4503..f201634d3d4a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java
@@ -62,6 +62,7 @@ public class LegacySplitDisplayLayout {
Rect mSecondary = null;
Rect mAdjustedPrimary = null;
Rect mAdjustedSecondary = null;
+ final Rect mTmpBounds = new Rect();
public LegacySplitDisplayLayout(Context ctx, DisplayLayout dl,
LegacySplitScreenTaskListener taskTiles) {
@@ -136,31 +137,41 @@ public class LegacySplitDisplayLayout {
return mMinimizedSnapAlgorithm;
}
- void resizeSplits(int position) {
+ /**
+ * 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;
- calcSplitBounds(position, mPrimary, mSecondary);
- }
-
- void resizeSplits(int position, WindowContainerTransaction t) {
- 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));
- }
-
- void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) {
int dockSide = getPrimarySplitSide();
- DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary,
+ 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), outSecondary, mDisplayLayout.width(),
+ 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() {
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
index d9409ec2dc17..b1fa2ac25fe7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java
@@ -204,7 +204,8 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @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
@@ -239,12 +240,12 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl
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);
+ 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).
- t.reparent(leash, info.getRootLeash());
- t.setLayer(leash, info.getChanges().size() - i);
+ 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,
@@ -271,12 +272,12 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl
if (transition == mPendingEnter
&& mListener.mPrimary.token.equals(change.getContainer())
|| mListener.mSecondary.token.equals(change.getContainer())) {
- t.setWindowCrop(leash, change.getStartAbsBounds().width(),
+ 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.
- t.setLayer(leash, info.getChanges().size() + 1);
+ startTransaction.setLayer(leash, info.getChanges().size() + 1);
}
}
boolean isOpening = Transitions.isOpeningType(info.getType());
@@ -289,7 +290,7 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl
// 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);
+ startTransaction.setAlpha(leash, 0.f);
} else {
startExampleAnimation(leash, false /* show */);
}
@@ -311,7 +312,7 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl
}
mSplitScreen.finishEnterSplitTransition(homeIsVisible);
}
- t.apply();
+ startTransaction.apply();
onFinish();
return true;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java
index 7cf4fb7a811d..ff333c8c659d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java
@@ -171,9 +171,22 @@ public final class OneHandedSettingsUtil {
* @return true if user enabled one-handed shortcut in settings, false otherwise.
*/
public boolean getShortcutEnabled(ContentResolver resolver, int userId) {
- final String targets = Settings.Secure.getStringForUser(resolver,
+ // Checks SOFTWARE_SHORTCUT_KEY
+ final String targetsSwKey = Settings.Secure.getStringForUser(resolver,
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, userId);
- return TextUtils.isEmpty(targets) ? false : targets.contains(ONE_HANDED_MODE_TARGET_NAME);
+ if (!TextUtils.isEmpty(targetsSwKey) && targetsSwKey.contains(
+ ONE_HANDED_MODE_TARGET_NAME)) {
+ return true;
+ }
+
+ // Checks HARDWARE_SHORTCUT_KEY
+ final String targetsHwKey = Settings.Secure.getStringForUser(resolver,
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userId);
+ if (!TextUtils.isEmpty(targetsHwKey) && targetsHwKey.contains(
+ ONE_HANDED_MODE_TARGET_NAME)) {
+ return true;
+ }
+ return false;
}
/**
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 200af7415eb1..05111a3d4436 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
@@ -38,6 +38,7 @@ import android.view.SurfaceSession;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.transition.Transitions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -617,14 +618,28 @@ public class PipAnimationController {
setCurrentValue(bounds);
final Rect insets = computeInsets(fraction);
final float degree, x, y;
- if (rotationDelta == ROTATION_90) {
- degree = 90 * fraction;
- x = fraction * (end.right - start.left) + start.left;
- y = fraction * (end.top - start.top) + start.top;
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ if (rotationDelta == ROTATION_90) {
+ degree = 90 * (1 - fraction);
+ x = fraction * (end.left - start.left)
+ + start.left + start.right * (1 - fraction);
+ y = fraction * (end.top - start.top) + start.top;
+ } else {
+ degree = -90 * (1 - fraction);
+ x = fraction * (end.left - start.left) + start.left;
+ y = fraction * (end.top - start.top)
+ + start.top + start.bottom * (1 - fraction);
+ }
} else {
- degree = -90 * fraction;
- x = fraction * (end.left - start.left) + start.left;
- y = fraction * (end.bottom - start.top) + start.top;
+ if (rotationDelta == ROTATION_90) {
+ degree = 90 * fraction;
+ x = fraction * (end.right - start.left) + start.left;
+ y = fraction * (end.top - start.top) + start.top;
+ } else {
+ degree = -90 * fraction;
+ x = fraction * (end.left - start.left) + start.left;
+ y = fraction * (end.bottom - start.top) + start.top;
+ }
}
getSurfaceTransactionHelper()
.rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds,
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 728794de0865..180e3fb48c9d 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
@@ -23,6 +23,7 @@ import android.graphics.RectF;
import android.view.SurfaceControl;
import com.android.wm.shell.R;
+import com.android.wm.shell.transition.Transitions;
/**
* Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition.
@@ -137,7 +138,8 @@ public class PipSurfaceTransactionHelper {
// destination are different.
final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH;
final Rect crop = mTmpDestinationRect;
- crop.set(0, 0, destW, destH);
+ crop.set(0, 0, Transitions.ENABLE_SHELL_TRANSITIONS ? destH
+ : destW, Transitions.ENABLE_SHELL_TRANSITIONS ? destW : destH);
// Inverse scale for crop to fit in screen coordinates.
crop.scale(1 / scale);
crop.offset(insets.left, insets.top);
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 f2bad6caf3e8..96867761cc7e 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
@@ -114,38 +114,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
*/
private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500;
- // Not a complete set of states but serves what we want right now.
- private enum State {
- UNDEFINED(0),
- TASK_APPEARED(1),
- ENTRY_SCHEDULED(2),
- ENTERING_PIP(3),
- ENTERED_PIP(4),
- EXITING_PIP(5);
-
- private final int mStateValue;
-
- State(int value) {
- mStateValue = value;
- }
-
- private boolean isInPip() {
- return mStateValue >= TASK_APPEARED.mStateValue
- && mStateValue != EXITING_PIP.mStateValue;
- }
-
- /**
- * Resize request can be initiated in other component, ignore if we are no longer in PIP,
- * still waiting for animation or we're exiting from it.
- *
- * @return {@code true} if the resize request should be blocked/ignored.
- */
- private boolean shouldBlockResizeRequest() {
- return mStateValue < ENTERING_PIP.mStateValue
- || mStateValue == EXITING_PIP.mStateValue;
- }
- }
-
private final Context mContext;
private final SyncTransactionQueue mSyncTransactionQueue;
private final PipBoundsState mPipBoundsState;
@@ -169,11 +137,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
public void onPipAnimationStart(TaskInfo taskInfo,
PipAnimationController.PipTransitionAnimator animator) {
final int direction = animator.getTransitionDirection();
- if (direction == TRANSITION_DIRECTION_TO_PIP) {
- // TODO (b//169221267): Add jank listener for transactions without buffer updates.
- //InteractionJankMonitor.getInstance().begin(
- // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000);
- }
sendOnPipTransitionStarted(direction);
}
@@ -201,7 +164,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
final boolean isExitPipDirection = isOutPipDirection(direction)
|| isRemovePipDirection(direction);
- if (mState != State.EXITING_PIP || isExitPipDirection) {
+ if (mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP
+ || isExitPipDirection) {
// Finish resize as long as we're not exiting PIP, or, if we are, only if this is
// the end of an exit PIP animation.
// This is necessary in case there was a resize animation ongoing when exit PIP
@@ -244,7 +208,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private ActivityManager.RunningTaskInfo mDeferredTaskInfo;
private WindowContainerToken mToken;
private SurfaceControl mLeash;
- private State mState = State.UNDEFINED;
+ private PipTransitionState mPipTransitionState;
private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
private long mLastOneShotAlphaAnimationTime;
private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
@@ -274,21 +238,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private @Surface.Rotation int mCurrentRotation;
/**
- * 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
- * auto PiP-able Activity to home.
- * See also {@link #startSwipePipToHome(ComponentName, ActivityInfo, PictureInPictureParams)}.
- */
- private boolean mInSwipePipToHomeTransition;
-
- /**
* An optional overlay used to mask content changing between an app in/out of PiP, only set if
- * {@link #mInSwipePipToHomeTransition} is true.
+ * {@link PipTransitionState#getInSwipePipToHomeTransition()} is true.
*/
private SurfaceControl mSwipePipToHomeOverlay;
public PipTaskOrganizer(Context context,
@NonNull SyncTransactionQueue syncTransactionQueue,
+ @NonNull PipTransitionState pipTransitionState,
@NonNull PipBoundsState pipBoundsState,
@NonNull PipBoundsAlgorithm boundsHandler,
@NonNull PipMenuController pipMenuController,
@@ -302,6 +259,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
@ShellMainThread ShellExecutor mainExecutor) {
mContext = context;
mSyncTransactionQueue = syncTransactionQueue;
+ mPipTransitionState = pipTransitionState;
mPipBoundsState = pipBoundsState;
mPipBoundsAlgorithm = boundsHandler;
mPipMenuController = pipMenuController;
@@ -337,14 +295,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
public boolean isInPip() {
- return mState.isInPip();
+ return mPipTransitionState.isInPip();
}
/**
* Returns whether the entry animation is waiting to be started.
*/
public boolean isEntryScheduled() {
- return mState == State.ENTRY_SCHEDULED;
+ return mPipTransitionState.getTransitionState() == PipTransitionState.ENTRY_SCHEDULED;
}
/**
@@ -372,7 +330,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
*/
public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
PictureInPictureParams pictureInPictureParams) {
- mInSwipePipToHomeTransition = true;
+ mPipTransitionState.setInSwipePipToHomeTransition(true);
sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo);
return mPipBoundsAlgorithm.getEntryDestinationBounds();
@@ -385,7 +343,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds,
SurfaceControl overlay) {
// do nothing if there is no startSwipePipToHome being called before
- if (mInSwipePipToHomeTransition) {
+ if (mPipTransitionState.getInSwipePipToHomeTransition()) {
mPipBoundsState.setBounds(destinationBounds);
mSwipePipToHomeOverlay = overlay;
}
@@ -412,9 +370,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
* @param animationDurationMs duration in millisecond for the exiting PiP transition
*/
public void exitPip(int animationDurationMs) {
- if (!mState.isInPip() || mState == State.EXITING_PIP || mToken == null) {
+ if (!mPipTransitionState.isInPip()
+ || mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP
+ || mToken == null) {
Log.wtf(TAG, "Not allowed to exitPip in current state"
- + " mState=" + mState + " mToken=" + mToken);
+ + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken);
return;
}
@@ -438,7 +398,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
wct.setBoundsChangeTransaction(mToken, tx);
// 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.
- mState = State.EXITING_PIP;
+ mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP);
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mPipTransitionController.startTransition(destinationBounds, wct);
+ return;
+ }
mSyncTransactionQueue.queue(wct);
mSyncTransactionQueue.runInSync(t -> {
// Make sure to grab the latest source hint rect as it could have been
@@ -476,9 +441,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
* Removes PiP immediately.
*/
public void removePip() {
- if (!mState.isInPip() || mToken == null) {
+ if (!mPipTransitionState.isInPip() || mToken == null) {
Log.wtf(TAG, "Not allowed to removePip in current state"
- + " mState=" + mState + " mToken=" + mToken);
+ + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken);
return;
}
@@ -492,10 +457,19 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
animator.setDuration(mExitAnimationDuration);
animator.setInterpolator(Interpolators.ALPHA_OUT);
animator.start();
- mState = State.EXITING_PIP;
+ mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP);
}
private void removePipImmediately() {
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(mToken, null);
+ wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+ wct.reorder(mToken, false);
+ mPipTransitionController.startTransition(null, wct);
+ return;
+ }
+
try {
// Reset the task bounds first to ensure the activity configuration is reset as well
final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -514,7 +488,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
Objects.requireNonNull(info, "Requires RunningTaskInfo");
mTaskInfo = info;
mToken = mTaskInfo.token;
- mState = State.TASK_APPEARED;
+ mPipTransitionState.setTransitionState(PipTransitionState.TASK_APPEARED);
mLeash = leash;
mPictureInPictureParams = mTaskInfo.pictureInPictureParams;
setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams,
@@ -530,7 +504,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mOnDisplayIdChangeCallback.accept(info.displayId);
}
- if (mInSwipePipToHomeTransition) {
+ if (mPipTransitionState.getInSwipePipToHomeTransition()) {
if (!mWaitForFixedRotation) {
onEndOfSwipePipToHomeTransition();
} else {
@@ -557,6 +531,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
mPipMenuController.attach(mLeash);
+ } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ mOneShotAnimationType = ANIM_TYPE_BOUNDS;
}
return;
}
@@ -568,7 +544,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */,
sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration,
null /* updateBoundsCallback */);
- mState = State.ENTERING_PIP;
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
} else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration);
mOneShotAnimationType = ANIM_TYPE_BOUNDS;
@@ -595,7 +571,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
animateResizePip(currentBounds, destinationBounds, sourceHintRect,
TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, 0 /* startingAngle */);
- mState = State.ENTERING_PIP;
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
}
/**
@@ -620,7 +596,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mSurfaceControlTransactionFactory.getTransaction();
tx.setAlpha(mLeash, 0f);
tx.apply();
- mState = State.ENTRY_SCHEDULED;
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED);
applyEnterPipSyncTransaction(destinationBounds, () -> {
mPipAnimationController
.getAnimator(mTaskInfo, mLeash, destinationBounds, 0f, 1f)
@@ -631,11 +607,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
.start();
// mState is set right after the animation is kicked off to block any resize
// requests such as offsetPip that may have been called prior to the transition.
- mState = State.ENTERING_PIP;
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
}, null /* boundsChangeTransaction */);
}
private void onEndOfSwipePipToHomeTransition() {
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mSwipePipToHomeOverlay = null;
+ return;
+ }
+
final Rect destinationBounds = mPipBoundsState.getBounds();
final SurfaceControl swipeToHomeOverlay = mSwipePipToHomeOverlay;
final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
@@ -655,7 +636,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
null /* callback */, false /* withStartDelay */);
}
}, tx);
- mInSwipePipToHomeTransition = false;
+ mPipTransitionState.setInSwipePipToHomeTransition(false);
mSwipePipToHomeOverlay = null;
}
@@ -679,7 +660,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private void sendOnPipTransitionStarted(
@PipAnimationController.TransitionDirection int direction) {
if (direction == TRANSITION_DIRECTION_TO_PIP) {
- mState = State.ENTERING_PIP;
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
}
mPipTransitionController.sendOnPipTransitionStarted(direction);
}
@@ -688,7 +669,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
void sendOnPipTransitionFinished(
@PipAnimationController.TransitionDirection int direction) {
if (direction == TRANSITION_DIRECTION_TO_PIP) {
- mState = State.ENTERED_PIP;
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP);
}
mPipTransitionController.sendOnPipTransitionFinished(direction);
// Apply the deferred RunningTaskInfo if applicable after all proper callbacks are sent.
@@ -713,7 +694,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
*/
@Override
public void onTaskVanished(ActivityManager.RunningTaskInfo info) {
- if (mState == State.UNDEFINED) {
+ if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
return;
}
final WindowContainerToken token = info.token;
@@ -723,9 +704,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
return;
}
clearWaitForFixedRotation();
- mInSwipePipToHomeTransition = false;
+ mPipTransitionState.setInSwipePipToHomeTransition(false);
mPictureInPictureParams = null;
- mState = State.UNDEFINED;
+ mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED);
// Re-set the PIP bounds to none.
mPipBoundsState.setBounds(new Rect());
mPipUiEventLoggerLogger.setTaskInfo(null);
@@ -750,8 +731,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) {
Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken");
- if (mState != State.ENTERED_PIP && mState != State.EXITING_PIP) {
- Log.d(TAG, "Defer onTaskInfoChange in current state: " + mState);
+ if (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP
+ && mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP) {
+ Log.d(TAG, "Defer onTaskInfoChange in current state: "
+ + mPipTransitionState.getTransitionState());
// Defer applying PiP parameters if the task is entering PiP to avoid disturbing
// the animation.
mDeferredTaskInfo = info;
@@ -784,7 +767,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mNextRotation = newRotation;
mWaitForFixedRotation = true;
- if (mState.isInPip()) {
+ if (mPipTransitionState.isInPip()) {
// Fade out the existing PiP to avoid jump cut during seamless rotation.
fadeExistingPip(false /* show */);
}
@@ -795,17 +778,19 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
if (!mWaitForFixedRotation) {
return;
}
- if (mState == State.TASK_APPEARED) {
- if (mInSwipePipToHomeTransition) {
+ if (mPipTransitionState.getTransitionState() == PipTransitionState.TASK_APPEARED) {
+ if (mPipTransitionState.getInSwipePipToHomeTransition()) {
onEndOfSwipePipToHomeTransition();
} else {
// Schedule a regular animation to ensure all the callbacks are still being sent.
enterPipWithAlphaAnimation(mPipBoundsAlgorithm.getEntryDestinationBounds(),
mEnterAnimationDuration);
}
- } else if (mState == State.ENTERED_PIP && mHasFadeOut) {
+ } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERED_PIP
+ && mHasFadeOut) {
fadeExistingPip(true /* show */);
- } else if (mState == State.ENTERING_PIP && mDeferredAnimEndTransaction != null) {
+ } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERING_PIP
+ && mDeferredAnimEndTransaction != null) {
final PipAnimationController.PipTransitionAnimator<?> animator =
mPipAnimationController.getCurrentAnimator();
final Rect destinationBounds = animator.getDestinationBounds();
@@ -859,13 +844,15 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// note that this can be called when swipe-to-home or fixed-rotation is happening.
// Skip this entirely if that's the case.
final boolean waitForFixedRotationOnEnteringPip = mWaitForFixedRotation
- && (mState != State.ENTERED_PIP);
- if ((mInSwipePipToHomeTransition || waitForFixedRotationOnEnteringPip) && fromRotation) {
+ && (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP);
+ if ((mPipTransitionState.getInSwipePipToHomeTransition()
+ || waitForFixedRotationOnEnteringPip) && fromRotation) {
if (DEBUG) {
Log.d(TAG, "Skip onMovementBoundsChanged on rotation change"
- + " mInSwipePipToHomeTransition=" + mInSwipePipToHomeTransition
+ + " InSwipePipToHomeTransition="
+ + mPipTransitionState.getInSwipePipToHomeTransition()
+ " mWaitForFixedRotation=" + mWaitForFixedRotation
- + " mState=" + mState);
+ + " getTransitionState=" + mPipTransitionState.getTransitionState());
}
return;
}
@@ -873,7 +860,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mPipAnimationController.getCurrentAnimator();
if (animator == null || !animator.isRunning()
|| animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) {
- final boolean rotatingPip = mState.isInPip() && fromRotation;
+ final boolean rotatingPip = mPipTransitionState.isInPip() && fromRotation;
if (rotatingPip && mWaitForFixedRotation && mHasFadeOut) {
// The position will be used by fade-in animation when the fixed rotation is done.
mPipBoundsState.setBounds(destinationBoundsOut);
@@ -1006,7 +993,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
Rect currentBounds, Rect destinationBounds, float startingAngle, Rect sourceHintRect,
@PipAnimationController.TransitionDirection int direction, int durationMs,
Consumer<Rect> updateBoundsCallback) {
- if (!mState.isInPip()) {
+ if (!mPipTransitionState.isInPip()) {
// TODO: tend to use shouldBlockResizeRequest here as well but need to consider
// the fact that when in exitPip, scheduleAnimateResizePip is executed in the window
// container transaction callback and we want to set the mState immediately.
@@ -1036,7 +1023,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
mSurfaceTransactionHelper
.crop(tx, mLeash, toBounds)
- .round(tx, mLeash, mState.isInPip());
+ .round(tx, mLeash, mPipTransitionState.isInPip());
if (mPipMenuController.isMenuVisible()) {
mPipMenuController.resizePipMenu(mLeash, tx, toBounds);
} else {
@@ -1114,7 +1101,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
public void scheduleFinishResizePip(Rect destinationBounds,
@PipAnimationController.TransitionDirection int direction,
Consumer<Rect> updateBoundsCallback) {
- if (mState.shouldBlockResizeRequest()) {
+ if (mPipTransitionState.shouldBlockResizeRequest()) {
return;
}
@@ -1131,7 +1118,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mSurfaceTransactionHelper
.crop(tx, mLeash, destinationBounds)
.resetScale(tx, mLeash, destinationBounds)
- .round(tx, mLeash, mState.isInPip());
+ .round(tx, mLeash, mPipTransitionState.isInPip());
return tx;
}
@@ -1140,7 +1127,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
*/
public void scheduleOffsetPip(Rect originalBounds, int offset, int duration,
Consumer<Rect> updateBoundsCallback) {
- if (mState.shouldBlockResizeRequest()) {
+ if (mPipTransitionState.shouldBlockResizeRequest()) {
return;
}
if (mWaitForFixedRotation) {
@@ -1384,7 +1371,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.0f);
animator.setDuration(mCrossFadeAnimationDuration);
animator.addUpdateListener(animation -> {
- if (mState == State.UNDEFINED) {
+ if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
// Could happen if onTaskVanished happens during the animation since we may have
// set a start delay on this animation.
Log.d(TAG, "Task vanished, skip fadeOutAndRemoveOverlay");
@@ -1410,7 +1397,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
private void removeContentOverlay(SurfaceControl surface, Runnable callback) {
- if (mState == State.UNDEFINED) {
+ if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
// Avoid double removal, which is fatal.
return;
}
@@ -1432,7 +1419,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
pw.println(innerPrefix + "mToken=" + mToken
+ " binder=" + (mToken != null ? mToken.asBinder() : null));
pw.println(innerPrefix + "mLeash=" + mLeash);
- pw.println(innerPrefix + "mState=" + mState);
+ pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState());
pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType);
pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams);
}
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 4759550c35c0..6fec1fbda7b0 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
@@ -18,6 +18,10 @@ package com.android.wm.shell.pip;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.util.RotationUtils.deltaRotation;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_PIP;
+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;
@@ -25,9 +29,12 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection;
import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
import android.app.TaskInfo;
import android.content.Context;
+import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.IBinder;
import android.view.Surface;
@@ -35,6 +42,7 @@ import android.view.SurfaceControl;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransactionCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -49,74 +57,218 @@ import com.android.wm.shell.transition.Transitions;
*/
public class PipTransition extends PipTransitionController {
+ private final PipTransitionState mPipTransitionState;
private final int mEnterExitAnimationDuration;
private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
private Transitions.TransitionFinishCallback mFinishCallback;
+ private Rect mExitDestinationBounds = new Rect();
public PipTransition(Context context,
- PipBoundsState pipBoundsState, PipMenuController pipMenuController,
+ PipBoundsState pipBoundsState,
+ PipTransitionState pipTransitionState,
+ PipMenuController pipMenuController,
PipBoundsAlgorithm pipBoundsAlgorithm,
PipAnimationController pipAnimationController,
Transitions transitions,
@NonNull ShellTaskOrganizer shellTaskOrganizer) {
super(pipBoundsState, pipMenuController, pipBoundsAlgorithm,
pipAnimationController, transitions, shellTaskOrganizer);
+ mPipTransitionState = pipTransitionState;
mEnterExitAnimationDuration = context.getResources()
.getInteger(R.integer.config_pipResizeAnimationDuration);
}
@Override
+ public void setIsFullAnimation(boolean isFullAnimation) {
+ setOneShotAnimationType(isFullAnimation ? ANIM_TYPE_BOUNDS : ANIM_TYPE_ALPHA);
+ }
+
+ /**
+ * Sets the preferred animation type for one time.
+ * This is typically used to set the animation type to
+ * {@link PipAnimationController#ANIM_TYPE_ALPHA}.
+ */
+ private void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) {
+ mOneShotAnimationType = animationType;
+ }
+
+ @Override
+ public void startTransition(Rect destinationBounds, WindowContainerTransaction out) {
+ if (destinationBounds != null) {
+ mExitDestinationBounds.set(destinationBounds);
+ mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this);
+ } else {
+ mTransitions.startTransition(TRANSIT_REMOVE_PIP, out, this);
+ }
+ }
+
+ @Override
public boolean startAnimation(@android.annotation.NonNull IBinder transition,
@android.annotation.NonNull TransitionInfo info,
- @android.annotation.NonNull SurfaceControl.Transaction t,
+ @android.annotation.NonNull SurfaceControl.Transaction startTransaction,
+ @android.annotation.NonNull SurfaceControl.Transaction finishTransaction,
@android.annotation.NonNull Transitions.TransitionFinishCallback finishCallback) {
+
+ if (info.getType() == TRANSIT_EXIT_PIP && info.getChanges().size() == 1) {
+ final TransitionInfo.Change change = info.getChanges().get(0);
+ mFinishCallback = finishCallback;
+ startTransaction.apply();
+ boolean success = startExpandAnimation(change.getTaskInfo(), change.getLeash(),
+ new Rect(mExitDestinationBounds));
+ mExitDestinationBounds.setEmpty();
+ return success;
+ }
+
+ if (info.getType() == TRANSIT_REMOVE_PIP) {
+ startTransaction.apply();
+ finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(),
+ mPipBoundsState.getDisplayBounds());
+ finishCallback.onTransitionFinished(null, null);
+ return true;
+ }
+
+ // We only 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 false;
+ }
+
+ // Search for an Enter PiP transition (along with a show wallpaper one)
+ 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().configuration.windowConfiguration.getWindowingMode()
== WINDOWING_MODE_PINNED) {
- mFinishCallback = finishCallback;
- return startEnterAnimation(change.getTaskInfo(), change.getLeash(), t);
+ enterPip = change;
+ } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
+ wallpaper = change;
}
}
- return false;
+ if (enterPip == null) {
+ return false;
+ }
+
+ // Show the wallpaper if there is a wallpaper change.
+ if (wallpaper != null) {
+ startTransaction.show(wallpaper.getLeash());
+ startTransaction.setAlpha(wallpaper.getLeash(), 1.f);
+ }
+
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
+ mFinishCallback = finishCallback;
+ return startEnterAnimation(enterPip.getTaskInfo(), enterPip.getLeash(),
+ startTransaction, finishTransaction, enterPip.getStartRotation(),
+ enterPip.getEndRotation());
}
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
- return null;
+ if (request.getType() == TRANSIT_PIP) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED);
+ if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ wct.setActivityWindowingMode(request.getTriggerTask().token,
+ WINDOWING_MODE_UNDEFINED);
+ final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
+ wct.setBounds(request.getTriggerTask().token, destinationBounds);
+ }
+ return wct;
+ } else {
+ return null;
+ }
}
@Override
public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds,
@PipAnimationController.TransitionDirection int direction,
SurfaceControl.Transaction tx) {
+
+ if (isInPipDirection(direction)) {
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP);
+ }
WindowContainerTransaction wct = new WindowContainerTransaction();
prepareFinishResizeTransaction(taskInfo, destinationBounds,
direction, tx, wct);
- mFinishCallback.onTransitionFinished(wct, null);
+ mFinishCallback.onTransitionFinished(wct, new WindowContainerTransactionCallback() {
+ @Override
+ public void onTransactionReady(int id, @NonNull SurfaceControl.Transaction t) {
+ t.merge(tx);
+ t.apply();
+ }
+ });
finishResizeForMenu(destinationBounds);
}
+ private boolean startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash,
+ final Rect destinationBounds) {
+ PipAnimationController.PipTransitionAnimator animator =
+ mPipAnimationController.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(),
+ mPipBoundsState.getBounds(), destinationBounds, null,
+ TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, Surface.ROTATION_0);
+
+ animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP)
+ .setPipAnimationCallback(mPipAnimationCallback)
+ .setDuration(mEnterExitAnimationDuration)
+ .start();
+
+ return true;
+ }
+
private boolean startEnterAnimation(final TaskInfo taskInfo, final SurfaceControl leash,
- final SurfaceControl.Transaction t) {
+ final SurfaceControl.Transaction startTransaction,
+ final SurfaceControl.Transaction finishTransaction,
+ final int startRotation, final int endRotation) {
setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams,
taskInfo.topActivityInfo);
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
final Rect currentBounds = taskInfo.configuration.windowConfiguration.getBounds();
PipAnimationController.PipTransitionAnimator animator;
+ finishTransaction.setPosition(leash, destinationBounds.left, destinationBounds.top);
+ if (taskInfo.pictureInPictureParams != null
+ && taskInfo.pictureInPictureParams.isAutoEnterEnabled()
+ && mPipTransitionState.getInSwipePipToHomeTransition()) {
+ mOneShotAnimationType = ANIM_TYPE_BOUNDS;
+
+ // PiP menu is attached late in the process here to avoid any artifacts on the leash
+ // caused by addShellRoot when in gesture navigation mode.
+ mPipMenuController.attach(leash);
+ SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, new float[9])
+ .setPosition(leash, destinationBounds.left, destinationBounds.top)
+ .setWindowCrop(leash, destinationBounds.width(), destinationBounds.height());
+ startTransaction.merge(tx);
+ startTransaction.apply();
+ mPipBoundsState.setBounds(destinationBounds);
+ onFinishResize(taskInfo, destinationBounds, TRANSITION_DIRECTION_TO_PIP, tx);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+ mFinishCallback = null;
+ mPipTransitionState.setInSwipePipToHomeTransition(false);
+ return true;
+ }
+
+ int rotationDelta = deltaRotation(endRotation, startRotation);
+ if (rotationDelta != Surface.ROTATION_0) {
+ Matrix tmpTransform = new Matrix();
+ tmpTransform.postRotate(rotationDelta == Surface.ROTATION_90
+ ? Surface.ROTATION_270 : Surface.ROTATION_90);
+ startTransaction.setMatrix(leash, tmpTransform, new float[9]);
+ }
if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
final Rect sourceHintRect =
PipBoundsAlgorithm.getValidSourceHintRect(
taskInfo.pictureInPictureParams, currentBounds);
animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds,
currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
- 0 /* startingAngle */, Surface.ROTATION_0);
+ 0 /* startingAngle */, rotationDelta);
} else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
- t.setAlpha(leash, 0f);
- t.apply();
+ startTransaction.setAlpha(leash, 0f);
+ // PiP menu is attached late in the process here to avoid any artifacts on the leash
+ // caused by addShellRoot when in gesture navigation mode.
+ mPipMenuController.attach(leash);
animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,
0f, 1f);
mOneShotAnimationType = ANIM_TYPE_BOUNDS;
@@ -124,10 +276,12 @@ public class PipTransition extends PipTransitionController {
throw new RuntimeException("Unrecognized animation type: "
+ mOneShotAnimationType);
}
+ startTransaction.apply();
animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP)
.setPipAnimationCallback(mPipAnimationCallback)
.setDuration(mEnterExitAnimationDuration)
.start();
+
return true;
}
@@ -158,6 +312,5 @@ public class PipTransition extends PipTransitionController {
}
wct.setBounds(taskInfo.token, taskBounds);
- wct.setBoundsChangeTransaction(taskInfo.token, tx);
}
}
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 d801c918973a..dbf603ca72d9 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
@@ -19,7 +19,6 @@ package com.android.wm.shell.pip;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
-import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
import android.app.PictureInPictureParams;
import android.app.TaskInfo;
@@ -29,6 +28,7 @@ import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.view.SurfaceControl;
+import android.window.WindowContainerTransaction;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.transition.Transitions;
@@ -46,6 +46,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH
protected final PipBoundsState mPipBoundsState;
protected final ShellTaskOrganizer mShellTaskOrganizer;
protected final PipMenuController mPipMenuController;
+ protected final Transitions mTransitions;
private final Handler mMainHandler;
private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
@@ -55,12 +56,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH
public void onPipAnimationStart(TaskInfo taskInfo,
PipAnimationController.PipTransitionAnimator animator) {
final int direction = animator.getTransitionDirection();
- if (direction == TRANSITION_DIRECTION_TO_PIP) {
- // TODO (b//169221267): Add jank listener for transactions without buffer
- // updates.
- //InteractionJankMonitor.getInstance().begin(
- // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000);
- }
sendOnPipTransitionStarted(direction);
}
@@ -74,12 +69,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH
}
onFinishResize(taskInfo, animator.getDestinationBounds(), direction, tx);
sendOnPipTransitionFinished(direction);
- if (direction == TRANSITION_DIRECTION_TO_PIP) {
- // TODO (b//169221267): Add jank listener for transactions without buffer
- // updates.
- //InteractionJankMonitor.getInstance().end(
- // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
- }
}
@Override
@@ -98,6 +87,22 @@ public abstract class PipTransitionController implements Transitions.TransitionH
SurfaceControl.Transaction tx) {
}
+ /**
+ * Called to inform the transition that the animation should start with the assumption that
+ * PiP is not animating from its original bounds, but rather a continuation of another
+ * animation. For example, gesture navigation would first fade out the PiP activity, and the
+ * transition should be responsible to animate in (such as fade in) the PiP.
+ */
+ public void setIsFullAnimation(boolean isFullAnimation) {
+ }
+
+ /**
+ * Called when the Shell wants to starts a transition/animation.
+ */
+ public void startTransition(Rect destinationBounds, WindowContainerTransaction out) {
+ // Default implementation does nothing.
+ }
+
public PipTransitionController(PipBoundsState pipBoundsState,
PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm,
PipAnimationController pipAnimationController, Transitions transitions,
@@ -107,6 +112,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH
mShellTaskOrganizer = shellTaskOrganizer;
mPipBoundsAlgorithm = pipBoundsAlgorithm;
mPipAnimationController = pipAnimationController;
+ mTransitions = transitions;
mMainHandler = new Handler(Looper.getMainLooper());
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
transitions.addHandler(this);
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
new file mode 100644
index 000000000000..85e56b7dd99f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
@@ -0,0 +1,97 @@
+/*
+ * 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.pip;
+
+import android.annotation.IntDef;
+import android.app.PictureInPictureParams;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used to keep track of PiP leash state as it appears and animates by {@link PipTaskOrganizer} and
+ * {@link PipTransition}.
+ */
+public class PipTransitionState {
+
+ public static final int UNDEFINED = 0;
+ public static final int TASK_APPEARED = 1;
+ public static final int ENTRY_SCHEDULED = 2;
+ public static final int ENTERING_PIP = 3;
+ public static final int ENTERED_PIP = 4;
+ public static final int EXITING_PIP = 5;
+
+ /**
+ * 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
+ * auto PiP-able Activity to home.
+ * See also {@link PipTaskOrganizer#startSwipePipToHome(ComponentName, ActivityInfo,
+ * PictureInPictureParams)}.
+ */
+ private boolean mInSwipePipToHomeTransition;
+
+ // Not a complete set of states but serves what we want right now.
+ @IntDef(prefix = { "TRANSITION_STATE_" }, value = {
+ UNDEFINED,
+ TASK_APPEARED,
+ ENTRY_SCHEDULED,
+ ENTERING_PIP,
+ ENTERED_PIP,
+ EXITING_PIP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TransitionState {}
+
+ private @TransitionState int mState;
+
+ public PipTransitionState() {
+ mState = UNDEFINED;
+ }
+
+ public void setTransitionState(@TransitionState int state) {
+ mState = state;
+ }
+
+ public @TransitionState int getTransitionState() {
+ return mState;
+ }
+
+ public boolean isInPip() {
+ return mState >= TASK_APPEARED
+ && mState != EXITING_PIP;
+ }
+
+ public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) {
+ mInSwipePipToHomeTransition = inSwipePipToHomeTransition;
+ }
+
+ public boolean getInSwipePipToHomeTransition() {
+ return mInSwipePipToHomeTransition;
+ }
+ /**
+ * Resize request can be initiated in other component, ignore if we are no longer in PIP,
+ * still waiting for animation or we're exiting from it.
+ *
+ * @return {@code true} if the resize request should be blocked/ignored.
+ */
+ public boolean shouldBlockResizeRequest() {
+ return mState < ENTERING_PIP
+ || mState == EXITING_PIP;
+ }
+}
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 a646b07c49dc..ae8c1b6f8c1a 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
@@ -60,8 +60,7 @@ public class PhonePipMenuController implements PipMenuController {
private static final boolean DEBUG = false;
public static final int MENU_STATE_NONE = 0;
- public static final int MENU_STATE_CLOSE = 1;
- public static final int MENU_STATE_FULL = 2;
+ public static final int MENU_STATE_FULL = 1;
/**
* A listener interface to receive notification on changes in PIP.
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 4f3ec96968b2..ac02075a49d8 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
@@ -21,7 +21,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static android.view.WindowManager.INPUT_CONSUMER_PIP;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SNAP_AFTER_RESIZE;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE;
import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection;
import android.app.ActivityManager;
@@ -52,6 +61,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor;
import com.android.wm.shell.R;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.common.DisplayChangeController;
@@ -67,6 +77,7 @@ import com.android.wm.shell.pip.IPip;
import com.android.wm.shell.pip.IPipAnimationListener;
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
import com.android.wm.shell.pip.Pip;
+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.PipMediaController;
@@ -74,6 +85,7 @@ 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.PipUtils;
+import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
import java.util.Objects;
@@ -445,11 +457,18 @@ public class PipController implements PipTransitionController.PipTransitionCallb
return;
}
Runnable updateDisplayLayout = () -> {
+ final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS
+ && mPipBoundsState.getDisplayLayout().rotation() != layout.rotation();
mPipBoundsState.setDisplayLayout(layout);
+ final WindowContainerTransaction wct =
+ fromRotation ? new WindowContainerTransaction() : null;
updateMovementBounds(null /* toBounds */,
- false /* fromRotation */, false /* fromImeAdjustment */,
+ fromRotation, false /* fromImeAdjustment */,
false /* fromShelfAdjustment */,
- null /* windowContainerTransaction */);
+ wct /* windowContainerTransaction */);
+ if (wct != null) {
+ mPipTaskOrganizer.applyFinishBoundsResize(wct, TRANSITION_DIRECTION_SAME);
+ }
};
if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) {
@@ -528,6 +547,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
private void setPinnedStackAnimationType(int animationType) {
mPipTaskOrganizer.setOneShotAnimationType(animationType);
+ mPipTransitionController.setIsFullAnimation(
+ animationType == PipAnimationController.ANIM_TYPE_BOUNDS);
}
private void setPinnedStackAnimationListener(IPipAnimationListener callback) {
@@ -564,8 +585,37 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mPipTaskOrganizer.stopSwipePipToHome(componentName, destinationBounds, overlay);
}
+ private String getTransitionTag(int direction) {
+ switch (direction) {
+ case TRANSITION_DIRECTION_TO_PIP:
+ return "TRANSITION_TO_PIP";
+ case TRANSITION_DIRECTION_LEAVE_PIP:
+ return "TRANSITION_LEAVE_PIP";
+ case TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN:
+ return "TRANSITION_LEAVE_PIP_TO_SPLIT_SCREEN";
+ case TRANSITION_DIRECTION_REMOVE_STACK:
+ return "TRANSITION_REMOVE_STACK";
+ case TRANSITION_DIRECTION_SNAP_AFTER_RESIZE:
+ return "TRANSITION_SNAP_AFTER_RESIZE";
+ case TRANSITION_DIRECTION_USER_RESIZE:
+ return "TRANSITION_USER_RESIZE";
+ case TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND:
+ return "TRANSITION_EXPAND_OR_UNEXPAND";
+ default:
+ return "TRANSITION_LEAVE_UNKNOWN";
+ }
+ }
+
@Override
public void onPipTransitionStarted(int direction, Rect pipBounds) {
+ // Begin InteractionJankMonitor with PIP transition CUJs
+ final InteractionJankMonitor.Configuration.Builder builder =
+ InteractionJankMonitor.Configuration.Builder.withSurface(
+ CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl())
+ .setTag(getTransitionTag(direction))
+ .setTimeout(2000);
+ InteractionJankMonitor.getInstance().begin(builder);
+
if (isOutPipDirection(direction)) {
// Exiting PIP, save the reentry state to restore to when re-entering.
saveReentryState(pipBounds);
@@ -604,6 +654,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
private void onPipTransitionFinishedOrCanceled(int direction) {
+ // End InteractionJankMonitor with PIP transition by CUJs
+ InteractionJankMonitor.getInstance().end(CUJ_PIP_TRANSITION);
+
// Re-enable touches after the animation completes
mTouchHandler.setTouchEnabled(true);
mTouchHandler.onPinnedStackAnimationEnded(direction);
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 1da9577fe49a..82092ac5ac3e 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
@@ -158,14 +158,16 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen
@Override
public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
- mMainExecutor.executeDelayed(() -> {
- mMotionHelper.notifyDismissalPending();
- mMotionHelper.animateDismiss();
- hideDismissTargetMaybe();
-
- mPipUiEventLogger.log(
- PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE);
- }, 0);
+ if (mEnableDismissDragToEdge) {
+ mMainExecutor.executeDelayed(() -> {
+ mMotionHelper.notifyDismissalPending();
+ mMotionHelper.animateDismiss();
+ hideDismissTargetMaybe();
+
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE);
+ }, 0);
+ }
}
});
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 67b1e6dd4cc7..8ef2b6b12030 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
@@ -23,7 +23,6 @@ import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTR
import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
-import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE;
import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL;
import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE;
@@ -203,7 +202,7 @@ public class PipMenuView extends FrameLayout {
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
- if (action == ACTION_CLICK && mMenuState == MENU_STATE_CLOSE) {
+ if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) {
mController.showMenu();
}
return super.performAccessibilityAction(host, action, args);
@@ -271,13 +270,12 @@ public class PipMenuView extends FrameLayout {
mDismissButton.getAlpha(), 1f);
ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA,
mResizeHandle.getAlpha(),
- ENABLE_RESIZE_HANDLE && menuState == MENU_STATE_CLOSE && showResizeHandle
- ? 1f : 0f);
+ ENABLE_RESIZE_HANDLE && showResizeHandle ? 1f : 0f);
if (menuState == MENU_STATE_FULL) {
mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim,
resizeAnim);
} else {
- mMenuContainerAnimator.playTogether(dismissAnim, resizeAnim);
+ mMenuContainerAnimator.playTogether(resizeAnim);
}
mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS);
@@ -429,7 +427,7 @@ public class PipMenuView extends FrameLayout {
FrameLayout.LayoutParams expandedLp =
(FrameLayout.LayoutParams) expandContainer.getLayoutParams();
- if (mActions.isEmpty() || menuState == MENU_STATE_CLOSE || menuState == MENU_STATE_NONE) {
+ if (mActions.isEmpty() || menuState == MENU_STATE_NONE) {
actionsContainer.setVisibility(View.INVISIBLE);
// Update the expand container margin to adjust the center of the expand button to
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 7867f933de4f..9f2f6a575aca 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
@@ -22,7 +22,6 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI
import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT;
import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE;
import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT;
-import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE;
import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL;
import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE;
import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE;
@@ -81,7 +80,6 @@ public class PipTouchHandler {
private final PhonePipMenuController mMenuController;
private final AccessibilityManager mAccessibilityManager;
- private boolean mShowPipMenuOnAnimationEnd = false;
/**
* Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the
@@ -280,7 +278,6 @@ public class PipTouchHandler {
public void onActivityPinned() {
mPipDismissTargetHandler.createOrUpdateDismissTarget();
- mShowPipMenuOnAnimationEnd = true;
mPipResizeGestureHandler.onActivityPinned();
mFloatingContentCoordinator.onContentAdded(mMotionHelper);
}
@@ -304,13 +301,6 @@ public class PipTouchHandler {
// Set the initial bounds as the user resize bounds.
mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
}
-
- if (mShowPipMenuOnAnimationEnd) {
- mMenuController.showMenu(MENU_STATE_CLOSE, mPipBoundsState.getBounds(),
- true /* allowMenuTimeout */, false /* willResizeMenu */,
- shouldShowResizeHandle());
- mShowPipMenuOnAnimationEnd = false;
- }
}
public void onConfigurationChanged() {
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 b7caf72641a3..551476dc9d54 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
@@ -58,7 +58,8 @@ public class TvPipTransition extends PipTransitionController {
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @android.annotation.NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
return false;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java
index 1fc4d12def1f..ab3cbd655ea1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java
@@ -47,6 +47,8 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
/** Callback for size compat UI interaction. */
public interface SizeCompatUICallback {
+ /** Called when the size compat restart button appears. */
+ void onSizeCompatRestartButtonAppeared(int taskId);
/** Called when the size compat restart button is clicked. */
void onSizeCompatRestartButtonClicked(int taskId);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
index 20021ebea834..7cf95593dbaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
@@ -106,6 +106,8 @@ class SizeCompatUILayout {
mShouldShowHint = false;
createSizeCompatHint();
}
+
+ mCallback.onSizeCompatRestartButtonAppeared(mTaskId);
}
/** Creates the restart button hint window. */
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 8f0892fdcbba..6ec514bd8331 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
@@ -20,6 +20,8 @@ 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.IRemoteTransition;
import com.android.wm.shell.splitscreen.ISplitScreenListener;
@@ -77,9 +79,24 @@ interface ISplitScreen {
int position, in Bundle options) = 9;
/**
- * Starts tasks simultaneously in one transition. The first task in the list will be in the
- * main-stage and on the left/top.
+ * Starts tasks simultaneously in one transition.
*/
oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
in Bundle sideOptions, int sidePosition, in IRemoteTransition 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/splitscreen/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java
new file mode 100644
index 000000000000..0b763f2d05f7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java
@@ -0,0 +1,127 @@
+/*
+ * 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.splitscreen;
+
+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.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.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+import android.view.WindowlessWindowManager;
+
+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 mOutlineBounds = new Rect();
+ private final Rect mTmpBounds = new Rect();
+ private SurfaceControlViewHost mViewHost;
+ private SurfaceControl mHostLeash;
+ private SurfaceControl mLeash;
+ private int mOutlineColor;
+
+ OutlineManager(Context context, Configuration configuration) {
+ super(configuration, null /* rootSurface */, null /* hostInputToken */);
+ mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
+ null /* options */);
+ }
+
+ @Override
+ protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+ b.setParent(mHostLeash);
+ }
+
+ boolean drawOutlineBounds(Rect rootBounds) {
+ if (mLeash == null || mViewHost == null) return false;
+
+ computeOutlineBounds(mContext, rootBounds, mTmpBounds);
+ if (mOutlineBounds.equals(mTmpBounds)) {
+ return false;
+ }
+ mOutlineBounds.set(mTmpBounds);
+
+ ((OutlineRoot) mViewHost.getView()).updateOutlineBounds(mOutlineBounds, mOutlineColor);
+ final WindowManager.LayoutParams lp =
+ (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
+ lp.width = rootBounds.width();
+ lp.height = rootBounds.height();
+ mViewHost.relayout(lp);
+
+ return true;
+ }
+
+ void inflate(SurfaceControl.Transaction t, SurfaceControl hostLeash, int color) {
+ if (mLeash != null || mViewHost != null) return;
+
+ mHostLeash = hostLeash;
+ mOutlineColor = color;
+ mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
+ final OutlineRoot rootView = (OutlineRoot) LayoutInflater.from(mContext)
+ .inflate(R.layout.split_outline, null);
+
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
+ FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
+ 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(rootView, lp);
+ mLeash = getSurfaceControl(mViewHost.getWindowToken());
+ t.setLayer(mLeash, Integer.MAX_VALUE);
+ }
+
+ void release() {
+ if (mViewHost != null) {
+ mViewHost.release();
+ }
+ }
+
+ private static void computeOutlineBounds(Context context, Rect rootBounds, Rect outBounds) {
+ computeDisplayStableBounds(context, outBounds);
+ outBounds.intersect(rootBounds);
+ // Offset the coordinate from screen based to surface based.
+ outBounds.offset(-rootBounds.left, -rootBounds.top);
+ }
+
+ private static void computeDisplayStableBounds(Context context, Rect outBounds) {
+ final WindowMetrics windowMetrics =
+ context.getSystemService(WindowManager.class).getMaximumWindowMetrics();
+ outBounds.set(windowMetrics.getBounds());
+ outBounds.inset(windowMetrics.getWindowInsets().getInsets(
+ WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()));
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java
new file mode 100644
index 000000000000..71d48eeca71d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java
@@ -0,0 +1,62 @@
+/*
+ * 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.splitscreen;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.R;
+
+/** Root layout for holding split outline. */
+public class OutlineRoot extends FrameLayout {
+ public OutlineRoot(@NonNull Context context) {
+ super(context);
+ }
+
+ public OutlineRoot(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public OutlineRoot(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public OutlineRoot(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private OutlineView mOutlineView;
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mOutlineView = findViewById(R.id.split_outline);
+ }
+
+ void updateOutlineBounds(Rect bounds, int color) {
+ mOutlineView.updateOutlineBounds(bounds, color);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java
new file mode 100644
index 000000000000..ea66180e3dd2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java
@@ -0,0 +1,76 @@
+/*
+ * 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.splitscreen;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.util.AttributeSet;
+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 Rect mBounds = new Rect();
+
+ public OutlineView(@NonNull Context context) {
+ super(context);
+ }
+
+ public OutlineView(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(getResources()
+ .getDimension(R.dimen.accessibility_focus_highlight_stroke_width));
+ }
+
+ void updateOutlineBounds(Rect bounds, int color) {
+ if (mBounds.equals(bounds) && mPaint.getColor() == color) return;
+ mBounds.set(bounds);
+ mPaint.setColor(color);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mBounds.isEmpty()) return;
+ final Path path = new Region(mBounds).getBoundaryPath();
+ canvas.drawPath(path, mPaint);
+ }
+}
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 82f95a4f32ea..2b19bb965fed 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,10 @@
package com.android.wm.shell.splitscreen;
+import android.annotation.CallSuper;
import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Color;
import android.graphics.Rect;
import android.view.SurfaceSession;
import android.window.WindowContainerToken;
@@ -28,15 +31,19 @@ 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 {
private static final String TAG = SideStage.class.getSimpleName();
+ private final Context mContext;
+ private OutlineManager mOutlineManager;
- SideStage(ShellTaskOrganizer taskOrganizer, int displayId,
+ SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
SurfaceSession surfaceSession) {
super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession);
+ mContext = context;
}
void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds,
@@ -44,7 +51,7 @@ class SideStage extends StageTaskListener {
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 repareted , or the root
+ // 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 */);
}
@@ -69,4 +76,34 @@ class SideStage extends StageTaskListener {
wct.reparent(task.token, newParent, false /* onTop */);
return true;
}
+
+ void enableOutline(boolean enable) {
+ if (enable) {
+ if (mOutlineManager == null && mRootTaskInfo != null) {
+ mOutlineManager = new OutlineManager(mContext, mRootTaskInfo.configuration);
+ mSyncQueue.runInSync(t -> mOutlineManager.inflate(t, mRootLeash, Color.YELLOW));
+ updateOutlineBounds();
+ }
+ } else {
+ if (mOutlineManager != null) {
+ mOutlineManager.release();
+ mOutlineManager = null;
+ }
+ }
+ }
+
+ private void updateOutlineBounds() {
+ if (mOutlineManager == null || mRootTaskInfo == null || !mRootTaskInfo.isVisible) return;
+ mOutlineManager.drawOutlineBounds(
+ mRootTaskInfo.configuration.windowConfiguration.getBounds());
+ }
+
+ @Override
+ @CallSuper
+ public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ super.onTaskInfoChanged(taskInfo);
+ if (mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId) {
+ updateOutlineBounds();
+ }
+ }
}
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 002bfb6e429f..e86462f666c9 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
@@ -17,10 +17,13 @@
package com.android.wm.shell.splitscreen;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.split.SplitLayout.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
@@ -53,10 +56,18 @@ public interface SplitScreen {
/** Callback interface for listening to changes in a split-screen stage. */
interface SplitScreenListener {
- void onStagePositionChanged(@StageType int stage, @SplitPosition int position);
- void onTaskStageChanged(int taskId, @StageType int stage, boolean visible);
+ 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.
*/
@@ -64,6 +75,18 @@ public interface SplitScreen {
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) {
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 9a457b5fd88e..437b52a31ee4 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
@@ -17,14 +17,12 @@
package com.android.wm.shell.splitscreen;
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.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
-import static com.android.wm.shell.common.split.SplitLayout.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 android.app.ActivityManager;
import android.app.ActivityTaskManager;
@@ -38,16 +36,27 @@ 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.IRemoteTransition;
+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;
@@ -55,10 +64,12 @@ import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
import com.android.wm.shell.draganddrop.DragAndDropPolicy;
-import com.android.wm.shell.splitscreen.ISplitScreenListener;
+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.concurrent.Executor;
/**
* Class manages split-screen multitasking mode and implements the main interface
@@ -76,8 +87,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
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 StageCoordinator mStageCoordinator;
@@ -85,6 +98,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
SyncTransactionQueue syncQueue, Context context,
RootTaskDisplayAreaOrganizer rootTDAOrganizer,
ShellExecutor mainExecutor, DisplayImeController displayImeController,
+ DisplayInsetsController displayInsetsController,
Transitions transitions, TransactionPool transactionPool) {
mTaskOrganizer = shellTaskOrganizer;
mSyncQueue = syncQueue;
@@ -92,8 +106,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
mRootTDAOrganizer = rootTDAOrganizer;
mMainExecutor = mainExecutor;
mDisplayImeController = displayImeController;
+ mDisplayInsetsController = displayInsetsController;
mTransitions = transitions;
mTransactionPool = transactionPool;
+ mLogger = new SplitscreenEventLogger();
}
public SplitScreen asSplitScreen() {
@@ -114,8 +130,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
if (mStageCoordinator == null) {
// TODO: Multi-display
mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
- mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, mTransitions,
- mTransactionPool);
+ mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
+ mDisplayInsetsController, mTransitions, mTransactionPool, mLogger);
}
}
@@ -140,8 +156,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
return mStageCoordinator.removeFromSideStage(taskId);
}
+ public void setSideStageOutline(boolean enable) {
+ mStageCoordinator.setSideStageOutline(enable);
+ }
+
public void setSideStagePosition(@SplitPosition int sideStagePosition) {
- mStageCoordinator.setSideStagePosition(sideStagePosition);
+ mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */);
}
public void setSideStageVisibility(boolean visible) {
@@ -153,8 +173,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT);
}
- public void exitSplitScreen() {
- mStageCoordinator.exitSplitScreen();
+ public void exitSplitScreen(int exitReason) {
+ mStageCoordinator.exitSplitScreen(exitReason);
+ }
+
+ public void onKeyguardOccludedChanged(boolean occluded) {
+ mStageCoordinator.onKeyguardOccludedChanged(occluded);
+ }
+
+ public void onKeyguardVisibilityChanged(boolean showing) {
+ mStageCoordinator.onKeyguardVisibilityChanged(showing);
}
public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
@@ -175,7 +203,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
public void startTask(int taskId, @SplitScreen.StageType int stage,
@SplitPosition int position, @Nullable Bundle options) {
- options = resolveStartStage(stage, position, options);
+ options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
try {
ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
@@ -187,7 +215,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
public void startShortcut(String packageName, String shortcutId,
@SplitScreen.StageType int stage, @SplitPosition int position,
@Nullable Bundle options, UserHandle user) {
- options = resolveStartStage(stage, position, options);
+ options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
try {
LauncherApps launcherApps =
@@ -202,64 +230,96 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
public void startIntent(PendingIntent intent, Intent fillInIntent,
@SplitScreen.StageType int stage, @SplitPosition int position,
@Nullable Bundle options) {
- options = resolveStartStage(stage, position, options);
-
- try {
- intent.send(mContext, 0, fillInIntent, null, null, null, options);
- } catch (PendingIntent.CanceledException e) {
- Slog.e(TAG, "Failed to launch activity", e);
+ if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+ startIntentLegacy(intent, fillInIntent, stage, position, options);
+ return;
}
+ mStageCoordinator.startIntent(intent, fillInIntent, stage, position, options,
+ null /* remote */);
}
- private Bundle resolveStartStage(@SplitScreen.StageType int stage,
- @SplitPosition int position, @Nullable Bundle options) {
- switch (stage) {
- case STAGE_TYPE_UNDEFINED: {
- // Use the stage of the specified position is valid.
- if (position != SPLIT_POSITION_UNDEFINED) {
- if (position == mStageCoordinator.getSideStagePosition()) {
- options = resolveStartStage(STAGE_TYPE_SIDE, position, options);
- } else {
- options = resolveStartStage(STAGE_TYPE_MAIN, position, options);
+ private void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
+ @SplitScreen.StageType int stage, @SplitPosition int position,
+ @Nullable Bundle options) {
+ final boolean wasInSplit = isSplitScreenVisible();
+
+ LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
+ @Override
+ public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+ IRemoteAnimationFinishedCallback finishedCallback,
+ SurfaceControl.Transaction t) {
+ boolean cancelled = apps == null || apps.length == 0;
+ mStageCoordinator.updateSurfaceBounds(null /* layout */, t);
+ if (cancelled) {
+ if (!wasInSplit) {
+ final WindowContainerTransaction undoWct = new WindowContainerTransaction();
+ mStageCoordinator.prepareExitSplitScreen(STAGE_TYPE_MAIN, undoWct);
+ mSyncQueue.queue(undoWct);
+ mSyncQueue.runInSync(undoT -> {
+ // looks weird, but we want undoT to execute after t but still want the
+ // rest of the syncQueue runnables to aggregate.
+ t.merge(undoT);
+ undoT.merge(t);
+ });
+ return;
}
} else {
- // Exit split-screen and launch fullscreen since stage wasn't specified.
- mStageCoordinator.exitSplitScreen();
- }
- break;
- }
- case STAGE_TYPE_SIDE: {
- if (position != SPLIT_POSITION_UNDEFINED) {
- mStageCoordinator.setSideStagePosition(position);
- } else {
- position = mStageCoordinator.getSideStagePosition();
- }
- if (options == null) {
- options = new Bundle();
+ for (int i = 0; i < apps.length; ++i) {
+ if (apps[i].mode == MODE_OPENING) {
+ t.show(apps[i].leash);
+ }
+ }
}
- mStageCoordinator.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;
- mStageCoordinator.setSideStagePosition(sideStagePosition);
- } else {
- position = mStageCoordinator.getMainStagePosition();
+ RemoteAnimationTarget divider = mStageCoordinator.getDividerBarLegacyTarget();
+ if (divider.leash != null) {
+ t.show(divider.leash);
}
- if (options == null) {
- options = new Bundle();
+ t.apply();
+ if (cancelled) return;
+ try {
+ finishedCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error finishing legacy transition: ", e);
}
- mStageCoordinator.updateActivityOptions(options, position);
- break;
}
- default:
- throw new IllegalArgumentException("Unknown stage=" + stage);
+ };
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ options = mStageCoordinator.resolveStartStage(stage, 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()};
+ }
- return options;
+ /**
+ * 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) {
@@ -275,6 +335,38 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
@ExternalThread
private class SplitScreenImpl implements SplitScreen {
private ISplitScreenImpl mISplitScreen;
+ private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>();
+ private final SplitScreen.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() {
@@ -284,6 +376,48 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
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);
+ });
+ }
}
/**
@@ -380,7 +514,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
public void exitSplitScreen() {
executeRemoteCallWithTaskPermission(mController, "exitSplitScreen",
(controller) -> {
- controller.exitSplitScreen();
+ controller.exitSplitScreen(
+ FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
});
}
@@ -417,6 +552,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
@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,
@@ -444,5 +589,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
controller.startIntent(intent, fillInIntent, stage, 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/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index c37789ecbc9d..69d0be6abc0b 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
@@ -84,17 +84,19 @@ class SplitScreenTransitions {
}
void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @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, t, mRemoteFinishCB);
+ mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction,
+ mRemoteFinishCB);
mRemoteHandler = null;
return;
}
- playInternalAnimation(transition, info, t, mainRoot, sideRoot);
+ playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot);
}
private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
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
new file mode 100644
index 000000000000..319079baaccf
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
@@ -0,0 +1,324 @@
+/*
+ * 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.splitscreen;
+
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitLayout.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.SplitLayout.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/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 0264c5a1c55a..9e6edd2a5381 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
@@ -20,11 +20,19 @@ import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+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 android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
+
+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.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
@@ -35,6 +43,7 @@ import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString;
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_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;
@@ -42,11 +51,23 @@ 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;
@@ -59,10 +80,12 @@ 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;
@@ -114,14 +137,20 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
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 boolean mExitSplitScreenOnHide = true;
+ 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 int mDismissTop = NO_DISMISS;
+ @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.
@@ -136,13 +165,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
- DisplayImeController displayImeController, Transitions transitions,
- TransactionPool transactionPool) {
+ DisplayImeController displayImeController,
+ DisplayInsetsController displayInsetsController, Transitions transitions,
+ TransactionPool transactionPool, SplitscreenEventLogger logger) {
mContext = context;
mDisplayId = displayId;
mSyncQueue = syncQueue;
mRootTDAOrganizer = rootTDAOrganizer;
mTaskOrganizer = taskOrganizer;
+ mLogger = logger;
mMainStage = new MainStage(
mTaskOrganizer,
mDisplayId,
@@ -150,13 +181,19 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSyncQueue,
mSurfaceSession);
mSideStage = new SideStage(
+ mContext,
mTaskOrganizer,
mDisplayId,
mSideStageListener,
mSyncQueue,
mSurfaceSession);
mDisplayImeController = displayImeController;
+ mDisplayInsetsController = displayInsetsController;
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);
@@ -166,7 +203,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController,
- SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) {
+ DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
+ Transitions transitions, TransactionPool transactionPool,
+ SplitscreenEventLogger logger) {
mContext = context;
mDisplayId = displayId;
mSyncQueue = syncQueue;
@@ -175,10 +214,12 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mMainStage = mainStage;
mSideStage = sideStage;
mDisplayImeController = displayImeController;
+ mDisplayInsetsController = displayInsetsController;
mRootTDAOrganizer.registerListener(displayId, this);
mSplitLayout = splitLayout;
mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
mOnTransitionAnimationComplete);
+ mLogger = logger;
transitions.addHandler(this);
}
@@ -194,7 +235,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
boolean moveToSideStage(ActivityManager.RunningTaskInfo task,
@SplitPosition int sideStagePosition) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
- setSideStagePosition(sideStagePosition);
+ setSideStagePosition(sideStagePosition, wct);
mMainStage.activate(getMainStageBounds(), wct);
mSideStage.addTask(task, getSideStageBounds(), wct);
mTaskOrganizer.applyTransaction(wct);
@@ -215,6 +256,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
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,
@@ -222,7 +267,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final WindowContainerTransaction wct = new WindowContainerTransaction();
mainOptions = mainOptions != null ? mainOptions : new Bundle();
sideOptions = sideOptions != null ? sideOptions : new Bundle();
- setSideStagePosition(sidePosition);
+ 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.
@@ -241,6 +286,138 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
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() {
+ try {
+ adapter.getRunner().onAnimationCancelled();
+ } 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 IRemoteTransition 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;
+ }
+
@SplitLayout.SplitPosition
int getSideStagePosition() {
return mSideStagePosition;
@@ -252,18 +429,24 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
}
- void setSideStagePosition(@SplitPosition int sideStagePosition) {
- setSideStagePosition(sideStagePosition, true /* updateVisibility */);
+ void setSideStagePosition(@SplitPosition int sideStagePosition,
+ @Nullable WindowContainerTransaction wct) {
+ setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
}
private void setSideStagePosition(@SplitPosition int sideStagePosition,
- boolean updateVisibility) {
+ boolean updateBounds, @Nullable WindowContainerTransaction wct) {
if (mSideStagePosition == sideStagePosition) return;
mSideStagePosition = sideStagePosition;
sendOnStagePositionChanged();
- if (mSideStageListener.mVisible && updateVisibility) {
- onStageVisibilityChanged(mSideStageListener);
+ if (mSideStageListener.mVisible && updateBounds) {
+ if (wct == null) {
+ // onBoundsChanged builds/applies a wct with the contents of updateWindowBounds.
+ onLayoutChanged(mSplitLayout);
+ } else {
+ updateWindowBounds(mSplitLayout, wct);
+ }
}
}
@@ -275,24 +458,49 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mTaskOrganizer.applyTransaction(wct);
}
- void exitSplitScreen() {
- exitSplitScreen(null /* childrenToTop */);
+ 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 exitSplitScreen(int exitReason) {
+ exitSplitScreen(null /* childrenToTop */, exitReason);
}
void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
mExitSplitScreenOnHide = exitSplitScreenOnHide;
}
- private void exitSplitScreen(StageTaskListener childrenToTop) {
+ private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
mSideStage.removeAllTasks(wct, childrenToTop == mSideStage);
mMainStage.deactivate(wct, childrenToTop == mMainStage);
mTaskOrganizer.applyTransaction(wct);
// Reset divider position.
mSplitLayout.resetDividerPosition();
+ mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+ if (childrenToTop != null) {
+ logExitToStage(exitReason, childrenToTop == mMainStage);
+ } else {
+ logExit(exitReason);
+ }
}
- private void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop,
+ /**
+ * 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);
@@ -309,29 +517,26 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
void updateActivityOptions(Bundle opts, @SplitPosition int position) {
addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage);
-
- if (!mMainStage.isActive()) {
- // Activate the main stage in anticipation of an app launch.
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- mMainStage.activate(getMainStageBounds(), wct);
- mSideStage.setBounds(getSideStageBounds(), wct);
- mTaskOrganizer.applyTransaction(wct);
- }
}
void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
if (mListeners.contains(listener)) return;
mListeners.add(listener);
- listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
- listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
- mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
- mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
+ 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);
@@ -340,9 +545,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
- private void onStageChildTaskStatusChanged(
- StageListenerImpl stageListener, int taskId, boolean present, boolean visible) {
-
+ private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId,
+ boolean present, boolean visible) {
int stage;
if (present) {
stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
@@ -350,12 +554,26 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// 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);
+ }
+ }
+
private void onStageRootTaskAppeared(StageListenerImpl stageListener) {
if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) {
mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit);
@@ -395,6 +613,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
} else {
mSplitLayout.release();
}
+ sendSplitVisibilityChanged();
}
private void onStageVisibilityChanged(StageListenerImpl stageListener) {
@@ -403,22 +622,37 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Divider is only visible if both the main stage and side stages are visible
setDividerVisibility(isSplitScreenVisible());
- if (mExitSplitScreenOnHide && !mainStageVisible && !sideStageVisible) {
- // Exit split-screen if both stage are not visible.
- // TODO: This is only a temporary request from UX and is likely to be removed soon...
- exitSplitScreen();
+ if (!mainStageVisible && !sideStageVisible) {
+ 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(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);
}
- if (mainStageVisible) {
+ // When both stage's visibility changed to visible, main stage might receives visibility
+ // changed before side stage if it has higher z-order than side stage. Make sure we only
+ // update main stage's windowing mode with the visibility changed of side stage to prevent
+ // stacking multiple windowing mode transactions which result to flicker issue.
+ if (mainStageVisible && stageListener == mSideStageListener) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (sideStageVisible) {
// The main stage configuration should to follow split layout when side stage is
// visible.
mMainStage.updateConfiguration(
WINDOWING_MODE_MULTI_WINDOW, getMainStageBounds(), wct);
- } else {
+ } else if (!mSideStage.mRootTaskInfo.isSleeping) {
// We want the main stage configuration to be fullscreen when the side stage isn't
// visible.
+ // We should not do it when side stage are not visible due to sleeping display too.
mMainStage.updateConfiguration(WINDOWING_MODE_FULLSCREEN, null, wct);
}
// TODO: Change to `mSyncQueue.queue(wct)` once BLAST is stable.
@@ -426,22 +660,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
mSyncQueue.runInSync(t -> {
- final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
final SurfaceControl sideStageLeash = mSideStage.mRootLeash;
final SurfaceControl mainStageLeash = mMainStage.mRootLeash;
- if (dividerLeash != null) {
- if (mDividerVisible) {
- t.show(dividerLeash)
- .setLayer(dividerLeash, Integer.MAX_VALUE)
- .setPosition(dividerLeash,
- mSplitLayout.getDividerBounds().left,
- mSplitLayout.getDividerBounds().top);
- } else {
- t.hide(dividerLeash);
- }
- }
-
if (sideStageVisible) {
final Rect sideStageBounds = getSideStageBounds();
t.show(sideStageLeash)
@@ -468,31 +689,54 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
} else {
t.hide(mainStageLeash);
}
+
+ applyDividerVisibility(t);
});
}
+ private void applyDividerVisibility(SurfaceControl.Transaction t) {
+ final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
+ if (dividerLeash == null) {
+ return;
+ }
+
+ if (mDividerVisible) {
+ t.show(dividerLeash)
+ .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER)
+ .setPosition(dividerLeash,
+ mSplitLayout.getDividerBounds().left,
+ mSplitLayout.getDividerBounds().top);
+ } else {
+ t.hide(dividerLeash);
+ }
+
+ }
+
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);
+ 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);
+ 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);
- // Reorder side stage to the top whenever there's a new child task appeared in side
- // stage. This is needed to prevent main stage occludes side stage and makes main stage
- // flipping between fullscreen and multi-window windowing mode.
- wct.reorder(mSideStage.mRootTaskInfo.token, true);
mTaskOrganizer.applyTransaction(wct);
}
+ if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren
+ && mSideStageListener.mHasChildren) {
+ mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
+ getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+ getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+ mSplitLayout.isLandscape());
+ }
}
@VisibleForTesting
@@ -511,38 +755,52 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
onSnappedToDismissTransition(mainStageToTop);
return;
}
- exitSplitScreen(mainStageToTop ? mMainStage : mSideStage);
+ 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);
+ ? 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 onLayoutChanging(SplitLayout layout) {
+ mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
}
@Override
- public void onBoundsChanging(SplitLayout layout) {
+ public void onLayoutChanged(SplitLayout layout) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ updateWindowBounds(layout, wct);
+ mSyncQueue.queue(wct);
+ mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
+ mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
+ }
+
+ /**
+ * 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;
-
- mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, topLeftStage.mRootLeash,
- bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer));
+ layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo);
}
- @Override
- public void onBoundsChanged(SplitLayout layout) {
+ void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) {
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.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo);
- mSyncQueue.queue(wct);
- mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, topLeftStage.mRootLeash,
- bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer));
+ (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash,
+ bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer);
}
@Override
@@ -561,6 +819,18 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
@Override
+ public void onLayoutShifted(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.applyLayoutShifted(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo,
+ bottomRightStage.mRootTaskInfo);
+ mTaskOrganizer.applyTransaction(wct);
+ }
+
+ @Override
public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) {
mDisplayAreaInfo = displayAreaInfo;
if (mSplitLayout == null) {
@@ -568,6 +838,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDisplayAreaInfo.configuration, this,
b -> mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b),
mDisplayImeController, mTaskOrganizer);
+ mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
}
}
@@ -580,8 +851,21 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) {
mDisplayAreaInfo = displayAreaInfo;
if (mSplitLayout != null
- && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)) {
- onBoundsChanged(mSplitLayout);
+ && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)
+ && mMainStage.isActive()) {
+ onLayoutChanged(mSplitLayout);
+ mSyncQueue.runInSync(t -> applyDividerVisibility(t));
+ }
+ }
+
+ 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;
}
}
@@ -672,7 +956,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Override
public boolean startAnimation(@NonNull IBinder transition,
@NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
if (transition != mSplitTransitions.mPendingDismiss
&& transition != mSplitTransitions.mPendingEnter) {
@@ -717,14 +1002,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
boolean shouldAnimate = true;
if (mSplitTransitions.mPendingEnter == transition) {
- shouldAnimate = startPendingEnterAnimation(transition, info, t);
+ shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction);
} else if (mSplitTransitions.mPendingDismiss == transition) {
- shouldAnimate = startPendingDismissAnimation(transition, info, t);
+ shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction);
}
if (!shouldAnimate) return false;
- mSplitTransitions.playAnimation(transition, info, t, finishCallback,
- mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+ mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction,
+ finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
return true;
}
@@ -754,7 +1039,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Update local states (before animating).
setDividerVisibility(true);
- setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateVisibility */);
+ setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */,
+ null /* wct */);
setSplitsVisible(true);
addDividerBarToTransition(info, t, true /* show */);
@@ -854,12 +1140,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// 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.setLayer(leash, SPLIT_DIVIDER_LAYER);
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);
+ }
+
@Override
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
@@ -884,6 +1180,36 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
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;
@@ -923,7 +1249,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Override
public void onNoLongerSupportMultiWindow() {
if (mMainStage.isActive()) {
- StageCoordinator.this.exitSplitScreen();
+ StageCoordinator.this.exitSplitScreen(
+ SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW);
}
}
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 0fd8eca6290e..3512a0c3727b 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
@@ -71,8 +71,8 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
private final StageListenerCallbacks mCallbacks;
- private final SyncTransactionQueue mSyncQueue;
private final SurfaceSession mSurfaceSession;
+ protected final SyncTransactionQueue mSyncQueue;
protected ActivityManager.RunningTaskInfo mRootTaskInfo;
protected SurfaceControl mRootLeash;
@@ -97,6 +97,29 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
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.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) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index 29326ec90e31..22865988e880 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -360,7 +360,7 @@ public class SplashscreenContentDrawer {
createIconDrawable(iconDrawable, false);
} else {
final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
- final int densityDpi = mContext.getResources().getDisplayMetrics().densityDpi;
+ final int densityDpi = mContext.getResources().getConfiguration().densityDpi;
final int scaledIconDpi =
(int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java
index 01c9b6630fa6..76105a39189b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java
@@ -36,4 +36,12 @@ public interface StartingSurface {
default int getBackgroundColor(TaskInfo taskInfo) {
return Color.BLACK;
}
+
+ /** Set the proxy to communicate with SysUi side components. */
+ void setSysuiProxy(SysuiProxy proxy);
+
+ /** Callback to tell SysUi components execute some methods. */
+ interface SysuiProxy {
+ void requestTopUi(boolean requestTopUi, String componentTag);
+ }
}
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 fc7c86d669cb..6c60bad31dba 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
@@ -52,6 +52,7 @@ import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
import android.widget.FrameLayout;
import android.window.SplashScreenView;
import android.window.SplashScreenView.SplashScreenViewParcelable;
@@ -115,6 +116,8 @@ public class StartingSurfaceDrawer {
@VisibleForTesting
final SplashscreenContentDrawer mSplashscreenContentDrawer;
private Choreographer mChoreographer;
+ private final WindowManagerGlobal mWindowManagerGlobal;
+ private StartingSurface.SysuiProxy mSysuiProxy;
/**
* @param splashScreenExecutor The thread used to control add and remove starting window.
@@ -126,6 +129,8 @@ public class StartingSurfaceDrawer {
mSplashScreenExecutor = splashScreenExecutor;
mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, pool);
mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance());
+ mWindowManagerGlobal = WindowManagerGlobal.getInstance();
+ mDisplayManager.getDisplay(DEFAULT_DISPLAY);
}
private final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>();
@@ -137,21 +142,8 @@ public class StartingSurfaceDrawer {
private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts =
new SparseArray<>(1);
- /** Obtain proper context for showing splash screen on the provided display. */
- private Context getDisplayContext(Context context, int displayId) {
- if (displayId == DEFAULT_DISPLAY) {
- // The default context fits.
- return context;
- }
-
- final Display targetDisplay = mDisplayManager.getDisplay(displayId);
- if (targetDisplay == null) {
- // Failed to obtain the non-default display where splash screen should be shown,
- // lets not show at all.
- return null;
- }
-
- return context.createDisplayContext(targetDisplay);
+ private Display getDisplay(int displayId) {
+ return mDisplayManager.getDisplay(displayId);
}
private int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) {
@@ -160,6 +152,11 @@ public class StartingSurfaceDrawer {
: activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource()
: com.android.internal.R.style.Theme_DeviceDefault_DayNight;
}
+
+ void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) {
+ mSysuiProxy = sysuiProxy;
+ }
+
/**
* Called when a task need a splash screen starting window.
*
@@ -186,13 +183,11 @@ public class StartingSurfaceDrawer {
+ " suggestType=" + suggestType);
}
- // Obtain proper context to launch on the right display.
- final Context displayContext = getDisplayContext(context, displayId);
- if (displayContext == null) {
+ final Display display = getDisplay(displayId);
+ if (display == null) {
// Can't show splash screen on requested display, so skip showing at all.
return;
}
- context = displayContext;
if (theme != context.getThemeResId()) {
try {
context = context.createPackageContextAsUser(activityInfo.packageName,
@@ -328,12 +323,13 @@ public class StartingSurfaceDrawer {
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
};
+ if (mSysuiProxy != null) {
+ mSysuiProxy.requestTopUi(true, TAG);
+ }
mSplashscreenContentDrawer.createContentView(context, suggestType, activityInfo, taskId,
viewSupplier::setView);
-
try {
- final WindowManager wm = context.getSystemService(WindowManager.class);
- if (addWindow(taskId, appToken, rootLayout, wm, params, suggestType)) {
+ if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) {
// We use the splash screen worker thread to create SplashScreenView while adding
// the window, as otherwise Choreographer#doFrame might be delayed on this thread.
// And since Choreographer#doFrame won't happen immediately after adding the window,
@@ -508,12 +504,14 @@ public class StartingSurfaceDrawer {
viewHost.getView().post(viewHost::release);
}
- protected boolean addWindow(int taskId, IBinder appToken, View view, WindowManager wm,
+ protected boolean addWindow(int taskId, IBinder appToken, View view, Display display,
WindowManager.LayoutParams params, @StartingWindowType int suggestType) {
boolean shouldSaveView = true;
+ final Context context = view.getContext();
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView");
- wm.addView(view, params);
+ mWindowManagerGlobal.addView(view, params, display,
+ null /* parentWindow */, context.getUserId());
} catch (WindowManager.BadTokenException e) {
// ignore
Slog.w(TAG, appToken + " already running, starting window not displayed. "
@@ -521,9 +519,9 @@ public class StartingSurfaceDrawer {
shouldSaveView = false;
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- if (view != null && view.getParent() == null) {
+ if (view.getParent() == null) {
Slog.w(TAG, "view not successfully added to wm, removing view");
- wm.removeViewImmediate(view);
+ mWindowManagerGlobal.removeView(view, true /* immediate */);
shouldSaveView = false;
}
}
@@ -584,13 +582,13 @@ public class StartingSurfaceDrawer {
}
private void removeWindowInner(View decorView, boolean hideView) {
+ if (mSysuiProxy != null) {
+ mSysuiProxy.requestTopUi(false, TAG);
+ }
if (hideView) {
decorView.setVisibility(View.GONE);
}
- final WindowManager wm = decorView.getContext().getSystemService(WindowManager.class);
- if (wm != null) {
- wm.removeView(decorView);
- }
+ mWindowManagerGlobal.removeView(decorView, false /* immediate */);
}
/**
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 e84d498a9258..a5c47c41180e 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
@@ -217,6 +217,11 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo
return color != Color.TRANSPARENT
? color : SplashscreenContentDrawer.getSystemBGColor();
}
+
+ @Override
+ public void setSysuiProxy(SysuiProxy proxy) {
+ mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.setSysuiProxy(proxy));
+ }
}
/**
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 6052d3dee891..7d011e6521a2 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
@@ -72,6 +72,7 @@ import android.view.IWindowSession;
import android.view.InputChannel;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
+import android.view.InsetsVisibilities;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.view.View;
@@ -205,7 +206,7 @@ public class TaskSnapshotWindow {
final SurfaceControl surfaceControl = new SurfaceControl();
final ClientWindowFrames tmpFrames = new ClientWindowFrames();
- final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0];
+ final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0];
final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration();
final TaskDescription taskDescription;
@@ -225,13 +226,14 @@ public class TaskSnapshotWindow {
delayRemovalTime, topWindowInsetsState, clearWindowHandler, splashScreenExecutor);
final Window window = snapshotSurface.mWindow;
- final InsetsState mTmpInsetsState = new InsetsState();
+ final InsetsState tmpInsetsState = new InsetsState();
+ final InsetsVisibilities tmpRequestedVisibilities = new InsetsVisibilities();
final InputChannel tmpInputChannel = new InputChannel();
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay");
final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId,
- mTmpInsetsState, tmpInputChannel, mTmpInsetsState, mTempControls);
+ tmpRequestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (res < 0) {
Slog.w(TAG, "Failed to add snapshot starting window res=" + res);
@@ -244,8 +246,8 @@ public class TaskSnapshotWindow {
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout");
session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, -1,
- tmpFrames, tmpMergedConfiguration, surfaceControl, mTmpInsetsState,
- mTempControls, TMP_SURFACE_SIZE);
+ tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState,
+ tmpControls, TMP_SURFACE_SIZE);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
} catch (RemoteException e) {
snapshotSurface.clearWindowSynced();
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 c6fb5af7d4be..4ba6acaba025 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
@@ -16,30 +16,55 @@
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_NONE;
+import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
+import static android.app.ActivityOptions.ANIM_SCALE_UP;
+import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN;
+import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
+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;
import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
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_DISPLAY_HAS_ALERT_WINDOWS;
+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;
import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
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_NONE;
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.graphics.Point;
import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
import android.os.IBinder;
+import android.os.SystemProperties;
+import android.os.UserHandle;
import android.util.ArrayMap;
import android.view.Choreographer;
import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Transformation;
@@ -48,9 +73,12 @@ import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.AttributeCache;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
+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.protolog.ShellProtoLogGroup;
@@ -61,33 +89,173 @@ import java.util.ArrayList;
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 ShellExecutor mMainExecutor;
private final ShellExecutor mAnimExecutor;
private final TransitionAnimation mTransitionAnimation;
+ private final SurfaceSession mSurfaceSession = new SurfaceSession();
+
/** Keeps track of the currently-running animations associated with each transition. */
private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>();
private final Rect mInsets = new Rect(0, 0, 0, 0);
private float mTransitionAnimationScaleSetting = 1.0f;
- DefaultTransitionHandler(@NonNull TransactionPool transactionPool, Context context,
+ private final int mCurrentUserId;
+
+ private ScreenRotationAnimation mRotationAnimation;
+
+ DefaultTransitionHandler(@NonNull DisplayController displayController,
+ @NonNull TransactionPool transactionPool, Context context,
@NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) {
+ mDisplayController = displayController;
mTransactionPool = transactionPool;
+ mContext = context;
mMainExecutor = mainExecutor;
mAnimExecutor = animExecutor;
mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG);
+ mCurrentUserId = UserHandle.myUserId();
AttributeCache.init(context);
}
+ @VisibleForTesting
+ static boolean isRotationSeamless(@NonNull TransitionInfo info,
+ DisplayController displayController) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Display is rotating, check if it should be seamless.");
+ boolean checkedDisplayLayout = false;
+ for (int i = info.getChanges().size() - 1; i >= 0; --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;
+
+ if ((change.getFlags() & FLAG_IS_DISPLAY) != 0) {
+ // In the presence of System Alert windows we can not seamlessly rotate.
+ 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;
+ }
+ } 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;
+ }
+ } else if (change.getTaskInfo() != null) {
+ // We only enable seamless rotation if all the visible task windows requested it.
+ if (change.getRotationAnimation() != 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;
+ }
+ }
+ }
+ }
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless.");
+ return true;
+ }
+
+ /**
+ * 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;
+
+ 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;
+ }
+ }
+ return ROTATION_ANIMATION_ROTATE;
+ }
+
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
"start default transition animation, info = %s", info);
+ // If keyguard goes away, we should loadKeyguardExitAnimation. Otherwise this just
+ // immediately finishes since there is no animation for screen-wake.
+ if (info.getType() == WindowManager.TRANSIT_WAKE && !info.isKeyguardGoingAway()) {
+ startTransaction.apply();
+ finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ return true;
+ }
+
if (mAnimations.containsKey(transition)) {
throw new IllegalStateException("Got a duplicate startAnimation call for "
+ transition);
@@ -97,19 +265,42 @@ 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 */);
};
+
+ final int wallpaperTransit = getWallpaperTransitType(info);
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
+
+ if (info.getType() == TRANSIT_CHANGE && change.getMode() == TRANSIT_CHANGE
+ && (change.getEndRotation() != change.getStartRotation())
+ && (change.getFlags() & FLAG_IS_DISPLAY) != 0) {
+ boolean isSeamless = isRotationSeamless(info, mDisplayController);
+ final int anim = getRotationAnimation(info);
+ if (!(isSeamless || anim == ROTATION_ANIMATION_JUMPCUT)) {
+ mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession,
+ mTransactionPool, startTransaction, change, info.getRootLeash());
+ mRotationAnimation.startAnimation(animations, onAnimFinish,
+ mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor);
+ continue;
+ }
+ }
+
if (change.getMode() == TRANSIT_CHANGE) {
// No default animation for this, so just update bounds/position.
- t.setPosition(change.getLeash(),
+ startTransaction.setPosition(change.getLeash(),
change.getEndAbsBounds().left - change.getEndRelOffset().x,
change.getEndAbsBounds().top - change.getEndRelOffset().y);
if (change.getTaskInfo() != null) {
// Skip non-tasks since those usually have null bounds.
- t.setWindowCrop(change.getLeash(),
+ startTransaction.setWindowCrop(change.getLeash(),
change.getEndAbsBounds().width(), change.getEndAbsBounds().height());
}
}
@@ -117,12 +308,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
// Don't animate anything that isn't independent.
if (!TransitionInfo.isIndependent(change, info)) continue;
- Animation a = loadAnimation(info.getType(), info.getFlags(), change);
+ Animation a = loadAnimation(info, change, wallpaperTransit);
if (a != null) {
- startAnimInternal(animations, a, change.getLeash(), onAnimFinish);
+ startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
+ mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */);
+
+ if (info.getAnimationOptions() != null) {
+ attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions());
+ }
}
}
- t.apply();
+ startTransaction.apply();
// run finish now in-case there are no animations
onAnimFinish.run();
return true;
@@ -141,87 +337,134 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
@Nullable
- private Animation loadAnimation(int type, int flags, TransitionInfo.Change change) {
- // TODO(b/178678389): It should handle more type animation here
+ private Animation loadAnimation(TransitionInfo info, TransitionInfo.Change change,
+ int wallpaperTransit) {
Animation a = null;
- final boolean isOpening = Transitions.isOpeningType(type);
+ final int type = info.getType();
+ final int flags = info.getFlags();
final int changeMode = change.getMode();
final int changeFlags = change.getFlags();
+ final boolean isOpeningType = Transitions.isOpeningType(type);
+ 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 : true;
- if (type == TRANSIT_RELAUNCH) {
- a = mTransitionAnimation.createRelaunchAnimation(
- change.getStartAbsBounds(), mInsets, change.getEndAbsBounds());
- } else if (type == TRANSIT_KEYGUARD_GOING_AWAY) {
+ if (info.isKeyguardGoingAway()) {
a = mTransitionAnimation.loadKeyguardExitAnimation(flags,
(changeFlags & FLAG_SHOW_WALLPAPER) != 0);
} else if (type == TRANSIT_KEYGUARD_UNOCCLUDE) {
a = mTransitionAnimation.loadKeyguardUnoccludeAnimation();
- } else if (changeMode == TRANSIT_OPEN && isOpening) {
- if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
- // This received a transferred starting window, so don't animate
- return null;
- }
-
- if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
- a = mTransitionAnimation.loadVoiceActivityOpenAnimation(true /** enter */);
- } else if (change.getTaskInfo() != null) {
- a = mTransitionAnimation.loadDefaultAnimationAttr(
- R.styleable.WindowAnimation_taskOpenEnterAnimation);
+ } else if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
+ if (isOpeningType) {
+ a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter);
} else {
- a = mTransitionAnimation.loadDefaultAnimationRes(
- (changeFlags & FLAG_TRANSLUCENT) == 0
- ? R.anim.activity_open_enter : R.anim.activity_translucent_open_enter);
- }
- } else if (changeMode == TRANSIT_TO_FRONT && isOpening) {
- if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
- // This received a transferred starting window, so don't animate
- return null;
- }
-
- if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
- a = mTransitionAnimation.loadVoiceActivityOpenAnimation(true /** enter */);
- } else {
- a = mTransitionAnimation.loadDefaultAnimationAttr(
- R.styleable.WindowAnimation_taskToFrontEnterAnimation);
- }
- } else if (changeMode == TRANSIT_CLOSE && !isOpening) {
- if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
- a = mTransitionAnimation.loadVoiceActivityExitAnimation(false /** enter */);
- } else if (change.getTaskInfo() != null) {
- a = mTransitionAnimation.loadDefaultAnimationAttr(
- R.styleable.WindowAnimation_taskCloseExitAnimation);
- } else {
- a = mTransitionAnimation.loadDefaultAnimationRes(
- (changeFlags & FLAG_TRANSLUCENT) == 0
- ? R.anim.activity_close_exit : R.anim.activity_translucent_close_exit);
- }
- } else if (changeMode == TRANSIT_TO_BACK && !isOpening) {
- if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
- a = mTransitionAnimation.loadVoiceActivityExitAnimation(false /** enter */);
- } else {
- a = mTransitionAnimation.loadDefaultAnimationAttr(
- R.styleable.WindowAnimation_taskToBackExitAnimation);
+ a = mTransitionAnimation.loadVoiceActivityExitAnimation(enter);
}
} else if (changeMode == TRANSIT_CHANGE) {
// In the absence of a specific adapter, we just want to keep everything stationary.
a = new AlphaAnimation(1.f, 1.f);
a.setDuration(TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION);
+ } else if (type == TRANSIT_RELAUNCH) {
+ a = mTransitionAnimation.createRelaunchAnimation(
+ change.getEndAbsBounds(), mInsets, change.getEndAbsBounds());
+ } else if (overrideType == ANIM_CUSTOM
+ && (canCustomContainer || options.getOverrideTaskTransition())) {
+ a = mTransitionAnimation.loadAnimationRes(options.getPackageName(), enter
+ ? options.getEnterResId() : options.getExitResId());
+ } else if (overrideType == ANIM_OPEN_CROSS_PROFILE_APPS && enter) {
+ a = mTransitionAnimation.loadCrossProfileAppEnterAnimation();
+ } else if (overrideType == ANIM_CLIP_REVEAL) {
+ a = mTransitionAnimation.createClipRevealAnimationLocked(type, wallpaperTransit, enter,
+ change.getEndAbsBounds(), change.getEndAbsBounds(),
+ options.getTransitionBounds());
+ } else if (overrideType == ANIM_SCALE_UP) {
+ a = mTransitionAnimation.createScaleUpAnimationLocked(type, wallpaperTransit, enter,
+ change.getEndAbsBounds(), options.getTransitionBounds());
+ } else if (overrideType == ANIM_THUMBNAIL_SCALE_UP
+ || overrideType == ANIM_THUMBNAIL_SCALE_DOWN) {
+ final boolean scaleUp = overrideType == ANIM_THUMBNAIL_SCALE_UP;
+ a = mTransitionAnimation.createThumbnailEnterExitAnimationLocked(enter, scaleUp,
+ change.getEndAbsBounds(), type, wallpaperTransit, options.getThumbnail(),
+ options.getTransitionBounds());
+ } else if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0 && isOpeningType) {
+ // This received a transferred starting window, so don't animate
+ return null;
+ } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation
+ : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation);
+ } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation
+ : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation);
+ } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation
+ : R.styleable.WindowAnimation_wallpaperOpenExitAnimation);
+ } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation
+ : R.styleable.WindowAnimation_wallpaperCloseExitAnimation);
+ } else if (type == TRANSIT_OPEN) {
+ if (isTask) {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_taskOpenEnterAnimation
+ : R.styleable.WindowAnimation_taskOpenExitAnimation);
+ } else {
+ if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) {
+ a = mTransitionAnimation.loadDefaultAnimationRes(
+ R.anim.activity_translucent_open_enter);
+ } else {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_activityOpenEnterAnimation
+ : R.styleable.WindowAnimation_activityOpenExitAnimation);
+ }
+ }
+ } else if (type == TRANSIT_TO_FRONT) {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_taskToFrontEnterAnimation
+ : R.styleable.WindowAnimation_taskToFrontExitAnimation);
+ } else if (type == TRANSIT_CLOSE) {
+ if (isTask) {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_taskCloseEnterAnimation
+ : R.styleable.WindowAnimation_taskCloseExitAnimation);
+ } else {
+ if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) {
+ a = mTransitionAnimation.loadDefaultAnimationRes(
+ R.anim.activity_translucent_close_exit);
+ } else {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_activityCloseEnterAnimation
+ : R.styleable.WindowAnimation_activityCloseExitAnimation);
+ }
+ }
+ } else if (type == TRANSIT_TO_BACK) {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+ ? R.styleable.WindowAnimation_taskToBackEnterAnimation
+ : R.styleable.WindowAnimation_taskToBackExitAnimation);
}
if (a != null) {
- Rect start = change.getStartAbsBounds();
- Rect end = change.getEndAbsBounds();
+ if (!a.isInitialized()) {
+ Rect end = change.getEndAbsBounds();
+ a.initialize(end.width(), end.height(), end.width(), end.height());
+ }
a.restrictDuration(MAX_ANIMATION_DURATION);
- a.initialize(end.width(), end.height(), start.width(), start.height());
a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
}
return a;
}
- private void startAnimInternal(@NonNull ArrayList<Animator> animations, @NonNull Animation anim,
- @NonNull SurfaceControl leash, @NonNull Runnable finishCallback) {
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+ static void startSurfaceAnimation(@NonNull ArrayList<Animator> animations,
+ @NonNull Animation anim, @NonNull SurfaceControl leash,
+ @NonNull Runnable finishCallback, @NonNull TransactionPool pool,
+ @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor,
+ @Nullable Point position) {
+ final SurfaceControl.Transaction transaction = pool.acquire();
final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
final Transformation transformation = new Transformation();
final float[] matrix = new float[9];
@@ -231,14 +474,16 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
va.addUpdateListener(animation -> {
final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
- applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix);
+ applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix,
+ position);
});
final Runnable finisher = () -> {
- applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix);
+ applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix,
+ position);
- mTransactionPool.release(transaction);
- mMainExecutor.execute(() -> {
+ pool.release(transaction);
+ mainExecutor.execute(() -> {
animations.remove(va);
finishCallback.run();
});
@@ -255,12 +500,116 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
});
animations.add(va);
- mAnimExecutor.execute(va::start);
+ animExecutor.execute(va::start);
+ }
+
+ private void attachThumbnail(@NonNull ArrayList<Animator> animations,
+ @NonNull Runnable finishCallback, TransitionInfo.Change change,
+ TransitionInfo.AnimationOptions options) {
+ 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) {
+ attachCrossProfileThunmbnailAnimation(animations, finishCallback, change);
+ } else if (options.getType() == ANIM_THUMBNAIL_SCALE_UP) {
+ attachThumbnailAnimation(animations, finishCallback, change, options);
+ }
+ } else if (isClose && options.getType() == ANIM_THUMBNAIL_SCALE_DOWN) {
+ attachThumbnailAnimation(animations, finishCallback, change, options);
+ }
+ }
+
+ private void attachCrossProfileThunmbnailAnimation(@NonNull ArrayList<Animator> animations,
+ @NonNull Runnable finishCallback, TransitionInfo.Change change) {
+ final int thumbnailDrawableRes = change.getTaskInfo().userId == mCurrentUserId
+ ? R.drawable.ic_account_circle : R.drawable.ic_corp_badge;
+ final Rect bounds = change.getEndAbsBounds();
+ final HardwareBuffer thumbnail = mTransitionAnimation.createCrossProfileAppsThumbnail(
+ thumbnailDrawableRes, bounds);
+ if (thumbnail == null) {
+ return;
+ }
+
+ final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+ final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+ change.getLeash(), thumbnail, transaction);
+ final Animation a =
+ mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(bounds);
+ if (a == null) {
+ return;
+ }
+
+ final Runnable finisher = () -> {
+ wt.destroy(transaction);
+ mTransactionPool.release(transaction);
+
+ finishCallback.run();
+ };
+ a.restrictDuration(MAX_ANIMATION_DURATION);
+ a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
+ mMainExecutor, mAnimExecutor, new Point(bounds.left, bounds.top));
+ }
+
+ private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations,
+ @NonNull Runnable finishCallback, TransitionInfo.Change change,
+ TransitionInfo.AnimationOptions options) {
+ final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+ final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+ change.getLeash(), options.getThumbnail(), transaction);
+ final Rect bounds = change.getEndAbsBounds();
+ final int orientation = mContext.getResources().getConfiguration().orientation;
+ final Animation a = mTransitionAnimation.createThumbnailAspectScaleAnimationLocked(bounds,
+ mInsets, options.getThumbnail(), orientation, null /* startRect */,
+ options.getTransitionBounds(), options.getType() == ANIM_THUMBNAIL_SCALE_UP);
+
+ final Runnable finisher = () -> {
+ wt.destroy(transaction);
+ mTransactionPool.release(transaction);
+
+ finishCallback.run();
+ };
+ a.restrictDuration(MAX_ANIMATION_DURATION);
+ a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
+ mMainExecutor, mAnimExecutor, null /* position */);
+ }
+
+ private static int getWallpaperTransitType(TransitionInfo info) {
+ boolean hasOpenWallpaper = false;
+ boolean hasCloseWallpaper = false;
+
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0) {
+ if (Transitions.isOpeningType(change.getMode())) {
+ hasOpenWallpaper = true;
+ } else if (Transitions.isClosingType(change.getMode())) {
+ hasCloseWallpaper = true;
+ }
+ }
+ }
+
+ if (hasOpenWallpaper && hasCloseWallpaper) {
+ return Transitions.isOpeningType(info.getType())
+ ? WALLPAPER_TRANSITION_INTRA_OPEN : WALLPAPER_TRANSITION_INTRA_CLOSE;
+ } else if (hasOpenWallpaper) {
+ return WALLPAPER_TRANSITION_OPEN;
+ } else if (hasCloseWallpaper) {
+ return WALLPAPER_TRANSITION_CLOSE;
+ } else {
+ return WALLPAPER_TRANSITION_NONE;
+ }
}
private static void applyTransformation(long time, SurfaceControl.Transaction t,
- SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix) {
+ SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix,
+ Point position) {
anim.getTransformation(time, transformation);
+ if (position != null) {
+ transformation.getMatrix().postTranslate(position.x, position.y);
+ }
t.setMatrix(leash, transformation.getMatrix(), matrix);
t.setAlpha(leash, transformation.getAlpha());
t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
new file mode 100644
index 000000000000..61e11e877b90
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
@@ -0,0 +1,124 @@
+/*
+ * 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.transition;
+
+import android.annotation.NonNull;
+import android.os.RemoteException;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IWindowContainerTransactionCallback;
+
+/**
+ * Utilities and interfaces for transition-like usage on top of the legacy app-transition and
+ * synctransaction tools.
+ */
+public class LegacyTransitions {
+
+ /**
+ * Interface for a "legacy" transition. Effectively wraps a sync callback + remoteAnimation
+ * into one callback.
+ */
+ public interface ILegacyTransition {
+ /**
+ * Called when both the associated sync transaction finishes and the remote animation is
+ * ready.
+ */
+ void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+ IRemoteAnimationFinishedCallback finishedCallback, SurfaceControl.Transaction t);
+ }
+
+ /**
+ * Makes sure that a remote animation and corresponding sync callback are called together
+ * such that the sync callback is called first. This assumes that both the callback receiver
+ * and the remoteanimation are in the same process so that order is preserved on both ends.
+ */
+ public static class LegacyTransition {
+ private final ILegacyTransition mLegacyTransition;
+ private int mSyncId = -1;
+ private SurfaceControl.Transaction mTransaction;
+ private int mTransit;
+ private RemoteAnimationTarget[] mApps;
+ private RemoteAnimationTarget[] mWallpapers;
+ private RemoteAnimationTarget[] mNonApps;
+ private IRemoteAnimationFinishedCallback mFinishCallback = null;
+ private boolean mCancelled = false;
+ private final SyncCallback mSyncCallback = new SyncCallback();
+ private final RemoteAnimationAdapter mAdapter =
+ new RemoteAnimationAdapter(new RemoteAnimationWrapper(), 0, 0);
+
+ public LegacyTransition(@WindowManager.TransitionType int type,
+ @NonNull ILegacyTransition legacyTransition) {
+ mLegacyTransition = legacyTransition;
+ mTransit = type;
+ }
+
+ public @WindowManager.TransitionType int getType() {
+ return mTransit;
+ }
+
+ public IWindowContainerTransactionCallback getSyncCallback() {
+ return mSyncCallback;
+ }
+
+ public RemoteAnimationAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ private class SyncCallback extends IWindowContainerTransactionCallback.Stub {
+ @Override
+ public void onTransactionReady(int id, SurfaceControl.Transaction t)
+ throws RemoteException {
+ mSyncId = id;
+ mTransaction = t;
+ checkApply();
+ }
+ }
+
+ private class RemoteAnimationWrapper extends IRemoteAnimationRunner.Stub {
+ @Override
+ public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+ IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
+ mTransit = transit;
+ mApps = apps;
+ mWallpapers = wallpapers;
+ mNonApps = nonApps;
+ mFinishCallback = finishedCallback;
+ checkApply();
+ }
+
+ @Override
+ public void onAnimationCancelled() throws RemoteException {
+ mCancelled = true;
+ mApps = mWallpapers = mNonApps = null;
+ checkApply();
+ }
+ }
+
+
+ private void checkApply() throws RemoteException {
+ if (mSyncId < 0 || (mFinishCallback == null && !mCancelled)) return;
+ mLegacyTransition.onAnimationStart(mTransit, mApps, mWallpapers,
+ mNonApps, mFinishCallback, mTransaction);
+ }
+ }
+}
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 4da6664aa3dc..6bd805323aa3 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
@@ -57,7 +57,8 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
if (mTransition != transition) return false;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote"
@@ -70,19 +71,24 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
};
IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
@Override
- public void onTransitionFinished(WindowContainerTransaction wct) {
+ public void onTransitionFinished(WindowContainerTransaction wct,
+ SurfaceControl.Transaction sct) {
if (mRemote.asBinder() != null) {
mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */);
}
- mMainExecutor.execute(
- () -> finishCallback.onTransitionFinished(wct, null /* wctCB */));
+ mMainExecutor.execute(() -> {
+ if (sct != null) {
+ finishTransaction.merge(sct);
+ }
+ finishCallback.onTransitionFinished(wct, null /* wctCB */);
+ });
}
};
try {
if (mRemote.asBinder() != null) {
mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */);
}
- mRemote.startAnimation(transition, info, t, cb);
+ mRemote.startAnimation(transition, info, startTransaction, cb);
} catch (RemoteException e) {
Log.e(Transitions.TAG, "Error running remote transition.", e);
if (mRemote.asBinder() != null) {
@@ -102,7 +108,8 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
@Override
- public void onTransitionFinished(WindowContainerTransaction wct) {
+ public void onTransitionFinished(WindowContainerTransaction wct,
+ SurfaceControl.Transaction sct) {
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 9bfb261fcb85..bda884cd80d9 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
@@ -56,14 +56,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
private final ArrayList<Pair<TransitionFilter, IRemoteTransition>> mFilters =
new ArrayList<>();
- private final IBinder.DeathRecipient mTransitionDeathRecipient =
- new IBinder.DeathRecipient() {
- @Override
- @BinderThread
- public void binderDied() {
- mMainExecutor.execute(() -> mFilters.clear());
- }
- };
+ private final ArrayMap<IBinder, RemoteDeathHandler> mDeathHandlers = new ArrayMap<>();
RemoteTransitionHandler(@NonNull ShellExecutor mainExecutor) {
mMainExecutor = mainExecutor;
@@ -71,7 +64,9 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
void addFiltered(TransitionFilter filter, IRemoteTransition remote) {
try {
- remote.asBinder().linkToDeath(mTransitionDeathRecipient, 0 /* flags */);
+ RemoteDeathHandler handler = new RemoteDeathHandler(remote.asBinder());
+ remote.asBinder().linkToDeath(handler, 0 /* flags */);
+ mDeathHandlers.put(remote.asBinder(), handler);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to link to death");
return;
@@ -88,7 +83,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
}
}
if (removed) {
- remote.asBinder().unlinkToDeath(mTransitionDeathRecipient, 0 /* flags */);
+ RemoteDeathHandler handler = mDeathHandlers.remove(remote.asBinder());
+ remote.asBinder().unlinkToDeath(handler, 0 /* flags */);
}
}
@@ -99,7 +95,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
IRemoteTransition pendingRemote = mRequestedRemotes.get(transition);
if (pendingRemote == null) {
@@ -110,6 +107,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Checking filter %s",
mFilters.get(i));
if (mFilters.get(i).first.matches(info)) {
+ Slog.d(TAG, "Found filter" + mFilters.get(i));
pendingRemote = mFilters.get(i).second;
// Add to requested list so that it can be found for merge requests.
mRequestedRemotes.put(transition, pendingRemote);
@@ -132,11 +130,15 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
};
IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
@Override
- public void onTransitionFinished(WindowContainerTransaction wct) {
+ public void onTransitionFinished(WindowContainerTransaction wct,
+ SurfaceControl.Transaction sct) {
if (remote.asBinder() != null) {
remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */);
}
mMainExecutor.execute(() -> {
+ if (sct != null) {
+ finishTransaction.merge(sct);
+ }
mRequestedRemotes.remove(transition);
finishCallback.onTransitionFinished(wct, null /* wctCB */);
});
@@ -146,7 +148,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
if (remote.asBinder() != null) {
remote.asBinder().linkToDeath(remoteDied, 0 /* flags */);
}
- remote.startAnimation(transition, info, t, cb);
+ remote.startAnimation(transition, info, startTransaction, cb);
} catch (RemoteException e) {
Log.e(Transitions.TAG, "Error running remote transition.", e);
if (remote.asBinder() != null) {
@@ -170,7 +172,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
@Override
- public void onTransitionFinished(WindowContainerTransaction wct) {
+ public void onTransitionFinished(WindowContainerTransaction wct,
+ SurfaceControl.Transaction sct) {
mMainExecutor.execute(() -> {
if (!mRequestedRemotes.containsKey(mergeTarget)) {
Log.e(TAG, "Merged transition finished after it's mergeTarget (the "
@@ -200,4 +203,25 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
+ " for %s: %s", transition, remote);
return new WindowContainerTransaction();
}
+
+ /** NOTE: binder deaths can alter the filter order */
+ private class RemoteDeathHandler implements IBinder.DeathRecipient {
+ private final IBinder mRemote;
+
+ RemoteDeathHandler(IBinder remote) {
+ mRemote = remote;
+ }
+
+ @Override
+ @BinderThread
+ public void binderDied() {
+ mMainExecutor.execute(() -> {
+ for (int i = mFilters.size() - 1; i >= 0; --i) {
+ if (mRemote.equals(mFilters.get(i).second.asBinder())) {
+ mFilters.remove(i);
+ }
+ }
+ });
+ }
+ }
}
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
new file mode 100644
index 000000000000..ada2ed27c114
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -0,0 +1,485 @@
+/*
+ * 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.transition;
+
+import static android.hardware.HardwareBuffer.RGBA_8888;
+import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
+import static android.util.RotationUtils.deltaRotation;
+import static android.view.WindowManagerPolicyConstants.SCREEN_FREEZE_LAYER_BASE;
+
+import static com.android.wm.shell.transition.DefaultTransitionHandler.startSurfaceAnimation;
+import static com.android.wm.shell.transition.Transitions.TAG;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+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;
+import android.media.Image;
+import android.media.ImageReader;
+import android.util.Slog;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.SurfaceSession;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.window.TransitionInfo;
+
+import com.android.internal.R;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.TransactionPool;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * This class handles the rotation animation when the device is rotated.
+ *
+ * <p>
+ * The screen rotation animation is composed of 4 different part:
+ * <ul>
+ * <li> The screenshot: <p>
+ * A screenshot of the whole screen prior the change of orientation is taken to hide the
+ * element resizing below. The screenshot is then animated to rotate and cross-fade to
+ * the new orientation with the content in the new orientation.
+ *
+ * <li> The windows on the display: <p>y
+ * Once the device is rotated, the screen and its content are in the new orientation. The
+ * animation first rotate the new content into the old orientation to then be able to
+ * animate to the new orientation
+ *
+ * <li> The Background color frame: <p>
+ * To have the animation seem more seamless, we add a color transitioning background behind the
+ * exiting and entering layouts. We compute the brightness of the start and end
+ * layouts and transition from the two brightness values as grayscale underneath the animation
+ *
+ * <li> The entering Blackframe: <p>
+ * The enter Blackframe is similar to the exit Blackframe but is only used when a custom
+ * rotation animation is used and matches the new content size instead of the screenshot.
+ * </ul>
+ */
+class ScreenRotationAnimation {
+ static final int MAX_ANIMATION_DURATION = 10 * 1000;
+
+ 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. */
+ private final SurfaceControl mSurfaceControl;
+ private final Rect mStartBounds = new Rect();
+ private final Rect mEndBounds = new Rect();
+
+ private final int mStartWidth;
+ private final int mStartHeight;
+ private final int mEndWidth;
+ private final int mEndHeight;
+ private final int mStartRotation;
+ private final int mEndRotation;
+
+ /** This layer contains the actual screenshot that is to be faded out. */
+ private SurfaceControl mScreenshotLayer;
+ /**
+ * Only used for screen rotation and not custom animations. Layered behind all other layers
+ * to avoid showing any "empty" spots
+ */
+ private SurfaceControl mBackColorSurface;
+ /** The leash using to animate screenshot layer. */
+ private SurfaceControl mAnimLeash;
+ private Transaction mTransaction;
+
+ // 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
+ // rotations.
+ private Animation mRotateExitAnimation;
+ private Animation mRotateEnterAnimation;
+
+ /** Intensity of light/whiteness of the layout before rotation occurs. */
+ private float mStartLuma;
+ /** Intensity of light/whiteness of the layout after rotation occurs. */
+ private float mEndLuma;
+
+ ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool,
+ Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash) {
+ mContext = context;
+ mTransactionPool = pool;
+
+ mSurfaceControl = change.getLeash();
+ mStartWidth = change.getStartAbsBounds().width();
+ mStartHeight = change.getStartAbsBounds().height();
+ mEndWidth = change.getEndAbsBounds().width();
+ mEndHeight = change.getEndAbsBounds().height();
+ mStartRotation = change.getStartRotation();
+ mEndRotation = change.getEndRotation();
+
+ mStartBounds.set(change.getStartAbsBounds());
+ mEndBounds.set(change.getEndAbsBounds());
+
+ mAnimLeash = new SurfaceControl.Builder(session)
+ .setParent(rootLeash)
+ .setEffectLayer()
+ .setCallsite("ShellRotationAnimation")
+ .setName("Animation leash of screenshot rotation")
+ .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;
+ }
+
+ mBackColorSurface = new SurfaceControl.Builder(session)
+ .setParent(rootLeash)
+ .setColorLayer()
+ .setCallsite("ShellRotationAnimation")
+ .setName("BackColorSurface")
+ .build();
+
+ mScreenshotLayer = new SurfaceControl.Builder(session)
+ .setParent(mAnimLeash)
+ .setBLASTLayer()
+ .setSecure(screenshotBuffer.containsSecureLayers())
+ .setCallsite("ShellRotationAnimation")
+ .setName("RotationLayer")
+ .build();
+
+ HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
+ mStartLuma = getMedianBorderLuma(hardwareBuffer, screenshotBuffer.getColorSpace());
+
+ GraphicBuffer buffer = GraphicBuffer.createFromHardwareBuffer(
+ screenshotBuffer.getHardwareBuffer());
+
+ t.setLayer(mBackColorSurface, -1);
+ t.setColor(mBackColorSurface, new float[]{mStartLuma, mStartLuma, mStartLuma});
+ t.setAlpha(mBackColorSurface, 1);
+ t.show(mBackColorSurface);
+
+ t.setPosition(mAnimLeash, 0, 0);
+ t.setAlpha(mAnimLeash, 1);
+ t.show(mAnimLeash);
+
+ t.setLayer(mScreenshotLayer, SCREEN_FREEZE_LAYER_BASE);
+ t.setBuffer(mScreenshotLayer, buffer);
+ t.setColorSpace(mScreenshotLayer, screenshotBuffer.getColorSpace());
+ t.show(mScreenshotLayer);
+
+ } catch (Surface.OutOfResourcesException e) {
+ Slog.w(TAG, "Unable to allocate freeze surface", e);
+ }
+
+ setRotation(t);
+ t.apply();
+ }
+
+ 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) {
+ if (mScreenshotLayer == null) {
+ return;
+ }
+ matrix.getValues(mTmpFloats);
+ float x = mTmpFloats[Matrix.MTRANS_X];
+ float y = mTmpFloats[Matrix.MTRANS_Y];
+ t.setPosition(mScreenshotLayer, x, y);
+ 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);
+ }
+
+ /**
+ * Returns true if animating.
+ */
+ public boolean startAnimation(@NonNull ArrayList<Animator> animations,
+ @NonNull Runnable finishCallback, float animationScale,
+ @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) {
+ if (mScreenshotLayer == null) {
+ // Can't do animation.
+ return false;
+ }
+
+ // TODO : Found a way to get right end luma and re-enable color frame animation.
+ // End luma value is very not stable so it will cause more flicker is we run background
+ // color frame animation.
+ //mEndLuma = getLumaOfSurfaceControl(mEndBounds, mSurfaceControl);
+
+ // Figure out how the screen has moved from the original rotation.
+ int delta = deltaRotation(mEndRotation, mStartRotation);
+ switch (delta) { /* Counter-Clockwise Rotations */
+ case Surface.ROTATION_0:
+ mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
+ R.anim.screen_rotate_0_exit);
+ mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
+ R.anim.rotation_animation_enter);
+ break;
+ case Surface.ROTATION_90:
+ mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
+ R.anim.screen_rotate_plus_90_exit);
+ mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
+ R.anim.screen_rotate_plus_90_enter);
+ break;
+ case Surface.ROTATION_180:
+ mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
+ R.anim.screen_rotate_180_exit);
+ mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
+ R.anim.screen_rotate_180_enter);
+ break;
+ case Surface.ROTATION_270:
+ mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
+ R.anim.screen_rotate_minus_90_exit);
+ mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
+ R.anim.screen_rotate_minus_90_enter);
+ break;
+ }
+
+ mRotateExitAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight);
+ mRotateExitAnimation.restrictDuration(MAX_ANIMATION_DURATION);
+ mRotateExitAnimation.scaleCurrentDuration(animationScale);
+ mRotateEnterAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight);
+ mRotateEnterAnimation.restrictDuration(MAX_ANIMATION_DURATION);
+ mRotateEnterAnimation.scaleCurrentDuration(animationScale);
+
+ mTransaction = mTransactionPool.acquire();
+ startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor);
+ startScreenshotRotationAnimation(animations, finishCallback, mainExecutor, animExecutor);
+ //startColorAnimation(mTransaction, animationScale);
+
+ return true;
+ }
+
+ private void startDisplayRotation(@NonNull ArrayList<Animator> animations,
+ @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor,
+ @NonNull ShellExecutor animExecutor) {
+ startSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback,
+ mTransactionPool, mainExecutor, animExecutor, null /* position */);
+ }
+
+ private void startScreenshotRotationAnimation(@NonNull ArrayList<Animator> animations,
+ @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor,
+ @NonNull ShellExecutor animExecutor) {
+ startSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback,
+ mTransactionPool, mainExecutor, animExecutor, null /* position */);
+ }
+
+ private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) {
+ int colorTransitionMs = mContext.getResources().getInteger(
+ R.integer.config_screen_rotation_color_transition);
+ final float[] rgbTmpFloat = new float[3];
+ final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma);
+ final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma);
+ final long duration = colorTransitionMs * (long) animationScale;
+ final Transaction t = mTransactionPool.acquire();
+
+ final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
+ // Animation length is already expected to be scaled.
+ va.overrideDurationScale(1.0f);
+ va.setDuration(duration);
+ va.addUpdateListener(animation -> {
+ final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
+ final float fraction = currentPlayTime / va.getDuration();
+ applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t);
+ });
+ va.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface,
+ t);
+ mTransactionPool.release(t);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface,
+ t);
+ mTransactionPool.release(t);
+ }
+ });
+ animExecutor.execute(va::start);
+ }
+
+ public void kill() {
+ Transaction t = mTransaction != null ? mTransaction : mTransactionPool.acquire();
+ if (mAnimLeash.isValid()) {
+ t.remove(mAnimLeash);
+ }
+
+ if (mScreenshotLayer != null) {
+ if (mScreenshotLayer.isValid()) {
+ t.remove(mScreenshotLayer);
+ }
+ mScreenshotLayer = null;
+
+ if (mBackColorSurface != null) {
+ if (mBackColorSurface.isValid()) {
+ t.remove(mBackColorSurface);
+ }
+ mBackColorSurface = null;
+ }
+ }
+ t.apply();
+ mTransactionPool.release(t);
+ }
+
+ /**
+ * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
+ * luminance at the borders of the bitmap
+ * @return the average luminance of all the pixels at the borders of the bitmap
+ */
+ private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
+ // Cannot read content from buffer with protected usage.
+ if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
+ || hasProtectedContent(hardwareBuffer)) {
+ return 0;
+ }
+
+ ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
+ hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
+ ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
+ Image image = ir.acquireLatestImage();
+ if (image == null || image.getPlanes().length == 0) {
+ return 0;
+ }
+
+ Image.Plane plane = image.getPlanes()[0];
+ ByteBuffer buffer = plane.getBuffer();
+ int width = image.getWidth();
+ int height = image.getHeight();
+ int pixelStride = plane.getPixelStride();
+ int rowStride = plane.getRowStride();
+ float[] borderLumas = new float[2 * width + 2 * height];
+
+ // Grab the top and bottom borders
+ int l = 0;
+ for (int x = 0; x < width; x++) {
+ borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
+ borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
+ }
+
+ // Grab the left and right borders
+ for (int y = 0; y < height; y++) {
+ borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
+ borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
+ }
+
+ // Cleanup
+ ir.close();
+
+ // Oh, is this too simple and inefficient for you?
+ // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
+ Arrays.sort(borderLumas);
+ return borderLumas[borderLumas.length / 2];
+ }
+
+ /**
+ * @return whether the hardwareBuffer passed in is marked as protected.
+ */
+ private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
+ return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
+ }
+
+ private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
+ int pixelStride, int rowStride) {
+ int offset = y * rowStride + x * pixelStride;
+ int pixel = 0;
+ pixel |= (buffer.get(offset) & 0xff) << 16; // R
+ pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G
+ pixel |= (buffer.get(offset + 2) & 0xff); // B
+ pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
+ return Color.valueOf(pixel).luminance();
+ }
+
+ /**
+ * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
+ * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
+ */
+ private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) {
+ if (surfaceControl == null) {
+ return 0;
+ }
+
+ Rect crop = new Rect(0, 0, bounds.width(), bounds.height());
+ SurfaceControl.ScreenshotHardwareBuffer buffer =
+ SurfaceControl.captureLayers(surfaceControl, crop, 1);
+ if (buffer == null) {
+ return 0;
+ }
+
+ 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,
+ endColor);
+ Color middleColor = Color.valueOf(color);
+ rgbFloat[0] = middleColor.red();
+ rgbFloat[1] = middleColor.green();
+ rgbFloat[2] = middleColor.blue();
+ if (surface.isValid()) {
+ t.setColor(surface, rgbFloat);
+ }
+ t.apply();
+ }
+}
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 60707ccdca30..8d21ce25bcd0 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
@@ -19,6 +19,7 @@ package com.android.wm.shell.transition;
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_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;
@@ -54,6 +55,7 @@ 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;
@@ -77,6 +79,15 @@ public class Transitions implements RemoteCallable<Transitions> {
/** Transition type for launching 2 tasks simultaneously. */
public static final int TRANSIT_SPLIT_SCREEN_PAIR_OPEN = TRANSIT_FIRST_CUSTOM + 2;
+ /** Transition type for exiting PIP via the Shell, via pressing the expand button. */
+ public static final int TRANSIT_EXIT_PIP = TRANSIT_FIRST_CUSTOM + 3;
+
+ /** Transition type for removing PIP via the Shell, either via Dismiss bubble or Close. */
+ public static final int TRANSIT_REMOVE_PIP = TRANSIT_FIRST_CUSTOM + 4;
+
+ /** Transition type for entering split by opening an app into side-stage. */
+ public static final int TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE = TRANSIT_FIRST_CUSTOM + 5;
+
private final WindowOrganizer mOrganizer;
private final Context mContext;
private final ShellExecutor mMainExecutor;
@@ -91,27 +102,29 @@ public class Transitions implements RemoteCallable<Transitions> {
private float mTransitionAnimationScaleSetting = 1.0f;
private static final class ActiveTransition {
- IBinder mToken = null;
- TransitionHandler mHandler = null;
- boolean mMerged = false;
- TransitionInfo mInfo = null;
- SurfaceControl.Transaction mStartT = null;
- SurfaceControl.Transaction mFinishT = null;
+ IBinder mToken;
+ TransitionHandler mHandler;
+ boolean mMerged;
+ boolean mAborted;
+ TransitionInfo mInfo;
+ SurfaceControl.Transaction mStartT;
+ SurfaceControl.Transaction mFinishT;
}
/** 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 Context context, @NonNull ShellExecutor mainExecutor,
- @NonNull ShellExecutor animExecutor) {
+ @NonNull DisplayController displayController, @NonNull Context context,
+ @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) {
mOrganizer = organizer;
mContext = context;
mMainExecutor = mainExecutor;
mAnimExecutor = animExecutor;
mPlayerImpl = new TransitionPlayerImpl();
// The very last handler (0 in the list) should be the default one.
- mHandlers.add(new DefaultTransitionHandler(pool, context, mainExecutor, animExecutor));
+ mHandlers.add(new DefaultTransitionHandler(displayController, pool, context, mainExecutor,
+ animExecutor));
// Next lowest priority is remote transitions.
mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor);
mHandlers.add(mRemoteTransitionHandler);
@@ -218,7 +231,7 @@ public class Transitions implements RemoteCallable<Transitions> {
public static boolean isOpeningType(@WindowManager.TransitionType int type) {
return type == TRANSIT_OPEN
|| type == TRANSIT_TO_FRONT
- || type == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+ || type == TRANSIT_KEYGUARD_GOING_AWAY;
}
/** @return true if the transition was triggered by closing something vs opening something */
@@ -382,7 +395,7 @@ public class Transitions implements RemoteCallable<Transitions> {
}
boolean startAnimation(@NonNull ActiveTransition active, TransitionHandler handler) {
- return handler.startAnimation(active.mToken, active.mInfo, active.mStartT,
+ return handler.startAnimation(active.mToken, active.mInfo, active.mStartT, active.mFinishT,
(wct, cb) -> onFinish(active.mToken, wct, cb));
}
@@ -416,17 +429,19 @@ public class Transitions implements RemoteCallable<Transitions> {
/** Special version of finish just for dealing with no-op/invalid transitions. */
private void onAbort(IBinder transition) {
- final int activeIdx = findActiveTransition(transition);
- if (activeIdx < 0) return;
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Transition animation aborted due to no-op, notifying core %s", transition);
- mActiveTransitions.remove(activeIdx);
- mOrganizer.finishTransition(transition, null /* wct */, null /* wctCB */);
+ onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */);
}
private void onFinish(IBinder transition,
@Nullable WindowContainerTransaction wct,
@Nullable WindowContainerTransactionCallback wctCB) {
+ onFinish(transition, wct, wctCB, false /* abort */);
+ }
+
+ private void onFinish(IBinder transition,
+ @Nullable WindowContainerTransaction wct,
+ @Nullable WindowContainerTransactionCallback wctCB,
+ boolean abort) {
int activeIdx = findActiveTransition(transition);
if (activeIdx < 0) {
Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or "
@@ -434,28 +449,37 @@ public class Transitions implements RemoteCallable<Transitions> {
return;
} else if (activeIdx > 0) {
// This transition was merged.
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s",
- transition);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged (abort=%b:"
+ + " %s", abort, transition);
final ActiveTransition active = mActiveTransitions.get(activeIdx);
active.mMerged = true;
+ active.mAborted = abort;
if (active.mHandler != null) {
active.mHandler.onTransitionMerged(active.mToken);
}
return;
}
+ mActiveTransitions.get(activeIdx).mAborted = abort;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Transition animation finished, notifying core %s", transition);
+ "Transition animation finished (abort=%b), notifying core %s", abort, transition);
// Merge all relevant transactions together
SurfaceControl.Transaction fullFinish = mActiveTransitions.get(activeIdx).mFinishT;
for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) {
final ActiveTransition toMerge = mActiveTransitions.get(iA);
if (!toMerge.mMerged) break;
+ // aborted transitions have no start/finish transactions
+ if (mActiveTransitions.get(iA).mStartT == null) break;
+ if (fullFinish == null) {
+ fullFinish = new SurfaceControl.Transaction();
+ }
// Include start. It will be a no-op if it was already applied. Otherwise, we need it
// to maintain consistent state.
fullFinish.merge(mActiveTransitions.get(iA).mStartT);
fullFinish.merge(mActiveTransitions.get(iA).mFinishT);
}
- fullFinish.apply();
+ if (fullFinish != null) {
+ fullFinish.apply();
+ }
// Now perform all the finishes.
mActiveTransitions.remove(activeIdx);
mOrganizer.finishTransition(transition, wct, wctCB);
@@ -464,6 +488,12 @@ public class Transitions implements RemoteCallable<Transitions> {
ActiveTransition merged = mActiveTransitions.remove(activeIdx);
mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */);
}
+ // sift through aborted transitions
+ while (mActiveTransitions.size() > activeIdx
+ && mActiveTransitions.get(activeIdx).mAborted) {
+ ActiveTransition aborted = mActiveTransitions.remove(activeIdx);
+ mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */);
+ }
if (mActiveTransitions.size() <= activeIdx) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations "
+ "finished");
@@ -494,6 +524,12 @@ public class Transitions implements RemoteCallable<Transitions> {
int mergeIdx = activeIdx + 1;
while (mergeIdx < mActiveTransitions.size()) {
ActiveTransition mergeCandidate = mActiveTransitions.get(mergeIdx);
+ if (mergeCandidate.mAborted) {
+ // transition was aborted, so we can skip for now (still leave it in the list
+ // so that it gets cleaned-up in the right order).
+ ++mergeIdx;
+ continue;
+ }
if (mergeCandidate.mMerged) {
throw new IllegalStateException("Can't merge a transition after not-merging"
+ " a preceding one.");
@@ -566,12 +602,19 @@ public class Transitions implements RemoteCallable<Transitions> {
* Starts a transition animation. This is always called if handleRequest returned non-null
* for a particular transition. Otherwise, it is only called if no other handler before
* it handled the transition.
- *
+ * @param startTransaction the transaction given to the handler to be applied before the
+ * transition animation. Note the handler is expected to call on
+ * {@link SurfaceControl.Transaction#apply()} for startTransaction.
+ * @param finishTransaction the transaction given to the handler to be applied after the
+ * transition animation. Unlike startTransaction, the handler is NOT
+ * expected to apply this transaction. The Transition system will
+ * apply it when finishCallback is called.
* @param finishCallback Call this when finished. This MUST be called on main thread.
* @return true if transition was handled, false if not (falls-back to default).
*/
boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
@NonNull TransitionFinishCallback finishCallback);
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
new file mode 100644
index 000000000000..2c668ed3d84d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
@@ -0,0 +1,71 @@
+/*
+ * 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.transition;
+
+import android.graphics.ColorSpace;
+import android.graphics.GraphicBuffer;
+import android.graphics.PixelFormat;
+import android.hardware.HardwareBuffer;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+/**
+ * Represents a surface that is displayed over a transition surface.
+ */
+class WindowThumbnail {
+
+ private SurfaceControl mSurfaceControl;
+
+ private WindowThumbnail() {}
+
+ /** Create a thumbnail surface and attach it over a parent surface. */
+ static WindowThumbnail createAndAttach(SurfaceSession surfaceSession, SurfaceControl parent,
+ HardwareBuffer thumbnailHeader, SurfaceControl.Transaction t) {
+ WindowThumbnail windowThumbnail = new WindowThumbnail();
+ windowThumbnail.mSurfaceControl = new SurfaceControl.Builder(surfaceSession)
+ .setParent(parent)
+ .setName("WindowThumanil : " + parent.toString())
+ .setCallsite("WindowThumanil")
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .build();
+
+ GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(thumbnailHeader);
+ t.setBuffer(windowThumbnail.mSurfaceControl, graphicBuffer);
+ t.setColorSpace(windowThumbnail.mSurfaceControl, ColorSpace.get(ColorSpace.Named.SRGB));
+ t.setLayer(windowThumbnail.mSurfaceControl, Integer.MAX_VALUE);
+ t.show(windowThumbnail.mSurfaceControl);
+ t.apply();
+
+ return windowThumbnail;
+ }
+
+ SurfaceControl getSurface() {
+ return mSurfaceControl;
+ }
+
+ /** Remove the thumbnail surface and release the surface. */
+ void destroy(SurfaceControl.Transaction t) {
+ if (mSurfaceControl == null) {
+ return;
+ }
+
+ t.remove(mSurfaceControl);
+ t.apply();
+ mSurfaceControl.release();
+ mSurfaceControl = null;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index 9dd25fe0e6fe..3ca5b9c38aff 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -25,11 +25,17 @@ package {
android_test {
name: "WMShellFlickerTests",
- srcs: ["src/**/*.java", "src/**/*.kt"],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
manifest: "AndroidManifest.xml",
test_config: "AndroidTest.xml",
platform_apis: true,
certificate: "platform",
+ optimize: {
+ enabled: false,
+ },
test_suites: ["device-tests"],
libs: ["android.test.runner"],
static_libs: [
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 c5b5b91d570b..b36468b7e9a5 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
@@ -16,97 +16,100 @@
package com.android.wm.shell.flicker
+import android.content.ComponentName
import android.graphics.Region
import android.view.Surface
import com.android.server.wm.flicker.FlickerTestParameter
import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.traces.layers.getVisibleBounds
-fun FlickerTestParameter.appPairsDividerIsVisible() {
+fun FlickerTestParameter.appPairsDividerIsVisibleAtEnd() {
assertLayersEnd {
- this.isVisible(APP_PAIR_SPLIT_DIVIDER)
+ this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT)
}
}
-fun FlickerTestParameter.appPairsDividerIsInvisible() {
+fun FlickerTestParameter.appPairsDividerIsInvisibleAtEnd() {
assertLayersEnd {
- this.notContains(APP_PAIR_SPLIT_DIVIDER)
+ this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT)
}
}
fun FlickerTestParameter.appPairsDividerBecomesVisible() {
assertLayers {
- this.isInvisible(DOCKED_STACK_DIVIDER)
+ this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
.then()
- .isVisible(DOCKED_STACK_DIVIDER)
+ .isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
}
}
-fun FlickerTestParameter.dockedStackDividerIsVisible() {
+fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() {
assertLayersEnd {
- this.isVisible(DOCKED_STACK_DIVIDER)
+ this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
}
}
fun FlickerTestParameter.dockedStackDividerBecomesVisible() {
assertLayers {
- this.isInvisible(DOCKED_STACK_DIVIDER)
+ this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
.then()
- .isVisible(DOCKED_STACK_DIVIDER)
+ .isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
}
}
fun FlickerTestParameter.dockedStackDividerBecomesInvisible() {
assertLayers {
- this.isVisible(DOCKED_STACK_DIVIDER)
+ this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
.then()
- .isInvisible(DOCKED_STACK_DIVIDER)
+ .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
}
}
-fun FlickerTestParameter.dockedStackDividerIsInvisible() {
+fun FlickerTestParameter.dockedStackDividerNotExistsAtEnd() {
assertLayersEnd {
- this.notContains(DOCKED_STACK_DIVIDER)
+ this.notContains(DOCKED_STACK_DIVIDER_COMPONENT)
}
}
-fun FlickerTestParameter.appPairsPrimaryBoundsIsVisible(rotation: Int, primaryLayerName: String) {
+fun FlickerTestParameter.appPairsPrimaryBoundsIsVisibleAtEnd(
+ rotation: Int,
+ primaryComponent: ComponentName
+) {
assertLayersEnd {
- val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
- visibleRegion(primaryLayerName)
+ val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
+ visibleRegion(primaryComponent)
.coversExactly(getPrimaryRegion(dividerRegion, rotation))
}
}
-fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisible(
+fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisibleAtEnd(
rotation: Int,
- primaryLayerName: String
+ primaryComponent: ComponentName
) {
assertLayersEnd {
- val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER)
- visibleRegion(primaryLayerName)
+ val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region
+ visibleRegion(primaryComponent)
.coversExactly(getPrimaryRegion(dividerRegion, rotation))
}
}
-fun FlickerTestParameter.appPairsSecondaryBoundsIsVisible(
+fun FlickerTestParameter.appPairsSecondaryBoundsIsVisibleAtEnd(
rotation: Int,
- secondaryLayerName: String
+ secondaryComponent: ComponentName
) {
assertLayersEnd {
- val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
- visibleRegion(secondaryLayerName)
+ val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
+ visibleRegion(secondaryComponent)
.coversExactly(getSecondaryRegion(dividerRegion, rotation))
}
}
-fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisible(
+fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd(
rotation: Int,
- secondaryLayerName: String
+ secondaryComponent: ComponentName
) {
assertLayersEnd {
- val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER)
- visibleRegion(secondaryLayerName)
+ val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region
+ visibleRegion(secondaryComponent)
.coversExactly(getSecondaryRegion(dividerRegion, rotation))
}
}
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 03b93c74233c..ff1a6e6d9d90 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
@@ -14,9 +14,11 @@
* limitations under the License.
*/
+@file:JvmName("CommonConstants")
package com.android.wm.shell.flicker
-const val IME_WINDOW_NAME = "InputMethod"
+import android.content.ComponentName
+
const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
-const val APP_PAIR_SPLIT_DIVIDER = "AppPairSplitDivider"
-const val DOCKED_STACK_DIVIDER = "DockedStackDivider" \ No newline at end of file
+val APP_PAIR_SPLIT_DIVIDER_COMPONENT = ComponentName("", "AppPairSplitDivider#")
+val DOCKED_STACK_DIVIDER_COMPONENT = ComponentName("", "DockedStackDivider#") \ No newline at end of file
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
index ef9f7421fd60..19374ed04be5 100644
--- 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
@@ -16,7 +16,6 @@
package com.android.wm.shell.flicker.apppairs
-import android.os.SystemClock
import android.platform.test.annotations.Presubmit
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
@@ -25,7 +24,7 @@ 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.appPairsDividerIsInvisible
+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
@@ -61,7 +60,7 @@ class AppPairsTestCannotPairNonResizeableApps(
// TODO pair apps through normal UX flow
executeShellCommand(
composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true))
- SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+ nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) }
}
}
@@ -85,15 +84,13 @@ class AppPairsTestCannotPairNonResizeableApps(
@Test
override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
- @FlakyTest
+ @Presubmit
@Test
- override fun navBarLayerIsAlwaysVisible() {
- super.navBarLayerIsAlwaysVisible()
- }
+ override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
@Presubmit
@Test
- fun appPairsDividerIsInvisible() = testSpec.appPairsDividerIsInvisible()
+ fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd()
@Presubmit
@Test
@@ -103,8 +100,8 @@ class AppPairsTestCannotPairNonResizeableApps(
"Non resizeable app not initialized"
}
testSpec.assertWmEnd {
- isVisible(nonResizeableApp.defaultWindowName)
- isInvisible(primaryApp.defaultWindowName)
+ isVisible(nonResizeableApp.component)
+ isInvisible(primaryApp.component)
}
}
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
index db63c4c43523..46ee89295a4e 100644
--- 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
@@ -16,7 +16,6 @@
package com.android.wm.shell.flicker.apppairs
-import android.os.SystemClock
import android.platform.test.annotations.Presubmit
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
@@ -25,10 +24,10 @@ 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.traces.layers.getVisibleBounds
-import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER
-import com.android.wm.shell.flicker.appPairsDividerIsVisible
+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.Test
import org.junit.runner.RunWith
@@ -54,10 +53,14 @@ class AppPairsTestPairPrimaryAndSecondaryApps(
// TODO pair apps through normal UX flow
executeShellCommand(
composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
- SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+ waitAppsShown(primaryApp, secondaryApp)
}
}
+ @Presubmit
+ @Test
+ override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
@FlakyTest
@Test
override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
@@ -68,14 +71,14 @@ class AppPairsTestPairPrimaryAndSecondaryApps(
@Presubmit
@Test
- fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible()
+ fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
@Presubmit
@Test
fun bothAppWindowsVisible() {
testSpec.assertWmEnd {
- isVisible(primaryApp.defaultWindowName)
- isVisible(secondaryApp.defaultWindowName)
+ isVisible(primaryApp.component)
+ isVisible(secondaryApp.component)
}
}
@@ -83,10 +86,10 @@ class AppPairsTestPairPrimaryAndSecondaryApps(
@Test
fun appsEndingBounds() {
testSpec.assertLayersEnd {
- val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
- visibleRegion(primaryApp.defaultWindowName)
+ val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
+ visibleRegion(primaryApp.component)
.coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion))
- visibleRegion(secondaryApp.defaultWindowName)
+ visibleRegion(secondaryApp.component)
.coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion))
}
}
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
index c8d34237231c..f7ced71afe8a 100644
--- 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
@@ -16,7 +16,6 @@
package com.android.wm.shell.flicker.apppairs
-import android.os.SystemClock
import android.platform.test.annotations.Presubmit
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
@@ -25,7 +24,7 @@ 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.appPairsDividerIsVisible
+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
@@ -61,7 +60,7 @@ class AppPairsTestSupportPairNonResizeableApps(
// TODO pair apps through normal UX flow
executeShellCommand(
composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true))
- SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+ nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) }
}
}
@@ -77,6 +76,10 @@ class AppPairsTestSupportPairNonResizeableApps(
resetMultiWindowConfig(instrumentation)
}
+ @Presubmit
+ @Test
+ override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
@FlakyTest
@Test
override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
@@ -87,7 +90,7 @@ class AppPairsTestSupportPairNonResizeableApps(
@Presubmit
@Test
- fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible()
+ fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
@Presubmit
@Test
@@ -97,8 +100,8 @@ class AppPairsTestSupportPairNonResizeableApps(
"Non resizeable app not initialized"
}
testSpec.assertWmEnd {
- isVisible(nonResizeableApp.defaultWindowName)
- isVisible(primaryApp.defaultWindowName)
+ isVisible(nonResizeableApp.component)
+ isVisible(primaryApp.component)
}
}
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
index 83df83600d11..3debdd3276e4 100644
--- 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
@@ -25,10 +25,10 @@ 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.traces.layers.getVisibleBounds
-import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER
-import com.android.wm.shell.flicker.appPairsDividerIsInvisible
+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.Test
import org.junit.runner.RunWith
@@ -51,9 +51,11 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps(
get() = {
super.transition(this, it)
setup {
- executeShellCommand(
- composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
- SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+ eachRun {
+ executeShellCommand(
+ composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
+ waitAppsShown(primaryApp, secondaryApp)
+ }
}
transitions {
// TODO pair apps through normal UX flow
@@ -73,14 +75,14 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps(
@Presubmit
@Test
- fun appPairsDividerIsInvisible() = testSpec.appPairsDividerIsInvisible()
+ fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd()
@Presubmit
@Test
fun bothAppWindowsInvisible() {
testSpec.assertWmEnd {
- isInvisible(primaryApp.defaultWindowName)
- isInvisible(secondaryApp.defaultWindowName)
+ isInvisible(primaryApp.component)
+ isInvisible(secondaryApp.component)
}
}
@@ -88,10 +90,10 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps(
@Test
fun appsStartingBounds() {
testSpec.assertLayersStart {
- val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
- visibleRegion(primaryApp.defaultWindowName)
+ val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
+ visibleRegion(primaryApp.component)
.coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion))
- visibleRegion(secondaryApp.defaultWindowName)
+ visibleRegion(secondaryApp.component)
.coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion))
}
}
@@ -100,16 +102,14 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps(
@Test
fun appsEndingBounds() {
testSpec.assertLayersEnd {
- notContains(primaryApp.defaultWindowName)
- notContains(secondaryApp.defaultWindowName)
+ notContains(primaryApp.component)
+ notContains(secondaryApp.component)
}
}
- @FlakyTest
+ @Presubmit
@Test
- override fun navBarLayerIsAlwaysVisible() {
- super.navBarLayerIsAlwaysVisible()
- }
+ override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
companion object {
@Parameterized.Parameters(name = "{0}")
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
index 1935bb97849c..cdf89a57fde8 100644
--- 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
@@ -30,14 +30,14 @@ import com.android.server.wm.flicker.endRotation
import com.android.server.wm.flicker.helpers.isRotated
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
import com.android.server.wm.flicker.repetitions
import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
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
@@ -154,26 +154,26 @@ abstract class AppPairsTransition(protected val testSpec: FlickerTestParameter)
@FlakyTest(bugId = 186510496)
@Test
- open fun navBarLayerIsAlwaysVisible() {
- testSpec.navBarLayerIsAlwaysVisible()
+ open fun navBarLayerIsVisible() {
+ testSpec.navBarLayerIsVisible()
}
@Presubmit
@Test
- open fun statusBarLayerIsAlwaysVisible() {
- testSpec.statusBarLayerIsAlwaysVisible()
+ open fun statusBarLayerIsVisible() {
+ testSpec.statusBarLayerIsVisible()
}
@Presubmit
@Test
- open fun navBarWindowIsAlwaysVisible() {
- testSpec.navBarWindowIsAlwaysVisible()
+ open fun navBarWindowIsVisible() {
+ testSpec.navBarWindowIsVisible()
}
@Presubmit
@Test
- open fun statusBarWindowIsAlwaysVisible() {
- testSpec.statusBarWindowIsAlwaysVisible()
+ open fun statusBarWindowIsVisible() {
+ testSpec.statusBarWindowIsVisible()
}
@Presubmit
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
index c875c0006703..3e782e608c86 100644
--- 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
@@ -16,7 +16,6 @@
package com.android.wm.shell.flicker.apppairs
-import android.os.SystemClock
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.FlakyTest
@@ -28,10 +27,10 @@ import com.android.server.wm.flicker.annotation.Group1
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.endRotation
import com.android.server.wm.flicker.helpers.setRotation
-import com.android.wm.shell.flicker.appPairsDividerIsVisible
-import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible
-import com.android.wm.shell.flicker.helpers.AppPairsHelper
+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.Test
@@ -57,41 +56,43 @@ class RotateTwoLaunchedAppsInAppPairsMode(
transitions {
executeShellCommand(composePairsCommand(
primaryTaskId, secondaryTaskId, true /* pair */))
- SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+ waitAppsShown(primaryApp, secondaryApp)
setRotation(testSpec.config.endRotation)
}
}
- @FlakyTest
+ @Presubmit
@Test
- override fun statusBarLayerIsAlwaysVisible() {
- super.statusBarLayerIsAlwaysVisible()
- }
+ override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
+ @Presubmit
+ @Test
+ override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
@Presubmit
@Test
fun bothAppWindowsVisible() {
testSpec.assertWmEnd {
- isVisible(primaryApp.defaultWindowName)
- .isVisible(secondaryApp.defaultWindowName)
+ isVisible(primaryApp.component)
+ .isVisible(secondaryApp.component)
}
}
@Presubmit
@Test
- fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible()
+ fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
- @FlakyTest(bugId = 172776659)
+ @Presubmit
@Test
- fun appPairsPrimaryBoundsIsVisible() =
- testSpec.appPairsPrimaryBoundsIsVisible(testSpec.config.endRotation,
- primaryApp.defaultWindowName)
+ fun appPairsPrimaryBoundsIsVisibleAtEnd() =
+ testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation,
+ primaryApp.component)
- @FlakyTest(bugId = 172776659)
+ @FlakyTest
@Test
- fun appPairsSecondaryBoundsIsVisible() =
- testSpec.appPairsSecondaryBoundsIsVisible(testSpec.config.endRotation,
- secondaryApp.defaultWindowName)
+ fun appPairsSecondaryBoundsIsVisibleAtEnd() =
+ testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation,
+ secondaryApp.component)
companion object {
@Parameterized.Parameters(name = "{0}")
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
index c3360ca0f7d3..ee28c7aa6beb 100644
--- 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
@@ -16,7 +16,6 @@
package com.android.wm.shell.flicker.apppairs
-import android.os.SystemClock
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.FlakyTest
@@ -28,12 +27,10 @@ import com.android.server.wm.flicker.annotation.Group1
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.endRotation
import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.appPairsDividerIsVisible
-import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible
-import com.android.wm.shell.flicker.helpers.AppPairsHelper
+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.Test
@@ -60,48 +57,50 @@ class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode(
this.setRotation(testSpec.config.endRotation)
executeShellCommand(
composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
- SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+ waitAppsShown(primaryApp, secondaryApp)
}
}
@Presubmit
@Test
- fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible()
+ fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
@Presubmit
@Test
- override fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
@Presubmit
@Test
- override fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
- @FlakyTest
+ @Presubmit
@Test
- override fun statusBarLayerIsAlwaysVisible() {
- super.statusBarLayerIsAlwaysVisible()
- }
+ override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible()
+
+ @Presubmit
+ @Test
+ override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
@Presubmit
@Test
fun bothAppWindowsVisible() {
testSpec.assertWmEnd {
- isVisible(primaryApp.defaultWindowName)
- isVisible(secondaryApp.defaultWindowName)
+ isVisible(primaryApp.component)
+ isVisible(secondaryApp.component)
}
}
@FlakyTest(bugId = 172776659)
@Test
- fun appPairsPrimaryBoundsIsVisible() =
- testSpec.appPairsPrimaryBoundsIsVisible(testSpec.config.endRotation,
- primaryApp.defaultWindowName)
+ fun appPairsPrimaryBoundsIsVisibleAtEnd() =
+ testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation,
+ primaryApp.component)
@FlakyTest(bugId = 172776659)
@Test
- fun appPairsSecondaryBoundsIsVisible() =
- testSpec.appPairsSecondaryBoundsIsVisible(testSpec.config.endRotation,
- secondaryApp.defaultWindowName)
+ fun appPairsSecondaryBoundsIsVisibleAtEnd() =
+ testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation,
+ secondaryApp.component)
companion object {
@Parameterized.Parameters(name = "{0}")
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
index 512fd9a58ea8..b95193a17265 100644
--- 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
@@ -22,7 +22,10 @@ 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.Test
abstract class RotateTwoLaunchedAppsTransition(
@@ -37,8 +40,8 @@ abstract class RotateTwoLaunchedAppsTransition(
test {
device.wakeUpAndGoToHomeScreen()
this.setRotation(Surface.ROTATION_0)
- primaryApp.launchViaIntent()
- secondaryApp.launchViaIntent()
+ primaryApp.launchViaIntent(wmHelper)
+ secondaryApp.launchViaIntent(wmHelper)
updateTasksId()
}
}
@@ -52,10 +55,17 @@ abstract class RotateTwoLaunchedAppsTransition(
}
}
+ @Before
+ override fun setup() {
+ // AppPairs hasn't been updated to Shell Transition. There will be conflict on rotation.
+ assumeFalse(isShellTransitionsEnabled())
+ super.setup()
+ }
+
@FlakyTest
@Test
- override fun navBarLayerIsAlwaysVisible() {
- super.navBarLayerIsAlwaysVisible()
+ override fun navBarLayerIsVisible() {
+ super.navBarLayerIsVisible()
}
@FlakyTest
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt
index 5b8cfb81016a..5a438af0b1f1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt
@@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.helpers
import android.app.Instrumentation
import android.content.ComponentName
import android.graphics.Region
+import com.android.server.wm.flicker.Flicker
import com.android.server.wm.flicker.helpers.WindowUtils
class AppPairsHelper(
@@ -43,5 +44,17 @@ class AppPairsHelper(
companion object {
const val TEST_REPETITIONS = 1
const val TIMEOUT_MS = 3_000L
+
+ fun Flicker.waitAppsShown(app1: SplitScreenHelper?, app2: SplitScreenHelper?) {
+ wmHelper.waitFor("primaryAndSecondaryAppsVisible") { dump ->
+ val primaryAppVisible = app1?.let {
+ dump.wmState.isWindowSurfaceShown(app1.defaultWindowName)
+ } ?: false
+ val secondaryAppVisible = app2?.let {
+ dump.wmState.isWindowSurfaceShown(app2.defaultWindowName)
+ } ?: false
+ primaryAppVisible && secondaryAppVisible
+ }
+ }
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt
index 4fe69ad7fabe..f15044ef37af 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt
@@ -20,6 +20,7 @@ import android.app.Instrumentation
import android.content.ComponentName
import android.content.pm.PackageManager.FEATURE_LEANBACK
import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY
+import android.os.SystemProperties
import android.support.test.launcherhelper.LauncherStrategyFactory
import android.util.Log
import androidx.test.uiautomator.By
@@ -60,6 +61,9 @@ abstract class BaseAppHelper(
companion object {
private const val APP_CLOSE_WAIT_TIME_MS = 3_000L
+ fun isShellTransitionsEnabled() =
+ SystemProperties.getBoolean("persist.debug.shell_transit", false)
+
fun executeShellCommand(instrumentation: Instrumentation, cmd: String) {
try {
SystemUtil.runShellCommand(instrumentation, cmd)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
index cac46fe676b3..086e8b792e0e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
@@ -61,7 +61,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
if (wmHelper == null) {
device.waitForIdle()
} else {
- require(wmHelper.waitImeWindowShown()) { "IME did not appear" }
+ require(wmHelper.waitImeShown()) { "IME did not appear" }
}
}
@@ -78,7 +78,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
if (wmHelper == null) {
uiDevice.waitForIdle()
} else {
- require(wmHelper.waitImeWindowGone()) { "IME did did not close" }
+ require(wmHelper.waitImeGone()) { "IME did did not close" }
}
} else {
// While pressing the back button should close the IME on TV as well, it may also lead
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 f4dd7decb1b7..d4b4e5daf7cb 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
@@ -62,7 +62,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
stringExtras: Map<String, String>
) {
super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras)
- wmHelper.waitFor { it.wmState.hasPipWindow() }
+ wmHelper.waitFor("hasPipWindow") { it.wmState.hasPipWindow() }
}
private fun focusOnObject(selector: BySelector): Boolean {
@@ -84,7 +84,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
clickObject(ENTER_PIP_BUTTON_ID)
// Wait on WMHelper or simply wait for 3 seconds
- wmHelper?.waitFor { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000)
+ wmHelper?.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000)
}
fun clickStartMediaSessionButton() {
@@ -137,7 +137,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
}
// Wait for animation to complete.
- wmHelper.waitFor { !it.wmState.hasPipWindow() }
+ wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() }
wmHelper.waitForHomeActivityVisible()
}
@@ -167,7 +167,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
val windowRect = windowRegion.bounds
uiDevice.click(windowRect.centerX(), windowRect.centerY())
uiDevice.click(windowRect.centerX(), windowRect.centerY())
- wmHelper.waitFor { !it.wmState.hasPipWindow() }
+ wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() }
wmHelper.waitForAppTransitionIdle()
}
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 901b7a393291..2d996ca1d6f7 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
@@ -18,6 +18,7 @@ package com.android.wm.shell.flicker.helpers
import android.app.Instrumentation
import android.content.ComponentName
+import android.content.res.Resources
import com.android.wm.shell.flicker.testapp.Components
class SplitScreenHelper(
@@ -30,6 +31,11 @@ class SplitScreenHelper(
const val TEST_REPETITIONS = 1
const val TIMEOUT_MS = 3_000L
+ // 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)
+
fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper =
SplitScreenHelper(instrumentation,
Components.SplitScreenActivity.LABEL,
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
index 4f12f2bb9f5f..508e93988aa6 100644
--- 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
@@ -16,22 +16,24 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
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.HOME_WINDOW_TITLE
import com.android.server.wm.flicker.annotation.Group1
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
+import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
import com.android.wm.shell.flicker.helpers.SplitScreenHelper
import org.junit.FixMethodOrder
import org.junit.Test
@@ -60,16 +62,16 @@ class EnterSplitScreenDockActivity(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(LAUNCHER_PACKAGE_NAME, LIVE_WALLPAPER_PACKAGE_NAME,
- splitScreenApp.defaultWindowName, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, *HOME_WINDOW_TITLE)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(LAUNCHER_COMPONENT, LIVE_WALLPAPER_COMPONENT,
+ splitScreenApp.component, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT, LAUNCHER_COMPONENT)
@Presubmit
@Test
- fun dockedStackPrimaryBoundsIsVisible() =
- testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
- splitScreenApp.defaultWindowName)
+ fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+ testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+ splitScreenApp.component)
@Presubmit
@Test
@@ -77,27 +79,39 @@ class EnterSplitScreenDockActivity(
@Presubmit
@Test
- fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
@Presubmit
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
@Presubmit
@Test
fun appWindowIsVisible() {
testSpec.assertWmEnd {
- isVisible(splitScreenApp.defaultWindowName)
+ isVisible(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
+ 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
index f91f634a00e5..12f3909b6c34 100644
--- 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -25,7 +26,7 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.helpers.launchSplitScreen
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
+import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
import com.android.wm.shell.flicker.helpers.SplitScreenHelper
import org.junit.FixMethodOrder
import org.junit.Test
@@ -61,24 +62,34 @@ class EnterSplitScreenFromDetachedRecentTask(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(LAUNCHER_PACKAGE_NAME,
- WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME,
- splitScreenApp.defaultWindowName)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(LAUNCHER_COMPONENT,
+ WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT,
+ splitScreenApp.component)
@Presubmit
@Test
- fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+ fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
@Presubmit
@Test
fun appWindowIsVisible() {
testSpec.assertWmEnd {
- isVisible(splitScreenApp.defaultWindowName)
+ isVisible(splitScreenApp.component)
}
}
+ @Presubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
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
index 85ded8a45233..ac85c4857c76 100644
--- 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -23,17 +24,16 @@ 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.appWindowBecomesVisible
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.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible
+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
@@ -62,22 +62,22 @@ class EnterSplitScreenLaunchToSide(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName,
- secondaryApp.defaultWindowName, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component,
+ secondaryApp.component, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
@Presubmit
@Test
- fun dockedStackPrimaryBoundsIsVisible() =
- testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
- splitScreenApp.defaultWindowName)
+ fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+ testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+ splitScreenApp.component)
@Presubmit
@Test
- fun dockedStackSecondaryBoundsIsVisible() =
- testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation,
- secondaryApp.defaultWindowName)
+ fun dockedStackSecondaryBoundsIsVisibleAtEnd() =
+ testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+ secondaryApp.component)
@Presubmit
@Test
@@ -85,15 +85,35 @@ class EnterSplitScreenLaunchToSide(
@Presubmit
@Test
- fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName)
+ 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, ignoreActivity = true)
+ .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 navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
@Presubmit
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
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
index e958bf39930e..964af2341439 100644
--- 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -26,7 +27,7 @@ import com.android.server.wm.flicker.annotation.Group1
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.helpers.canSplitScreen
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.dockedStackDividerIsInvisible
+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
@@ -70,12 +71,12 @@ class EnterSplitScreenNotSupportNonResizable(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(LAUNCHER_PACKAGE_NAME,
- WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME,
- nonResizeableApp.defaultWindowName,
- splitScreenApp.defaultWindowName)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(LAUNCHER_COMPONENT,
+ WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT,
+ nonResizeableApp.component,
+ splitScreenApp.component)
@Before
override fun setup() {
@@ -91,7 +92,12 @@ class EnterSplitScreenNotSupportNonResizable(
@Presubmit
@Test
- fun dockedStackDividerIsInvisible() = testSpec.dockedStackDividerIsInvisible()
+ fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
+
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
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
index d3acc82121b0..1b8afa668802 100644
--- 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -26,7 +27,7 @@ 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.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
+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
@@ -67,12 +68,12 @@ class EnterSplitScreenSupportNonResizable(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(LAUNCHER_PACKAGE_NAME,
- WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME,
- nonResizeableApp.defaultWindowName,
- splitScreenApp.defaultWindowName)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(LAUNCHER_COMPONENT,
+ WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT,
+ nonResizeableApp.component,
+ splitScreenApp.component)
@Before
override fun setup() {
@@ -88,16 +89,21 @@ class EnterSplitScreenSupportNonResizable(
@Presubmit
@Test
- fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+ fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
@Presubmit
@Test
fun appWindowIsVisible() {
testSpec.assertWmEnd {
- isVisible(nonResizeableApp.defaultWindowName)
+ isVisible(nonResizeableApp.component)
}
}
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
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
index bad46836dcb7..247965f8071d 100644
--- 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
@@ -16,7 +16,8 @@
package com.android.wm.shell.flicker.legacysplitscreen
-import android.platform.test.annotations.Presubmit
+import android.content.ComponentName
+import android.platform.test.annotations.Postsubmit
import android.view.Surface
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
@@ -24,15 +25,13 @@ 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.appWindowBecomesInVisible
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.layerBecomesInvisible
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
+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
@@ -67,31 +66,52 @@ class ExitLegacySplitScreenFromBottom(
}
}
transitions {
- device.exitSplitScreenFromBottom()
+ device.exitSplitScreenFromBottom(wmHelper)
}
}
- override val ignoredWindows: List<String>
- get() = listOf(LAUNCHER_PACKAGE_NAME, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- splitScreenApp.defaultWindowName, secondaryApp.defaultWindowName,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(LAUNCHER_COMPONENT, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ splitScreenApp.component, secondaryApp.component,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
- @Presubmit
+ @Postsubmit
@Test
- fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(DOCKED_STACK_DIVIDER)
+ fun layerBecomesInvisible() {
+ testSpec.assertLayers {
+ this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
+ .then()
+ .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
+ }
+ }
@FlakyTest
@Test
- fun appWindowBecomesInVisible() =
- testSpec.appWindowBecomesInVisible(secondaryApp.defaultWindowName)
+ fun appWindowBecomesInVisible() {
+ testSpec.assertWm {
+ this.isAppWindowVisible(secondaryApp.component)
+ .then()
+ .isAppWindowInvisible(secondaryApp.component)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
- @Presubmit
+ @Postsubmit
@Test
- fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
- @Presubmit
+ @FlakyTest
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ @FlakyTest
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
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
index 76dcd8b89242..af99fc4af1a0 100644
--- 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
@@ -16,6 +16,8 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
+import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.FlakyTest
@@ -24,15 +26,13 @@ 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.appWindowBecomesInVisible
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.layerBecomesInvisible
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.dockedStackDividerIsInvisible
+import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd
import com.android.wm.shell.flicker.helpers.SplitScreenHelper
import org.junit.FixMethodOrder
import org.junit.Test
@@ -71,31 +71,52 @@ class ExitPrimarySplitScreenShowSecondaryFullscreen(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(LAUNCHER_PACKAGE_NAME, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- splitScreenApp.defaultWindowName, secondaryApp.defaultWindowName,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(LAUNCHER_COMPONENT, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ splitScreenApp.component, secondaryApp.component,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
- @FlakyTest(bugId = 175687842)
+ @Presubmit
@Test
- fun dockedStackDividerIsInvisible() = testSpec.dockedStackDividerIsInvisible()
+ fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
@FlakyTest
@Test
- fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName)
+ fun layerBecomesInvisible() {
+ testSpec.assertLayers {
+ this.isVisible(splitScreenApp.component)
+ .then()
+ .isInvisible(splitScreenApp.component)
+ }
+ }
@FlakyTest
@Test
- fun appWindowBecomesInVisible() =
- testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName)
+ fun appWindowBecomesInVisible() {
+ testSpec.assertWm {
+ this.isAppWindowVisible(splitScreenApp.component)
+ .then()
+ .isAppWindowInvisible(splitScreenApp.component)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
@Presubmit
@Test
- fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
@Presubmit
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ @Postsubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
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
index d0a64b3774c7..95e4085db4eb 100644
--- 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -23,15 +24,11 @@ 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.appWindowBecomesInVisible
-import com.android.server.wm.flicker.appWindowBecomesVisible
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.layerBecomesInvisible
-import com.android.server.wm.flicker.layerBecomesVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
-import com.android.wm.shell.flicker.dockedStackDividerIsInvisible
+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
@@ -72,11 +69,11 @@ class LegacySplitScreenFromIntentNotSupportNonResizable(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME,
- nonResizeableApp.defaultWindowName, splitScreenApp.defaultWindowName,
- WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
+ nonResizeableApp.component, splitScreenApp.component,
+ WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
@Before
override fun setup() {
@@ -92,44 +89,110 @@ class LegacySplitScreenFromIntentNotSupportNonResizable(
@Presubmit
@Test
- fun resizableAppLayerBecomesInvisible() =
- testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName)
+ 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 nonResizableAppLayerBecomesVisible() =
- testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName)
+ 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, ignoreActivity = 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
+ .isAppWindowInvisible(splitScreenApp.component, ignoreActivity = true)
+ }
+ }
+ /**
+ * 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 resizableAppWindowBecomesInvisible() =
- testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName)
+ 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,
+ ignoreActivity = true, 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, ignoreActivity = true)
+ }
+ }
+ /**
+ * Asserts that both the app window and the activity are visible at the end of the trace
+ */
@Presubmit
@Test
- fun nonResizableAppWindowBecomesVisible() =
- testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName)
+ fun nonResizableAppWindowBecomesVisibleAtEnd() {
+ testSpec.assertWmEnd {
+ this.isVisible(nonResizeableApp.component)
+ }
+ }
@Presubmit
@Test
- fun dockedStackDividerIsInvisibleAtEnd() = testSpec.dockedStackDividerIsInvisible()
+ fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
@Presubmit
@Test
fun onlyNonResizableAppWindowIsVisibleAtEnd() {
testSpec.assertWmEnd {
- isInvisible(splitScreenApp.defaultWindowName)
- isVisible(nonResizeableApp.defaultWindowName)
+ isInvisible(splitScreenApp.component)
+ isVisible(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
+ 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
index c26c05fa8db6..65346aa8ea5d 100644
--- 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -23,13 +24,11 @@ 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.appWindowBecomesVisible
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.layerBecomesVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
+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
@@ -70,11 +69,11 @@ class LegacySplitScreenFromIntentSupportNonResizable(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME,
- nonResizeableApp.defaultWindowName, splitScreenApp.defaultWindowName,
- WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
+ nonResizeableApp.component, splitScreenApp.component,
+ WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
@Before
override fun setup() {
@@ -90,27 +89,60 @@ class LegacySplitScreenFromIntentSupportNonResizable(
@Presubmit
@Test
- fun nonResizableAppLayerBecomesVisible() =
- testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName)
+ 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.appWindowBecomesVisible(nonResizeableApp.defaultWindowName)
+ 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,
+ ignoreActivity = true, 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, ignoreActivity = true)
+ }
+ }
@Presubmit
@Test
- fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisible()
+ fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
@Presubmit
@Test
fun bothAppsWindowsAreVisibleAtEnd() {
testSpec.assertWmEnd {
- isVisible(splitScreenApp.defaultWindowName)
- isVisible(nonResizeableApp.defaultWindowName)
+ isVisible(splitScreenApp.component)
+ isVisible(nonResizeableApp.component)
}
}
+ @Presubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
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
index fb1758975442..547341a14cdd 100644
--- 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
@@ -16,6 +16,8 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
+import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -23,16 +25,12 @@ 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.appWindowBecomesInVisible
-import com.android.server.wm.flicker.appWindowBecomesVisible
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.layerBecomesInvisible
-import com.android.server.wm.flicker.layerBecomesVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
-import com.android.wm.shell.flicker.dockedStackDividerIsInvisible
+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
@@ -73,11 +71,11 @@ class LegacySplitScreenFromRecentNotSupportNonResizable(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, TOAST_NAME,
- splitScreenApp.defaultWindowName, nonResizeableApp.defaultWindowName,
- WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
+ TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component,
+ WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
@Before
override fun setup() {
@@ -93,37 +91,73 @@ class LegacySplitScreenFromRecentNotSupportNonResizable(
@Presubmit
@Test
- fun resizableAppLayerBecomesInvisible() =
- testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName)
+ fun resizableAppLayerBecomesInvisible() {
+ testSpec.assertLayers {
+ this.isVisible(splitScreenApp.component)
+ .then()
+ .isInvisible(splitScreenApp.component)
+ }
+ }
@Presubmit
@Test
- fun nonResizableAppLayerBecomesVisible() =
- testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName)
+ fun nonResizableAppLayerBecomesVisible() {
+ testSpec.assertLayers {
+ this.isInvisible(nonResizeableApp.component)
+ .then()
+ .isVisible(nonResizeableApp.component)
+ }
+ }
@Presubmit
@Test
- fun resizableAppWindowBecomesInvisible() =
- testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName)
+ 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, ignoreActivity = 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
+ .isAppWindowInvisible(splitScreenApp.component, ignoreActivity = true)
+ }
+ }
- @Presubmit
+ @Postsubmit
@Test
- fun nonResizableAppWindowBecomesVisible() =
- testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName)
+ fun nonResizableAppWindowBecomesVisible() {
+ testSpec.assertWm {
+ this.isAppWindowInvisible(nonResizeableApp.component)
+ .then()
+ .isAppWindowVisible(nonResizeableApp.component)
+ }
+ }
@Presubmit
@Test
- fun dockedStackDividerIsInvisibleAtEnd() = testSpec.dockedStackDividerIsInvisible()
+ fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
@Presubmit
@Test
fun onlyNonResizableAppWindowIsVisibleAtEnd() {
testSpec.assertWmEnd {
- isInvisible(splitScreenApp.defaultWindowName)
- isVisible(nonResizeableApp.defaultWindowName)
+ isInvisible(splitScreenApp.component)
+ isVisible(nonResizeableApp.component)
}
}
+ @Presubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
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
index a9c28efcdf44..3f86658297fe 100644
--- 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -23,14 +24,12 @@ 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.appWindowBecomesVisible
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.layerBecomesVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
+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
@@ -71,11 +70,11 @@ class LegacySplitScreenFromRecentSupportNonResizable(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, TOAST_NAME,
- splitScreenApp.defaultWindowName, nonResizeableApp.defaultWindowName,
- WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
+ TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component,
+ WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
@Before
override fun setup() {
@@ -91,27 +90,60 @@ class LegacySplitScreenFromRecentSupportNonResizable(
@Presubmit
@Test
- fun nonResizableAppLayerBecomesVisible() =
- testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName)
+ fun nonResizableAppLayerBecomesVisible() {
+ testSpec.assertLayers {
+ this.isInvisible(nonResizeableApp.component)
+ .then()
+ .isVisible(nonResizeableApp.component)
+ }
+ }
@Presubmit
@Test
- fun nonResizableAppWindowBecomesVisible() =
- testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName)
+ 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, ignoreActivity = true)
+ .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.dockedStackDividerIsVisible()
+ fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
@Presubmit
@Test
fun bothAppsWindowsAreVisibleAtEnd() {
testSpec.assertWmEnd {
- isVisible(splitScreenApp.defaultWindowName)
- isVisible(nonResizeableApp.defaultWindowName)
+ isVisible(splitScreenApp.component)
+ isVisible(nonResizeableApp.component)
}
}
+ @Presubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
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
index a4d2ab51e358..7b4b71b41967 100644
--- 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
@@ -16,10 +16,10 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
+import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
-import android.support.test.launcherhelper.LauncherStrategyFactory
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
@@ -27,20 +27,18 @@ 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.endRotation
-import com.android.server.wm.flicker.focusDoesNotChange
+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.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.layerBecomesInvisible
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+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.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import com.android.wm.shell.flicker.dockedStackDividerBecomesInvisible
import com.android.wm.shell.flicker.helpers.SimpleAppHelper
@@ -62,8 +60,6 @@ import org.junit.runners.Parameterized
class LegacySplitScreenToLauncher(
testSpec: FlickerTestParameter
) : LegacySplitScreenTransition(testSpec) {
- private val launcherPackageName = LauncherStrategyFactory.getInstance(instrumentation)
- .launcherStrategy.supportedLauncherPackage
private val testApp = SimpleAppHelper(instrumentation)
override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
@@ -90,25 +86,25 @@ class LegacySplitScreenToLauncher(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(launcherPackageName, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(LAUNCHER_COMPONENT, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
@Presubmit
@Test
- fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
@Presubmit
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
@Presubmit
@Test
- fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+ fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
@Presubmit
@Test
- fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.endRotation)
+ fun entireScreenCovered() = testSpec.entireScreenCovered(testSpec.config.endRotation)
@Presubmit
@Test
@@ -122,19 +118,39 @@ class LegacySplitScreenToLauncher(
@Presubmit
@Test
- fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+ fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
- @Presubmit
+ @Postsubmit
@Test
fun dockedStackDividerBecomesInvisible() = testSpec.dockedStackDividerBecomesInvisible()
+ @Postsubmit
+ @Test
+ fun layerBecomesInvisible() {
+ testSpec.assertLayers {
+ this.isVisible(testApp.component)
+ .then()
+ .isInvisible(testApp.component)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun focusDoesNotChange() {
+ testSpec.assertEventLog {
+ this.focusDoesNotChange()
+ }
+ }
+
@Presubmit
@Test
- fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(testApp.getPackage())
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
- @FlakyTest(bugId = 151179149)
+ @Presubmit
@Test
- fun focusDoesNotChange() = testSpec.focusDoesNotChange()
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{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
index e8d4d1e9ada2..311769313a7a 100644
--- 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
@@ -17,6 +17,7 @@
package com.android.wm.shell.flicker.legacysplitscreen
import android.app.Instrumentation
+import android.content.ComponentName
import android.content.Context
import android.support.test.launcherhelper.LauncherStrategyFactory
import android.view.Surface
@@ -32,10 +33,13 @@ import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
import com.android.server.wm.flicker.repetitions
import com.android.server.wm.flicker.startRotation
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+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
@@ -46,12 +50,17 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa
protected val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation)
protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation)
protected val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation)
- protected val LAUNCHER_PACKAGE_NAME = LauncherStrategyFactory.getInstance(instrumentation)
- .launcherStrategy.supportedLauncherPackage
+ protected val LAUNCHER_COMPONENT = ComponentName("",
+ 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
@@ -70,8 +79,9 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa
*
* b/182720234
*/
- open val ignoredWindows: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ open val ignoredWindows: List<ComponentName> = listOf(
+ WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
protected open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
get() = { configuration ->
@@ -138,9 +148,9 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa
}
companion object {
- internal const val LIVE_WALLPAPER_PACKAGE_NAME =
- "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2"
- internal const val LETTERBOX_NAME = "Letterbox"
- internal const val TOAST_NAME = "Toast"
+ internal val LIVE_WALLPAPER_COMPONENT = ComponentName("",
+ "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2")
+ internal val LETTERBOX_COMPONENT = ComponentName("", "Letterbox")
+ internal val TOAST_COMPONENT = ComponentName("", "Toast")
}
}
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
index 05eb5f49a641..ec0c73a58846 100644
--- 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.flicker.legacysplitscreen
+import android.content.ComponentName
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.FlakyTest
@@ -24,14 +25,11 @@ 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.appWindowBecomesVisible
import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.focusChanges
+import com.android.server.wm.flicker.entireScreenCovered
import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.layerBecomesVisible
-import com.android.server.wm.flicker.noUncoveredRegions
import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import com.android.wm.shell.flicker.appPairsDividerBecomesVisible
import com.android.wm.shell.flicker.helpers.SplitScreenHelper
@@ -62,22 +60,28 @@ class OpenAppToLegacySplitScreen(
}
}
- override val ignoredWindows: List<String>
- get() = listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName,
- WindowManagerStateHelper.SPLASH_SCREEN_NAME,
- WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+ override val ignoredWindows: List<ComponentName>
+ get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component,
+ WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT,
+ WindowManagerStateHelper.SNAPSHOT_COMPONENT)
@FlakyTest
@Test
- fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(splitScreenApp.getPackage())
+ fun appWindowBecomesVisible() {
+ testSpec.assertWm {
+ this.isAppWindowInvisible(splitScreenApp.component)
+ .then()
+ .isAppWindowVisible(splitScreenApp.component)
+ }
+ }
- @FlakyTest
+ @Presubmit
@Test
- fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation)
+ fun entireScreenCovered() = testSpec.entireScreenCovered(testSpec.config.startRotation)
@Presubmit
@Test
- fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+ fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
@Presubmit
@Test
@@ -85,12 +89,27 @@ class OpenAppToLegacySplitScreen(
@FlakyTest
@Test
- fun layerBecomesVisible() = testSpec.layerBecomesVisible(splitScreenApp.getPackage())
+ 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")
+ }
+ }
- @FlakyTest(bugId = 151179149)
+ @Presubmit
@Test
- fun focusChanges() = testSpec.focusChanges(splitScreenApp.`package`,
- "recents_animation_input_consumer", "NexusLauncherActivity")
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
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
index 3e83b6382939..d7f71a83ba7e 100644
--- 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
@@ -28,23 +28,23 @@ 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.endRotation
+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.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.navBarWindowIsVisible
import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.traces.layers.getVisibleBounds
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+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
@@ -101,16 +101,16 @@ class ResizeLegacySplitScreen(
}
@Test
- fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
@FlakyTest(bugId = 156223549)
@Test
fun topAppWindowIsAlwaysVisible() {
testSpec.assertWm {
- this.showsAppWindow(sSimpleActivity)
+ this.isAppWindowVisible(Components.SimpleActivity.COMPONENT)
}
}
@@ -118,18 +118,18 @@ class ResizeLegacySplitScreen(
@Test
fun bottomAppWindowIsAlwaysVisible() {
testSpec.assertWm {
- this.showsAppWindow(sImeActivity)
+ this.isAppWindowVisible(Components.ImeActivity.COMPONENT)
}
}
@Test
- fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+ fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
@Test
- fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+ fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
@Test
- fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.endRotation)
+ fun entireScreenCovered() = testSpec.entireScreenCovered(testSpec.config.endRotation)
@Test
fun navBarLayerRotatesAndScales() =
@@ -142,21 +142,21 @@ class ResizeLegacySplitScreen(
@Test
fun topAppLayerIsAlwaysVisible() {
testSpec.assertLayers {
- this.isVisible(sSimpleActivity)
+ this.isVisible(Components.SimpleActivity.COMPONENT)
}
}
@Test
fun bottomAppLayerIsAlwaysVisible() {
testSpec.assertLayers {
- this.isVisible(sImeActivity)
+ this.isVisible(Components.ImeActivity.COMPONENT)
}
}
@Test
fun dividerLayerIsAlwaysVisible() {
testSpec.assertLayers {
- this.isVisible(DOCKED_STACK_DIVIDER)
+ this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
}
}
@@ -166,7 +166,7 @@ class ResizeLegacySplitScreen(
testSpec.assertLayersStart {
val displayBounds = WindowUtils.displayBounds
val dividerBounds =
- entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds
+ layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds
val topAppBounds = Region(0, 0, dividerBounds.right,
dividerBounds.top + WindowUtils.dockedStackDividerInset)
@@ -174,8 +174,8 @@ class ResizeLegacySplitScreen(
dividerBounds.bottom - WindowUtils.dockedStackDividerInset,
displayBounds.right,
displayBounds.bottom - WindowUtils.navigationBarHeight)
- visibleRegion("SimpleActivity").coversExactly(topAppBounds)
- visibleRegion("ImeActivity").coversExactly(bottomAppBounds)
+ visibleRegion(Components.SimpleActivity.COMPONENT).coversExactly(topAppBounds)
+ visibleRegion(Components.ImeActivity.COMPONENT).coversExactly(bottomAppBounds)
}
}
@@ -185,7 +185,7 @@ class ResizeLegacySplitScreen(
testSpec.assertLayersStart {
val displayBounds = WindowUtils.displayBounds
val dividerBounds =
- entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds
+ layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds
val topAppBounds = Region(0, 0, dividerBounds.right,
dividerBounds.top + WindowUtils.dockedStackDividerInset)
@@ -194,8 +194,8 @@ class ResizeLegacySplitScreen(
displayBounds.right,
displayBounds.bottom - WindowUtils.navigationBarHeight)
- visibleRegion(sSimpleActivity).coversExactly(topAppBounds)
- visibleRegion(sImeActivity).coversExactly(bottomAppBounds)
+ visibleRegion(Components.SimpleActivity.COMPONENT).coversExactly(topAppBounds)
+ visibleRegion(Components.ImeActivity.COMPONENT).coversExactly(bottomAppBounds)
}
}
@@ -207,8 +207,6 @@ class ResizeLegacySplitScreen(
}
companion object {
- private const val sSimpleActivity = "SimpleActivity"
- private const val sImeActivity = "ImeActivity"
private val startRatio = Rational(1, 3)
private val stopRatio = Rational(2, 3)
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
index 58482eaae3f5..8a2b55b6fce0 100644
--- 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
@@ -24,18 +24,17 @@ 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.appWindowBecomesVisible
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.endRotation
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.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
import com.android.server.wm.flicker.startRotation
import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
+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
@@ -66,21 +65,21 @@ class RotateOneLaunchedAppAndEnterSplitScreen(
@Presubmit
@Test
- fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+ fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
@Presubmit
@Test
- fun dockedStackPrimaryBoundsIsVisible() =
- testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
- splitScreenApp.defaultWindowName)
+ fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+ testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+ splitScreenApp.component)
- @FlakyTest(bugId = 169271943)
+ @Presubmit
@Test
fun navBarLayerRotatesAndScales() =
testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation,
testSpec.config.endRotation)
- @FlakyTest(bugId = 169271943)
+ @Presubmit
@Test
fun statusBarLayerRotatesScales() =
testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation,
@@ -88,16 +87,26 @@ class RotateOneLaunchedAppAndEnterSplitScreen(
@Presubmit
@Test
- fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
@Presubmit
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
@FlakyTest
@Test
- fun appWindowBecomesVisible() =
- testSpec.appWindowBecomesVisible(splitScreenApp.defaultWindowName)
+ 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}")
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
index 06828d6adb26..b3251573c942 100644
--- 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
@@ -24,18 +24,17 @@ 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.appWindowBecomesVisible
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.endRotation
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.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
import com.android.server.wm.flicker.startRotation
import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
+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
@@ -66,35 +65,45 @@ class RotateOneLaunchedAppInSplitScreenMode(
@Presubmit
@Test
- fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+ fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
@Presubmit
@Test
- fun dockedStackPrimaryBoundsIsVisible() = testSpec.dockedStackPrimaryBoundsIsVisible(
- testSpec.config.startRotation, splitScreenApp.defaultWindowName)
+ fun dockedStackPrimaryBoundsIsVisibleAtEnd() = testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(
+ testSpec.config.startRotation, splitScreenApp.component)
- @FlakyTest(bugId = 169271943)
+ @Presubmit
@Test
fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales(
testSpec.config.startRotation, testSpec.config.endRotation)
- @FlakyTest(bugId = 169271943)
+ @Presubmit
@Test
fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales(
testSpec.config.startRotation, testSpec.config.endRotation)
@Presubmit
@Test
- fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
@Presubmit
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
@FlakyTest
@Test
- fun appWindowBecomesVisible() =
- testSpec.appWindowBecomesVisible(splitScreenApp.defaultWindowName)
+ 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}")
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
index f8e32bf171d8..2be693631b26 100644
--- 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
@@ -18,26 +18,24 @@ 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.appWindowBecomesVisible
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.endRotation
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.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
import com.android.server.wm.flicker.startRotation
import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible
+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
@@ -69,42 +67,66 @@ class RotateTwoLaunchedAppAndEnterSplitScreen(
@Presubmit
@Test
- fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+ fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
@Presubmit
@Test
- fun dockedStackPrimaryBoundsIsVisible() =
- testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
- splitScreenApp.defaultWindowName)
+ fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+ testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+ splitScreenApp.component)
@Presubmit
@Test
- fun dockedStackSecondaryBoundsIsVisible() =
- testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation,
- secondaryApp.defaultWindowName)
+ fun dockedStackSecondaryBoundsIsVisibleAtEnd() =
+ testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+ secondaryApp.component)
- @FlakyTest(bugId = 169271943)
+ @Presubmit
@Test
fun navBarLayerRotatesAndScales() =
testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation,
testSpec.config.endRotation)
- @FlakyTest(bugId = 169271943)
+ @Presubmit
@Test
fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales(
testSpec.config.startRotation, testSpec.config.endRotation)
@Presubmit
@Test
- fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName)
+ 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, ignoreActivity = true)
+ .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 navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
@Presubmit
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
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
index cb246ca0b694..5782f145c00f 100644
--- 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
@@ -24,20 +24,19 @@ 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.appWindowBecomesVisible
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.endRotation
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.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
import com.android.server.wm.flicker.startRotation
import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible
+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
@@ -74,27 +73,27 @@ class RotateTwoLaunchedAppInSplitScreenMode(
@Presubmit
@Test
- fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+ fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
@Presubmit
@Test
- fun dockedStackPrimaryBoundsIsVisible() =
- testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
- splitScreenApp.defaultWindowName)
+ fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+ testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+ splitScreenApp.component)
@Presubmit
@Test
- fun dockedStackSecondaryBoundsIsVisible() =
- testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation,
- secondaryApp.defaultWindowName)
+ fun dockedStackSecondaryBoundsIsVisibleAtEnd() =
+ testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+ secondaryApp.component)
- @FlakyTest(bugId = 169271943)
+ @Presubmit
@Test
fun navBarLayerRotatesAndScales() =
testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation,
testSpec.config.endRotation)
- @FlakyTest(bugId = 169271943)
+ @Presubmit
@Test
fun statusBarLayerRotatesScales() =
testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation,
@@ -102,16 +101,31 @@ class RotateTwoLaunchedAppInSplitScreenMode(
@FlakyTest
@Test
- fun appWindowBecomesVisible() =
- testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName)
+ 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
- fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
@Presubmit
@Test
- fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt
index 2a660747bc1d..443204c245db 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -16,4 +16,4 @@
package com.android.wm.shell.flicker.pip
-internal const val PIP_WINDOW_TITLE = "PipMenuActivity"
+internal const val PIP_WINDOW_COMPONENT = "PipMenuActivity"
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt
index 00e50e7fe3b5..39e89fbd9b71 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.flicker.pip
+import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -62,18 +63,21 @@ class EnterExitPipTest(
@Test
fun pipAppRemainInsideVisibleBounds() {
testSpec.assertWm {
- coversAtMost(displayBounds, pipApp.defaultWindowName)
+ coversAtMost(displayBounds, pipApp.component)
}
}
- @Presubmit
+ @Postsubmit
@Test
fun showBothAppWindowsThenHidePip() {
testSpec.assertWm {
- showsAppWindow(testApp.defaultWindowName)
- .showsAppWindowOnTop(pipApp.defaultWindowName)
+ // when the activity is STOPPING, sometimes it becomes invisible in an entry before
+ // the window, sometimes in the same entry. This occurs because we log 1x per frame
+ // thus we ignore activity here
+ isAppWindowVisible(testApp.component, ignoreActivity = true)
+ .isAppWindowOnTop(pipApp.component)
.then()
- .hidesAppWindow(testApp.defaultWindowName)
+ .isAppWindowInvisible(testApp.component)
}
}
@@ -81,10 +85,10 @@ class EnterExitPipTest(
@Test
fun showBothAppLayersThenHidePip() {
testSpec.assertLayers {
- isVisible(testApp.defaultWindowName)
- .isVisible(pipApp.defaultWindowName)
+ isVisible(testApp.component)
+ .isVisible(pipApp.component)
.then()
- .isInvisible(testApp.defaultWindowName)
+ .isInvisible(testApp.component)
}
}
@@ -92,8 +96,8 @@ class EnterExitPipTest(
@Test
fun testAppCoversFullScreenWithPipOnDisplay() {
testSpec.assertLayersStart {
- visibleRegion(testApp.defaultWindowName).coversExactly(displayBounds)
- visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds)
+ visibleRegion(testApp.component).coversExactly(displayBounds)
+ visibleRegion(pipApp.component).coversAtMost(displayBounds)
}
}
@@ -101,7 +105,7 @@ class EnterExitPipTest(
@Test
fun pipAppCoversFullScreen() {
testSpec.assertLayersEnd {
- visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds)
+ visibleRegion(pipApp.component).coversExactly(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 b6af26060050..0f0a4abe30ef 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
@@ -44,30 +44,24 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
get() = buildTransition(eachRun = true, stringExtras = emptyMap()) {
transitions {
- pipApp.clickEnterPipButton()
+ pipApp.clickEnterPipButton(wmHelper)
pipApp.expandPipWindow(wmHelper)
}
}
- @FlakyTest
- @Test
- override fun noUncoveredRegions() {
- super.noUncoveredRegions()
- }
-
@Presubmit
@Test
fun pipAppWindowAlwaysVisible() {
testSpec.assertWm {
- this.showsAppWindow(pipApp.defaultWindowName)
+ this.isAppWindowVisible(pipApp.component)
}
}
- @FlakyTest
+ @Presubmit
@Test
- fun pipLayerBecomesVisible() {
+ fun pipAppLayerAlwaysVisible() {
testSpec.assertLayers {
- this.isVisible(pipApp.windowName)
+ this.isVisible(pipApp.component)
}
}
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 3a1456e53f87..67ad322f19f6 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
@@ -92,15 +92,13 @@ class EnterPipToOtherOrientationTest(
@FlakyTest
@Test
- override fun noUncoveredRegions() {
- super.noUncoveredRegions()
- }
+ override fun entireScreenCovered() = super.entireScreenCovered()
@Presubmit
@Test
fun pipAppWindowIsAlwaysOnTop() {
testSpec.assertWm {
- showsAppWindowOnTop(pipApp.defaultWindowName)
+ isAppWindowOnTop(pipApp.component)
}
}
@@ -108,7 +106,7 @@ class EnterPipToOtherOrientationTest(
@Test
fun pipAppHidesTestApp() {
testSpec.assertWmStart {
- isInvisible(testApp.defaultWindowName)
+ isInvisible(testApp.component)
}
}
@@ -116,7 +114,7 @@ class EnterPipToOtherOrientationTest(
@Test
fun testAppWindowIsVisible() {
testSpec.assertWmEnd {
- isVisible(testApp.defaultWindowName)
+ isVisible(testApp.component)
}
}
@@ -124,8 +122,8 @@ class EnterPipToOtherOrientationTest(
@Test
fun pipAppLayerHidesTestApp() {
testSpec.assertLayersStart {
- visibleRegion(pipApp.defaultWindowName).coversExactly(startingBounds)
- isInvisible(testApp.defaultWindowName)
+ visibleRegion(pipApp.component).coversExactly(startingBounds)
+ isInvisible(testApp.component)
}
}
@@ -133,7 +131,7 @@ class EnterPipToOtherOrientationTest(
@Test
fun testAppLayerCoversFullScreen() {
testSpec.assertLayersEnd {
- visibleRegion(testApp.defaultWindowName).coversExactly(endingBounds)
+ visibleRegion(testApp.component).coversExactly(endingBounds)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt
deleted file mode 100644
index 0037059e2c51..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt
+++ /dev/null
@@ -1,29 +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.pip
-
-import android.content.ComponentName
-import com.android.server.wm.traces.common.windowmanager.WindowManagerState
-import com.android.server.wm.traces.parser.toWindowName
-
-/**
- * Checks that an activity [activity] is in PIP mode
- */
-fun WindowManagerState.isInPipMode(activity: ComponentName): Boolean {
- val windowName = activity.toWindowName()
- return isInPipMode(windowName)
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt
index eae7e973711c..28b1028d41ca 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt
@@ -21,8 +21,8 @@ import android.view.Surface
import androidx.test.filters.FlakyTest
import com.android.server.wm.flicker.FlickerTestParameter
import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.focusChanges
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.startRotation
import org.junit.Test
@@ -47,9 +47,9 @@ abstract class PipCloseTransition(testSpec: FlickerTestParameter) : PipTransitio
@Test
open fun pipWindowBecomesInvisible() {
testSpec.assertWm {
- this.showsAppWindow(PIP_WINDOW_TITLE)
+ this.invoke("hasPipWindow") { it.isPinned(pipApp.component) }
.then()
- .hidesAppWindow(PIP_WINDOW_TITLE)
+ .isAppWindowInvisible(pipApp.component)
}
}
@@ -57,15 +57,21 @@ abstract class PipCloseTransition(testSpec: FlickerTestParameter) : PipTransitio
@Test
open fun pipLayerBecomesInvisible() {
testSpec.assertLayers {
- this.isVisible(PIP_WINDOW_TITLE)
+ this.isVisible(pipApp.component)
+ .isVisible(LAUNCHER_COMPONENT)
.then()
- .isInvisible(PIP_WINDOW_TITLE)
+ .isInvisible(pipApp.component)
+ .isVisible(LAUNCHER_COMPONENT)
}
}
@FlakyTest(bugId = 151179149)
@Test
- open fun focusChanges() = testSpec.focusChanges(pipApp.launcherName, "NexusLauncherActivity")
+ open fun focusChanges() {
+ testSpec.assertEventLog {
+ this.focusChanges(pipApp.launcherName, "NexusLauncherActivity")
+ }
+ }
companion object {
@Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt
index cf84a2c696d0..1c5d77f68f43 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt
@@ -48,13 +48,9 @@ class PipCloseWithDismissButtonTest(testSpec: FlickerTestParameter) : PipCloseTr
@FlakyTest
@Test
- override fun pipLayerBecomesInvisible() {
- super.pipLayerBecomesInvisible()
- }
+ override fun pipLayerBecomesInvisible() = super.pipLayerBecomesInvisible()
@FlakyTest
@Test
- override fun pipWindowBecomesInvisible() {
- super.pipWindowBecomesInvisible()
- }
+ override fun pipWindowBecomesInvisible() = super.pipWindowBecomesInvisible()
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt
index 524a1b404591..356ec94b97e3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt
@@ -56,19 +56,19 @@ class PipCloseWithSwipeTest(testSpec: FlickerTestParameter) : PipCloseTransition
@Presubmit
@Test
- override fun navBarLayerIsAlwaysVisible() = super.navBarLayerIsAlwaysVisible()
+ override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
@Presubmit
@Test
- override fun statusBarLayerIsAlwaysVisible() = super.statusBarLayerIsAlwaysVisible()
+ override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
@Presubmit
@Test
- override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+ override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
@Presubmit
@Test
- override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+ override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible()
@FlakyTest
@Test
@@ -85,7 +85,7 @@ class PipCloseWithSwipeTest(testSpec: FlickerTestParameter) : PipCloseTransition
@Presubmit
@Test
- override fun noUncoveredRegions() = super.noUncoveredRegions()
+ override fun entireScreenCovered() = super.entireScreenCovered()
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
index d88f94d5954a..5719413aff25 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
@@ -27,7 +27,7 @@ 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.startRotation
-import com.android.wm.shell.flicker.IME_WINDOW_NAME
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import com.android.wm.shell.flicker.helpers.ImeAppHelper
import org.junit.FixMethodOrder
import org.junit.Test
@@ -79,7 +79,7 @@ class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec)
fun pipInVisibleBounds() {
testSpec.assertWm {
val displayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
- coversAtMost(displayBounds, pipApp.defaultWindowName)
+ coversAtMost(displayBounds, pipApp.component)
}
}
@@ -90,7 +90,7 @@ class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec)
@Test
fun pipIsAboveAppWindow() {
testSpec.assertWmTag(TAG_IME_VISIBLE) {
- isAboveWindow(IME_WINDOW_NAME, pipApp.defaultWindowName)
+ isAboveWindow(WindowManagerStateHelper.IME_COMPONENT, pipApp.component)
}
}
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
index 6833b96a802b..086165289d2d 100644
--- 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
@@ -27,11 +27,16 @@ import com.android.server.wm.flicker.annotation.Group3
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.wm.shell.flicker.helpers.ImeAppHelper
-import com.android.wm.shell.flicker.helpers.FixedAppHelper
import com.android.server.wm.flicker.repetitions
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
@@ -46,12 +51,19 @@ import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@FlakyTest(bugId = 161435597)
@Group3
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.(Map<String, Any?>) -> Unit
get() = {
withTestName { testSpec.name }
@@ -80,11 +92,11 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t
}
}
- @Presubmit
+ @FlakyTest(bugId = 161435597)
@Test
fun pipWindowInsideDisplayBounds() {
testSpec.assertWm {
- coversAtMost(displayBounds, pipApp.defaultWindowName)
+ coversAtMost(displayBounds, pipApp.component)
}
}
@@ -92,25 +104,17 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t
@Test
fun bothAppWindowsVisible() {
testSpec.assertWmEnd {
- isVisible(testApp.defaultWindowName)
- isVisible(imeApp.defaultWindowName)
- noWindowsOverlap(testApp.defaultWindowName, imeApp.defaultWindowName)
+ isVisible(testApp.component)
+ isVisible(imeApp.component)
+ noWindowsOverlap(testApp.component, imeApp.component)
}
}
- @Presubmit
- @Test
- override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
- @Presubmit
- @Test
- override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
- @Presubmit
+ @FlakyTest(bugId = 161435597)
@Test
fun pipLayerInsideDisplayBounds() {
testSpec.assertLayers {
- coversAtMost(displayBounds, pipApp.defaultWindowName)
+ coversAtMost(displayBounds, pipApp.component)
}
}
@@ -118,18 +122,14 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t
@Test
fun bothAppLayersVisible() {
testSpec.assertLayersEnd {
- visibleRegion(testApp.defaultWindowName).coversAtMost(displayBounds)
- visibleRegion(imeApp.defaultWindowName).coversAtMost(displayBounds)
+ visibleRegion(testApp.component).coversAtMost(displayBounds)
+ visibleRegion(imeApp.component).coversAtMost(displayBounds)
}
}
- @Presubmit
- @Test
- override fun navBarLayerIsAlwaysVisible() = super.navBarLayerIsAlwaysVisible()
-
- @Presubmit
+ @FlakyTest(bugId = 161435597)
@Test
- override fun statusBarLayerIsAlwaysVisible() = super.statusBarLayerIsAlwaysVisible()
+ override fun entireScreenCovered() = super.entireScreenCovered()
companion object {
const val TEST_REPETITIONS = 2
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 d531af28e2ad..45cb152e5b0d 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
@@ -26,12 +26,14 @@ 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.endRotation
+import com.android.server.wm.flicker.entireScreenCovered
import com.android.server.wm.flicker.helpers.WindowUtils
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.noUncoveredRegions
import com.android.server.wm.flicker.startRotation
import com.android.server.wm.flicker.statusBarLayerRotatesScales
+import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.parser.minus
import com.android.wm.shell.flicker.helpers.FixedAppHelper
import org.junit.FixMethodOrder
import org.junit.Test
@@ -73,9 +75,9 @@ class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec)
}
}
- @FlakyTest(bugId = 185400889)
+ @Presubmit
@Test
- override fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation,
+ override fun entireScreenCovered() = testSpec.entireScreenCovered(testSpec.config.startRotation,
testSpec.config.endRotation, allStates = false)
@FlakyTest
@@ -90,21 +92,27 @@ class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec)
testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation,
testSpec.config.endRotation)
- @FlakyTest(bugId = 185400889)
+ @Presubmit
@Test
fun appLayerRotates_StartingBounds() {
testSpec.assertLayersStart {
- visibleRegion(fixedApp.defaultWindowName).coversExactly(startingBounds)
- visibleRegion(pipApp.defaultWindowName).coversAtMost(startingBounds)
+ val pipRegion = visibleRegion(pipApp.component).region
+ val expectedWithoutPip = Region(startingBounds.bounds.left, startingBounds.bounds.top,
+ startingBounds.bounds.right, startingBounds.bounds.bottom).minus(pipRegion)
+ visibleRegion(fixedApp.component).coversExactly(expectedWithoutPip)
+ visibleRegion(pipApp.component).coversAtMost(startingBounds)
}
}
- @FlakyTest(bugId = 185400889)
+ @Presubmit
@Test
fun appLayerRotates_EndingBounds() {
testSpec.assertLayersEnd {
- visibleRegion(fixedApp.defaultWindowName).coversExactly(endingBounds)
- visibleRegion(pipApp.defaultWindowName).coversAtMost(endingBounds)
+ val pipRegion = visibleRegion(pipApp.component).region
+ val expectedWithoutPip = Region(endingBounds.bounds.left, endingBounds.bounds.top,
+ endingBounds.bounds.right, endingBounds.bounds.bottom).minus(pipRegion)
+ visibleRegion(fixedApp.component).coversExactly(expectedWithoutPip)
+ visibleRegion(pipApp.component).coversAtMost(endingBounds)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt
index 1294ac93f647..914bc8b4d1c9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt
@@ -63,13 +63,13 @@ class PipShelfHeightTest(testSpec: FlickerTestParameter) : PipTransition(testSpe
@Presubmit
@Test
- fun pipAlwaysVisible() = testSpec.assertWm { this.showsAppWindow(pipApp.windowName) }
+ fun pipAlwaysVisible() = testSpec.assertWm { this.isAppWindowVisible(pipApp.component) }
@Presubmit
@Test
fun pipLayerInsideDisplay() {
testSpec.assertLayersStart {
- visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds)
+ visibleRegion(pipApp.component).coversAtMost(displayBounds)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt
index 55e5c4128967..5abcf39f3fbd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt
@@ -22,9 +22,9 @@ 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.LAUNCHER_COMPONENT
import com.android.server.wm.flicker.annotation.Group3
import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.focusChanges
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.startRotation
import org.junit.FixMethodOrder
@@ -64,9 +64,11 @@ class PipToAppTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
@Test
fun appReplacesPipWindow() {
testSpec.assertWm {
- this.showsAppWindow(PIP_WINDOW_TITLE)
+ this.invoke("hasPipWindow") { it.isPinned(pipApp.component) }
+ .isAppWindowOnTop(pipApp.component)
.then()
- .showsAppWindowOnTop(pipApp.launcherName)
+ .invoke("hasNotPipWindow") { it.isNotPinned(pipApp.component) }
+ .isAppWindowOnTop(pipApp.component)
}
}
@@ -74,9 +76,11 @@ class PipToAppTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
@Test
fun appReplacesPipLayer() {
testSpec.assertLayers {
- this.isVisible(PIP_WINDOW_TITLE)
+ this.isVisible(pipApp.component)
+ .isVisible(LAUNCHER_COMPONENT)
.then()
- .isVisible(pipApp.launcherName)
+ .isVisible(pipApp.component)
+ .isInvisible(LAUNCHER_COMPONENT)
}
}
@@ -84,22 +88,26 @@ class PipToAppTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
@Test
fun testAppCoversFullScreen() {
testSpec.assertLayersStart {
- visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds)
+ visibleRegion(pipApp.component).coversExactly(displayBounds)
}
}
@FlakyTest(bugId = 151179149)
@Test
- fun focusChanges() = testSpec.focusChanges("NexusLauncherActivity",
- pipApp.launcherName, "NexusLauncherActivity")
+ fun focusChanges() {
+ testSpec.assertEventLog {
+ this.focusChanges("NexusLauncherActivity",
+ pipApp.launcherName, "NexusLauncherActivity")
+ }
+ }
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
fun getParams(): List<FlickerTestParameter> {
return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 5)
+ .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
+ repetitions = 5)
}
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
index b4c75a6d1165..ca80d1837ea8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
@@ -20,25 +20,24 @@ import android.app.Instrumentation
import android.content.Intent
import android.platform.test.annotations.Presubmit
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.entireScreenCovered
import com.android.server.wm.flicker.helpers.WindowUtils
import com.android.server.wm.flicker.helpers.isRotated
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.navBarWindowIsVisible
import com.android.server.wm.flicker.repetitions
import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
import com.android.wm.shell.flicker.helpers.PipAppHelper
import com.android.wm.shell.flicker.testapp.Components
import org.junit.Test
@@ -162,19 +161,19 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) {
@Presubmit
@Test
- open fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+ open fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
@Presubmit
@Test
- open fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+ open fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
- @FlakyTest
+ @Presubmit
@Test
- open fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+ open fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
- @FlakyTest
+ @Presubmit
@Test
- open fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+ open fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
@Presubmit
@Test
@@ -188,6 +187,6 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) {
@Presubmit
@Test
- open fun noUncoveredRegions() =
- testSpec.noUncoveredRegions(testSpec.config.startRotation, Surface.ROTATION_0)
+ open fun entireScreenCovered() =
+ testSpec.entireScreenCovered(testSpec.config.startRotation, Surface.ROTATION_0)
} \ No newline at end of file
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 1f58bb2bf9db..e7b61970cbeb 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
@@ -16,7 +16,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
@@ -83,54 +82,70 @@ class SetRequestedOrientationWhilePinnedTest(
@FlakyTest
@Test
+ override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
+ @FlakyTest
+ @Test
+ override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
+
+ @FlakyTest
+ @Test
+ override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
+
+ @FlakyTest
+ @Test
+ override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible()
+
+ @FlakyTest
+ @Test
override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
@FlakyTest
@Test
override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
- @Presubmit
+ @FlakyTest
@Test
fun pipWindowInsideDisplay() {
testSpec.assertWmStart {
- frameRegion(pipApp.defaultWindowName).coversAtMost(startingBounds)
+ frameRegion(pipApp.component).coversAtMost(startingBounds)
}
}
- @Presubmit
+ @FlakyTest
@Test
fun pipAppShowsOnTop() {
testSpec.assertWmEnd {
- showsAppWindowOnTop(pipApp.defaultWindowName)
+ isAppWindowOnTop(pipApp.component)
}
}
- @Presubmit
+ @FlakyTest
@Test
fun pipLayerInsideDisplay() {
testSpec.assertLayersStart {
- visibleRegion(pipApp.defaultWindowName).coversAtMost(startingBounds)
+ visibleRegion(pipApp.component).coversAtMost(startingBounds)
}
}
- @Presubmit
+ @FlakyTest
@Test
fun pipAlwaysVisible() = testSpec.assertWm {
- this.showsAppWindow(pipApp.windowName)
+ this.isAppWindowVisible(pipApp.component)
}
- @Presubmit
+ @FlakyTest
@Test
fun pipAppLayerCoversFullScreen() {
testSpec.assertLayersEnd {
- visibleRegion(pipApp.defaultWindowName).coversExactly(endingBounds)
+ visibleRegion(pipApp.component).coversExactly(endingBounds)
}
}
@FlakyTest
@Test
- override fun noUncoveredRegions() {
- super.noUncoveredRegions()
+ override fun entireScreenCovered() {
+ super.entireScreenCovered()
}
companion object {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
index 0110ba3f5b30..061218a015e4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
@@ -37,14 +37,17 @@ class TvPipMenuTests : TvPipTestBase() {
private val systemUiResources =
packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME)
private val pipBoundsWhileInMenu: Rect = systemUiResources.run {
- val bounds = getString(getIdentifier("pip_menu_bounds", "string", SYSTEM_UI_PACKAGE_NAME))
+ val bounds = getString(getIdentifier("pip_menu_bounds", "string",
+ SYSTEM_UI_PACKAGE_NAME))
Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds")
}
private val playButtonDescription = systemUiResources.run {
- getString(getIdentifier("pip_play", "string", SYSTEM_UI_PACKAGE_NAME))
+ getString(getIdentifier("pip_play", "string",
+ SYSTEM_UI_PACKAGE_NAME))
}
private val pauseButtonDescription = systemUiResources.run {
- getString(getIdentifier("pip_pause", "string", SYSTEM_UI_PACKAGE_NAME))
+ getString(getIdentifier("pip_pause", "string",
+ SYSTEM_UI_PACKAGE_NAME))
}
@Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
index 1b73920046dc..1c663409b913 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
@@ -70,7 +70,8 @@ fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? {
// descendant and then retrieve the element from the menu and return to the caller of this
// method.
val elementSelector = By.desc(desc)
- val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector)
+ val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR)
+ .hasDescendant(elementSelector)
return wait(Until.findObject(menuContainingElementSelector), WAIT_TIME_MS)
?.findObject(elementSelector)
@@ -94,7 +95,8 @@ fun UiDevice.clickTvPipMenuFullscreenButton() {
}
fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) {
- focusOnAndClickTvPipMenuElement(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) ||
+ focusOnAndClickTvPipMenuElement(By.desc(desc)
+ .pkg(SYSTEM_UI_PACKAGE_NAME)) ||
error("Could not focus on the Pip menu object with \"$desc\" description")
// So apparently Accessibility framework on TV is not very reliable and sometimes the state of
// the tree of accessibility nodes as seen by the accessibility clients kind of lags behind of
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 20ac5bf8fa84..1cbad155ba7b 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
@@ -47,6 +47,8 @@ import android.window.WindowContainerToken;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.common.HandlerExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable;
import org.junit.After;
import org.junit.Before;
@@ -71,6 +73,8 @@ public class TaskViewTest extends ShellTestCase {
ShellTaskOrganizer mOrganizer;
@Mock
HandlerExecutor mExecutor;
+ @Mock
+ SyncTransactionQueue mSyncQueue;
SurfaceSession mSession;
SurfaceControl mLeash;
@@ -99,7 +103,14 @@ public class TaskViewTest extends ShellTestCase {
}).when(mExecutor).execute(any());
when(mOrganizer.getExecutor()).thenReturn(mExecutor);
- mTaskView = new TaskView(mContext, mOrganizer);
+
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final TransactionRunnable r = invocationOnMock.getArgument(0);
+ r.runWithTransaction(new SurfaceControl.Transaction());
+ return null;
+ }).when(mSyncQueue).runInSync(any());
+
+ mTaskView = new TaskView(mContext, mOrganizer, mSyncQueue);
mTaskView.setListener(mExecutor, mViewListener);
}
@@ -112,7 +123,7 @@ public class TaskViewTest extends ShellTestCase {
@Test
public void testSetPendingListener_throwsException() {
- TaskView taskView = new TaskView(mContext, mOrganizer);
+ TaskView taskView = new TaskView(mContext, mOrganizer, mSyncQueue);
taskView.setListener(mExecutor, mViewListener);
try {
taskView.setListener(mExecutor, mViewListener);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 3e3195fe8dc5..b0312e6d6f3c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -869,6 +869,35 @@ public class BubbleDataTest extends ShellTestCase {
assertNotNull(mBubbleData.getOverflowBubbleWithKey(mBubbleA2.getKey()));
}
+ /**
+ * Verifies that after the stack is collapsed with the overflow selected, it will select
+ * the top bubble upon next expansion.
+ */
+ @Test
+ public void test_collapseWithOverflowSelected_nextExpansion() {
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ sendUpdatedEntryAtTime(mEntryA2, 2000);
+ mBubbleData.setExpanded(true);
+
+ mBubbleData.setListener(mListener);
+
+ // Select the overflow
+ mBubbleData.setShowingOverflow(true);
+ mBubbleData.setSelectedBubble(mBubbleData.getOverflow());
+ verifyUpdateReceived();
+ assertSelectionChangedTo(mBubbleData.getOverflow());
+
+ // Collapse
+ mBubbleData.setExpanded(false);
+ verifyUpdateReceived();
+ assertSelectionNotChanged();
+
+ // Expand (here we should select the new bubble)
+ mBubbleData.setExpanded(true);
+ verifyUpdateReceived();
+ assertSelectionChangedTo(mBubbleA2);
+ }
+
private void verifyUpdateReceived() {
verify(mListener).applyUpdate(mUpdateCaptor.capture());
reset(mListener);
@@ -902,7 +931,7 @@ public class BubbleDataTest extends ShellTestCase {
assertWithMessage("selectionChanged").that(update.selectionChanged).isFalse();
}
- private void assertSelectionChangedTo(Bubble bubble) {
+ private void assertSelectionChangedTo(BubbleViewProvider bubble) {
BubbleData.Update update = mUpdateCaptor.getValue();
assertWithMessage("selectionChanged").that(update.selectionChanged).isTrue();
assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble);
@@ -925,7 +954,6 @@ public class BubbleDataTest extends ShellTestCase {
assertThat(update.overflowBubbles).isEqualTo(bubbles);
}
-
private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName,
NotificationListenerService.Ranking ranking) {
return createBubbleEntry(userId, notifKey, packageName, ranking, 1000);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java
index 6644eaf28a62..5c1bcb9753a4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java
@@ -63,7 +63,7 @@ public class BubbleFlyoutViewTest extends ShellTestCase {
mFlyoutMessage.senderName = "Josh";
mFlyoutMessage.message = "Hello";
- mFlyout = new BubbleFlyoutView(getContext());
+ mFlyout = new BubbleFlyoutView(getContext(), mPositioner);
mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text);
mSenderName = mFlyout.findViewById(R.id.bubble_flyout_name);
@@ -75,9 +75,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase {
public void testShowFlyout_isVisible() {
mFlyout.setupFlyoutStartingAsDot(
mFlyoutMessage,
- new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
- false,
- mPositioner);
+ new PointF(100, 100), true, Color.WHITE, null, null, mDotCenter,
+ false);
mFlyout.setVisibility(View.VISIBLE);
assertEquals("Hello", mFlyoutText.getText());
@@ -89,9 +88,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase {
public void testFlyoutHide_runsCallback() {
Runnable after = mock(Runnable.class);
mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
- new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter,
- false,
- mPositioner);
+ new PointF(100, 100), true, Color.WHITE, null, after, mDotCenter,
+ false);
mFlyout.hideFlyout();
verify(after).run();
@@ -100,9 +98,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase {
@Test
public void testSetCollapsePercent() {
mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
- new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
- false,
- mPositioner);
+ new PointF(100, 100), true, Color.WHITE, null, null, mDotCenter,
+ false);
mFlyout.setVisibility(View.VISIBLE);
mFlyout.setCollapsePercent(1f);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
index 1eba3c266358..9732a8890e0e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles.animation;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import android.annotation.SuppressLint;
import android.content.res.Configuration;
@@ -41,7 +42,6 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Spy;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -49,26 +49,26 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
private int mDisplayWidth = 500;
private int mDisplayHeight = 1000;
- private int mExpandedViewPadding = 10;
private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class);
- @Spy
ExpandedAnimationController mExpandedController;
private int mStackOffset;
private PointF mExpansionPoint;
+ private BubblePositioner mPositioner;
@SuppressLint("VisibleForTests")
@Before
public void setUp() throws Exception {
super.setUp();
- BubblePositioner positioner = new BubblePositioner(getContext(), mock(WindowManager.class));
- positioner.updateInternal(Configuration.ORIENTATION_PORTRAIT,
+ mPositioner = new BubblePositioner(getContext(), mock(WindowManager.class));
+ mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT,
Insets.of(0, 0, 0, 0),
new Rect(0, 0, mDisplayWidth, mDisplayHeight));
- mExpandedController = new ExpandedAnimationController(positioner, mExpandedViewPadding,
+ mExpandedController = new ExpandedAnimationController(mPositioner,
mOnBubbleAnimatedOutAction);
+ spyOn(mExpandedController);
addOneMoreThanBubbleLimitBubbles();
mLayout.setActiveController(mExpandedController);
@@ -141,13 +141,16 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
/** Check that children are in the correct positions for being expanded. */
private void testBubblesInCorrectExpandedPositions() {
+ boolean onLeft = mPositioner.isStackOnLeft(mExpansionPoint);
// Check all the visible bubbles to see if they're in the right place.
for (int i = 0; i < mLayout.getChildCount(); i++) {
- float expectedPosition = mExpandedController.getBubbleXOrYForOrientation(i);
- assertEquals(expectedPosition,
+ PointF expectedPosition = mPositioner.getExpandedBubbleXY(i,
+ mLayout.getChildCount(),
+ onLeft);
+ assertEquals(expectedPosition.x,
mLayout.getChildAt(i).getTranslationX(),
2f);
- assertEquals(expectedPosition,
+ assertEquals(expectedPosition.y,
mLayout.getChildAt(i).getTranslationY(), 2f);
}
}
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 ef046d48e1cf..b88845044263 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
@@ -58,7 +58,7 @@ public class DisplayImeControllerTest {
mT = mock(SurfaceControl.Transaction.class);
mMock = mock(IInputMethodManager.class);
mExecutor = spy(Runnable::run);
- mPerDisplay = new DisplayImeController(null, null, mExecutor, new TransactionPool() {
+ mPerDisplay = new DisplayImeController(null, null, null, mExecutor, new TransactionPool() {
@Override
public SurfaceControl.Transaction acquire() {
return mT;
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
new file mode 100644
index 000000000000..b66c2b4aee9b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common;
+
+import 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.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.RemoteException;
+import android.util.SparseArray;
+import android.view.IDisplayWindowInsetsController;
+import android.view.IWindowManager;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.TestShellExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+@SmallTest
+public class DisplayInsetsControllerTest {
+
+ private static final int SECOND_DISPLAY = DEFAULT_DISPLAY + 10;
+
+ @Mock
+ private IWindowManager mWm;
+ @Mock
+ private DisplayController mDisplayController;
+ private DisplayInsetsController mController;
+ private SparseArray<IDisplayWindowInsetsController> mInsetsControllersByDisplayId;
+ private TestShellExecutor mExecutor;
+
+ private ArgumentCaptor<Integer> mDisplayIdCaptor;
+ private ArgumentCaptor<IDisplayWindowInsetsController> mInsetsControllerCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mExecutor = new TestShellExecutor();
+ mInsetsControllersByDisplayId = new SparseArray<>();
+ mDisplayIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ mInsetsControllerCaptor = ArgumentCaptor.forClass(IDisplayWindowInsetsController.class);
+ mController = new DisplayInsetsController(mWm, mDisplayController, mExecutor);
+ addDisplay(DEFAULT_DISPLAY);
+ }
+
+ @Test
+ public void testOnDisplayAdded_setsDisplayWindowInsetsControllerOnWMService()
+ throws RemoteException {
+ addDisplay(SECOND_DISPLAY);
+
+ verify(mWm).setDisplayWindowInsetsController(eq(SECOND_DISPLAY), notNull());
+ }
+
+ @Test
+ public void testOnDisplayRemoved_unsetsDisplayWindowInsetsControllerInWMService()
+ throws RemoteException {
+ addDisplay(SECOND_DISPLAY);
+ removeDisplay(SECOND_DISPLAY);
+
+ verify(mWm).setDisplayWindowInsetsController(SECOND_DISPLAY, null);
+ }
+
+ @Test
+ public void testPerDisplayListenerCallback() throws RemoteException {
+ TrackedListener defaultListener = new TrackedListener();
+ TrackedListener secondListener = new TrackedListener();
+ addDisplay(SECOND_DISPLAY);
+ mController.addInsetsChangedListener(DEFAULT_DISPLAY, defaultListener);
+ mController.addInsetsChangedListener(SECOND_DISPLAY, secondListener);
+
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false);
+ mExecutor.flushAll();
+
+ assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
+ assertTrue(defaultListener.insetsChangedCount == 1);
+ assertTrue(defaultListener.insetsControlChangedCount == 1);
+ assertTrue(defaultListener.showInsetsCount == 1);
+ assertTrue(defaultListener.hideInsetsCount == 1);
+
+ assertTrue(secondListener.topFocusedWindowChangedCount == 0);
+ assertTrue(secondListener.insetsChangedCount == 0);
+ assertTrue(secondListener.insetsControlChangedCount == 0);
+ assertTrue(secondListener.showInsetsCount == 0);
+ assertTrue(secondListener.hideInsetsCount == 0);
+
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false);
+ mExecutor.flushAll();
+
+ assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
+ assertTrue(defaultListener.insetsChangedCount == 1);
+ assertTrue(defaultListener.insetsControlChangedCount == 1);
+ assertTrue(defaultListener.showInsetsCount == 1);
+ assertTrue(defaultListener.hideInsetsCount == 1);
+
+ assertTrue(secondListener.topFocusedWindowChangedCount == 1);
+ assertTrue(secondListener.insetsChangedCount == 1);
+ assertTrue(secondListener.insetsControlChangedCount == 1);
+ assertTrue(secondListener.showInsetsCount == 1);
+ assertTrue(secondListener.hideInsetsCount == 1);
+ }
+
+ private void addDisplay(int displayId) throws RemoteException {
+ mController.onDisplayAdded(displayId);
+ verify(mWm, times(mInsetsControllersByDisplayId.size() + 1))
+ .setDisplayWindowInsetsController(mDisplayIdCaptor.capture(),
+ mInsetsControllerCaptor.capture());
+ List<Integer> displayIds = mDisplayIdCaptor.getAllValues();
+ List<IDisplayWindowInsetsController> insetsControllers =
+ mInsetsControllerCaptor.getAllValues();
+ for (int i = 0; i < displayIds.size(); i++) {
+ mInsetsControllersByDisplayId.put(displayIds.get(i), insetsControllers.get(i));
+ }
+ }
+
+ private void removeDisplay(int displayId) {
+ mController.onDisplayRemoved(displayId);
+ mInsetsControllersByDisplayId.remove(displayId);
+ }
+
+ private static class TrackedListener implements
+ DisplayInsetsController.OnInsetsChangedListener {
+ int topFocusedWindowChangedCount = 0;
+ int insetsChangedCount = 0;
+ int insetsControlChangedCount = 0;
+ int showInsetsCount = 0;
+ int hideInsetsCount = 0;
+
+ @Override
+ public void topFocusedWindowChanged(String packageName) {
+ topFocusedWindowChangedCount++;
+ }
+
+ @Override
+ public void insetsChanged(InsetsState insetsState) {
+ insetsChangedCount++;
+ }
+
+ @Override
+ public void insetsControlChanged(InsetsState insetsState,
+ InsetsSourceControl[] activeControls) {
+ insetsControlChangedCount++;
+ }
+
+ @Override
+ public void showInsets(int types, boolean fromIme) {
+ showInsetsCount++;
+ }
+
+ @Override
+ public void hideInsets(int types, boolean fromIme) {
+ hideInsetsCount++;
+ }
+ }
+}
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 952dc31cdaee..3557906531b2 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
@@ -24,6 +24,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.content.res.Configuration;
@@ -42,6 +43,8 @@ import com.android.wm.shell.common.DisplayImeController;
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;
@@ -53,39 +56,53 @@ public class SplitLayoutTests extends ShellTestCase {
@Mock SurfaceControl mRootLeash;
@Mock DisplayImeController mDisplayImeController;
@Mock ShellTaskOrganizer mTaskOrganizer;
+ @Captor ArgumentCaptor<Runnable> mRunnableCaptor;
private SplitLayout mSplitLayout;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mSplitLayout = new SplitLayout(
+ mSplitLayout = spy(new SplitLayout(
"TestSplitLayout",
mContext,
- getConfiguration(false),
+ getConfiguration(),
mSplitLayoutHandler,
b -> b.setParent(mRootLeash),
mDisplayImeController,
- mTaskOrganizer);
+ mTaskOrganizer));
}
@Test
@UiThreadTest
public void testUpdateConfiguration() {
- mSplitLayout.init();
- assertThat(mSplitLayout.updateConfiguration(getConfiguration(false))).isFalse();
- assertThat(mSplitLayout.updateConfiguration(getConfiguration(true))).isTrue();
+ final Configuration config = getConfiguration();
+
+ // Verify it returns true if new config won't affect split layout.
+ assertThat(mSplitLayout.updateConfiguration(config)).isFalse();
+
+ // Verify updateConfiguration returns true if the orientation changed.
+ config.orientation = ORIENTATION_LANDSCAPE;
+ assertThat(mSplitLayout.updateConfiguration(config)).isTrue();
+
+ // Verify updateConfiguration returns true if it rotated.
+ config.windowConfiguration.setRotation(1);
+ assertThat(mSplitLayout.updateConfiguration(config)).isTrue();
+
+ // Verify updateConfiguration returns true if the root bounds changed.
+ config.windowConfiguration.setBounds(new Rect(0, 0, 2160, 1080));
+ assertThat(mSplitLayout.updateConfiguration(config)).isTrue();
}
@Test
public void testUpdateDivideBounds() {
mSplitLayout.updateDivideBounds(anyInt());
- verify(mSplitLayoutHandler).onBoundsChanging(any(SplitLayout.class));
+ verify(mSplitLayoutHandler).onLayoutChanging(any(SplitLayout.class));
}
@Test
public void testSetDividePosition() {
mSplitLayout.setDividePosition(anyInt());
- verify(mSplitLayoutHandler).onBoundsChanged(any(SplitLayout.class));
+ verify(mSplitLayoutHandler).onLayoutChanged(any(SplitLayout.class));
}
@Test
@@ -96,24 +113,40 @@ public class SplitLayoutTests extends ShellTestCase {
@Test
@UiThreadTest
- public void testSnapToDismissTarget() {
+ public void testSnapToDismissStart() {
// verify it callbacks properly when the snap target indicates dismissing split.
DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START);
+
mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
+ waitDividerFlingFinished();
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false));
- snapTarget = getSnapTarget(0 /* position */,
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSnapToDismissEnd() {
+ // verify it callbacks properly when the snap target indicates dismissing split.
+ DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END);
+
mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
+ waitDividerFlingFinished();
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true));
}
- private static Configuration getConfiguration(boolean isLandscape) {
+ private void waitDividerFlingFinished() {
+ verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture());
+ mRunnableCaptor.getValue().run();
+ }
+
+ private static Configuration getConfiguration() {
final Configuration configuration = new Configuration();
configuration.unset();
- configuration.orientation = isLandscape ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
+ configuration.orientation = ORIENTATION_PORTRAIT;
+ configuration.windowConfiguration.setRotation(0);
configuration.windowConfiguration.setBounds(
- new Rect(0, 0, isLandscape ? 2160 : 1080, isLandscape ? 1080 : 2160));
+ new Rect(0, 0, 1080, 2160));
return configuration;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java
index 698315a77d8e..c456c7de8821 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java
@@ -22,6 +22,7 @@ import static org.mockito.Mockito.when;
import android.content.res.Configuration;
import android.graphics.Rect;
+import android.view.InsetsState;
import android.view.SurfaceControl;
import androidx.test.annotation.UiThreadTest;
@@ -59,7 +60,7 @@ public class SplitWindowManagerTests extends ShellTestCase {
@Test
@UiThreadTest
public void testInitRelease() {
- mSplitWindowManager.init(mSplitLayout);
+ mSplitWindowManager.init(mSplitLayout, new InsetsState());
assertThat(mSplitWindowManager.getSurfaceControl()).isNotNull();
mSplitWindowManager.release();
assertThat(mSplitWindowManager.getSurfaceControl()).isNull();
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 ba73d555e334..734b97b69c87 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
@@ -25,6 +25,7 @@ import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;
import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM;
@@ -64,6 +65,7 @@ import android.view.DisplayInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.internal.logging.InstanceId;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -95,6 +97,9 @@ public class DragAndDropPolicyTest {
@Mock
private SplitScreenController mSplitScreenStarter;
+ @Mock
+ private InstanceId mLoggerSessionId;
+
private DisplayLayout mLandscapeDisplayLayout;
private DisplayLayout mPortraitDisplayLayout;
private Insets mInsets;
@@ -200,7 +205,7 @@ public class DragAndDropPolicyTest {
@Test
public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() {
setRunningTask(mHomeTask);
- mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
+ mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
mPolicy.getTargets(mInsets), TYPE_FULLSCREEN);
@@ -210,15 +215,15 @@ public class DragAndDropPolicyTest {
}
@Test
- public void testDragAppOverFullscreenApp_expectSplitScreenAndFullscreenTargets() {
+ public void testDragAppOverFullscreenApp_expectSplitScreenTargets() {
setRunningTask(mFullscreenAppTask);
- mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
+ mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
- mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
+ mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
- mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
+ mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), mActivityClipData);
verify(mSplitScreenStarter).startIntent(any(), any(),
- eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
+ eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_TOP_OR_LEFT), any());
reset(mSplitScreenStarter);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData);
@@ -227,15 +232,15 @@ public class DragAndDropPolicyTest {
}
@Test
- public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenAndFullscreenTargets() {
+ public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenTargets() {
setRunningTask(mFullscreenAppTask);
- mPolicy.start(mPortraitDisplayLayout, mActivityClipData);
+ mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
- mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
+ mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
- mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
+ mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), mActivityClipData);
verify(mSplitScreenStarter).startIntent(any(), any(),
- eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
+ eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_TOP_OR_LEFT), any());
reset(mSplitScreenStarter);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData);
@@ -244,71 +249,61 @@ public class DragAndDropPolicyTest {
}
@Test
- public void testDragAppOverFullscreenNonResizeableApp_expectOnlyFullscreenTargets() {
- setRunningTask(mNonResizeableFullscreenAppTask);
- mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
+ public void testDragAppOverSplitApp_expectSplitTargets_DropLeft() {
+ setInSplitScreen(true);
+ setRunningTask(mSplitPrimaryAppTask);
+ mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
- mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
+ mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
- mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
+ mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), mActivityClipData);
verify(mSplitScreenStarter).startIntent(any(), any(),
- eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
+ eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_TOP_OR_LEFT), any());
}
@Test
- public void testDragNonResizeableAppOverFullscreenApp_expectOnlyFullscreenTargets() {
- setRunningTask(mFullscreenAppTask);
- mPolicy.start(mLandscapeDisplayLayout, mNonResizeableActivityClipData);
+ public void testDragAppOverSplitApp_expectSplitTargets_DropRight() {
+ setInSplitScreen(true);
+ setRunningTask(mSplitPrimaryAppTask);
+ mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
- mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
+ mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
- mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
+ mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData);
verify(mSplitScreenStarter).startIntent(any(), any(),
- eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
+ eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
}
@Test
- public void testDragAppOverSplitApp_expectFullscreenAndSplitTargets() {
+ public void testDragAppOverSplitAppPhone_expectVerticalSplitTargets_DropTop() {
setInSplitScreen(true);
setRunningTask(mSplitPrimaryAppTask);
- mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
+ mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
- mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
+ mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
- mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
- verify(mSplitScreenStarter).startIntent(any(), any(),
- eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
- reset(mSplitScreenStarter);
-
- // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs
- mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData);
+ mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), mActivityClipData);
verify(mSplitScreenStarter).startIntent(any(), any(),
- eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
+ eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_TOP_OR_LEFT), any());
}
@Test
- public void testDragAppOverSplitAppPhone_expectFullscreenAndVerticalSplitTargets() {
+ public void testDragAppOverSplitAppPhone_expectVerticalSplitTargets_DropBottom() {
setInSplitScreen(true);
setRunningTask(mSplitPrimaryAppTask);
- mPolicy.start(mPortraitDisplayLayout, mActivityClipData);
+ mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
- mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
+ mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
- mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
- verify(mSplitScreenStarter).startIntent(any(), any(),
- eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
- reset(mSplitScreenStarter);
-
- // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData);
verify(mSplitScreenStarter).startIntent(any(), any(),
- eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
+ eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
}
@Test
public void testTargetHitRects() {
setRunningTask(mFullscreenAppTask);
- mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
+ mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
ArrayList<Target> targets = mPolicy.getTargets(mInsets);
for (Target t : targets) {
assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.top) == t);
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 9d7c82bb8550..0270093da938 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
@@ -79,6 +79,7 @@ public class PipTaskOrganizerTest extends ShellTestCase {
@Mock private ShellTaskOrganizer mMockShellTaskOrganizer;
private TestShellExecutor mMainExecutor;
private PipBoundsState mPipBoundsState;
+ private PipTransitionState mPipTransitionState;
private PipBoundsAlgorithm mPipBoundsAlgorithm;
private ComponentName mComponent1;
@@ -90,11 +91,12 @@ public class PipTaskOrganizerTest extends ShellTestCase {
mComponent1 = new ComponentName(mContext, "component1");
mComponent2 = new ComponentName(mContext, "component2");
mPipBoundsState = new PipBoundsState(mContext);
+ mPipTransitionState = new PipTransitionState();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
new PipSnapAlgorithm());
mMainExecutor = new TestShellExecutor();
mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext,
- mMockSyncTransactionQueue, mPipBoundsState,
+ mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState,
mPipBoundsAlgorithm, mMockPhonePipMenuController,
mMockPipAnimationController, mMockPipSurfaceTransactionHelper,
mMockPipTransitionController, mMockOptionalSplitScreen, mMockDisplayController,
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 56a005642ce2..69ead3ac9cf9 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
@@ -33,6 +33,7 @@ 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.SyncTransactionQueue;
@@ -46,7 +47,7 @@ import org.mockito.Spy;
/** Tests for {@link SideStage} */
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class SideStageTests {
+public class SideStageTests extends ShellTestCase {
@Mock private ShellTaskOrganizer mTaskOrganizer;
@Mock private StageTaskListener.StageListenerCallbacks mCallbacks;
@Mock private SyncTransactionQueue mSyncQueue;
@@ -60,8 +61,8 @@ public class SideStageTests {
public void setup() {
MockitoAnnotations.initMocks(this);
mRootTask = new TestRunningTaskInfoBuilder().build();
- mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue,
- mSurfaceSession);
+ mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
+ mSyncQueue, mSurfaceSession);
mSideStage.onTaskAppeared(mRootTask, mRootLeash);
}
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 ab6f76996398..736566e5b4d3 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
@@ -33,6 +33,7 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito;
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;
@@ -65,9 +66,12 @@ public class SplitTestUtils {
TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
MainStage mainStage, SideStage sideStage, DisplayImeController imeController,
- SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) {
+ DisplayInsetsController insetsController, SplitLayout splitLayout,
+ Transitions transitions, TransactionPool transactionPool,
+ SplitscreenEventLogger logger) {
super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage,
- sideStage, imeController, splitLayout, transitions, transactionPool);
+ sideStage, imeController, insetsController, splitLayout, transitions,
+ transactionPool, logger);
// Prepare default TaskDisplayArea for testing.
mDisplayAreaInfo = new DisplayAreaInfo(
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 aca80f3556b9..a53d2e8b1268 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
@@ -58,6 +58,7 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.DisplayImeController;
+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.common.TransactionPool;
@@ -79,9 +80,11 @@ public class SplitTransitionTests extends ShellTestCase {
@Mock private SyncTransactionQueue mSyncQueue;
@Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
@Mock private DisplayImeController mDisplayImeController;
+ @Mock private DisplayInsetsController mDisplayInsetsController;
@Mock private TransactionPool mTransactionPool;
@Mock private Transitions mTransitions;
@Mock private SurfaceSession mSurfaceSession;
+ @Mock private SplitscreenEventLogger mLogger;
private SplitLayout mSplitLayout;
private MainStage mMainStage;
private SideStage mSideStage;
@@ -102,12 +105,14 @@ public class SplitTransitionTests extends ShellTestCase {
mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession);
mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
- mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mock(
+ mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession);
mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
- mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
- mDisplayImeController, mSplitLayout, mTransitions, mTransactionPool);
+ mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
+ mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
+ mTransactionPool,
+ mLogger);
mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
.when(mTransitions).startTransition(anyInt(), any(), any());
@@ -131,6 +136,7 @@ public class SplitTransitionTests extends ShellTestCase {
mSideStage.onTaskAppeared(mSideChild, createMockSurface());
boolean accepted = mStageCoordinator.startAnimation(transition, info,
mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class),
mock(Transitions.TransitionFinishCallback.class));
assertTrue(accepted);
@@ -168,6 +174,7 @@ public class SplitTransitionTests extends ShellTestCase {
mSideStage.onTaskAppeared(newTask, createMockSurface());
boolean accepted = mStageCoordinator.startAnimation(transition, info,
mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class),
mock(Transitions.TransitionFinishCallback.class));
assertFalse(accepted);
assertTrue(mStageCoordinator.isSplitScreenVisible());
@@ -188,6 +195,7 @@ public class SplitTransitionTests extends ShellTestCase {
mSideStage.onTaskVanished(newTask);
accepted = mStageCoordinator.startAnimation(transition, info,
mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class),
mock(Transitions.TransitionFinishCallback.class));
assertFalse(accepted);
assertTrue(mStageCoordinator.isSplitScreenVisible());
@@ -223,6 +231,7 @@ public class SplitTransitionTests extends ShellTestCase {
mSideStage.onTaskVanished(mSideChild);
mStageCoordinator.startAnimation(transition, info,
mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class),
mock(Transitions.TransitionFinishCallback.class));
assertFalse(mStageCoordinator.isSplitScreenVisible());
}
@@ -244,6 +253,7 @@ public class SplitTransitionTests extends ShellTestCase {
mSideStage.onTaskVanished(mSideChild);
boolean accepted = mStageCoordinator.startAnimation(transition, info,
mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class),
mock(Transitions.TransitionFinishCallback.class));
assertTrue(accepted);
assertFalse(mStageCoordinator.isSplitScreenVisible());
@@ -274,6 +284,7 @@ public class SplitTransitionTests extends ShellTestCase {
mSideStage.onTaskVanished(mSideChild);
boolean accepted = mStageCoordinator.startAnimation(transition, info,
mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class),
mock(Transitions.TransitionFinishCallback.class));
assertTrue(accepted);
assertFalse(mStageCoordinator.isSplitScreenVisible());
@@ -298,6 +309,7 @@ public class SplitTransitionTests extends ShellTestCase {
mSideStage.onTaskAppeared(mSideChild, createMockSurface());
mStageCoordinator.startAnimation(enterTransit, enterInfo,
mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class),
mock(Transitions.TransitionFinishCallback.class));
mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction());
}
@@ -335,10 +347,11 @@ public class SplitTransitionTests extends ShellTestCase {
@Override
public void startAnimation(IBinder transition, TransitionInfo info,
- SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)
+ SurfaceControl.Transaction startTransaction,
+ IRemoteTransitionFinishedCallback finishCallback)
throws RemoteException {
mCalled = true;
- finishCallback.onTransitionFinished(mRemoteFinishWCT);
+ finishCallback.onTransitionFinished(mRemoteFinishWCT, null /* sct */);
}
@Override
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 06b08686bf4c..6cce0ab26df7 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
@@ -37,6 +37,7 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.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.transition.Transitions;
@@ -57,8 +58,10 @@ public class StageCoordinatorTests extends ShellTestCase {
@Mock private MainStage mMainStage;
@Mock private SideStage mSideStage;
@Mock private DisplayImeController mDisplayImeController;
+ @Mock private DisplayInsetsController mDisplayInsetsController;
@Mock private Transitions mTransitions;
@Mock private TransactionPool mTransactionPool;
+ @Mock private SplitscreenEventLogger mLogger;
private StageCoordinator mStageCoordinator;
@Before
@@ -66,7 +69,8 @@ public class StageCoordinatorTests extends ShellTestCase {
MockitoAnnotations.initMocks(this);
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
- mDisplayImeController, null /* splitLayout */, mTransitions, mTransactionPool);
+ mDisplayImeController, mDisplayInsetsController, null /* splitLayout */,
+ mTransitions, mTransactionPool, mLogger);
}
@Test
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 90b5b37694c6..1a30f164f9a8 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
@@ -21,11 +21,13 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
+import android.os.SystemProperties;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
@@ -52,6 +54,9 @@ import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class StageTaskListenerTests {
+ private static final boolean ENABLE_SHELL_TRANSITIONS =
+ SystemProperties.getBoolean("persist.debug.shell_transit", false);
+
@Mock private ShellTaskOrganizer mTaskOrganizer;
@Mock private StageTaskListener.StageListenerCallbacks mCallbacks;
@Mock private SyncTransactionQueue mSyncQueue;
@@ -93,6 +98,8 @@ public final class StageTaskListenerTests {
@Test
public void testChildTaskAppeared() {
+ // With shell transitions, the transition manages status changes, so skip this test.
+ assumeFalse(ENABLE_SHELL_TRANSITIONS);
final ActivityManager.RunningTaskInfo childTask =
new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
@@ -110,6 +117,8 @@ public final class StageTaskListenerTests {
@Test
public void testTaskVanished() {
+ // With shell transitions, the transition manages status changes, so skip this test.
+ assumeFalse(ENABLE_SHELL_TRANSITIONS);
final ActivityManager.RunningTaskInfo childTask =
new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
mStageTaskListener.mRootTaskInfo = mRootTask;
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 d536adb9f8ae..160b3673aa8a 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
@@ -42,6 +42,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.UserHandle;
import android.testing.TestableContext;
+import android.view.Display;
import android.view.SurfaceControl;
import android.view.View;
import android.view.WindowManager;
@@ -92,8 +93,8 @@ public class StartingSurfaceDrawerTests {
}
@Override
- protected boolean addWindow(int taskId, IBinder appToken,
- View view, WindowManager wm, WindowManager.LayoutParams params, int suggestType) {
+ protected boolean addWindow(int taskId, IBinder appToken, View view, Display display,
+ WindowManager.LayoutParams params, int suggestType) {
// listen for addView
mAddWindowForTask = taskId;
mViewThemeResId = view.getContext().getThemeResId();
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 2d2ab2c9f674..54eacee8a9c3 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
@@ -20,12 +20,20 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
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.view.Display.DEFAULT_DISPLAY;
+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;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
+import static android.view.WindowManager.TRANSIT_FLAG_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.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
+import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -48,6 +56,9 @@ import android.content.Context;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
+import android.view.IDisplayWindowListener;
+import android.view.IWindowManager;
+import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.IRemoteTransition;
@@ -65,17 +76,23 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
import java.util.ArrayList;
/**
* Tests for the shell transitions.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:ShellTransitionTests
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -97,8 +114,7 @@ public class ShellTransitionTests {
@Test
public void testBasicTransitionFlow() {
- Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
- mMainExecutor, mAnimExecutor);
+ Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
IBinder transitToken = new Binder();
@@ -117,8 +133,7 @@ public class ShellTransitionTests {
@Test
public void testNonDefaultHandler() {
- Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
- mMainExecutor, mAnimExecutor);
+ Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
@@ -127,11 +142,13 @@ public class ShellTransitionTests {
TestTransitionHandler testHandler = new TestTransitionHandler() {
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @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, t, finishCallback);
+ return super.startAnimation(transition, info, startTransaction,
+ finishTransaction, finishCallback);
}
}
return false;
@@ -199,8 +216,7 @@ public class ShellTransitionTests {
@Test
public void testRequestRemoteTransition() {
- Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
- mMainExecutor, mAnimExecutor);
+ Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
final boolean[] remoteCalled = new boolean[]{false};
@@ -211,7 +227,7 @@ public class ShellTransitionTests {
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
remoteCalled[0] = true;
- finishCallback.onTransitionFinished(remoteFinishWCT);
+ finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */);
}
@Override
@@ -273,9 +289,76 @@ public class ShellTransitionTests {
}
@Test
+ public void testTransitionFilterNotRequirement() {
+ // filter that requires one opening and NO translucent apps
+ TransitionFilter filter = new TransitionFilter();
+ filter.mRequirements = new TransitionFilter.Requirement[]{
+ new TransitionFilter.Requirement(), new TransitionFilter.Requirement()};
+ filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+ filter.mRequirements[1].mFlags = FLAG_TRANSLUCENT;
+ filter.mRequirements[1].mNot = true;
+
+ final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ assertTrue(filter.matches(openOnly));
+
+ final TransitionInfo openAndTranslucent = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ openAndTranslucent.getChanges().get(1).setFlags(FLAG_TRANSLUCENT);
+ assertFalse(filter.matches(openAndTranslucent));
+ }
+
+ @Test
+ public void testTransitionFilterChecksTypeSet() {
+ TransitionFilter filter = new TransitionFilter();
+ filter.mTypeSet = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+
+ final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ assertTrue(filter.matches(openOnly));
+
+ final TransitionInfo toFrontOnly = new TransitionInfoBuilder(TRANSIT_TO_FRONT)
+ .addChange(TRANSIT_TO_FRONT).build();
+ assertTrue(filter.matches(toFrontOnly));
+
+ final TransitionInfo closeOnly = new TransitionInfoBuilder(TRANSIT_CLOSE)
+ .addChange(TRANSIT_CLOSE).build();
+ assertFalse(filter.matches(closeOnly));
+ }
+
+ @Test
+ public void testTransitionFilterChecksFlags() {
+ TransitionFilter filter = new TransitionFilter();
+ filter.mFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+
+ final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK,
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY)
+ .addChange(TRANSIT_TO_BACK).build();
+ assertTrue(filter.matches(withFlag));
+
+ final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ assertFalse(filter.matches(withoutFlag));
+ }
+
+ @Test
+ public void testTransitionFilterChecksNotFlags() {
+ TransitionFilter filter = new TransitionFilter();
+ filter.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+
+ final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK,
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY)
+ .addChange(TRANSIT_TO_BACK).build();
+ assertFalse(filter.matches(withFlag));
+
+ final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ assertTrue(filter.matches(withoutFlag));
+ }
+
+ @Test
public void testRegisteredRemoteTransition() {
- Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
- mMainExecutor, mAnimExecutor);
+ Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
final boolean[] remoteCalled = new boolean[]{false};
@@ -285,7 +368,7 @@ public class ShellTransitionTests {
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
remoteCalled[0] = true;
- finishCallback.onTransitionFinished(null /* wct */);
+ finishCallback.onTransitionFinished(null /* wct */, null /* sct */);
}
@Override
@@ -320,8 +403,7 @@ public class ShellTransitionTests {
@Test
public void testOneShotRemoteHandler() {
- Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
- mMainExecutor, mAnimExecutor);
+ Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
final boolean[] remoteCalled = new boolean[]{false};
@@ -332,7 +414,7 @@ public class ShellTransitionTests {
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
remoteCalled[0] = true;
- finishCallback.onTransitionFinished(remoteFinishWCT);
+ finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */);
}
@Override
@@ -358,15 +440,16 @@ public class ShellTransitionTests {
oneShot.setTransition(transitToken);
IBinder anotherToken = new Binder();
assertFalse(oneShot.startAnimation(anotherToken, new TransitionInfo(transitType, 0),
- mock(SurfaceControl.Transaction.class), testFinish));
+ mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class),
+ testFinish));
assertTrue(oneShot.startAnimation(transitToken, new TransitionInfo(transitType, 0),
- mock(SurfaceControl.Transaction.class), testFinish));
+ mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class),
+ testFinish));
}
@Test
public void testTransitionQueueing() {
- Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
- mMainExecutor, mAnimExecutor);
+ Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
IBinder transitToken1 = new Binder();
@@ -406,8 +489,7 @@ public class ShellTransitionTests {
@Test
public void testTransitionMerging() {
- Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
- mMainExecutor, mAnimExecutor);
+ Transitions transitions = createTestTransitions();
mDefaultHandler.setSimulateMerge(true);
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
@@ -443,11 +525,73 @@ public class ShellTransitionTests {
assertEquals(0, mDefaultHandler.activeCount());
}
+ @Test
+ public void testShouldRotateSeamlessly() throws Exception {
+ final RunningTaskInfo taskInfo =
+ createTaskInfo(1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final RunningTaskInfo taskInfoPip =
+ createTaskInfo(1, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
+
+ final DisplayController displays = createTestDisplayController();
+ final @Surface.Rotation int upsideDown = displays
+ .getDisplayLayout(DEFAULT_DISPLAY).getUpsideDownRotation();
+
+ final TransitionInfo normalDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE)
+ .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
+ .build())
+ .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo).setRotate().build())
+ .build();
+ assertFalse(DefaultTransitionHandler.isRotationSeamless(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(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
+ .setRotate(ROTATION_ANIMATION_SEAMLESS).build())
+ .build();
+ assertTrue(DefaultTransitionHandler.isRotationSeamless(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(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));
+
+ // Not seamless if one of rotations is upside-down
+ final TransitionInfo seamlessUpsideDown = new TransitionInfoBuilder(TRANSIT_CHANGE)
+ .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
+ .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build())
+ .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
+ .setRotate(upsideDown, ROTATION_ANIMATION_SEAMLESS).build())
+ .build();
+ assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessUpsideDown, displays));
+
+ // Not seamless if system alert windows
+ final TransitionInfo seamlessButAlert = new TransitionInfoBuilder(TRANSIT_CHANGE)
+ .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(
+ FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build())
+ .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
+ .setRotate(ROTATION_ANIMATION_SEAMLESS).build())
+ .build();
+ assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessButAlert, displays));
+ }
+
class TransitionInfoBuilder {
final TransitionInfo mInfo;
TransitionInfoBuilder(@WindowManager.TransitionType int type) {
- mInfo = new TransitionInfo(type, 0 /* flags */);
+ this(type, 0 /* flags */);
+ }
+
+ TransitionInfoBuilder(@WindowManager.TransitionType int type,
+ @WindowManager.TransitionFlags int flags) {
+ mInfo = new TransitionInfo(type, flags);
mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0);
}
@@ -465,11 +609,53 @@ public class ShellTransitionTests {
return addChange(mode, null /* taskInfo */);
}
+ TransitionInfoBuilder addChange(TransitionInfo.Change change) {
+ mInfo.addChange(change);
+ return this;
+ }
+
TransitionInfo build() {
return mInfo;
}
}
+ class ChangeBuilder {
+ final TransitionInfo.Change mChange;
+
+ ChangeBuilder(@WindowManager.TransitionType int mode) {
+ mChange = new TransitionInfo.Change(null /* token */, null /* leash */);
+ mChange.setMode(mode);
+ }
+
+ ChangeBuilder setFlags(@TransitionInfo.ChangeFlags int flags) {
+ mChange.setFlags(flags);
+ return this;
+ }
+
+ ChangeBuilder setTask(RunningTaskInfo taskInfo) {
+ mChange.setTaskInfo(taskInfo);
+ return this;
+ }
+
+ ChangeBuilder setRotate(int anim) {
+ return setRotate(Surface.ROTATION_90, anim);
+ }
+
+ ChangeBuilder setRotate() {
+ return setRotate(ROTATION_ANIMATION_UNSPECIFIED);
+ }
+
+ ChangeBuilder setRotate(@Surface.Rotation int target, int anim) {
+ mChange.setRotation(Surface.ROTATION_0, target);
+ mChange.setRotationAnimation(anim);
+ return this;
+ }
+
+ TransitionInfo.Change build() {
+ return mChange;
+ }
+ }
+
class TestTransitionHandler implements Transitions.TransitionHandler {
ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>();
final ArrayList<IBinder> mMerged = new ArrayList<>();
@@ -477,7 +663,8 @@ public class ShellTransitionTests {
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
mFinishes.add(finishCallback);
return true;
@@ -540,4 +727,46 @@ public class ShellTransitionTests {
return taskInfo;
}
+ private DisplayController createTestDisplayController() {
+ IWindowManager mockWM = mock(IWindowManager.class);
+ final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1];
+ try {
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) {
+ displayListener[0] = invocation.getArgument(0);
+ return null;
+ }
+ }).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();
+ try {
+ displayListener[0].onDisplayAdded(DEFAULT_DISPLAY);
+ mMainExecutor.flushAll();
+ } catch (RemoteException e) {
+ // Again, no remote stuff
+ }
+ return out;
+ }
+
+ private Transitions createTestTransitions() {
+ return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(),
+ mContext, mMainExecutor, mAnimExecutor);
+ }
+//
+// 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/hwui/apex/android_matrix.cpp b/libs/hwui/apex/android_matrix.cpp
index 693b22b62663..04ac3cf0ebc8 100644
--- a/libs/hwui/apex/android_matrix.cpp
+++ b/libs/hwui/apex/android_matrix.cpp
@@ -35,3 +35,10 @@ bool AMatrix_getContents(JNIEnv* env, jobject matrixObj, float values[9]) {
}
return false;
}
+
+jobject AMatrix_newInstance(JNIEnv* env, float values[9]) {
+ jobject matrixObj = android::android_graphics_Matrix_newInstance(env);
+ SkMatrix* m = android::android_graphics_Matrix_getSkMatrix(env, matrixObj);
+ m->set9(values);
+ return matrixObj;
+}
diff --git a/libs/hwui/apex/include/android/graphics/matrix.h b/libs/hwui/apex/include/android/graphics/matrix.h
index 987ad13f7635..5705ba485ba3 100644
--- a/libs/hwui/apex/include/android/graphics/matrix.h
+++ b/libs/hwui/apex/include/android/graphics/matrix.h
@@ -34,6 +34,16 @@ __BEGIN_DECLS
*/
ANDROID_API bool AMatrix_getContents(JNIEnv* env, jobject matrixObj, float values[9]);
+/**
+ * Returns a new Matrix jobject that contains the values passed in as initial values.
+ * @param values The 9 values of the 3x3 matrix in the following order.
+ * values[0] = scaleX values[1] = skewX values[2] = transX
+ * values[3] = skewY values[4] = scaleY values[5] = transY
+ * values[6] = persp0 values[7] = persp1 values[8] = persp2
+ * @return The matrix jobject
+ */
+ANDROID_API jobject AMatrix_newInstance(JNIEnv* env, float values[9]);
+
__END_DECLS
#endif // ANDROID_GRAPHICS_MATRIX_H
diff --git a/libs/hwui/jni/android_graphics_Matrix.cpp b/libs/hwui/jni/android_graphics_Matrix.cpp
index 7338ef24cb58..cf6702e45fff 100644
--- a/libs/hwui/jni/android_graphics_Matrix.cpp
+++ b/libs/hwui/jni/android_graphics_Matrix.cpp
@@ -378,13 +378,17 @@ static const JNINativeMethod methods[] = {
{"nEquals", "(JJ)Z", (void*) SkMatrixGlue::equals}
};
+static jclass sClazz;
static jfieldID sNativeInstanceField;
+static jmethodID sCtor;
int register_android_graphics_Matrix(JNIEnv* env) {
int result = RegisterMethodsOrDie(env, "android/graphics/Matrix", methods, NELEM(methods));
jclass clazz = FindClassOrDie(env, "android/graphics/Matrix");
+ sClazz = MakeGlobalRefOrDie(env, clazz);
sNativeInstanceField = GetFieldIDOrDie(env, clazz, "native_instance", "J");
+ sCtor = GetMethodIDOrDie(env, clazz, "<init>", "()V");
return result;
}
@@ -393,4 +397,7 @@ SkMatrix* android_graphics_Matrix_getSkMatrix(JNIEnv* env, jobject matrixObj) {
return reinterpret_cast<SkMatrix*>(env->GetLongField(matrixObj, sNativeInstanceField));
}
+jobject android_graphics_Matrix_newInstance(JNIEnv* env) {
+ return env->NewObject(sClazz, sCtor);
+}
}
diff --git a/libs/hwui/jni/android_graphics_Matrix.h b/libs/hwui/jni/android_graphics_Matrix.h
index fe90d2ef945d..79de48b46954 100644
--- a/libs/hwui/jni/android_graphics_Matrix.h
+++ b/libs/hwui/jni/android_graphics_Matrix.h
@@ -25,6 +25,9 @@ namespace android {
/* Gets the underlying SkMatrix from a Matrix object. */
SkMatrix* android_graphics_Matrix_getSkMatrix(JNIEnv* env, jobject matrixObj);
+/* Creates a new Matrix java object. */
+jobject android_graphics_Matrix_newInstance(JNIEnv* env);
+
} // namespace android
#endif // _ANDROID_GRAPHICS_MATRIX_H_
diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt
index 73de0d12a60b..77b8a44d85a1 100644
--- a/libs/hwui/libhwui.map.txt
+++ b/libs/hwui/libhwui.map.txt
@@ -28,6 +28,7 @@ LIBHWUI {
register_android_graphics_GraphicsStatsService;
zygote_preload_graphics;
AMatrix_getContents;
+ AMatrix_newInstance;
APaint_createPaint;
APaint_destroyPaint;
APaint_setBlendMode;
diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp
index acd8bced0612..d10e68816d28 100644
--- a/libs/input/SpriteController.cpp
+++ b/libs/input/SpriteController.cpp
@@ -153,8 +153,7 @@ void SpriteController::doUpdateSprites() {
|| update.state.surfaceHeight < desiredHeight) {
needApplyTransaction = true;
- t.setSize(update.state.surfaceControl,
- desiredWidth, desiredHeight);
+ update.state.surfaceControl->updateDefaultBufferSize(desiredWidth, desiredHeight);
update.state.surfaceWidth = desiredWidth;
update.state.surfaceHeight = desiredHeight;
update.state.surfaceDrawn = false;