summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java119
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java253
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java41
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java74
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java49
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/Android.bp1
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java451
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java78
-rw-r--r--libs/WindowManager/Shell/Android.bp2
-rw-r--r--libs/WindowManager/Shell/res/values-television/config.xml6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/FlingToDismissUtils.kt42
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/OWNERS1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java133
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java119
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java107
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java22
-rw-r--r--libs/WindowManager/Shell/tests/flicker/Android.bp58
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt42
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt99
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt96
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java67
-rw-r--r--libs/androidfw/AssetManager2.cpp9
-rw-r--r--libs/hwui/aconfig/hwui_flags.aconfig7
-rw-r--r--libs/hwui/pipeline/skia/SkiaMemoryTracer.cpp41
-rw-r--r--libs/hwui/pipeline/skia/SkiaMemoryTracer.h15
-rw-r--r--libs/hwui/renderthread/CanvasContext.cpp7
-rw-r--r--libs/hwui/renderthread/HintSessionWrapper.cpp1
-rw-r--r--libs/hwui/tests/unit/HintSessionWrapperTests.cpp59
50 files changed, 1917 insertions, 300 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java
new file mode 100644
index 000000000000..ff49cdcab349
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static java.util.Objects.requireNonNull;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * The parameter to create an overlay container that retrieved from
+ * {@link android.app.ActivityOptions} bundle.
+ */
+class OverlayCreateParams {
+
+ // TODO(b/295803704): Move them to WM Extensions so that we can reuse in WM Jetpack.
+ @VisibleForTesting
+ static final String KEY_OVERLAY_CREATE_PARAMS =
+ "androidx.window.extensions.OverlayCreateParams";
+
+ @VisibleForTesting
+ static final String KEY_OVERLAY_CREATE_PARAMS_TASK_ID =
+ "androidx.window.extensions.OverlayCreateParams.taskId";
+
+ @VisibleForTesting
+ static final String KEY_OVERLAY_CREATE_PARAMS_TAG =
+ "androidx.window.extensions.OverlayCreateParams.tag";
+
+ @VisibleForTesting
+ static final String KEY_OVERLAY_CREATE_PARAMS_BOUNDS =
+ "androidx.window.extensions.OverlayCreateParams.bounds";
+
+ private final int mTaskId;
+
+ @NonNull
+ private final String mTag;
+
+ @NonNull
+ private final Rect mBounds;
+
+ OverlayCreateParams(int taskId, @NonNull String tag, @NonNull Rect bounds) {
+ mTaskId = taskId;
+ mTag = requireNonNull(tag);
+ mBounds = requireNonNull(bounds);
+ }
+
+ int getTaskId() {
+ return mTaskId;
+ }
+
+ @NonNull
+ String getTag() {
+ return mTag;
+ }
+
+ @NonNull
+ Rect getBounds() {
+ return mBounds;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mTaskId;
+ result = 31 * result + mTag.hashCode();
+ result = 31 * result + mBounds.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (!(obj instanceof OverlayCreateParams thatParams)) return false;
+ return mTaskId == thatParams.mTaskId
+ && mTag.equals(thatParams.mTag)
+ && mBounds.equals(thatParams.mBounds);
+ }
+
+ @Override
+ public String toString() {
+ return OverlayCreateParams.class.getSimpleName() + ": {"
+ + "taskId=" + mTaskId
+ + ", tag=" + mTag
+ + ", bounds=" + mBounds
+ + "}";
+ }
+
+ /** Retrieves the {@link OverlayCreateParams} from {@link android.app.ActivityOptions} bundle */
+ @Nullable
+ static OverlayCreateParams fromBundle(@NonNull Bundle bundle) {
+ final Bundle paramsBundle = bundle.getBundle(KEY_OVERLAY_CREATE_PARAMS);
+ if (paramsBundle == null) {
+ return null;
+ }
+ final int taskId = paramsBundle.getInt(KEY_OVERLAY_CREATE_PARAMS_TASK_ID);
+ final String tag = requireNonNull(paramsBundle.getString(KEY_OVERLAY_CREATE_PARAMS_TAG));
+ final Rect bounds = requireNonNull(paramsBundle.getParcelable(
+ KEY_OVERLAY_CREATE_PARAMS_BOUNDS, Rect.class));
+
+ return new OverlayCreateParams(taskId, tag, bounds);
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index cdfc4c87d271..49606f0bf485 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -40,9 +40,10 @@ import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceh
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
+import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions;
import static androidx.window.extensions.embedding.SplitPresenter.getActivitiesMinDimensionsPair;
import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair;
-import static androidx.window.extensions.embedding.SplitPresenter.getTaskWindowMetrics;
+import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit;
import android.app.Activity;
@@ -87,6 +88,7 @@ import androidx.window.extensions.embedding.TransactionManager.TransactionRecord
import androidx.window.extensions.layout.WindowLayoutComponentImpl;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.Collections;
@@ -123,8 +125,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* and unregistered via {@link #clearSplitAttributesCalculator()}.
* This is called when:
* <ul>
- * <li>{@link SplitPresenter#updateSplitContainer(SplitContainer, TaskFragmentContainer,
- * WindowContainerTransaction)}</li>
+ * <li>{@link SplitPresenter#updateSplitContainer}</li>
* <li>There's a started Activity which matches {@link SplitPairRule} </li>
* <li>Checking whether the place holder should be launched if there's a Activity matches
* {@link SplitPlaceholderRule} </li>
@@ -291,8 +292,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Resets the isolated navigation and updates the container.
final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
final WindowContainerTransaction wct = transactionRecord.getTransaction();
- mPresenter.setTaskFragmentIsolatedNavigation(wct,
- containerToUnpin.getTaskFragmentToken(), false /* isolated */);
+ mPresenter.setTaskFragmentIsolatedNavigation(wct, containerToUnpin,
+ false /* isolated */);
updateContainer(wct, containerToUnpin);
transactionRecord.apply(false /* shouldApplyIndependently */);
updateCallbackIfNecessary();
@@ -759,6 +760,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (targetContainer == null) {
// When there is no embedding rule matched, try to place it in the top container
// like a normal launch.
+ // TODO(b/301034784): Check if it makes sense to place the activity in overlay
+ // container.
targetContainer = taskContainer.getTopNonFinishingTaskFragmentContainer();
}
if (targetContainer == null) {
@@ -877,19 +880,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return true;
}
- // Skip resolving if the activity is on a pinned TaskFragmentContainer.
- // TODO(b/243518738): skip resolving for overlay container.
- final TaskContainer taskContainer = container != null ? container.getTaskContainer() : null;
- if (container != null && taskContainer != null
- && taskContainer.isTaskFragmentContainerPinned(container)) {
+ // Skip resolving if the activity is on an isolated navigated TaskFragmentContainer.
+ if (container != null && container.isIsolatedNavigationEnabled()) {
return true;
}
+ final TaskContainer taskContainer = container != null ? container.getTaskContainer() : null;
if (!isOnReparent && taskContainer != null
&& taskContainer.getTopNonFinishingTaskFragmentContainer(false /* includePin */)
!= container) {
// Do not resolve if the launched activity is not the top-most container (excludes
- // the pinned container) in the Task.
+ // the pinned and overlay container) in the Task.
return true;
}
@@ -1007,6 +1008,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (taskContainer == null) {
return;
}
+ // TODO(b/301034784): Check if it makes sense to place the activity in overlay container.
final TaskFragmentContainer targetContainer =
taskContainer.getTopNonFinishingTaskFragmentContainer();
if (targetContainer == null) {
@@ -1166,7 +1168,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity));
if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer()
&& canReuseContainer(splitRule, splitContainer.getSplitRule(),
- getTaskWindowMetrics(taskProperties.getConfiguration()),
+ taskProperties.getTaskMetrics(),
calculatedSplitAttributes, splitContainer.getCurrentSplitAttributes())) {
// Can launch in the existing secondary container if the rules share the same
// presentation.
@@ -1308,15 +1310,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) {
- // Skip resolving if started from pinned TaskFragmentContainer.
- // TODO(b/243518738): skip resolving for overlay container.
+ // Skip resolving if started from an isolated navigated TaskFragmentContainer.
if (launchingActivity != null) {
final TaskFragmentContainer taskFragmentContainer = getContainerWithActivity(
launchingActivity);
- final TaskContainer taskContainer =
- taskFragmentContainer != null ? taskFragmentContainer.getTaskContainer() : null;
- if (taskContainer != null && taskContainer.isTaskFragmentContainerPinned(
- taskFragmentContainer)) {
+ if (taskFragmentContainer != null
+ && taskFragmentContainer.isIsolatedNavigationEnabled()) {
return null;
}
}
@@ -1408,6 +1407,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
private TaskFragmentContainer createEmptyExpandedContainer(
@NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
@Nullable Activity launchingActivity) {
+ return createEmptyContainer(wct, intent, taskId, new Rect(), launchingActivity,
+ null /* overlayTag */);
+ }
+
+ /**
+ * Returns an empty {@link TaskFragmentContainer} that we can launch an activity into.
+ * If {@code overlayTag} is set, it means the created {@link TaskFragmentContainer} is an
+ * overlay container.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ @Nullable
+ TaskFragmentContainer createEmptyContainer(
+ @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
+ @NonNull Rect bounds, @Nullable Activity launchingActivity,
+ @Nullable String overlayTag) {
// We need an activity in the organizer process in the same Task to use as the owner
// activity, as well as to get the Task window info.
final Activity activityInTask;
@@ -1423,13 +1438,46 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Can't find any activity in the Task that we can use as the owner activity.
return null;
}
- final TaskFragmentContainer expandedContainer = newContainer(intent, activityInTask,
- taskId);
- mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(),
- activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED);
- mPresenter.updateAnimationParams(wct, expandedContainer.getTaskFragmentToken(),
+ final TaskFragmentContainer container = newContainer(null /* pendingAppearedActivity */,
+ intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag);
+ final IBinder taskFragmentToken = container.getTaskFragmentToken();
+ // Note that taskContainer will not exist before calling #newContainer if the container
+ // is the first embedded TF in the task.
+ final TaskContainer taskContainer = container.getTaskContainer();
+ final Rect taskBounds = taskContainer.getTaskProperties().getTaskMetrics().getBounds();
+ final Rect sanitizedBounds = sanitizeBounds(bounds, intent, taskBounds);
+ final int windowingMode = taskContainer
+ .getWindowingModeForSplitTaskFragment(sanitizedBounds);
+ mPresenter.createTaskFragment(wct, taskFragmentToken, activityInTask.getActivityToken(),
+ sanitizedBounds, windowingMode);
+ mPresenter.updateAnimationParams(wct, taskFragmentToken,
TaskFragmentAnimationParams.DEFAULT);
- return expandedContainer;
+ mPresenter.setTaskFragmentIsolatedNavigation(wct, taskFragmentToken,
+ overlayTag != null && !sanitizedBounds.isEmpty());
+
+ return container;
+ }
+
+ /**
+ * Returns the expanded bounds if the {@code bounds} violate minimum dimension or are not fully
+ * covered by the task bounds. Otherwise, returns {@code bounds}.
+ */
+ @NonNull
+ private static Rect sanitizeBounds(@NonNull Rect bounds, @NonNull Intent intent,
+ @NonNull Rect taskBounds) {
+ if (bounds.isEmpty()) {
+ // Don't need to check if the bounds follows the task bounds.
+ return bounds;
+ }
+ if (boundsSmallerThanMinDimensions(bounds, getMinDimensions(intent))) {
+ // Expand the bounds if the bounds are smaller than minimum dimensions.
+ return new Rect();
+ }
+ if (!taskBounds.contains(bounds)) {
+ // Expand the bounds if the bounds exceed the task bounds.
+ return new Rect();
+ }
+ return bounds;
}
/**
@@ -1449,8 +1497,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer);
final TaskContainer.TaskProperties taskProperties = mPresenter
.getTaskProperties(primaryActivity);
- final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(
- taskProperties.getConfiguration());
+ final WindowMetrics taskWindowMetrics = taskProperties.getTaskMetrics();
final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes(
taskProperties, splitRule, splitRule.getDefaultSplitAttributes(),
getActivityIntentMinDimensionsPair(primaryActivity, intent));
@@ -1519,14 +1566,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity,
@NonNull Activity activityInTask, int taskId) {
return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */,
- activityInTask, taskId, null /* pairedPrimaryContainer */);
+ activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */);
}
@GuardedBy("mLock")
TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent,
@NonNull Activity activityInTask, int taskId) {
return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
- activityInTask, taskId, null /* pairedPrimaryContainer */);
+ activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */);
+ }
+
+ @GuardedBy("mLock")
+ TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent,
+ @NonNull Activity activityInTask, int taskId,
+ @NonNull TaskFragmentContainer pairedPrimaryContainer) {
+ return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
+ activityInTask, taskId, pairedPrimaryContainer, null /* tag */);
}
/**
@@ -1540,11 +1595,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* @param taskId parent Task of the new TaskFragment.
* @param pairedPrimaryContainer the paired primary {@link TaskFragmentContainer}. When it is
* set, the new container will be added right above it.
+ * @param overlayTag The tag for the new created overlay container. It must be
+ * needed if {@code isOverlay} is {@code true}. Otherwise,
+ * it should be {@code null}.
*/
@GuardedBy("mLock")
TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity,
@Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId,
- @Nullable TaskFragmentContainer pairedPrimaryContainer) {
+ @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag) {
if (activityInTask == null) {
throw new IllegalArgumentException("activityInTask must not be null,");
}
@@ -1553,7 +1611,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
final TaskContainer taskContainer = mTaskContainers.get(taskId);
final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity,
- pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer);
+ pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag);
return container;
}
@@ -1693,31 +1751,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
/**
- * Returns the topmost not finished container in Task of given task id.
- */
- @GuardedBy("mLock")
- @Nullable
- TaskFragmentContainer getTopActiveContainer(int taskId) {
- final TaskContainer taskContainer = mTaskContainers.get(taskId);
- if (taskContainer == null) {
- return null;
- }
- final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers();
- for (int i = containers.size() - 1; i >= 0; i--) {
- final TaskFragmentContainer container = containers.get(i);
- if (!container.isFinished() && (container.getRunningActivityCount() > 0
- // We may be waiting for the top TaskFragment to become non-empty after
- // creation. In that case, we don't want to treat the TaskFragment below it as
- // top active, otherwise it may incorrectly launch placeholder on top of the
- // pending TaskFragment.
- || container.isWaitingActivityAppear())) {
- 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.
*/
@@ -1754,13 +1787,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Updates {@link SplitContainer} with the given {@link SplitAttributes} if the
* {@link SplitContainer} is the top most and not finished. If passed {@link SplitAttributes}
* are {@code null}, the {@link SplitAttributes} will be calculated with
- * {@link SplitPresenter#computeSplitAttributes(TaskContainer.TaskProperties, SplitRule, Pair)}.
+ * {@link SplitPresenter#computeSplitAttributes}.
*
* @param splitContainer The {@link SplitContainer} to update
* @param splitAttributes Update with this {@code splitAttributes} if it is not {@code null}.
* Otherwise, use the value calculated by
- * {@link SplitPresenter#computeSplitAttributes(
- * TaskContainer.TaskProperties, SplitRule, Pair)}
+ * {@link SplitPresenter#computeSplitAttributes}
*
* @return {@code true} if the update succeed. Otherwise, returns {@code false}.
*/
@@ -1906,7 +1938,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
/** Whether or not to allow activity in this container to launch placeholder. */
@GuardedBy("mLock")
private boolean allowLaunchPlaceholder(@NonNull TaskFragmentContainer container) {
- final TaskFragmentContainer topContainer = getTopActiveContainer(container.getTaskId());
+ final TaskFragmentContainer topContainer = container.getTaskContainer()
+ .getTopNonFinishingTaskFragmentContainer();
if (container != topContainer) {
// The container is not the top most.
if (!container.isVisible()) {
@@ -2255,6 +2288,96 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return shouldRetainAssociatedContainer(finishingContainer, associatedContainer);
}
+ /**
+ * Gets all overlay containers from all tasks in this process, or an empty list if there's
+ * no overlay container.
+ * <p>
+ * Note that we only support one overlay container for each task, but an app could have multiple
+ * tasks.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ @NonNull
+ List<TaskFragmentContainer> getAllOverlayTaskFragmentContainers() {
+ final List<TaskFragmentContainer> overlayContainers = new ArrayList<>();
+ for (int i = 0; i < mTaskContainers.size(); i++) {
+ final TaskContainer taskContainer = mTaskContainers.valueAt(i);
+ final TaskFragmentContainer overlayContainer = taskContainer.getOverlayContainer();
+ if (overlayContainer != null) {
+ overlayContainers.add(overlayContainer);
+ }
+ }
+ return overlayContainers;
+ }
+
+ @VisibleForTesting
+ // Suppress GuardedBy warning because lint ask to mark this method as
+ // @GuardedBy(container.mController.mLock), which is mLock itself
+ @SuppressWarnings("GuardedBy")
+ @GuardedBy("mLock")
+ @Nullable
+ TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(
+ @NonNull WindowContainerTransaction wct,
+ @NonNull OverlayCreateParams overlayCreateParams, int launchTaskId,
+ @NonNull Intent intent, @NonNull Activity launchActivity) {
+ final int taskId = overlayCreateParams.getTaskId();
+ if (taskId != launchTaskId) {
+ // The task ID doesn't match the launch activity's. Cannot determine the host task
+ // to launch the overlay.
+ throw new IllegalArgumentException("The task ID of "
+ + "OverlayCreateParams#launchingActivity must match the task ID of "
+ + "the activity to #startActivity with the activity options that takes "
+ + "OverlayCreateParams.");
+ }
+ final List<TaskFragmentContainer> overlayContainers =
+ getAllOverlayTaskFragmentContainers();
+ final String overlayTag = overlayCreateParams.getTag();
+
+ // If the requested bounds of OverlayCreateParams are smaller than minimum dimensions
+ // specified by Intent, expand the overlay container to fill the parent task instead.
+ final Rect bounds = overlayCreateParams.getBounds();
+ final Size minDimensions = getMinDimensions(intent);
+ final boolean shouldExpandContainer = boundsSmallerThanMinDimensions(bounds,
+ minDimensions);
+ if (!overlayContainers.isEmpty()) {
+ for (final TaskFragmentContainer overlayContainer : overlayContainers) {
+ if (!overlayTag.equals(overlayContainer.getOverlayTag())
+ && taskId == overlayContainer.getTaskId()) {
+ // If there's an overlay container with different tag shown in the same
+ // task, dismiss the existing overlay container.
+ overlayContainer.finish(false /* shouldFinishDependant */, mPresenter,
+ wct, SplitController.this);
+ }
+ if (overlayTag.equals(overlayContainer.getOverlayTag())
+ && taskId != overlayContainer.getTaskId()) {
+ // If there's an overlay container with same tag in a different task,
+ // dismiss the overlay container since the tag must be unique per process.
+ overlayContainer.finish(false /* shouldFinishDependant */, mPresenter,
+ wct, SplitController.this);
+ }
+ if (overlayTag.equals(overlayContainer.getOverlayTag())
+ && taskId == overlayContainer.getTaskId()) {
+ // If there's an overlay container with the same tag and task ID, we treat
+ // the OverlayCreateParams as the update to the container.
+ final Rect taskBounds = overlayContainer.getTaskContainer().getTaskProperties()
+ .getTaskMetrics().getBounds();
+ final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
+ final Rect sanitizedBounds = sanitizeBounds(bounds, intent, taskBounds);
+ mPresenter.resizeTaskFragment(wct, overlayToken, sanitizedBounds);
+ mPresenter.setTaskFragmentIsolatedNavigation(wct, overlayToken,
+ !sanitizedBounds.isEmpty());
+ // We can just return the updated overlay container and don't need to
+ // check other condition since we only have one OverlayCreateParams, and
+ // if the tag and task are matched, it's impossible to match another task
+ // or tag since tags and tasks are all unique.
+ return overlayContainer;
+ }
+ }
+ }
+ return createEmptyContainer(wct, intent, taskId,
+ (shouldExpandContainer ? new Rect() : bounds), launchActivity, overlayTag);
+ }
+
private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter {
@Override
@@ -2417,8 +2540,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final TaskFragmentContainer launchedInTaskFragment;
if (launchingActivity != null) {
final int taskId = getTaskId(launchingActivity);
- launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent,
- launchingActivity);
+ final OverlayCreateParams overlayCreateParams =
+ OverlayCreateParams.fromBundle(options);
+ if (Flags.activityEmbeddingOverlayPresentationFlag()
+ && overlayCreateParams != null) {
+ launchedInTaskFragment = createOrUpdateOverlayTaskFragmentIfNeeded(wct,
+ overlayCreateParams, taskId, intent, launchingActivity);
+ } else {
+ launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent,
+ launchingActivity);
+ }
} else {
launchedInTaskFragment = resolveStartActivityIntentFromNonActivityContext(wct,
intent);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index d894487fafb6..faf7c3999402 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -30,12 +30,10 @@ import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
-import android.util.DisplayMetrics;
import android.util.LayoutDirection;
import android.util.Pair;
import android.util.Size;
import android.view.View;
-import android.view.WindowInsets;
import android.view.WindowMetrics;
import android.window.TaskFragmentAnimationParams;
import android.window.TaskFragmentCreationParams;
@@ -307,8 +305,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
}
final int taskId = primaryContainer.getTaskId();
- final TaskFragmentContainer secondaryContainer = mController.newContainer(
- null /* pendingAppearedActivity */, activityIntent, launchingActivity, taskId,
+ final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent,
+ launchingActivity, taskId,
// Pass in the primary container to make sure it is added right above the primary.
primaryContainer);
final TaskContainer taskContainer = mController.getTaskContainer(taskId);
@@ -390,14 +388,27 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
return;
}
- setTaskFragmentIsolatedNavigation(wct, secondaryContainer.getTaskFragmentToken(),
- !isStacked /* isolatedNav */);
+ setTaskFragmentIsolatedNavigation(wct, secondaryContainer, !isStacked /* isolatedNav */);
if (isStacked && !splitPinRule.isSticky()) {
secondaryContainer.getTaskContainer().removeSplitPinContainer();
}
}
/**
+ * Sets whether to enable isolated navigation for this {@link TaskFragmentContainer}
+ */
+ void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer taskFragmentContainer,
+ boolean isolatedNavigationEnabled) {
+ if (taskFragmentContainer.isIsolatedNavigationEnabled() == isolatedNavigationEnabled) {
+ return;
+ }
+ taskFragmentContainer.setIsolatedNavigationEnabled(isolatedNavigationEnabled);
+ setTaskFragmentIsolatedNavigation(wct, taskFragmentContainer.getTaskFragmentToken(),
+ isolatedNavigationEnabled);
+ }
+
+ /**
* Resizes the task fragment if it was already registered. Skips the operation if the container
* creation has not been reported from the server yet.
*/
@@ -618,7 +629,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes,
@Nullable Pair<Size, Size> minDimensionsPair) {
final Configuration taskConfiguration = taskProperties.getConfiguration();
- final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration);
+ final WindowMetrics taskWindowMetrics = taskProperties.getTaskMetrics();
final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
mController.getSplitAttributesCalculator();
final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics);
@@ -713,11 +724,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
return new Size(windowLayout.minWidth, windowLayout.minHeight);
}
- private static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds,
+ static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds,
@Nullable Size minDimensions) {
if (minDimensions == null) {
return false;
}
+ // Empty bounds mean the bounds follow the parent host task's bounds. Skip the check.
+ if (bounds.isEmpty()) {
+ return false;
+ }
return bounds.width() < minDimensions.getWidth()
|| bounds.height() < minDimensions.getHeight();
}
@@ -1066,14 +1081,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull
WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) {
- return getTaskWindowMetrics(getTaskProperties(activity).getConfiguration());
- }
-
- @NonNull
- static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) {
- final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds();
- // TODO(b/190433398): Supply correct insets.
- final float density = taskConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
- return new WindowMetrics(taskBounds, WindowInsets.CONSUMED, density);
+ return getTaskProperties(activity).getTaskMetrics();
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 9a0769a82d99..9e533808ccc0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -30,7 +30,10 @@ import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.IBinder;
import android.util.ArraySet;
+import android.util.DisplayMetrics;
import android.util.Log;
+import android.view.WindowInsets;
+import android.view.WindowMetrics;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentParentInfo;
import android.window.WindowContainerTransaction;
@@ -61,6 +64,10 @@ class TaskContainer {
@Nullable
private SplitPinContainer mSplitPinContainer;
+ /** The overlay container in this Task. */
+ @Nullable
+ private TaskFragmentContainer mOverlayContainer;
+
@NonNull
private final Configuration mConfiguration;
@@ -184,11 +191,20 @@ class TaskContainer {
@Nullable
TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin) {
+ return getTopNonFinishingTaskFragmentContainer(includePin, false /* includeOverlay */);
+ }
+
+ @Nullable
+ TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin,
+ boolean includeOverlay) {
for (int i = mContainers.size() - 1; i >= 0; i--) {
final TaskFragmentContainer container = mContainers.get(i);
if (!includePin && isTaskFragmentContainerPinned(container)) {
continue;
}
+ if (!includeOverlay && container.isOverlay()) {
+ continue;
+ }
if (!container.isFinished()) {
return container;
}
@@ -221,6 +237,12 @@ class TaskContainer {
return null;
}
+ /** Returns the overlay container in the task, or {@code null} if it doesn't exist. */
+ @Nullable
+ TaskFragmentContainer getOverlayContainer() {
+ return mOverlayContainer;
+ }
+
int indexOf(@NonNull TaskFragmentContainer child) {
return mContainers.indexOf(child);
}
@@ -311,8 +333,8 @@ class TaskContainer {
onTaskFragmentContainerUpdated();
}
- void removeTaskFragmentContainers(@NonNull List<TaskFragmentContainer> taskFragmentContainer) {
- mContainers.removeAll(taskFragmentContainer);
+ void removeTaskFragmentContainers(@NonNull List<TaskFragmentContainer> taskFragmentContainers) {
+ mContainers.removeAll(taskFragmentContainers);
onTaskFragmentContainerUpdated();
}
@@ -332,6 +354,15 @@ class TaskContainer {
}
private void onTaskFragmentContainerUpdated() {
+ // TODO(b/300211704): Find a better mechanism to handle the z-order in case we introduce
+ // another special container that should also be on top in the future.
+ updateSplitPinContainerIfNecessary();
+ // Update overlay container after split pin container since the overlay should be on top of
+ // pin container.
+ updateOverlayContainerIfNecessary();
+ }
+
+ private void updateSplitPinContainerIfNecessary() {
if (mSplitPinContainer == null) {
return;
}
@@ -344,10 +375,7 @@ class TaskContainer {
}
// Ensure the pinned container is top-most.
- if (pinnedContainerIndex != mContainers.size() - 1) {
- mContainers.remove(pinnedContainer);
- mContainers.add(pinnedContainer);
- }
+ moveContainerToLastIfNecessary(pinnedContainer);
// Update the primary container adjacent to the pinned container if needed.
final TaskFragmentContainer adjacentContainer =
@@ -359,6 +387,31 @@ class TaskContainer {
}
}
+ private void updateOverlayContainerIfNecessary() {
+ final List<TaskFragmentContainer> overlayContainers = mContainers.stream()
+ .filter(TaskFragmentContainer::isOverlay).toList();
+ if (overlayContainers.size() > 1) {
+ throw new IllegalStateException("There must be at most one overlay container per Task");
+ }
+ mOverlayContainer = overlayContainers.isEmpty() ? null : overlayContainers.get(0);
+ if (mOverlayContainer != null) {
+ moveContainerToLastIfNecessary(mOverlayContainer);
+ }
+ }
+
+ /** Moves the {@code container} to the last to align taskFragments' z-order. */
+ private void moveContainerToLastIfNecessary(@NonNull TaskFragmentContainer container) {
+ final int index = mContainers.indexOf(container);
+ if (index < 0) {
+ Log.w(TAG, "The container:" + container + " is not in the container list!");
+ return;
+ }
+ if (index != mContainers.size() - 1) {
+ mContainers.remove(container);
+ mContainers.add(container);
+ }
+ }
+
/**
* Gets the descriptors of split states in this Task.
*
@@ -398,6 +451,15 @@ class TaskContainer {
return mConfiguration;
}
+ /** A helper method to return task {@link WindowMetrics} from this {@link TaskProperties} */
+ @NonNull
+ WindowMetrics getTaskMetrics() {
+ final Rect taskBounds = mConfiguration.windowConfiguration.getBounds();
+ // TODO(b/190433398): Supply correct insets.
+ final float density = mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+ return new WindowMetrics(taskBounds, WindowInsets.CONSUMED, density);
+ }
+
/** Translates the given absolute bounds to relative bounds in this Task coordinate. */
void translateAbsoluteBoundsToRelativeBounds(@NonNull Rect inOutBounds) {
if (inOutBounds.isEmpty()) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 0a694b5c3b64..3e7f99b96421 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -102,6 +102,9 @@ class TaskFragmentContainer {
*/
private final List<IBinder> mActivitiesToFinishOnExit = new ArrayList<>();
+ @Nullable
+ private final String mOverlayTag;
+
/** Indicates whether the container was cleaned up after the last activity was removed. */
private boolean mIsFinished;
@@ -157,15 +160,32 @@ class TaskFragmentContainer {
*/
private boolean mHasCrossProcessActivities;
+ /** Whether this TaskFragment enable isolated navigation. */
+ private boolean mIsIsolatedNavigationEnabled;
+
+ /**
+ * @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController,
+ * TaskFragmentContainer, String)
+ */
+ TaskFragmentContainer(@Nullable Activity pendingAppearedActivity,
+ @Nullable Intent pendingAppearedIntent,
+ @NonNull TaskContainer taskContainer,
+ @NonNull SplitController controller,
+ @Nullable TaskFragmentContainer pairedPrimaryContainer) {
+ this(pendingAppearedActivity, pendingAppearedIntent, taskContainer,
+ controller, pairedPrimaryContainer, null /* overlayTag */);
+ }
+
/**
* Creates a container with an existing activity that will be re-parented to it in a window
* container transaction.
* @param pairedPrimaryContainer when it is set, the new container will be add right above it
+ * @param overlayTag Sets to indicate this taskFragment is an overlay container
*/
TaskFragmentContainer(@Nullable Activity pendingAppearedActivity,
@Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer,
@NonNull SplitController controller,
- @Nullable TaskFragmentContainer pairedPrimaryContainer) {
+ @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag) {
if ((pendingAppearedActivity == null && pendingAppearedIntent == null)
|| (pendingAppearedActivity != null && pendingAppearedIntent != null)) {
throw new IllegalArgumentException(
@@ -174,6 +194,8 @@ class TaskFragmentContainer {
mController = controller;
mToken = new Binder("TaskFragmentContainer");
mTaskContainer = taskContainer;
+ mOverlayTag = overlayTag;
+
if (pairedPrimaryContainer != null) {
// The TaskFragment will be positioned right above the paired container.
if (pairedPrimaryContainer.getTaskContainer() != taskContainer) {
@@ -786,6 +808,16 @@ class TaskFragmentContainer {
mLastCompanionTaskFragment = fragmentToken;
}
+ /** Returns whether to enable isolated navigation or not. */
+ boolean isIsolatedNavigationEnabled() {
+ return mIsIsolatedNavigationEnabled;
+ }
+
+ /** Sets whether to enable isolated navigation or not. */
+ void setIsolatedNavigationEnabled(boolean isolatedNavigationEnabled) {
+ mIsIsolatedNavigationEnabled = isolatedNavigationEnabled;
+ }
+
/**
* Adds the pending appeared activity that has requested to be launched in this task fragment.
* @see android.app.ActivityClient#isRequestedToLaunchInTaskFragment
@@ -863,6 +895,20 @@ class TaskFragmentContainer {
return mTaskContainer.indexOf(this) > mTaskContainer.indexOf(other);
}
+ /** Returns whether this taskFragment container is an overlay container. */
+ boolean isOverlay() {
+ return mOverlayTag != null;
+ }
+
+ /**
+ * Returns the tag specified in {@link OverlayCreateParams#getTag()}. {@code null} if this
+ * taskFragment container is not an overlay container.
+ */
+ @Nullable
+ String getOverlayTag() {
+ return mOverlayTag;
+ }
+
@Override
public String toString() {
return toString(true /* includeContainersToFinishOnExit */);
@@ -881,6 +927,7 @@ class TaskFragmentContainer {
+ " topNonFinishingActivity=" + getTopNonFinishingActivity()
+ " runningActivityCount=" + getRunningActivityCount()
+ " isFinished=" + mIsFinished
+ + " overlayTag=" + mOverlayTag
+ " lastRequestedBounds=" + mLastRequestedBounds
+ " pendingAppearedActivities=" + mPendingAppearedActivities
+ (includeContainersToFinishOnExit ? " containersToFinishOnExit="
diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
index ed2ff2de245b..4ddbd13978d5 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp
+++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
@@ -36,6 +36,7 @@ android_test {
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
+ "flag-junit",
"mockito-target-extended-minus-junit4",
"truth",
"testables",
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
new file mode 100644
index 000000000000..405f0b2f51dc
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder;
+import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS;
+import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS_BOUNDS;
+import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS_TAG;
+import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS_TASK_ID;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.window.TaskFragmentInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
+import androidx.window.extensions.layout.WindowLayoutComponentImpl;
+import androidx.window.extensions.layout.WindowLayoutInfo;
+
+import com.android.window.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test class for overlay presentation feature.
+ *
+ * Build/Install/Run:
+ * atest WMJetpackUnitTests:OverlayPresentationTest
+ */
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class OverlayPresentationTest {
+
+ @Rule
+ public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
+
+ private static final OverlayCreateParams TEST_OVERLAY_CREATE_PARAMS =
+ new OverlayCreateParams(TASK_ID, "test,", new Rect(0, 0, 200, 200));
+
+ private SplitController.ActivityStartMonitor mMonitor;
+
+ private Intent mIntent;
+
+ private TaskFragmentContainer mOverlayContainer1;
+
+ private TaskFragmentContainer mOverlayContainer2;
+
+ private Activity mActivity;
+ @Mock
+ private Resources mActivityResources;
+
+ @Mock
+ private WindowContainerTransaction mTransaction;
+ @Mock
+ private Handler mHandler;
+ @Mock
+ private WindowLayoutComponentImpl mWindowLayoutComponent;
+
+ private SplitController mSplitController;
+ private SplitPresenter mSplitPresenter;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent)
+ .getCurrentWindowLayoutInfo(anyInt(), any());
+ DeviceStateManagerFoldingFeatureProducer producer =
+ mock(DeviceStateManagerFoldingFeatureProducer.class);
+ mSplitController = new SplitController(mWindowLayoutComponent, producer);
+ mSplitPresenter = mSplitController.mPresenter;
+ mMonitor = mSplitController.getActivityStartMonitor();
+ mIntent = new Intent();
+
+ spyOn(mSplitController);
+ spyOn(mSplitPresenter);
+ spyOn(mMonitor);
+
+ doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean());
+ final Configuration activityConfig = new Configuration();
+ activityConfig.windowConfiguration.setBounds(TASK_BOUNDS);
+ activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
+ doReturn(activityConfig).when(mActivityResources).getConfiguration();
+ doReturn(mHandler).when(mSplitController).getHandler();
+ mActivity = createMockActivity();
+
+ mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG);
+ }
+
+ /** Creates a mock activity in the organizer process. */
+ @NonNull
+ private Activity createMockActivity() {
+ final Activity activity = mock(Activity.class);
+ doReturn(mActivityResources).when(activity).getResources();
+ final IBinder activityToken = new Binder();
+ doReturn(activityToken).when(activity).getActivityToken();
+ doReturn(activity).when(mSplitController).getActivity(activityToken);
+ doReturn(TASK_ID).when(activity).getTaskId();
+ doReturn(new ActivityInfo()).when(activity).getActivityInfo();
+ doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId();
+ return activity;
+ }
+
+ @Test
+ public void testOverlayCreateParamsFromBundle() {
+ assertThat(OverlayCreateParams.fromBundle(new Bundle())).isNull();
+
+ assertThat(OverlayCreateParams.fromBundle(createOverlayCreateParamsTestBundle()))
+ .isEqualTo(TEST_OVERLAY_CREATE_PARAMS);
+ }
+
+ @Test
+ public void testStartActivity_overlayFeatureDisabled_notInvokeCreateOverlayContainer() {
+ mSetFlagRule.disableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG);
+
+ mMonitor.onStartActivity(mActivity, mIntent, createOverlayCreateParamsTestBundle());
+
+ verify(mSplitController, never()).createOrUpdateOverlayTaskFragmentIfNeeded(any(), any(),
+ anyInt(), any(), any());
+ }
+
+ @NonNull
+ private static Bundle createOverlayCreateParamsTestBundle() {
+ final Bundle bundle = new Bundle();
+
+ final Bundle paramsBundle = new Bundle();
+ paramsBundle.putInt(KEY_OVERLAY_CREATE_PARAMS_TASK_ID,
+ TEST_OVERLAY_CREATE_PARAMS.getTaskId());
+ paramsBundle.putString(KEY_OVERLAY_CREATE_PARAMS_TAG, TEST_OVERLAY_CREATE_PARAMS.getTag());
+ paramsBundle.putObject(KEY_OVERLAY_CREATE_PARAMS_BOUNDS,
+ TEST_OVERLAY_CREATE_PARAMS.getBounds());
+
+ bundle.putBundle(KEY_OVERLAY_CREATE_PARAMS, paramsBundle);
+
+ return bundle;
+ }
+
+ @Test
+ public void testGetOverlayContainers() {
+ assertThat(mSplitController.getAllOverlayTaskFragmentContainers()).isEmpty();
+
+ final TaskFragmentContainer overlayContainer1 =
+ createTestOverlayContainer(TASK_ID, "test1");
+
+ assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(overlayContainer1);
+
+ assertThrows(
+ "The exception must throw if there are two overlay containers in the same task.",
+ IllegalStateException.class,
+ () -> createTestOverlayContainer(TASK_ID, "test2"));
+
+ final TaskFragmentContainer overlayContainer3 =
+ createTestOverlayContainer(TASK_ID + 1, "test3");
+
+ assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(overlayContainer1, overlayContainer3);
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_taskIdNotMatch_throwException() {
+ assertThrows("The method must return null due to task mismatch between"
+ + " launchingActivity and OverlayCreateParams", IllegalArgumentException.class,
+ () -> createOrUpdateOverlayTaskFragmentIfNeeded(
+ TEST_OVERLAY_CREATE_PARAMS, TASK_ID + 1));
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_anotherTagInTask_dismissOverlay() {
+ createExistingOverlayContainers();
+
+ final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
+ new OverlayCreateParams(TASK_ID, "test3", new Rect(0, 0, 100, 100)), TASK_ID);
+
+ assertWithMessage("overlayContainer1 must be dismissed since the new overlay container"
+ + " is launched to the same task")
+ .that(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(mOverlayContainer2, overlayContainer);
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_sameTagAnotherTask_dismissOverlay() {
+ createExistingOverlayContainers();
+
+ final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
+ new OverlayCreateParams(TASK_ID + 2, "test1", new Rect(0, 0, 100, 100)),
+ TASK_ID + 2);
+
+ assertWithMessage("overlayContainer1 must be dismissed since the new overlay container"
+ + " is launched with the same tag as an existing overlay container in a different "
+ + "task")
+ .that(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(mOverlayContainer2, overlayContainer);
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_sameTagAndTask_updateOverlay() {
+ createExistingOverlayContainers();
+
+ final Rect bounds = new Rect(0, 0, 100, 100);
+ final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
+ new OverlayCreateParams(TASK_ID, "test1", bounds),
+ TASK_ID);
+
+ assertWithMessage("overlayContainer1 must be updated since the new overlay container"
+ + " is launched with the same tag and task")
+ .that(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(mOverlayContainer1, mOverlayContainer2);
+
+ assertThat(overlayContainer).isEqualTo(mOverlayContainer1);
+ verify(mSplitPresenter).resizeTaskFragment(eq(mTransaction),
+ eq(mOverlayContainer1.getTaskFragmentToken()), eq(bounds));
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_dismissMultipleOverlays() {
+ createExistingOverlayContainers();
+
+ final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
+ new OverlayCreateParams(TASK_ID, "test2", new Rect(0, 0, 100, 100)),
+ TASK_ID);
+
+ // OverlayContainer1 is dismissed since new container is launched in the same task with
+ // different tag. OverlayContainer2 is dismissed since new container is launched with the
+ // same tag in different task.
+ assertWithMessage("overlayContainer1 and overlayContainer2 must be dismissed")
+ .that(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(overlayContainer);
+ }
+
+ private void createExistingOverlayContainers() {
+ mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1");
+ mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2");
+ List<TaskFragmentContainer> overlayContainers = mSplitController
+ .getAllOverlayTaskFragmentContainers();
+ assertThat(overlayContainers).containsExactly(mOverlayContainer1, mOverlayContainer2);
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_smallerThanMinDimens_expandOverlay() {
+ mIntent.setComponent(new ComponentName(ApplicationProvider.getApplicationContext(),
+ MinimumDimensionActivity.class));
+
+ final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
+ TEST_OVERLAY_CREATE_PARAMS, TASK_ID);
+ final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
+
+ assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(overlayContainer);
+ assertThat(overlayContainer.areLastRequestedBoundsEqual(new Rect())).isTrue();
+ verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken,
+ false);
+
+ // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case.
+ clearInvocations(mSplitPresenter);
+ createOrUpdateOverlayTaskFragmentIfNeeded(TEST_OVERLAY_CREATE_PARAMS, TASK_ID);
+
+ verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect());
+ verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken,
+ false);
+ assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(overlayContainer);
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_notInTaskBounds_expandOverlay() {
+ final Rect bounds = new Rect(TASK_BOUNDS);
+ bounds.offset(10, 10);
+ final OverlayCreateParams paramsOutsideTaskBounds = new OverlayCreateParams(TASK_ID,
+ "test", bounds);
+
+ final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
+ paramsOutsideTaskBounds, TASK_ID);
+ final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
+
+ assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(overlayContainer);
+ assertThat(overlayContainer.areLastRequestedBoundsEqual(new Rect())).isTrue();
+ verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken,
+ false);
+
+ // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case.
+ clearInvocations(mSplitPresenter);
+ createOrUpdateOverlayTaskFragmentIfNeeded(paramsOutsideTaskBounds, TASK_ID);
+
+ verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect());
+ verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken,
+ false);
+ assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(overlayContainer);
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_createOverlay() {
+ final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
+ TEST_OVERLAY_CREATE_PARAMS, TASK_ID);
+
+ assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
+ .containsExactly(overlayContainer);
+ assertThat(overlayContainer.getTaskId()).isEqualTo(TASK_ID);
+ assertThat(overlayContainer
+ .areLastRequestedBoundsEqual(TEST_OVERLAY_CREATE_PARAMS.getBounds())).isTrue();
+ assertThat(overlayContainer.getOverlayTag()).isEqualTo(TEST_OVERLAY_CREATE_PARAMS.getTag());
+ }
+
+ @Test
+ public void testGetTopNonFishingTaskFragmentContainerWithOverlay() {
+ final TaskFragmentContainer overlayContainer =
+ createTestOverlayContainer(TASK_ID, "test1");
+
+ // Add a SplitPinContainer, the overlay should be on top
+ final Activity primaryActivity = createMockActivity();
+ final Activity secondaryActivity = createMockActivity();
+
+ final TaskFragmentContainer primaryContainer =
+ createMockTaskFragmentContainer(primaryActivity);
+ final TaskFragmentContainer secondaryContainer =
+ createMockTaskFragmentContainer(secondaryActivity);
+ final SplitPairRule splitPairRule = createSplitPairRuleBuilder(
+ activityActivityPair -> true /* activityPairPredicate */,
+ activityIntentPair -> true /* activityIntentPairPredicate */,
+ parentWindowMetrics -> true /* parentWindowMetricsPredicate */).build();
+ mSplitController.registerSplit(mTransaction, primaryContainer, primaryActivity,
+ secondaryContainer, splitPairRule, splitPairRule.getDefaultSplitAttributes());
+ SplitPinRule splitPinRule = new SplitPinRule.Builder(new SplitAttributes.Builder().build(),
+ parentWindowMetrics -> true /* parentWindowMetricsPredicate */).build();
+ mSplitController.pinTopActivityStack(TASK_ID, splitPinRule);
+ final TaskFragmentContainer topPinnedContainer = mSplitController.getTaskContainer(TASK_ID)
+ .getSplitPinContainer().getSecondaryContainer();
+
+ // Add a normal container after the overlay, the overlay should still on top,
+ // and the SplitPinContainer should also on top of the normal one.
+ final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity);
+
+ final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+
+ assertThat(taskContainer.getTaskFragmentContainers())
+ .containsExactly(primaryContainer, container, secondaryContainer, overlayContainer)
+ .inOrder();
+
+ assertWithMessage("The pinned container must be returned excluding the overlay")
+ .that(taskContainer.getTopNonFinishingTaskFragmentContainer())
+ .isEqualTo(topPinnedContainer);
+
+ assertThat(taskContainer.getTopNonFinishingTaskFragmentContainer(false))
+ .isEqualTo(container);
+
+ assertWithMessage("The overlay container must be returned since it's always on top")
+ .that(taskContainer.getTopNonFinishingTaskFragmentContainer(
+ false /* includePin */, true /* includeOverlay */))
+ .isEqualTo(overlayContainer);
+ }
+
+ /**
+ * A simplified version of {@link SplitController.ActivityStartMonitor
+ * #createOrUpdateOverlayTaskFragmentIfNeeded}
+ */
+ @Nullable
+ private TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(
+ @NonNull OverlayCreateParams params, int taskId) {
+ return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction, params,
+ taskId, mIntent, mActivity);
+ }
+
+ /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
+ @NonNull
+ private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
+ final TaskFragmentContainer container = mSplitController.newContainer(activity,
+ activity.getTaskId());
+ setupTaskFragmentInfo(container, activity);
+ return container;
+ }
+
+ @NonNull
+ private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag) {
+ TaskFragmentContainer overlayContainer = mSplitController.newContainer(
+ null /* pendingAppearedActivity */, mIntent, mActivity, taskId,
+ null /* pairedPrimaryContainer */, tag);
+ setupTaskFragmentInfo(overlayContainer, mActivity);
+ return overlayContainer;
+ }
+
+ private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container,
+ @NonNull Activity activity) {
+ final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity);
+ container.setInfo(mTransaction, info);
+ mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info);
+ }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index d440a3eb95de..96839c57d745 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -48,18 +48,18 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealM
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.google.common.truth.Truth.assertWithMessage;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.clearInvocations;
@@ -175,52 +175,6 @@ public class SplitControllerTest {
}
@Test
- public void testGetTopActiveContainer() {
- final TaskContainer taskContainer = createTestTaskContainer();
- // tf1 has no running activity so is not active.
- final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */,
- new Intent(), taskContainer, mSplitController, null /* pairedPrimaryContainer */);
- // tf2 has running activity so is active.
- final TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class);
- doReturn(1).when(tf2).getRunningActivityCount();
- taskContainer.addTaskFragmentContainer(tf2);
- // tf3 is finished so is not active.
- final TaskFragmentContainer tf3 = mock(TaskFragmentContainer.class);
- doReturn(true).when(tf3).isFinished();
- doReturn(false).when(tf3).isWaitingActivityAppear();
- taskContainer.addTaskFragmentContainer(tf3);
- mSplitController.mTaskContainers.put(TASK_ID, taskContainer);
-
- assertWithMessage("Must return tf2 because tf3 is not active.")
- .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
-
- taskContainer.removeTaskFragmentContainer(tf3);
-
- assertWithMessage("Must return tf2 because tf2 has running activity.")
- .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
-
- taskContainer.removeTaskFragmentContainer(tf2);
-
- assertWithMessage("Must return tf because we are waiting for tf1 to appear.")
- .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
-
- final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
- doReturn(new ArrayList<>()).when(info).getActivities();
- doReturn(true).when(info).isEmpty();
- tf1.setInfo(mTransaction, info);
-
- assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after"
- + " creation.")
- .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
-
- doReturn(false).when(info).isEmpty();
- tf1.setInfo(mTransaction, info);
-
- assertWithMessage("Must return null because tf1 becomes empty.")
- .that(mSplitController.getTopActiveContainer(TASK_ID)).isNull();
- }
-
- @Test
public void testOnTaskFragmentVanished() {
final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken();
@@ -305,7 +259,9 @@ public class SplitControllerTest {
mSplitController.updateContainer(mTransaction, tf);
- verify(mSplitController, never()).getTopActiveContainer(TASK_ID);
+ TaskContainer taskContainer = tf.getTaskContainer();
+ spyOn(taskContainer);
+ verify(taskContainer, never()).getTopNonFinishingTaskFragmentContainer();
// Verify if tf is not in split, dismissPlaceholderIfNecessary won't be called.
doReturn(false).when(mSplitController).shouldContainerBeExpanded(tf);
@@ -320,7 +276,7 @@ public class SplitControllerTest {
doReturn(tf).when(splitContainer).getSecondaryContainer();
doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer();
doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule();
- final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+ taskContainer = mSplitController.getTaskContainer(TASK_ID);
taskContainer.addSplitContainer(splitContainer);
// Add a mock SplitContainer on top of splitContainer
final SplitContainer splitContainer2 = mock(SplitContainer.class);
@@ -595,13 +551,12 @@ public class SplitControllerTest {
}
@Test
- public void testResolveStartActivityIntent_skipIfPinned() {
+ public void testResolveStartActivityIntent_skipIfIsolatedNavEnabled() {
final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity);
- final TaskContainer taskContainer = container.getTaskContainer();
- spyOn(taskContainer);
+ container.setIsolatedNavigationEnabled(true);
+
final Intent intent = new Intent();
setupSplitRule(mActivity, intent);
- doReturn(true).when(taskContainer).isTaskFragmentContainerPinned(container);
assertNull(mSplitController.resolveStartActivityIntent(mTransaction, TASK_ID, intent,
mActivity));
}
@@ -634,7 +589,8 @@ public class SplitControllerTest {
false /* isOnReparent */);
assertFalse(result);
- verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any());
+ verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
+ anyString());
}
@Test
@@ -796,7 +752,8 @@ public class SplitControllerTest {
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any());
+ verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
+ anyString());
verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
}
@@ -838,7 +795,8 @@ public class SplitControllerTest {
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any());
+ verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
+ anyString());
verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
}
@@ -1565,9 +1523,9 @@ public class SplitControllerTest {
addSplitTaskFragments(primaryActivity, thirdActivity);
// Ensure another SplitContainer is added and the pinned TaskFragment still on top
- assertTrue(taskContainer.getSplitContainers().size() == splitContainerCount + +1);
- assertTrue(mSplitController.getTopActiveContainer(TASK_ID).getTopNonFinishingActivity()
- == secondaryActivity);
+ assertEquals(taskContainer.getSplitContainers().size(), splitContainerCount + +1);
+ assertSame(taskContainer.getTopNonFinishingTaskFragmentContainer()
+ .getTopNonFinishingActivity(), secondaryActivity);
}
/** Creates a mock activity in the organizer process. */
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index c72a42cce2bd..fd4522e02438 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -151,6 +151,7 @@ android_library {
static_libs: [
"androidx.appcompat_appcompat",
"androidx.core_core-animation",
+ "androidx.core_core-ktx",
"androidx.arch.core_core-runtime",
"androidx-constraintlayout_constraintlayout",
"androidx.dynamicanimation_dynamicanimation",
@@ -171,4 +172,5 @@ android_library {
kotlincflags: ["-Xjvm-default=all"],
manifest: "AndroidManifest.xml",
plugins: ["dagger2-compiler"],
+ use_resource_processor: true,
}
diff --git a/libs/WindowManager/Shell/res/values-television/config.xml b/libs/WindowManager/Shell/res/values-television/config.xml
index da8abde8407f..8d2e28b9492f 100644
--- a/libs/WindowManager/Shell/res/values-television/config.xml
+++ b/libs/WindowManager/Shell/res/values-television/config.xml
@@ -45,13 +45,13 @@
<integer name="config_pipForceCloseDelay">5000</integer>
<!-- Animation duration when exit starting window: fade out icon -->
- <integer name="starting_window_app_reveal_icon_fade_out_duration">500</integer>
+ <integer name="starting_window_app_reveal_icon_fade_out_duration">200</integer>
<!-- Animation delay when exit starting window: reveal app -->
- <integer name="starting_window_app_reveal_anim_delay">0</integer>
+ <integer name="starting_window_app_reveal_anim_delay">200</integer>
<!-- Animation duration when exit starting window: reveal app -->
- <integer name="starting_window_app_reveal_anim_duration">500</integer>
+ <integer name="starting_window_app_reveal_anim_duration">300</integer>
<!-- Default animation type when hiding the starting window. The possible values are:
- 0 for radial vanish + slide up
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 3790f04b56eb..d08c573736d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.back;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
@@ -70,7 +71,6 @@ import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
-
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -317,7 +317,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
(controller) -> controller.registerAnimation(
BackNavigationInfo.TYPE_RETURN_TO_HOME,
- new BackAnimationRunner(callback, runner)));
+ new BackAnimationRunner(
+ callback,
+ runner,
+ controller.mContext,
+ CUJ_PREDICTIVE_BACK_HOME)));
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index 431df212f099..dc413b059fd7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -19,6 +19,7 @@ package com.android.wm.shell.back;
import static android.view.WindowManager.TRANSIT_OLD_UNSET;
import android.annotation.NonNull;
+import android.content.Context;
import android.os.RemoteException;
import android.util.Log;
import android.view.IRemoteAnimationFinishedCallback;
@@ -27,16 +28,22 @@ import android.view.RemoteAnimationTarget;
import android.window.IBackAnimationRunner;
import android.window.IOnBackInvokedCallback;
+import com.android.internal.jank.InteractionJankMonitor;
+import com.android.wm.shell.common.InteractionJankMonitorUtils;
+
/**
* Used to register the animation callback and runner, it will trigger result if gesture was finish
* before it received IBackAnimationRunner#onAnimationStart, so the controller could continue
* trigger the real back behavior.
*/
public class BackAnimationRunner {
+ private static final int NO_CUJ = -1;
private static final String TAG = "ShellBackPreview";
private final IOnBackInvokedCallback mCallback;
private final IRemoteAnimationRunner mRunner;
+ private final @InteractionJankMonitor.CujType int mCujType;
+ private final Context mContext;
// Whether we are waiting to receive onAnimationStart
private boolean mWaitingAnimation;
@@ -45,9 +52,21 @@ public class BackAnimationRunner {
private boolean mAnimationCancelled;
public BackAnimationRunner(
- @NonNull IOnBackInvokedCallback callback, @NonNull IRemoteAnimationRunner runner) {
+ @NonNull IOnBackInvokedCallback callback,
+ @NonNull IRemoteAnimationRunner runner,
+ @NonNull Context context,
+ @InteractionJankMonitor.CujType int cujType) {
mCallback = callback;
mRunner = runner;
+ mCujType = cujType;
+ mContext = context;
+ }
+
+ public BackAnimationRunner(
+ @NonNull IOnBackInvokedCallback callback,
+ @NonNull IRemoteAnimationRunner runner,
+ @NonNull Context context) {
+ this(callback, runner, context, NO_CUJ);
}
/** Returns the registered animation runner */
@@ -70,10 +89,17 @@ public class BackAnimationRunner {
new IRemoteAnimationFinishedCallback.Stub() {
@Override
public void onAnimationFinished() {
+ if (shouldMonitorCUJ(apps)) {
+ InteractionJankMonitorUtils.endTracing(mCujType);
+ }
finishedCallback.run();
}
};
mWaitingAnimation = false;
+ if (shouldMonitorCUJ(apps)) {
+ InteractionJankMonitorUtils.beginTracing(
+ mCujType, mContext, apps[0].leash, /* tag */ null);
+ }
try {
getRunner().onAnimationStart(TRANSIT_OLD_UNSET, apps, wallpapers,
nonApps, callback);
@@ -82,6 +108,10 @@ public class BackAnimationRunner {
}
}
+ private boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) {
+ return apps.length > 0 && mCujType != NO_CUJ;
+ }
+
void startGesture() {
mWaitingAnimation = true;
mAnimationCancelled = false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
index 114486e848f0..24479d7b5f39 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
@@ -19,6 +19,7 @@ package com.android.wm.shell.back;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
import static com.android.wm.shell.back.BackAnimationConstants.PROGRESS_COMMIT_THRESHOLD;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
@@ -135,7 +136,8 @@ public class CrossActivityAnimation extends ShellBackAnimation {
@Inject
public CrossActivityAnimation(Context context, BackAnimationBackground background) {
mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
+ mBackAnimationRunner = new BackAnimationRunner(
+ new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY);
mBackground = background;
mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
mEnteringProgressSpring.setSpring(new SpringForce()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index 209d8533ee7d..fc5ff017ebe5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -20,6 +20,7 @@ import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.window.BackEvent.EDGE_RIGHT;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_TASK;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
import android.animation.Animator;
@@ -108,6 +109,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
private final float[] mTmpTranslate = {0, 0, 0};
private final BackAnimationRunner mBackAnimationRunner;
private final BackAnimationBackground mBackground;
+ private final Context mContext;
private RemoteAnimationTarget mEnteringTarget;
private RemoteAnimationTarget mClosingTarget;
private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
@@ -120,8 +122,10 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
@Inject
public CrossTaskBackAnimation(Context context, BackAnimationBackground background) {
+ mContext = context;
mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
+ mBackAnimationRunner = new BackAnimationRunner(
+ new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_TASK);
mBackground = background;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
index aca638c1a5cf..5254ff466123 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
@@ -19,6 +19,7 @@ package com.android.wm.shell.back;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
import android.animation.Animator;
@@ -97,7 +98,8 @@ public class CustomizeActivityAnimation extends ShellBackAnimation {
SurfaceControl.Transaction transaction, Choreographer choreographer) {
mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
mBackground = background;
- mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
+ mBackAnimationRunner = new BackAnimationRunner(
+ new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY);
mCustomAnimationLoader = new CustomAnimationLoader(context);
mProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
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 dddcbd4c96c0..f0da35df39ee 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
@@ -317,7 +317,8 @@ public class BubbleController implements ConfigurationChangeListener,
mBubbleIconFactory = new BubbleIconFactory(context,
context.getResources().getDimensionPixelSize(R.dimen.bubble_size),
context.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size),
- context.getResources().getColor(R.color.important_conversation),
+ context.getResources().getColor(
+ com.android.launcher3.icons.R.color.important_conversation),
context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.importance_ring_stroke_width));
mDisplayController = displayController;
@@ -949,7 +950,8 @@ public class BubbleController implements ConfigurationChangeListener,
mBubbleIconFactory = new BubbleIconFactory(mContext,
mContext.getResources().getDimensionPixelSize(R.dimen.bubble_size),
mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size),
- mContext.getResources().getColor(R.color.important_conversation),
+ mContext.getResources().getColor(
+ com.android.launcher3.icons.R.color.important_conversation),
mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.importance_ring_stroke_width));
@@ -988,7 +990,8 @@ public class BubbleController implements ConfigurationChangeListener,
mBubbleIconFactory = new BubbleIconFactory(mContext,
mContext.getResources().getDimensionPixelSize(R.dimen.bubble_size),
mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size),
- mContext.getResources().getColor(R.color.important_conversation),
+ mContext.getResources().getColor(
+ com.android.launcher3.icons.R.color.important_conversation),
mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.importance_ring_stroke_width));
mStackView.onDisplaySizeChanged();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
index dc099d9abda4..22e836aacfc5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
@@ -113,7 +113,7 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl
context,
res.getDimensionPixelSize(R.dimen.bubble_size),
res.getDimensionPixelSize(R.dimen.bubble_badge_size),
- res.getColor(R.color.important_conversation),
+ res.getColor(com.android.launcher3.icons.R.color.important_conversation),
res.getDimensionPixelSize(com.android.internal.R.dimen.importance_ring_stroke_width)
)
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 2241c343a208..ac5ba51ec139 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
@@ -1784,13 +1784,14 @@ public class BubbleStackView extends FrameLayout
mStackOnLeftOrWillBe = mPositioner.isStackOnLeft(startPosition);
mStackAnimationController.setStackPosition(startPosition);
mExpandedAnimationController.setCollapsePoint(startPosition);
- // Set the translation x so that this bubble will animate in from the same side they
- // expand / collapse on.
- bubble.getIconView().setTranslationX(startPosition.x);
} else if (firstBubble) {
mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
}
+ // Set the view translation x so that this bubble will animate in from the same side they
+ // expand / collapse on.
+ bubble.getIconView().setTranslationX(mStackAnimationController.getStackPosition().x);
+
mBubbleContainer.addView(bubble.getIconView(), 0,
new FrameLayout.LayoutParams(mPositioner.getBubbleSize(),
mPositioner.getBubbleSize()));
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 738c94e82a95..79f306ece283 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
@@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles.animation;
import static android.view.View.LAYOUT_DIRECTION_RTL;
import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
+import static com.android.wm.shell.bubbles.animation.FlingToDismissUtils.getFlingToDismissTargetWidth;
import android.content.res.Resources;
import android.graphics.Path;
@@ -375,6 +376,9 @@ public class ExpandedAnimationController
mMagnetizedBubbleDraggingOut.setMagnetListener(listener);
mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ int screenWidthPx = mLayout.getContext().getResources().getDisplayMetrics().widthPixels;
+ mMagnetizedBubbleDraggingOut.setFlingToTargetWidthPercent(
+ getFlingToDismissTargetWidth(screenWidthPx));
}
private void springBubbleTo(View bubble, float x, float y) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/FlingToDismissUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/FlingToDismissUtils.kt
new file mode 100644
index 000000000000..2a44f04f1358
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/FlingToDismissUtils.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles.animation
+
+/** Utils related to the fling to dismiss animation. */
+object FlingToDismissUtils {
+
+ /** The target width surrounding the dismiss target on a small width screen, e.g. phone. */
+ private const val FLING_TO_DISMISS_TARGET_WIDTH_SMALL = 3f
+ /**
+ * The target width surrounding the dismiss target on a medium width screen, e.g. tablet in
+ * portrait.
+ */
+ private const val FLING_TO_DISMISS_TARGET_WIDTH_MEDIUM = 4.5f
+ /**
+ * The target width surrounding the dismiss target on a large width screen, e.g. tablet in
+ * landscape.
+ */
+ private const val FLING_TO_DISMISS_TARGET_WIDTH_LARGE = 6f
+
+ /** Returns the dismiss target width for the specified [screenWidthPx]. */
+ @JvmStatic
+ fun getFlingToDismissTargetWidth(screenWidthPx: Int) = when {
+ screenWidthPx >= 2000 -> FLING_TO_DISMISS_TARGET_WIDTH_LARGE
+ screenWidthPx >= 1500 -> FLING_TO_DISMISS_TARGET_WIDTH_MEDIUM
+ else -> FLING_TO_DISMISS_TARGET_WIDTH_SMALL
+ }
+}
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 aad268394305..e48732801094 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -17,6 +17,7 @@
package com.android.wm.shell.bubbles.animation;
import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
+import static com.android.wm.shell.bubbles.animation.FlingToDismissUtils.getFlingToDismissTargetWidth;
import android.content.ContentResolver;
import android.content.res.Resources;
@@ -851,6 +852,15 @@ public class StackAnimationController extends
if (mLayout != null) {
Resources res = mLayout.getContext().getResources();
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
+ updateFlingToDismissTargetWidth();
+ }
+ }
+
+ private void updateFlingToDismissTargetWidth() {
+ if (mLayout != null && mMagnetizedStack != null) {
+ int screenWidthPx = mLayout.getResources().getDisplayMetrics().widthPixels;
+ mMagnetizedStack.setFlingToTargetWidthPercent(
+ getFlingToDismissTargetWidth(screenWidthPx));
}
}
@@ -1022,23 +1032,8 @@ public class StackAnimationController extends
};
mMagnetizedStack.setHapticsEnabled(true);
mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ updateFlingToDismissTargetWidth();
}
-
- final ContentResolver contentResolver = mLayout.getContext().getContentResolver();
- final float minVelocity = Settings.Secure.getFloat(contentResolver,
- "bubble_dismiss_fling_min_velocity",
- mMagnetizedStack.getFlingToTargetMinVelocity() /* default */);
- final float maxVelocity = Settings.Secure.getFloat(contentResolver,
- "bubble_dismiss_stick_max_velocity",
- mMagnetizedStack.getStickToTargetMaxXVelocity() /* default */);
- final float targetWidth = Settings.Secure.getFloat(contentResolver,
- "bubble_dismiss_target_width_percent",
- mMagnetizedStack.getFlingToTargetWidthPercent() /* default */);
-
- mMagnetizedStack.setFlingToTargetMinVelocity(minVelocity);
- mMagnetizedStack.setStickToTargetMaxXVelocity(maxVelocity);
- mMagnetizedStack.setFlingToTargetWidthPercent(targetWidth);
-
return mMagnetizedStack;
}
@@ -1053,7 +1048,7 @@ public class StackAnimationController extends
* property directly to move the first bubble and cause the stack to 'follow' to the new
* location.
*
- * This could also be achieved by simply animating the first bubble view and adding an update
+ * <p>This could also be achieved by simply animating the first bubble view and adding an update
* listener to dispatch movement to the rest of the stack. However, this would require
* duplication of logic in that update handler - it's simpler to keep all logic contained in the
* {@link #moveFirstBubbleWithStackFollowing} method.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index c51af46accdb..ea7b2e92fefb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -605,6 +605,7 @@ public abstract class WMShellBaseModule {
@Provides
static Transitions provideTransitions(Context context,
ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
ShellController shellController,
ShellTaskOrganizer organizer,
TransactionPool pool,
@@ -612,14 +613,13 @@ public abstract class WMShellBaseModule {
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellAnimationThread ShellExecutor animExecutor,
- ShellCommandHandler shellCommandHandler,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) {
// TODO(b/238217847): Force override shell init if registration is disabled
shellInit = new ShellInit(mainExecutor);
}
- return new Transitions(context, shellInit, shellController, organizer, pool,
- displayController, mainExecutor, mainHandler, animExecutor, shellCommandHandler,
+ return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer,
+ pool, displayController, mainExecutor, mainHandler, animExecutor,
rootTaskDisplayAreaOrganizer);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/OWNERS
new file mode 100644
index 000000000000..74a29ddad073
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/OWNERS
@@ -0,0 +1 @@
+hwwang@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 8a6403705c1c..1898ea737729 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -17,19 +17,28 @@
package com.android.wm.shell.dagger.pip;
import android.annotation.NonNull;
+import android.content.Context;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.dagger.WMShellBaseModule;
import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.pip2.phone.PipController;
import com.android.wm.shell.pip2.phone.PipTransition;
+import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import dagger.Module;
import dagger.Provides;
+import java.util.Optional;
+
/**
* Provides dependencies from {@link com.android.wm.shell.pip2}, this implementation is meant to be
* the successor of its sibling {@link Pip1Module}.
@@ -42,8 +51,26 @@ public abstract class Pip2Module {
@NonNull ShellTaskOrganizer shellTaskOrganizer,
@NonNull Transitions transitions,
PipBoundsState pipBoundsState,
- PipBoundsAlgorithm pipBoundsAlgorithm) {
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ Optional<PipController> pipController) {
return new PipTransition(shellInit, shellTaskOrganizer, transitions, pipBoundsState, null,
pipBoundsAlgorithm);
}
+
+ @WMSingleton
+ @Provides
+ static Optional<PipController> providePipController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
+ PipDisplayLayoutState pipDisplayLayoutState) {
+ if (!PipUtils.isPip2ExperimentEnabled()) {
+ return Optional.empty();
+ } else {
+ return Optional.ofNullable(PipController.create(
+ context, shellInit, shellController, displayController, displayInsetsController,
+ pipDisplayLayoutState));
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
new file mode 100644
index 000000000000..186cb615f4ec
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.view.InsetsState;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipUtils;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+
+/**
+ * Manages the picture-in-picture (PIP) UI and states for Phones.
+ */
+public class PipController implements ConfigurationChangeListener,
+ DisplayController.OnDisplaysChangedListener {
+ private static final String TAG = PipController.class.getSimpleName();
+
+ private Context mContext;
+ private ShellController mShellController;
+ private DisplayController mDisplayController;
+ private DisplayInsetsController mDisplayInsetsController;
+ private PipDisplayLayoutState mPipDisplayLayoutState;
+
+ private PipController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
+ PipDisplayLayoutState pipDisplayLayoutState) {
+ mContext = context;
+ mShellController = shellController;
+ mDisplayController = displayController;
+ mDisplayInsetsController = displayInsetsController;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
+
+ if (PipUtils.isPip2ExperimentEnabled()) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
+ }
+
+ private void onInit() {
+ // Ensure that we have the display info in case we get calls to update the bounds before the
+ // listener calls back
+ mPipDisplayLayoutState.setDisplayId(mContext.getDisplayId());
+ DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay());
+ mPipDisplayLayoutState.setDisplayLayout(layout);
+
+ mShellController.addConfigurationChangeListener(this);
+ mDisplayController.addDisplayWindowListener(this);
+ mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(),
+ new DisplayInsetsController.OnInsetsChangedListener() {
+ @Override
+ public void insetsChanged(InsetsState insetsState) {
+ onDisplayChanged(mDisplayController
+ .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()));
+ }
+ });
+ }
+
+ /**
+ * Instantiates {@link PipController}, returns {@code null} if the feature not supported.
+ */
+ public static PipController create(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
+ PipDisplayLayoutState pipDisplayLayoutState) {
+ if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Device doesn't support Pip feature", TAG);
+ return null;
+ }
+ return new PipController(context, shellInit, shellController, displayController,
+ displayInsetsController, pipDisplayLayoutState);
+ }
+
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfiguration) {
+ mPipDisplayLayoutState.onConfigurationChanged();
+ }
+
+ @Override
+ public void onThemeChanged() {
+ onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()));
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+ if (displayId != mPipDisplayLayoutState.getDisplayId()) {
+ return;
+ }
+ onDisplayChanged(mDisplayController.getDisplayLayout(displayId));
+ }
+
+ @Override
+ public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
+ if (displayId != mPipDisplayLayoutState.getDisplayId()) {
+ return;
+ }
+ onDisplayChanged(mDisplayController.getDisplayLayout(displayId));
+ }
+
+ private void onDisplayChanged(DisplayLayout layout) {
+ mPipDisplayLayoutState.setDisplayLayout(layout);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
new file mode 100644
index 000000000000..f561aa5568be
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+
+import static com.android.wm.shell.transition.Transitions.TransitionObserver;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SingleInstanceRemoteListener;
+import com.android.wm.shell.util.TransitionUtil;
+
+/**
+ * The {@link TransitionObserver} that observes for transitions involving the home
+ * activity. It reports transitions to the caller via {@link IHomeTransitionListener}.
+ */
+public class HomeTransitionObserver implements TransitionObserver,
+ RemoteCallable<HomeTransitionObserver> {
+ private final SingleInstanceRemoteListener<HomeTransitionObserver, IHomeTransitionListener>
+ mListener;
+
+ private @NonNull final Context mContext;
+ private @NonNull final ShellExecutor mMainExecutor;
+ private @NonNull final Transitions mTransitions;
+
+ public HomeTransitionObserver(@NonNull Context context,
+ @NonNull ShellExecutor mainExecutor,
+ @NonNull Transitions transitions) {
+ mContext = context;
+ mMainExecutor = mainExecutor;
+ mTransitions = transitions;
+
+ mListener = new SingleInstanceRemoteListener<>(this,
+ c -> mTransitions.registerObserver(this),
+ c -> mTransitions.unregisterObserver(this));
+
+ }
+
+ @Override
+ public void onTransitionReady(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ for (TransitionInfo.Change change : info.getChanges()) {
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue;
+ }
+
+ final int mode = change.getMode();
+ if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME
+ && TransitionUtil.isOpenOrCloseMode(mode)) {
+ mListener.call(l -> l.onHomeVisibilityChanged(TransitionUtil.isOpeningType(mode)));
+ }
+ }
+ }
+
+ @Override
+ public void onTransitionStarting(@NonNull IBinder transition) {}
+
+ @Override
+ public void onTransitionMerged(@NonNull IBinder merged,
+ @NonNull IBinder playing) {}
+
+ @Override
+ public void onTransitionFinished(@NonNull IBinder transition,
+ boolean aborted) {}
+
+ /**
+ * Sets the home transition listener that receives any transitions resulting in a change of
+ *
+ */
+ public void setHomeTransitionListener(IHomeTransitionListener listener) {
+ if (listener != null) {
+ mListener.register(listener);
+ } else {
+ mListener.unregister();
+ }
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public ShellExecutor getRemoteCallExecutor() {
+ return mMainExecutor;
+ }
+
+ /**
+ * Invalidates this controller, preventing future calls to send updates.
+ */
+ public void invalidate() {
+ mTransitions.unregisterObserver(this);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl
new file mode 100644
index 000000000000..18716c68da27
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import android.window.RemoteTransition;
+import android.window.TransitionFilter;
+
+/**
+ * Listener interface that Launcher attaches to SystemUI to get home activity transition callbacks.
+ */
+interface IHomeTransitionListener {
+
+ /**
+ * Called when a transition changes the visibility of the home activity.
+ */
+ void onHomeVisibilityChanged(in boolean isVisible);
+}
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
index cc4d268a0000..644a6a5114a7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
@@ -19,6 +19,8 @@ package com.android.wm.shell.transition;
import android.window.RemoteTransition;
import android.window.TransitionFilter;
+import com.android.wm.shell.transition.IHomeTransitionListener;
+
/**
* Interface that is exposed to remote callers to manipulate the transitions feature.
*/
@@ -39,4 +41,7 @@ interface IShellTransitions {
* Retrieves the apply-token used by transactions in Shell
*/
IBinder getShellApplyToken() = 3;
+
+ /** Set listener that will receive callbacks about transitions involving home activity */
+ oneway void setHomeTransitionListener(in IHomeTransitionListener listener) = 4;
}
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 8b050e524038..b1fc16ddf19b 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
@@ -63,7 +63,7 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
@NonNull Transitions.TransitionFinishCallback finishCallback) {
if (mTransition != transition) return false;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote"
- + " transition %s for #%d.", mRemote, info.getDebugId());
+ + " transition %s for (#%d).", mRemote, info.getDebugId());
final IBinder.DeathRecipient remoteDied = () -> {
Log.e(Transitions.TAG, "Remote transition died, finishing");
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 592b22a47bc4..ca2c3b4fbff1 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
@@ -126,7 +126,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
}
}
}
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Delegate animation for #%d to %s",
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Delegate animation for (#%d) to %s",
info.getDebugId(), pendingRemote);
if (pendingRemote == null) return false;
@@ -241,7 +241,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
if (remote == null) return null;
mRequestedRemotes.put(transition, remote);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested"
- + " for %s: %s", transition, remote);
+ + " for (#%d) %s: %s", request.getDebugId(), transition, remote);
return new WindowContainerTransaction();
}
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 0d9a9e9f07ff..baa9acaafa4b 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
@@ -179,6 +179,7 @@ public class Transitions implements RemoteCallable<Transitions>,
private final DefaultTransitionHandler mDefaultTransitionHandler;
private final RemoteTransitionHandler mRemoteTransitionHandler;
private final DisplayController mDisplayController;
+ private final ShellCommandHandler mShellCommandHandler;
private final ShellController mShellController;
private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
private final SleepHandler mSleepHandler = new SleepHandler();
@@ -188,9 +189,6 @@ public class Transitions implements RemoteCallable<Transitions>,
/** List of possible handlers. Ordered by specificity (eg. tapped back to front). */
private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>();
- @Nullable
- private final ShellCommandHandler mShellCommandHandler;
-
private final ArrayList<TransitionObserver> mObservers = new ArrayList<>();
/** List of {@link Runnable} instances to run when the last active transition has finished. */
@@ -237,7 +235,7 @@ public class Transitions implements RemoteCallable<Transitions>,
@Override
public String toString() {
if (mInfo != null && mInfo.getDebugId() >= 0) {
- return "(#" + mInfo.getDebugId() + ")" + mToken + "@" + getTrack();
+ return "(#" + mInfo.getDebugId() + ") " + mToken + "@" + getTrack();
}
return mToken.toString() + "@" + getTrack();
}
@@ -275,13 +273,14 @@ public class Transitions implements RemoteCallable<Transitions>,
@NonNull ShellExecutor mainExecutor,
@NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor) {
- this(context, shellInit, shellController, organizer, pool, displayController, mainExecutor,
- mainHandler, animExecutor, null,
+ this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool,
+ displayController, mainExecutor, mainHandler, animExecutor,
new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit));
}
public Transitions(@NonNull Context context,
@NonNull ShellInit shellInit,
+ @Nullable ShellCommandHandler shellCommandHandler,
@NonNull ShellController shellController,
@NonNull WindowOrganizer organizer,
@NonNull TransactionPool pool,
@@ -289,7 +288,6 @@ public class Transitions implements RemoteCallable<Transitions>,
@NonNull ShellExecutor mainExecutor,
@NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor,
- @Nullable ShellCommandHandler shellCommandHandler,
@NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer) {
mOrganizer = organizer;
mContext = context;
@@ -300,13 +298,13 @@ public class Transitions implements RemoteCallable<Transitions>,
mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit,
displayController, pool, mainExecutor, mainHandler, animExecutor, rootTDAOrganizer);
mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
+ mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
// The very last handler (0 in the list) should be the default one.
mHandlers.add(mDefaultTransitionHandler);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default");
// Next lowest priority is remote transitions.
mHandlers.add(mRemoteTransitionHandler);
- mShellCommandHandler = shellCommandHandler;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
shellInit.addInitCallback(this::onInit, this);
}
@@ -339,9 +337,8 @@ public class Transitions implements RemoteCallable<Transitions>,
TransitionMetrics.getInstance();
}
- if (mShellCommandHandler != null) {
- mShellCommandHandler.addCommandCallback("transitions", this, this);
- }
+ mShellCommandHandler.addCommandCallback("transitions", this, this);
+ mShellCommandHandler.addDumpCallback(this::dump, this);
}
public boolean isRegistered() {
@@ -359,7 +356,7 @@ public class Transitions implements RemoteCallable<Transitions>,
}
private ExternalInterfaceBinder createExternalInterface() {
- return new IShellTransitionsImpl(this);
+ return new IShellTransitionsImpl(mContext, getMainExecutor(), this);
}
@Override
@@ -655,8 +652,8 @@ public class Transitions implements RemoteCallable<Transitions>,
void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
info.setUnreleasedWarningCallSiteForAllSurfaces("Transitions.onTransitionReady");
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s",
- transitionToken, info);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady (#%d) %s: %s",
+ info.getDebugId(), transitionToken, info);
final int activeIdx = findByToken(mPendingTransitions, transitionToken);
if (activeIdx < 0) {
throw new IllegalStateException("Got transitionReady for non-pending transition "
@@ -1073,8 +1070,8 @@ public class Transitions implements RemoteCallable<Transitions>,
void requestStartTransition(@NonNull IBinder transitionToken,
@Nullable TransitionRequestInfo request) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: %s %s",
- transitionToken, request);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested (#%d): %s %s",
+ request.getDebugId(), transitionToken, request);
if (isTransitionKnown(transitionToken)) {
throw new RuntimeException("Transition already started " + transitionToken);
}
@@ -1403,9 +1400,12 @@ public class Transitions implements RemoteCallable<Transitions>,
private static class IShellTransitionsImpl extends IShellTransitions.Stub
implements ExternalInterfaceBinder {
private Transitions mTransitions;
+ private final HomeTransitionObserver mHomeTransitionObserver;
- IShellTransitionsImpl(Transitions transitions) {
+ IShellTransitionsImpl(Context context, ShellExecutor executor, Transitions transitions) {
mTransitions = transitions;
+ mHomeTransitionObserver = new HomeTransitionObserver(context, executor,
+ mTransitions);
}
/**
@@ -1413,6 +1413,7 @@ public class Transitions implements RemoteCallable<Transitions>,
*/
@Override
public void invalidate() {
+ mHomeTransitionObserver.invalidate();
mTransitions = null;
}
@@ -1437,6 +1438,14 @@ public class Transitions implements RemoteCallable<Transitions>,
public IBinder getShellApplyToken() {
return SurfaceControl.Transaction.getDefaultApplyToken();
}
+
+ @Override
+ public void setHomeTransitionListener(IHomeTransitionListener listener) {
+ executeRemoteCallWithTaskPermission(mTransitions, "setHomeTransitionListener",
+ (transitions) -> {
+ mHomeTransitionObserver.setHomeTransitionListener(listener);
+ });
+ }
}
private class SettingsObserver extends ContentObserver {
@@ -1475,4 +1484,68 @@ public class Transitions implements RemoteCallable<Transitions>,
pw.println(prefix + "tracing");
mTracer.printShellCommandHelp(pw, prefix + " ");
}
+
+ private void dump(@NonNull PrintWriter pw, String prefix) {
+ pw.println(prefix + TAG);
+
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + "Handlers:");
+ for (TransitionHandler handler : mHandlers) {
+ pw.print(innerPrefix);
+ pw.print(handler.getClass().getSimpleName());
+ pw.println(" (" + Integer.toHexString(System.identityHashCode(handler)) + ")");
+ }
+
+ pw.println(prefix + "Observers:");
+ for (TransitionObserver observer : mObservers) {
+ pw.print(innerPrefix);
+ pw.println(observer.getClass().getSimpleName());
+ }
+
+ pw.println(prefix + "Pending Transitions:");
+ for (ActiveTransition transition : mPendingTransitions) {
+ pw.print(innerPrefix + "token=");
+ pw.println(transition.mToken);
+ pw.print(innerPrefix + "id=");
+ pw.println(transition.mInfo != null
+ ? transition.mInfo.getDebugId()
+ : -1);
+ pw.print(innerPrefix + "handler=");
+ pw.println(transition.mHandler != null
+ ? transition.mHandler.getClass().getSimpleName()
+ : null);
+ }
+ if (mPendingTransitions.isEmpty()) {
+ pw.println(innerPrefix + "none");
+ }
+
+ pw.println(prefix + "Ready-during-sync Transitions:");
+ for (ActiveTransition transition : mReadyDuringSync) {
+ pw.print(innerPrefix + "token=");
+ pw.println(transition.mToken);
+ pw.print(innerPrefix + "id=");
+ pw.println(transition.mInfo != null
+ ? transition.mInfo.getDebugId()
+ : -1);
+ pw.print(innerPrefix + "handler=");
+ pw.println(transition.mHandler != null
+ ? transition.mHandler.getClass().getSimpleName()
+ : null);
+ }
+ if (mReadyDuringSync.isEmpty()) {
+ pw.println(innerPrefix + "none");
+ }
+
+ pw.println(prefix + "Tracks:");
+ for (int i = 0; i < mTracks.size(); i++) {
+ final ActiveTransition transition = mTracks.get(i).mActiveTransition;
+ pw.println(innerPrefix + "Track #" + i);
+ pw.print(innerPrefix + "active=");
+ pw.println(transition);
+ if (transition != null) {
+ pw.print(innerPrefix + "hander=");
+ pw.println(transition.mHandler);
+ }
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 248e83747c48..bb262d3df07f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -116,7 +116,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
this (context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig,
handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
- WindowContainerTransaction::new, new SurfaceControlViewHostFactory() {});
+ WindowContainerTransaction::new, SurfaceControl::new,
+ new SurfaceControlViewHostFactory() {});
}
DesktopModeWindowDecoration(
@@ -133,10 +134,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
+ Supplier<SurfaceControl> surfaceControlSupplier,
SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
super(context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig,
surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
- windowContainerTransactionSupplier, surfaceControlViewHostFactory);
+ windowContainerTransactionSupplier, surfaceControlSupplier,
+ surfaceControlViewHostFactory);
mHandler = handler;
mChoreographer = choreographer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index 389db62ef9d2..dadd264596fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.view.Surface;
import android.view.SurfaceControl;
import android.window.WindowContainerTransaction;
@@ -45,6 +46,7 @@ class FluidResizeTaskPositioner implements DragPositioningCallback {
private final int mDisallowedAreaForEndBoundsHeight;
private boolean mHasDragResized;
private int mCtrlType;
+ @Surface.Rotation private int mRotation;
FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
DisplayController displayController, int disallowedAreaForEndBoundsHeight) {
@@ -78,7 +80,10 @@ class FluidResizeTaskPositioner implements DragPositioningCallback {
mTaskOrganizer.applyTransaction(wct);
}
mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
- if (mStableBounds.isEmpty()) {
+ int rotation = mWindowDecoration
+ .mTaskInfo.configuration.windowConfiguration.getDisplayRotation();
+ if (mStableBounds.isEmpty() || mRotation != rotation) {
+ mRotation = rotation;
mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId())
.getStableBounds(mStableBounds);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 303954a2d9fb..852c037baad5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -21,6 +21,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.IBinder;
+import android.view.Surface;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
@@ -58,6 +59,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback,
private final int mDisallowedAreaForEndBoundsHeight;
private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
private int mCtrlType;
+ @Surface.Rotation private int mRotation;
public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
DesktopModeWindowDecoration windowDecoration, DisplayController displayController,
@@ -98,7 +100,10 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback,
}
mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
- if (mStableBounds.isEmpty()) {
+ int rotation = mDesktopWindowDecoration
+ .mTaskInfo.configuration.windowConfiguration.getDisplayRotation();
+ if (mStableBounds.isEmpty() || mRotation != rotation) {
+ mRotation = rotation;
mDisplayController.getDisplayLayout(mDesktopWindowDecoration.mDisplay.getDisplayId())
.getStableBounds(mStableBounds);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 0548a8e751cc..044c0331282c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.view.WindowInsets.Type.statusBars;
+import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.WindowConfiguration.WindowingMode;
import android.content.Context;
@@ -137,7 +138,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
Configuration windowDecorConfig) {
this(context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
- WindowContainerTransaction::new, new SurfaceControlViewHostFactory() {});
+ WindowContainerTransaction::new, SurfaceControl::new,
+ new SurfaceControlViewHostFactory() {});
}
WindowDecoration(
@@ -145,17 +147,18 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
DisplayController displayController,
ShellTaskOrganizer taskOrganizer,
RunningTaskInfo taskInfo,
- SurfaceControl taskSurface,
+ @NonNull SurfaceControl taskSurface,
Configuration windowDecorConfig,
Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
+ Supplier<SurfaceControl> surfaceControlSupplier,
SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
mContext = context;
mDisplayController = displayController;
mTaskOrganizer = taskOrganizer;
mTaskInfo = taskInfo;
- mTaskSurface = taskSurface;
+ mTaskSurface = cloneSurfaceControl(taskSurface, surfaceControlSupplier);
mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier;
mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
mWindowContainerTransactionSupplier = windowContainerTransactionSupplier;
@@ -297,7 +300,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
}
// Task surface itself
- float shadowRadius = loadDimension(resources, params.mShadowRadiusId);
+ float shadowRadius;
final Point taskPosition = mTaskInfo.positionInParent;
if (isFullscreen) {
// Setting the task crop to the width/height stops input events from being sent to
@@ -308,9 +311,12 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
// drag-resized by the window decoration.
startT.setWindowCrop(mTaskSurface, null);
finishT.setWindowCrop(mTaskSurface, null);
+ // Shadow is not needed for fullscreen tasks
+ shadowRadius = 0;
} else {
startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight);
finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight);
+ shadowRadius = loadDimension(resources, params.mShadowRadiusId);
}
startT.setShadowRadius(mTaskSurface, shadowRadius)
.show(mTaskSurface);
@@ -450,6 +456,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
public void close() {
mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener);
releaseViews();
+ mTaskSurface.release();
}
static int loadDimensionPixelSize(Resources resources, int resourceId) {
@@ -466,6 +473,13 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
return resources.getDimension(resourceId);
}
+ private static SurfaceControl cloneSurfaceControl(SurfaceControl sc,
+ Supplier<SurfaceControl> surfaceControlSupplier) {
+ final SurfaceControl copy = surfaceControlSupplier.get();
+ copy.copyFrom(sc, "WindowDecoration");
+ return copy;
+ }
+
/**
* Create a window associated with this WindowDecoration.
* Note that subclass must dispose of this when the task is hidden/closed.
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index 7f020725d61f..acfb259d7359 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -39,7 +39,25 @@ filegroup {
}
filegroup {
- name: "WMShellFlickerTestsPip-src",
+ name: "WMShellFlickerTestsPip1-src",
+ srcs: [
+ "src/com/android/wm/shell/flicker/pip/A*.kt",
+ "src/com/android/wm/shell/flicker/pip/B*.kt",
+ "src/com/android/wm/shell/flicker/pip/C*.kt",
+ "src/com/android/wm/shell/flicker/pip/D*.kt",
+ "src/com/android/wm/shell/flicker/pip/S*.kt",
+ ],
+}
+
+filegroup {
+ name: "WMShellFlickerTestsPip2-src",
+ srcs: [
+ "src/com/android/wm/shell/flicker/pip/E*.kt",
+ ],
+}
+
+filegroup {
+ name: "WMShellFlickerTestsPip3-src",
srcs: ["src/com/android/wm/shell/flicker/pip/*.kt"],
}
@@ -176,7 +194,9 @@ android_test {
],
exclude_srcs: [
":WMShellFlickerTestsBubbles-src",
- ":WMShellFlickerTestsPip-src",
+ ":WMShellFlickerTestsPip1-src",
+ ":WMShellFlickerTestsPip2-src",
+ ":WMShellFlickerTestsPip3-src",
":WMShellFlickerTestsPipCommon-src",
":WMShellFlickerTestsPipApps-src",
":WMShellFlickerTestsSplitScreenGroup1-src",
@@ -200,19 +220,49 @@ android_test {
}
android_test {
- name: "WMShellFlickerTestsPip",
+ name: "WMShellFlickerTestsPip1",
+ defaults: ["WMShellFlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestPip.xml"],
+ package_name: "com.android.wm.shell.flicker.pip",
+ instrumentation_target_package: "com.android.wm.shell.flicker.pip",
+ srcs: [
+ ":WMShellFlickerTestsBase-src",
+ ":WMShellFlickerTestsPip1-src",
+ ":WMShellFlickerTestsPipCommon-src",
+ ],
+}
+
+android_test {
+ name: "WMShellFlickerTestsPip2",
defaults: ["WMShellFlickerTestsDefault"],
additional_manifests: ["manifests/AndroidManifestPip.xml"],
package_name: "com.android.wm.shell.flicker.pip",
instrumentation_target_package: "com.android.wm.shell.flicker.pip",
srcs: [
":WMShellFlickerTestsBase-src",
- ":WMShellFlickerTestsPip-src",
+ ":WMShellFlickerTestsPip2-src",
":WMShellFlickerTestsPipCommon-src",
],
}
android_test {
+ name: "WMShellFlickerTestsPip3",
+ defaults: ["WMShellFlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestPip.xml"],
+ package_name: "com.android.wm.shell.flicker.pip",
+ instrumentation_target_package: "com.android.wm.shell.flicker.pip",
+ srcs: [
+ ":WMShellFlickerTestsBase-src",
+ ":WMShellFlickerTestsPip3-src",
+ ":WMShellFlickerTestsPipCommon-src",
+ ],
+ exclude_srcs: [
+ ":WMShellFlickerTestsPip1-src",
+ ":WMShellFlickerTestsPip2-src",
+ ],
+}
+
+android_test {
name: "WMShellFlickerTestsPipApps",
defaults: ["WMShellFlickerTestsDefault"],
additional_manifests: ["manifests/AndroidManifestPip.xml"],
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
index 19c8435facaf..94e3959782ed 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -22,6 +22,7 @@ import android.tools.device.flicker.legacy.FlickerBuilder
import android.tools.device.flicker.legacy.LegacyFlickerTest
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.pip.common.EnterPipTransition
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
@@ -55,7 +56,7 @@ import org.junit.runners.Parameterized
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) :
- EnterPipViaAppUiButtonTest(flicker) {
+ EnterPipTransition(flicker) {
override val thisTransition: FlickerBuilder.() -> Unit = { transitions { tapl.goHome() } }
override val defaultEnterPip: FlickerBuilder.() -> Unit = {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt
index 2cd08a4a58a6..596580547d59 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt
@@ -26,6 +26,7 @@ import android.tools.device.flicker.legacy.FlickerBuilder
import android.tools.device.flicker.legacy.LegacyFlickerTest
import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
@@ -66,8 +67,10 @@ open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransit
standardAppHelper.launchViaIntent(
wmHelper,
NetflixAppHelper.getNetflixWatchVideoIntent("70184207"),
- ComponentNameMatcher(NetflixAppHelper.PACKAGE_NAME,
- NetflixAppHelper.WATCH_ACTIVITY)
+ ComponentNameMatcher(
+ NetflixAppHelper.PACKAGE_NAME,
+ NetflixAppHelper.WATCH_ACTIVITY
+ )
)
standardAppHelper.waitForVideoPlaying()
}
@@ -99,6 +102,41 @@ open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransit
super.focusChanges()
}
+ @Postsubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() {
+ Assume.assumeTrue(flicker.scenario.isTablet)
+ // Netflix starts in immersive fullscreen mode, so taskbar bar is not visible at start
+ flicker.assertLayersStart { this.isInvisible(ComponentNameMatcher.TASK_BAR) }
+ flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) }
+ }
+
+ @Postsubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() {
+ // Netflix plays in immersive fullscreen mode, so taskbar will be gone at some point
+ }
+
+ @Postsubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() {
+ // Netflix starts in immersive fullscreen mode, so status bar is not visible at start
+ flicker.assertLayersStart { this.isInvisible(ComponentNameMatcher.STATUS_BAR) }
+ flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
+ }
+
+ @Postsubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() {
+ // Netflix starts in immersive fullscreen mode, so status bar is not visible at start
+ flicker.statusBarLayerPositionAtEnd()
+ }
+
+ @Postsubmit
+ @Test override fun statusBarWindowIsAlwaysVisible() {
+ // Netflix plays in immersive fullscreen mode, so taskbar will be gone at some point
+ }
+
companion object {
/**
* Creates the test configurations.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index e7d0f601ff5a..d6141cfd21ba 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -568,8 +568,12 @@ public class BackAnimationControllerTest extends ShellTestCase {
}
private void registerAnimation(int type) {
- mController.registerAnimation(type,
- new BackAnimationRunner(mAnimatorCallback, mBackAnimationRunner));
+ mController.registerAnimation(
+ type,
+ new BackAnimationRunner(
+ mAnimatorCallback,
+ mBackAnimationRunner,
+ mContext));
}
private void unregisterAnimation(int type) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index a2dbab197fb5..18fcdd00df9d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -111,7 +111,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mConfiguration,
mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
SurfaceControl.Builder::new, mMockTransactionSupplier,
- WindowContainerTransaction::new, mMockSurfaceControlViewHostFactory);
+ WindowContainerTransaction::new, SurfaceControl::new,
+ mMockSurfaceControlViewHostFactory);
}
private ActivityManager.RunningTaskInfo createTaskInfo(boolean visible) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index c0c4498e3ebf..add78b2ee8b3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -6,6 +6,9 @@ import android.graphics.Rect
import android.os.IBinder
import android.testing.AndroidTestingRunner
import android.view.Display
+import android.view.Surface
+import android.view.Surface.ROTATION_270
+import android.view.Surface.ROTATION_90
import android.view.SurfaceControl
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
@@ -24,6 +27,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
+import org.mockito.Mockito
import org.mockito.Mockito.any
import org.mockito.Mockito.argThat
import org.mockito.Mockito.never
@@ -76,7 +80,15 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
- (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+ if (mockWindowDecoration.mTaskInfo.configuration.windowConfiguration
+ .displayRotation == ROTATION_90 ||
+ mockWindowDecoration.mTaskInfo.configuration.windowConfiguration
+ .displayRotation == ROTATION_270
+ ) {
+ (i.arguments.first() as Rect).set(STABLE_BOUNDS_LANDSCAPE)
+ } else {
+ (i.arguments.first() as Rect).set(STABLE_BOUNDS_PORTRAIT)
+ }
}
`when`(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS)
`when`(mockTransactionFactory.get()).thenReturn(mockTransaction)
@@ -89,6 +101,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
defaultMinSize = DEFAULT_MIN
displayId = DISPLAY_ID
configuration.windowConfiguration.setBounds(STARTING_BOUNDS)
+ configuration.windowConfiguration.displayRotation = ROTATION_90
}
mockWindowDecoration.mDisplay = mockDisplay
whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
@@ -623,7 +636,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
)
val newX = STARTING_BOUNDS.left.toFloat()
- val newY = STABLE_BOUNDS.top.toFloat() - 5
+ val newY = STABLE_BOUNDS_LANDSCAPE.top.toFloat() - 5
taskPositioner.onDragPositioningMove(
newX,
newY
@@ -641,9 +654,81 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
token == taskBinder &&
(change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
change.configuration.windowConfiguration.bounds.top ==
- STABLE_BOUNDS.top
+ STABLE_BOUNDS_LANDSCAPE.top
+ }
+ })
+ }
+
+ @Test
+ fun testDragResize_drag_updatesStableBoundsOnRotate() {
+ // Test landscape stable bounds
+ performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
+ STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000,
+ CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM)
+ val rectAfterDrag = Rect(STARTING_BOUNDS)
+ rectAfterDrag.right += 2000
+ // First drag; we should fetch stable bounds.
+ verify(mockDisplayLayout, Mockito.times(1)).getStableBounds(any())
+ verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterDrag
+ }
+ })
+ // Drag back to starting bounds.
+ performDrag(
+ STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat(),
+ STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
+ CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM)
+
+ // Display did not rotate; we should use previous stable bounds
+ verify(mockDisplayLayout, Mockito.times(1)).getStableBounds(any())
+
+ // Rotate the screen to portrait
+ mockWindowDecoration.mTaskInfo.apply {
+ configuration.windowConfiguration.displayRotation = Surface.ROTATION_0
+ }
+ // Test portrait stable bounds
+ performDrag(
+ STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
+ STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000,
+ CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM)
+ rectAfterDrag.right -= 2000
+ rectAfterDrag.bottom += 2000
+
+ verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterDrag
}
})
+ // Display has rotated; we expect a new stable bounds.
+ verify(mockDisplayLayout, Mockito.times(2)).getStableBounds(any())
+ }
+
+ private fun performDrag(
+ startX: Float,
+ startY: Float,
+ endX: Float,
+ endY: Float,
+ ctrlType: Int
+ ) {
+ taskPositioner.onDragPositioningStart(
+ ctrlType,
+ startX,
+ startY
+ )
+ taskPositioner.onDragPositioningMove(
+ endX,
+ endY
+ )
+
+ taskPositioner.onDragPositioningEnd(
+ endX,
+ endY
+ )
}
companion object {
@@ -664,11 +749,17 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT,
DISPLAY_BOUNDS.right,
DISPLAY_BOUNDS.bottom)
- private val STABLE_BOUNDS = Rect(
+ private val STABLE_BOUNDS_LANDSCAPE = Rect(
DISPLAY_BOUNDS.left,
DISPLAY_BOUNDS.top + CAPTION_HEIGHT,
DISPLAY_BOUNDS.right,
DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT
)
+ private val STABLE_BOUNDS_PORTRAIT = Rect(
+ DISPLAY_BOUNDS.top,
+ DISPLAY_BOUNDS.left + CAPTION_HEIGHT,
+ DISPLAY_BOUNDS.bottom,
+ DISPLAY_BOUNDS.right - NAVBAR_HEIGHT
+ )
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 8913453aa578..a70ebf14324a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -21,6 +21,9 @@ import android.graphics.Rect
import android.os.IBinder
import android.testing.AndroidTestingRunner
import android.view.Display
+import android.view.Surface.ROTATION_0
+import android.view.Surface.ROTATION_270
+import android.view.Surface.ROTATION_90
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
import android.window.WindowContainerToken
@@ -30,6 +33,7 @@ import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED
@@ -93,10 +97,17 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
- (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+ if (mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration
+ .displayRotation == ROTATION_90 ||
+ mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration
+ .displayRotation == ROTATION_270
+ ) {
+ (i.arguments.first() as Rect).set(STABLE_BOUNDS_LANDSCAPE)
+ } else {
+ (i.arguments.first() as Rect).set(STABLE_BOUNDS_PORTRAIT)
+ }
}
`when`(mockTransactionFactory.get()).thenReturn(mockTransaction)
-
mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
taskId = TASK_ID
token = taskToken
@@ -105,6 +116,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
defaultMinSize = DEFAULT_MIN
displayId = DISPLAY_ID
configuration.windowConfiguration.setBounds(STARTING_BOUNDS)
+ configuration.windowConfiguration.displayRotation = ROTATION_90
}
mockDesktopWindowDecoration.mDisplay = mockDisplay
whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
@@ -343,7 +355,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
)
val newX = STARTING_BOUNDS.left.toFloat()
- val newY = STABLE_BOUNDS.top.toFloat() - 5
+ val newY = STABLE_BOUNDS_LANDSCAPE.top.toFloat() - 5
taskPositioner.onDragPositioningMove(
newX,
newY
@@ -361,11 +373,79 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
token == taskBinder &&
(change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
change.configuration.windowConfiguration.bounds.top ==
- STABLE_BOUNDS.top
+ STABLE_BOUNDS_LANDSCAPE.top
}
})
}
+ @Test
+ fun testDragResize_drag_updatesStableBoundsOnRotate() {
+ // Test landscape stable bounds
+ performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
+ STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000,
+ CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM)
+ val rectAfterDrag = Rect(STARTING_BOUNDS)
+ rectAfterDrag.right += 2000
+ // First drag; we should fetch stable bounds.
+ verify(mockDisplayLayout, times(1)).getStableBounds(any())
+ verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterDrag}},
+ eq(taskPositioner))
+ // Drag back to starting bounds.
+ performDrag(STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat(),
+ STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
+ CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM)
+
+ // Display did not rotate; we should use previous stable bounds
+ verify(mockDisplayLayout, times(1)).getStableBounds(any())
+
+ // Rotate the screen to portrait
+ mockDesktopWindowDecoration.mTaskInfo.apply {
+ configuration.windowConfiguration.displayRotation = ROTATION_0
+ }
+ // Test portrait stable bounds
+ performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
+ STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000,
+ CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM)
+ rectAfterDrag.right -= 2000
+ rectAfterDrag.bottom += 2000
+
+ verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterDrag}},
+ eq(taskPositioner))
+ // Display has rotated; we expect a new stable bounds.
+ verify(mockDisplayLayout, times(2)).getStableBounds(any())
+ }
+
+ private fun performDrag(
+ startX: Float,
+ startY: Float,
+ endX: Float,
+ endY: Float,
+ ctrlType: Int
+ ) {
+ taskPositioner.onDragPositioningStart(
+ ctrlType,
+ startX,
+ startY
+ )
+ taskPositioner.onDragPositioningMove(
+ endX,
+ endY
+ )
+
+ taskPositioner.onDragPositioningEnd(
+ endX,
+ endY
+ )
+ }
+
companion object {
private const val TASK_ID = 5
private const val MIN_WIDTH = 10
@@ -378,11 +458,17 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
private const val DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT = 10
private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
private val STARTING_BOUNDS = Rect(100, 100, 200, 200)
- private val STABLE_BOUNDS = Rect(
+ private val STABLE_BOUNDS_LANDSCAPE = Rect(
DISPLAY_BOUNDS.left,
DISPLAY_BOUNDS.top + CAPTION_HEIGHT,
DISPLAY_BOUNDS.right,
DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT
)
+ private val STABLE_BOUNDS_PORTRAIT = Rect(
+ DISPLAY_BOUNDS.top,
+ DISPLAY_BOUNDS.left + CAPTION_HEIGHT,
+ DISPLAY_BOUNDS.bottom,
+ DISPLAY_BOUNDS.right - NAVBAR_HEIGHT
+ )
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 8061aa3f844a..8e42f74b8d17 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -121,6 +121,8 @@ public class WindowDecorationTests extends ShellTestCase {
private WindowContainerTransaction mMockWindowContainerTransaction;
@Mock
private SurfaceSyncGroup mMockSurfaceSyncGroup;
+ @Mock
+ private SurfaceControl mMockTaskSurface;
private final List<SurfaceControl.Transaction> mMockSurfaceControlTransactions =
new ArrayList<>();
@@ -189,8 +191,7 @@ public class WindowDecorationTests extends ShellTestCase {
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo);
@@ -199,7 +200,7 @@ public class WindowDecorationTests extends ShellTestCase {
verify(captionContainerSurfaceBuilder, never()).build();
verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
- verify(mMockSurfaceControlFinishT).hide(taskSurface);
+ verify(mMockSurfaceControlFinishT).hide(mMockTaskSurface);
assertNull(mRelayoutResult.mRootView);
}
@@ -229,12 +230,11 @@ public class WindowDecorationTests extends ShellTestCase {
taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo);
- verify(decorContainerSurfaceBuilder).setParent(taskSurface);
+ verify(decorContainerSurfaceBuilder).setParent(mMockTaskSurface);
verify(decorContainerSurfaceBuilder).setContainerLayer();
verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true);
verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 300, 100);
@@ -262,14 +262,15 @@ public class WindowDecorationTests extends ShellTestCase {
}
verify(mMockSurfaceControlFinishT)
- .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y);
+ .setPosition(mMockTaskSurface, TASK_POSITION_IN_PARENT.x,
+ TASK_POSITION_IN_PARENT.y);
verify(mMockSurfaceControlFinishT)
- .setWindowCrop(taskSurface, 300, 100);
- verify(mMockSurfaceControlStartT).setCornerRadius(taskSurface, CORNER_RADIUS);
- verify(mMockSurfaceControlFinishT).setCornerRadius(taskSurface, CORNER_RADIUS);
+ .setWindowCrop(mMockTaskSurface, 300, 100);
+ verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
+ verify(mMockSurfaceControlFinishT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
verify(mMockSurfaceControlStartT)
- .show(taskSurface);
- verify(mMockSurfaceControlStartT).setShadowRadius(taskSurface, 10);
+ .show(mMockTaskSurface);
+ verify(mMockSurfaceControlStartT).setShadowRadius(mMockTaskSurface, 10);
assertEquals(300, mRelayoutResult.mWidth);
assertEquals(100, mRelayoutResult.mHeight);
@@ -308,8 +309,7 @@ public class WindowDecorationTests extends ShellTestCase {
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
mWindowConfiguration.densityDpi = taskInfo.configuration.densityDpi;
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo);
@@ -346,8 +346,7 @@ public class WindowDecorationTests extends ShellTestCase {
.setVisible(true)
.build();
- final TestWindowDecoration windowDecor =
- createWindowDecoration(taskInfo, new SurfaceControl());
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo);
// It shouldn't show the window decoration when it can't obtain the display instance.
@@ -405,8 +404,7 @@ public class WindowDecorationTests extends ShellTestCase {
.build();
taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo);
final SurfaceControl additionalWindowSurface = mock(SurfaceControl.class);
@@ -465,8 +463,7 @@ public class WindowDecorationTests extends ShellTestCase {
.build();
taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo);
@@ -506,8 +503,7 @@ public class WindowDecorationTests extends ShellTestCase {
.build();
taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */);
@@ -545,12 +541,11 @@ public class WindowDecorationTests extends ShellTestCase {
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
taskInfo.isFocused = true;
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo);
- verify(mMockSurfaceControlStartT).setColor(taskSurface, new float[]{1.f, 1.f, 0.f});
+ verify(mMockSurfaceControlStartT).setColor(mMockTaskSurface, new float[]{1.f, 1.f, 0.f});
mockitoSession.finishMocking();
}
@@ -568,8 +563,7 @@ public class WindowDecorationTests extends ShellTestCase {
.setTaskDescriptionBuilder(taskDescriptionBuilder)
.setVisible(true)
.build();
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
assertTrue(mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, statusBars())
.isVisible());
@@ -613,12 +607,11 @@ public class WindowDecorationTests extends ShellTestCase {
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
.build();
taskInfo.isFocused = true;
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo);
- verify(mMockSurfaceControlStartT).unsetColor(taskSurface);
+ verify(mMockSurfaceControlStartT).unsetColor(mMockTaskSurface);
mockitoSession.finishMocking();
}
@@ -639,8 +632,7 @@ public class WindowDecorationTests extends ShellTestCase {
.setTaskDescriptionBuilder(taskDescriptionBuilder)
.setVisible(true)
.build();
- final SurfaceControl taskSurface = mock(SurfaceControl.class);
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
windowDecor.relayout(taskInfo);
@@ -650,15 +642,15 @@ public class WindowDecorationTests extends ShellTestCase {
eq(0) /* index */, eq(mandatorySystemGestures()));
}
- private TestWindowDecoration createWindowDecoration(
- ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
+ private TestWindowDecoration createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo) {
return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
- taskInfo, testSurface, mWindowConfiguration,
+ taskInfo, mMockTaskSurface, mWindowConfiguration,
new MockObjectSupplier<>(mMockSurfaceControlBuilders,
() -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))),
new MockObjectSupplier<>(mMockSurfaceControlTransactions,
() -> mock(SurfaceControl.Transaction.class)),
- () -> mMockWindowContainerTransaction, mMockSurfaceControlViewHostFactory);
+ () -> mMockWindowContainerTransaction, () -> mMockTaskSurface,
+ mMockSurfaceControlViewHostFactory);
}
private class MockObjectSupplier<T> implements Supplier<T> {
@@ -697,11 +689,12 @@ public class WindowDecorationTests extends ShellTestCase {
Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
+ Supplier<SurfaceControl> surfaceControlSupplier,
SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
super(context, displayController, taskOrganizer, taskInfo, taskSurface,
windowConfiguration, surfaceControlBuilderSupplier,
surfaceControlTransactionSupplier, windowContainerTransactionSupplier,
- surfaceControlViewHostFactory);
+ surfaceControlSupplier, surfaceControlViewHostFactory);
}
@Override
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index 7f226939ffaa..d056248273b2 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -785,7 +785,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry(
has_locale = true;
}
- // if we don't have a result yet
+ // if we don't have a result yet
if (!final_result ||
// or this config is better before the locale than the existing result
result->config.isBetterThanBeforeLocale(final_result->config, desired_config) ||
@@ -863,9 +863,12 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal(
// We can skip calling ResTable_config::match() if the caller does not care for the
// configuration to match or if we're using the list of types that have already had their
- // configuration matched.
+ // configuration matched. The exception to this is when the user has multiple locales set
+ // because the filtered list will then have values from multiple locales and we will need to
+ // call match() to make sure the current entry matches the config we are currently checking.
const ResTable_config& this_config = type_entry->config;
- if (!(use_filtered || ignore_configuration || this_config.match(desired_config))) {
+ if (!((use_filtered && (configurations_.size() == 1))
+ || ignore_configuration || this_config.match(desired_config))) {
continue;
}
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index e672b983d509..e986c38a845a 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -27,3 +27,10 @@ flag {
description: "APIs to create a new gainmap with a bitmap for metadata."
bug: "304478551"
}
+
+flag {
+ name: "clip_surfaceviews"
+ namespace: "core_graphics"
+ description: "Clip z-above surfaceviews to global clip rect"
+ bug: "298621623"
+}
diff --git a/libs/hwui/pipeline/skia/SkiaMemoryTracer.cpp b/libs/hwui/pipeline/skia/SkiaMemoryTracer.cpp
index 10427039c35a..814b682a2b98 100644
--- a/libs/hwui/pipeline/skia/SkiaMemoryTracer.cpp
+++ b/libs/hwui/pipeline/skia/SkiaMemoryTracer.cpp
@@ -32,13 +32,13 @@ SkiaMemoryTracer::SkiaMemoryTracer(const char* categoryKey, bool itemizeType)
, mTotalSize("bytes", 0)
, mPurgeableSize("bytes", 0) {}
-const char* SkiaMemoryTracer::mapName(const char* resourceName) {
+std::optional<std::string> SkiaMemoryTracer::mapName(const std::string& resourceName) {
for (auto& resource : mResourceMap) {
- if (SkStrContains(resourceName, resource.first)) {
+ if (resourceName.find(resource.first) != std::string::npos) {
return resource.second;
}
}
- return nullptr;
+ return std::nullopt;
}
void SkiaMemoryTracer::processElement() {
@@ -62,7 +62,7 @@ void SkiaMemoryTracer::processElement() {
}
// find the type if one exists
- const char* type;
+ std::string type;
auto typeResult = mCurrentValues.find("type");
if (typeResult != mCurrentValues.end()) {
type = typeResult->second.units;
@@ -71,14 +71,13 @@ void SkiaMemoryTracer::processElement() {
}
// compute the type if we are itemizing or use the default "size" if we are not
- const char* key = (mItemizeType) ? type : sizeResult->first;
- SkASSERT(key != nullptr);
+ std::string key = (mItemizeType) ? type : sizeResult->first;
// compute the top level element name using either the map or category key
- const char* resourceName = mapName(mCurrentElement.c_str());
- if (mCategoryKey != nullptr) {
+ std::optional<std::string> resourceName = mapName(mCurrentElement);
+ if (mCategoryKey) {
// find the category if one exists
- auto categoryResult = mCurrentValues.find(mCategoryKey);
+ auto categoryResult = mCurrentValues.find(*mCategoryKey);
if (categoryResult != mCurrentValues.end()) {
resourceName = categoryResult->second.units;
} else if (mItemizeType) {
@@ -87,11 +86,11 @@ void SkiaMemoryTracer::processElement() {
}
// if we don't have a pretty name then use the dumpName
- if (resourceName == nullptr) {
- resourceName = mCurrentElement.c_str();
+ if (!resourceName) {
+ resourceName = mCurrentElement;
}
- auto result = mResults.find(resourceName);
+ auto result = mResults.find(*resourceName);
if (result != mResults.end()) {
auto& resourceValues = result->second;
typeResult = resourceValues.find(key);
@@ -106,7 +105,7 @@ void SkiaMemoryTracer::processElement() {
TraceValue sizeValue = sizeResult->second;
mCurrentValues.clear();
mCurrentValues.insert({key, sizeValue});
- mResults.insert({resourceName, mCurrentValues});
+ mResults.insert({*resourceName, mCurrentValues});
}
}
@@ -139,8 +138,9 @@ void SkiaMemoryTracer::logOutput(String8& log) {
for (const auto& typedValue : namedItem.second) {
TraceValue traceValue = convertUnits(typedValue.second);
const char* entry = (traceValue.count > 1) ? "entries" : "entry";
- log.appendFormat(" %s: %.2f %s (%d %s)\n", typedValue.first, traceValue.value,
- traceValue.units, traceValue.count, entry);
+ log.appendFormat(" %s: %.2f %s (%d %s)\n", typedValue.first.c_str(),
+ traceValue.value, traceValue.units.c_str(), traceValue.count,
+ entry);
}
} else {
auto result = namedItem.second.find("size");
@@ -148,7 +148,8 @@ void SkiaMemoryTracer::logOutput(String8& log) {
TraceValue traceValue = convertUnits(result->second);
const char* entry = (traceValue.count > 1) ? "entries" : "entry";
log.appendFormat(" %s: %.2f %s (%d %s)\n", namedItem.first.c_str(),
- traceValue.value, traceValue.units, traceValue.count, entry);
+ traceValue.value, traceValue.units.c_str(), traceValue.count,
+ entry);
}
}
}
@@ -156,7 +157,7 @@ void SkiaMemoryTracer::logOutput(String8& log) {
size_t SkiaMemoryTracer::total() {
processElement();
- if (!strcmp("bytes", mTotalSize.units)) {
+ if ("bytes" == mTotalSize.units) {
return mTotalSize.value;
}
return 0;
@@ -166,16 +167,16 @@ void SkiaMemoryTracer::logTotals(String8& log) {
TraceValue total = convertUnits(mTotalSize);
TraceValue purgeable = convertUnits(mPurgeableSize);
log.appendFormat(" %.0f bytes, %.2f %s (%.2f %s is purgeable)\n", mTotalSize.value,
- total.value, total.units, purgeable.value, purgeable.units);
+ total.value, total.units.c_str(), purgeable.value, purgeable.units.c_str());
}
SkiaMemoryTracer::TraceValue SkiaMemoryTracer::convertUnits(const TraceValue& value) {
TraceValue output(value);
- if (SkString("bytes") == SkString(output.units) && output.value >= 1024) {
+ if ("bytes" == output.units && output.value >= 1024) {
output.value = output.value / 1024.0f;
output.units = "KB";
}
- if (SkString("KB") == SkString(output.units) && output.value >= 1024) {
+ if ("KB" == output.units && output.value >= 1024) {
output.value = output.value / 1024.0f;
output.units = "MB";
}
diff --git a/libs/hwui/pipeline/skia/SkiaMemoryTracer.h b/libs/hwui/pipeline/skia/SkiaMemoryTracer.h
index cba3b0422c6f..dbfc86bc033c 100644
--- a/libs/hwui/pipeline/skia/SkiaMemoryTracer.h
+++ b/libs/hwui/pipeline/skia/SkiaMemoryTracer.h
@@ -16,8 +16,9 @@
#pragma once
-#include <SkString.h>
#include <SkTraceMemoryDump.h>
+#include <optional>
+#include <string>
#include <utils/String8.h>
#include <unordered_map>
#include <vector>
@@ -60,17 +61,17 @@ private:
TraceValue(const char* units, uint64_t value) : units(units), value(value), count(1) {}
TraceValue(const TraceValue& v) : units(v.units), value(v.value), count(v.count) {}
- const char* units;
+ std::string units;
float value;
int count;
};
- const char* mapName(const char* resourceName);
+ std::optional<std::string> mapName(const std::string& resourceName);
void processElement();
TraceValue convertUnits(const TraceValue& value);
const std::vector<ResourcePair> mResourceMap;
- const char* mCategoryKey = nullptr;
+ std::optional<std::string> mCategoryKey;
const bool mItemizeType;
// variables storing the size of all elements being dumped
@@ -79,12 +80,12 @@ private:
// variables storing information on the current node being dumped
std::string mCurrentElement;
- std::unordered_map<const char*, TraceValue> mCurrentValues;
+ std::unordered_map<std::string, TraceValue> mCurrentValues;
// variable that stores the final format of the data after the individual elements are processed
- std::unordered_map<std::string, std::unordered_map<const char*, TraceValue>> mResults;
+ std::unordered_map<std::string, std::unordered_map<std::string, TraceValue>> mResults;
};
} /* namespace skiapipeline */
} /* namespace uirenderer */
-} /* namespace android */ \ No newline at end of file
+} /* namespace android */
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index b00fc699e515..7fac0c9776ac 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -139,6 +139,7 @@ CanvasContext::~CanvasContext() {
mRenderNodes.clear();
mRenderThread.cacheManager().unregisterCanvasContext(this);
mRenderThread.renderState().removeContextCallback(this);
+ mHintSessionWrapper->destroy();
}
void CanvasContext::addRenderNode(RenderNode* node, bool placeFront) {
@@ -561,7 +562,11 @@ Frame CanvasContext::getFrame() {
void CanvasContext::draw(bool solelyTextureViewUpdates) {
if (auto grContext = getGrContext()) {
if (grContext->abandoned()) {
- LOG_ALWAYS_FATAL("GrContext is abandoned/device lost at start of CanvasContext::draw");
+ if (grContext->isDeviceLost()) {
+ LOG_ALWAYS_FATAL("Lost GPU device unexpectedly");
+ return;
+ }
+ LOG_ALWAYS_FATAL("GrContext is abandoned at start of CanvasContext::draw");
return;
}
}
diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp
index 1c3399a6650d..2362331aca26 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.cpp
+++ b/libs/hwui/renderthread/HintSessionWrapper.cpp
@@ -158,7 +158,6 @@ void HintSessionWrapper::sendLoadResetHint() {
void HintSessionWrapper::sendLoadIncreaseHint() {
if (!init()) return;
mBinding->sendHint(mHintSession, static_cast<int32_t>(SessionHint::CPU_LOAD_UP));
- mLastFrameNotification = systemTime();
}
bool HintSessionWrapper::alive() {
diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
index a14ae1cc46ec..10a740a1f803 100644
--- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
+++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
@@ -259,6 +259,31 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesAfterAsyncCreationFinishe
TEST_F(HintSessionWrapperTests, delayedDeletionDoesNotKillReusedSession) {
EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(0);
+ EXPECT_CALL(*sMockBinding, fakeReportActualWorkDuration(sessionPtr, 5_ms)).Times(1);
+
+ mWrapper->init();
+ waitForWrapperReady();
+ // Init a second time just to grab the wrapper from the promise
+ mWrapper->init();
+ EXPECT_EQ(mWrapper->alive(), true);
+
+ // First schedule the deletion
+ scheduleDelayedDestroyManaged();
+
+ // Then, report an actual duration
+ mWrapper->reportActualWorkDuration(5_ms);
+
+ // Then, run the delayed deletion after sending the update
+ allowDelayedDestructionToStart();
+ waitForDelayedDestructionToFinish();
+
+ // Ensure it didn't close within the timeframe of the test
+ Mock::VerifyAndClearExpectations(sMockBinding.get());
+ EXPECT_EQ(mWrapper->alive(), true);
+}
+
+TEST_F(HintSessionWrapperTests, loadUpDoesNotResetDeletionTimer) {
+ EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
EXPECT_CALL(*sMockBinding,
fakeSendHint(sessionPtr, static_cast<int32_t>(SessionHint::CPU_LOAD_UP)))
.Times(1);
@@ -272,16 +297,46 @@ TEST_F(HintSessionWrapperTests, delayedDeletionDoesNotKillReusedSession) {
// First schedule the deletion
scheduleDelayedDestroyManaged();
- // Then, send a hint to update the timestamp
+ // Then, send a load_up hint
mWrapper->sendLoadIncreaseHint();
// Then, run the delayed deletion after sending the update
allowDelayedDestructionToStart();
waitForDelayedDestructionToFinish();
- // Ensure it didn't close within the timeframe of the test
+ // Ensure it closed within the timeframe of the test
Mock::VerifyAndClearExpectations(sMockBinding.get());
+ EXPECT_EQ(mWrapper->alive(), false);
+}
+
+TEST_F(HintSessionWrapperTests, manualSessionDestroyPlaysNiceWithDelayedDestruct) {
+ EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
+
+ mWrapper->init();
+ waitForWrapperReady();
+ // Init a second time just to grab the wrapper from the promise
+ mWrapper->init();
EXPECT_EQ(mWrapper->alive(), true);
+
+ // First schedule the deletion
+ scheduleDelayedDestroyManaged();
+
+ // Then, kill the session
+ mWrapper->destroy();
+
+ // Verify it died
+ Mock::VerifyAndClearExpectations(sMockBinding.get());
+ EXPECT_EQ(mWrapper->alive(), false);
+
+ EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(0);
+
+ // Then, run the delayed deletion after manually killing the session
+ allowDelayedDestructionToStart();
+ waitForDelayedDestructionToFinish();
+
+ // Ensure it didn't close again and is still dead
+ Mock::VerifyAndClearExpectations(sMockBinding.get());
+ EXPECT_EQ(mWrapper->alive(), false);
}
} // namespace android::uirenderer::renderthread \ No newline at end of file