diff options
Diffstat (limited to 'libs')
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 |