diff options
Diffstat (limited to 'libs')
277 files changed, 10325 insertions, 3736 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index 16c77d0c3c81..ecf47209a802 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -24,6 +24,7 @@ import android.app.Application; import android.app.compat.CompatChanges; import android.content.Context; import android.hardware.devicestate.DeviceStateManager; +import android.os.SystemProperties; import android.util.Log; import androidx.annotation.NonNull; @@ -50,6 +51,11 @@ class WindowExtensionsImpl implements WindowExtensions { private static final String TAG = "WindowExtensionsImpl"; /** + * The value of the system property that indicates no override is set. + */ + private static final int NO_LEVEL_OVERRIDE = -1; + + /** * The min version of the WM Extensions that must be supported in the current platform version. */ @VisibleForTesting @@ -66,14 +72,30 @@ class WindowExtensionsImpl implements WindowExtensions { WindowExtensionsImpl() { mIsActivityEmbeddingEnabled = isActivityEmbeddingEnabled(); - Log.i(TAG, "Initializing Window Extensions, vendor API level=" + mVersion - + ", activity embedding enabled=" + mIsActivityEmbeddingEnabled); + + Log.i(TAG, generateLogMessage()); + } + + private String generateLogMessage() { + final StringBuilder logBuilder = new StringBuilder("Initializing Window Extensions, " + + "vendor API level=" + mVersion); + final int levelOverride = getLevelOverride(); + if (levelOverride != NO_LEVEL_OVERRIDE) { + logBuilder.append(", override to ").append(levelOverride); + } + logBuilder.append(", activity embedding enabled=").append(mIsActivityEmbeddingEnabled); + return logBuilder.toString(); } // TODO(b/241126279) Introduce constants to better version functionality @Override public int getVendorApiLevel() { - return mVersion; + final int levelOverride = getLevelOverride(); + return (levelOverride != NO_LEVEL_OVERRIDE) ? levelOverride : mVersion; + } + + private int getLevelOverride() { + return SystemProperties.getInt("persist.wm.debug.ext_version_override", NO_LEVEL_OVERRIDE); } @NonNull diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index 23dc96c39bde..822a07c23950 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -16,6 +16,9 @@ package androidx.window.extensions.embedding; +import static android.content.pm.ActivityInfo.CONFIG_DENSITY; +import static android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION; +import static android.content.pm.ActivityInfo.CONFIG_WINDOW_CONFIGURATION; import static android.util.TypedValue.COMPLEX_UNIT_DIP; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; @@ -40,7 +43,6 @@ import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityThread; import android.content.Context; -import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.PixelFormat; @@ -167,6 +169,11 @@ class DividerPresenter implements View.OnTouchListener { @GuardedBy("mLock") private int mDividerPosition; + /** Indicates if there are containers to be finished since the divider has appeared. */ + @GuardedBy("mLock") + @VisibleForTesting + private boolean mHasContainersToFinish = false; + DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback, @NonNull Executor callbackExecutor) { mTaskId = taskId; @@ -178,7 +185,8 @@ class DividerPresenter implements View.OnTouchListener { void updateDivider( @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentParentInfo parentInfo, - @Nullable SplitContainer topSplitContainer) { + @Nullable SplitContainer topSplitContainer, + boolean isTaskFragmentVanished) { if (!Flags.activityEmbeddingInteractiveDividerFlag()) { return; } @@ -186,6 +194,18 @@ class DividerPresenter implements View.OnTouchListener { synchronized (mLock) { // Clean up the decor surface if top SplitContainer is null. if (topSplitContainer == null) { + // Check if there are containers to finish but the TaskFragment hasn't vanished yet. + // Don't remove the decor surface and divider if so as the removal should happen in + // a following step when the TaskFragment has vanished. This ensures that the decor + // surface is removed only after the resulting Activity is ready to be shown, + // otherwise there may be flicker. + if (mHasContainersToFinish) { + if (isTaskFragmentVanished) { + setHasContainersToFinish(false); + } else { + return; + } + } removeDecorSurfaceAndDivider(wct); return; } @@ -683,52 +703,72 @@ class DividerPresenter implements View.OnTouchListener { ? taskBounds.width() - mProperties.mDividerWidthPx : taskBounds.height() - mProperties.mDividerWidthPx; - if (isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)) { - final float displayDensity = getDisplayDensity(); - return dividerPositionWithDraggingToFullscreenAllowed( - dividerPosition, - minPosition, - maxPosition, - fullyExpandedPosition, - velocity, - displayDensity); - } - return Math.clamp(dividerPosition, minPosition, maxPosition); + final float displayDensity = getDisplayDensity(); + final boolean isDraggingToFullscreenAllowed = + isDraggingToFullscreenAllowed(mProperties.mDividerAttributes); + return dividerPositionWithPositionOptions( + dividerPosition, + minPosition, + maxPosition, + fullyExpandedPosition, + velocity, + displayDensity, + isDraggingToFullscreenAllowed); } /** - * Returns the divider position given a set of position options. A snap algorithm is used to - * adjust the ending position to either fully expand one container or move the divider back to - * the specified min/max ratio depending on the dragging velocity. + * Returns the divider position given a set of position options. A snap algorithm can adjust + * the ending position to either fully expand one container or move the divider back to + * the specified min/max ratio depending on the dragging velocity and if dragging to fullscreen + * is allowed. */ @VisibleForTesting - static int dividerPositionWithDraggingToFullscreenAllowed(int dividerPosition, int minPosition, - int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity) { - final float minDismissVelocityPxPerSecond = - MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity; + static int dividerPositionWithPositionOptions(int dividerPosition, int minPosition, + int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity, + boolean isDraggingToFullscreenAllowed) { + if (isDraggingToFullscreenAllowed) { + final float minDismissVelocityPxPerSecond = + MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity; + if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) { + return 0; + } + if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) { + return fullyExpandedPosition; + } + } final float minFlingVelocityPxPerSecond = MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity; - if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) { - return 0; + if (Math.abs(velocity) >= minFlingVelocityPxPerSecond) { + return dividerPositionForFling( + dividerPosition, minPosition, maxPosition, velocity); } - if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) { - return fullyExpandedPosition; + if (dividerPosition >= minPosition && dividerPosition <= maxPosition) { + return dividerPosition; } - if (Math.abs(velocity) < minFlingVelocityPxPerSecond) { - if (dividerPosition >= minPosition && dividerPosition <= maxPosition) { - return dividerPosition; - } - int[] possiblePositions = {0, minPosition, maxPosition, fullyExpandedPosition}; - return snap(dividerPosition, possiblePositions); - } - if (velocity < 0) { - return minPosition; + return snap( + dividerPosition, + isDraggingToFullscreenAllowed + ? new int[] {0, minPosition, maxPosition, fullyExpandedPosition} + : new int[] {minPosition, maxPosition}); + } + + /** + * Returns the closest position that is in the fling direction. + */ + private static int dividerPositionForFling(int dividerPosition, int minPosition, + int maxPosition, float velocity) { + final boolean isBackwardDirection = velocity < 0; + if (isBackwardDirection) { + return dividerPosition < maxPosition ? minPosition : maxPosition; } else { - return maxPosition; + return dividerPosition > minPosition ? maxPosition : minPosition; } } - /** Calculates the snapped divider position based on the possible positions and distance. */ + /** + * Returns the snapped position from a list of possible positions. Currently, this method + * snaps to the closest position by distance from the divider position. + */ private static int snap(int dividerPosition, int[] possiblePositions) { int snappedPosition = dividerPosition; float minDistance = Float.MAX_VALUE; @@ -846,6 +886,12 @@ class DividerPresenter implements View.OnTouchListener { } } + void setHasContainersToFinish(boolean hasContainersToFinish) { + synchronized (mLock) { + mHasContainersToFinish = hasContainersToFinish; + } + } + private static boolean isDraggingToFullscreenAllowed( @NonNull DividerAttributes dividerAttributes) { // TODO(b/293654166) Use DividerAttributes.isDraggingToFullscreenAllowed when extension is @@ -946,7 +992,7 @@ class DividerPresenter implements View.OnTouchListener { @VisibleForTesting static class Properties { private static final int CONFIGURATION_MASK_FOR_DIVIDER = - ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_WINDOW_CONFIGURATION; + CONFIG_DENSITY | CONFIG_WINDOW_CONFIGURATION | CONFIG_LAYOUT_DIRECTION; @NonNull private final Configuration mConfiguration; @NonNull @@ -1215,6 +1261,12 @@ class DividerPresenter implements View.OnTouchListener { FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, PixelFormat.TRANSLUCENT); lp.setTitle(WINDOW_NAME); + + // Ensure that the divider layout is always LTR regardless of the locale, because we + // already considered the locale when determining the split layout direction and the + // computed divider line position always starts from the left. This only affects the + // horizontal layout and does not have any effect on the top-to-bottom layout. + mDividerLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); mViewHost.setView(mDividerLayout, lp); mViewHost.relayout(lp); } @@ -1366,10 +1418,16 @@ class DividerPresenter implements View.OnTouchListener { primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop; secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom; } - t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height()); - t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height()); - t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top); - t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top); + if (mPrimaryVeil != null) { + t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height()); + t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top); + t.setVisibility(mPrimaryVeil, !primaryBounds.isEmpty()); + } + if (mSecondaryVeil != null) { + t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height()); + t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top); + t.setVisibility(mSecondaryVeil, !secondaryBounds.isEmpty()); + } } private static float[] colorToFloatArray(@NonNull Color color) { 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 f78e2b5170fc..7ddda1f98809 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -673,7 +673,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen break; case TYPE_TASK_FRAGMENT_VANISHED: mPresenter.removeTaskFragmentInfo(info); - onTaskFragmentVanished(wct, info); + onTaskFragmentVanished(wct, info, taskId); break; case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED: onTaskFragmentParentInfoChanged(wct, taskId, @@ -834,7 +834,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @VisibleForTesting @GuardedBy("mLock") void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct, - @NonNull TaskFragmentInfo taskFragmentInfo) { + @NonNull TaskFragmentInfo taskFragmentInfo, int taskId) { final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); if (container != null) { // Cleanup if the TaskFragment vanished is not requested by the organizer. @@ -843,6 +843,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen updateContainersInTaskIfVisible(wct, container.getTaskId()); } cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer != null) { + // Update the divider to clean up any decor surfaces. + updateDivider(wct, taskContainer, true /* isTaskFragmentVanished */); + } } /** @@ -884,7 +889,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // The divider need to be updated even if shouldUpdateContainer is false, because the decor // surface may change in TaskFragmentParentInfo, which requires divider update but not // container update. - updateDivider(wct, taskContainer); + updateDivider(wct, taskContainer, false /* isTaskFragmentVanished */); // If the last direct activity of the host task is dismissed and there's an always-on-top // overlay container in the task, the overlay container should also be dismissed. @@ -899,14 +904,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) { final TaskContainer taskContainer = getTaskContainer(taskId); - if (taskContainer != null && taskContainer.isVisible()) { + if (taskContainer == null) { + return; + } + + if (taskContainer.isVisible()) { updateContainersInTask(wct, taskContainer); + } else if (Flags.fixNoContainerUpdateWithoutResize()) { + // the TaskFragmentContainers need to be updated when the task becomes visible + taskContainer.mTaskFragmentContainersNeedsUpdate = true; } } @GuardedBy("mLock") private void updateContainersInTask(@NonNull WindowContainerTransaction wct, @NonNull TaskContainer taskContainer) { + taskContainer.mTaskFragmentContainersNeedsUpdate = false; + // Update all TaskFragments in the Task. Make a copy of the list since some may be // removed on updating. final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers(); @@ -3257,12 +3271,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @GuardedBy("mLock") - void updateDivider( - @NonNull WindowContainerTransaction wct, @NonNull TaskContainer taskContainer) { + void updateDivider(@NonNull WindowContainerTransaction wct, + @NonNull TaskContainer taskContainer, boolean isTaskFragmentVanished) { final DividerPresenter dividerPresenter = mDividerPresenters.get(taskContainer.getTaskId()); final TaskFragmentParentInfo parentInfo = taskContainer.getTaskFragmentParentInfo(); - dividerPresenter.updateDivider( - wct, parentInfo, taskContainer.getTopNonFinishingSplitContainer()); + final SplitContainer topSplitContainer = taskContainer.getTopNonFinishingSplitContainer(); + if (dividerPresenter != null) { + dividerPresenter.updateDivider( + wct, parentInfo, topSplitContainer, isTaskFragmentVanished); + } } @Override @@ -3292,6 +3309,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final List<TaskFragmentContainer> containersToFinish = new ArrayList<>(); taskContainer.updateTopSplitContainerForDivider( dividerPresenter, containersToFinish); + if (!containersToFinish.isEmpty()) { + dividerPresenter.setHasContainersToFinish(true); + } for (final TaskFragmentContainer container : containersToFinish) { mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } 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 eade86e50659..d0e49d8c403f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -374,7 +374,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes); updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); - mController.updateDivider(wct, taskContainer); + mController.updateDivider(wct, taskContainer, false /* isTaskFragmentVanished */); } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @@ -658,27 +658,28 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } /** - * Returns the expanded bounds if the {@code bounds} violate minimum dimension or are not fully - * covered by the task bounds. Otherwise, returns {@code bounds}. + * Returns the expanded bounds if the {@code relBounds} violate minimum dimension or are not + * fully covered by the task bounds. Otherwise, returns {@code relBounds}. */ @NonNull - static Rect sanitizeBounds(@NonNull Rect bounds, @Nullable Size minDimension, + static Rect sanitizeBounds(@NonNull Rect relBounds, @Nullable Size minDimension, @NonNull TaskFragmentContainer container) { - if (bounds.isEmpty()) { + if (relBounds.isEmpty()) { // Don't need to check if the bounds follows the task bounds. - return bounds; + return relBounds; } - if (boundsSmallerThanMinDimensions(bounds, minDimension)) { + if (boundsSmallerThanMinDimensions(relBounds, minDimension)) { // Expand the bounds if the bounds are smaller than minimum dimensions. return new Rect(); } final TaskContainer taskContainer = container.getTaskContainer(); - final Rect taskBounds = taskContainer.getBounds(); - if (!taskBounds.contains(bounds)) { + final Rect relTaskBounds = new Rect(taskContainer.getBounds()); + relTaskBounds.offsetTo(0, 0); + if (!relTaskBounds.contains(relBounds)) { // Expand the bounds if the bounds exceed the task bounds. return new Rect(); } - return bounds; + return relBounds; } @Override @@ -756,7 +757,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { void expandTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { super.expandTaskFragment(wct, container); - mController.updateDivider(wct, container.getTaskContainer()); + mController.updateDivider( + wct, container.getTaskContainer(), false /* isTaskFragmentVanished */); } static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) { 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 ee00c4cd67eb..20ad53ee19a8 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -108,6 +108,12 @@ class TaskContainer { private boolean mPlaceholderRuleSuppressed; /** + * {@code true} if the TaskFragments in this Task needs to be updated next time the Task + * becomes visible. See {@link #shouldUpdateContainer(TaskFragmentParentInfo)} + */ + boolean mTaskFragmentContainersNeedsUpdate; + + /** * The {@link TaskContainer} constructor * * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with @@ -185,7 +191,8 @@ class TaskContainer { // If the task properties equals regardless of starting position, don't // need to update the container. - return mInfo.getConfiguration().diffPublicOnly(configuration) != 0 + return mTaskFragmentContainersNeedsUpdate + || mInfo.getConfiguration().diffPublicOnly(configuration) != 0 || mInfo.getDisplayId() != info.getDisplayId(); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index 070fa5bcfae4..859bc2cc40f3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -17,6 +17,7 @@ package androidx.window.extensions.layout; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT; import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED; @@ -41,6 +42,7 @@ import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiContext; +import androidx.annotation.VisibleForTesting; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; @@ -138,6 +140,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { throw new IllegalArgumentException("Context must be a UI Context, which should be" + " an Activity, WindowContext or InputMethodService"); } + if (context.getAssociatedDisplayId() == INVALID_DISPLAY) { + Log.w(TAG, "The registered Context is a UI Context but not associated with any" + + " display. This Context may not receive any WindowLayoutInfo update"); + } mFoldingFeatureProducer.getData((features) -> { WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features); consumer.accept(newWindowLayout); @@ -257,7 +263,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } } - private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { + @VisibleForTesting + void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { synchronized (mLock) { mLastReportedFoldingFeatures.clear(); mLastReportedFoldingFeatures.addAll(storedFeatures); @@ -409,9 +416,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * @return true if the display features should be reported for the UI Context, false otherwise. */ private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) { - int displayId = context.getDisplay().getDisplayId(); + int displayId = context.getAssociatedDisplayId(); if (displayId != DEFAULT_DISPLAY) { - // Display features are not supported on secondary displays. + // Display features are not supported on secondary displays or the context is not + // associated with any display. return false; } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java index 3f676079f080..bc18cd289e05 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -82,6 +82,7 @@ import java.util.concurrent.Executor; */ @Presubmit @SmallTest +@SuppressWarnings("GuardedBy") @RunWith(AndroidJUnit4.class) public class DividerPresenterTest { @Rule @@ -186,7 +187,8 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - mSplitContainer); + mSplitContainer, + false /* isTaskFragmentVanished */); assertNotEquals(mProperties, mDividerPresenter.mProperties); verify(mRenderer).update(); @@ -206,7 +208,8 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - mSplitContainer); + mSplitContainer, + false /* isTaskFragmentVanished */); assertNotEquals(mProperties, mDividerPresenter.mProperties); verify(mRenderer).update(); @@ -222,7 +225,8 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - mSplitContainer); + mSplitContainer, + false /* isTaskFragmentVanished */); assertEquals(mProperties, mDividerPresenter.mProperties); verify(mRenderer, never()).update(); @@ -234,7 +238,42 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - null /* splitContainer */); + null /* splitContainer */, + false /* isTaskFragmentVanished */); + final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + + verify(mTransaction).addTaskFragmentOperation( + mPrimaryContainerToken, taskFragmentOperation); + verify(mRenderer).release(); + assertNull(mDividerPresenter.mRenderer); + assertNull(mDividerPresenter.mProperties); + assertNull(mDividerPresenter.mDecorSurfaceOwner); + } + + @Test + public void testUpdateDivider_noChangeWhenHasContainersToFinishButTaskFragmentNotVanished() { + mDividerPresenter.setHasContainersToFinish(true); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + null /* splitContainer */, + false /* isTaskFragmentVanished */); + + assertEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer, never()).update(); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test + public void testUpdateDivider_dividerRemovedWhenHasContainersToFinishAndTaskFragmentVanished() { + mDividerPresenter.setHasContainersToFinish(true); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + null /* splitContainer */, + true /* isTaskFragmentVanished */); final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) .build(); @@ -254,7 +293,8 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - mSplitContainer); + mSplitContainer, + false /* isTaskFragmentVanished */); final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) .build(); @@ -660,82 +700,241 @@ public class DividerPresenterTest { // Divider position is less than minPosition and the velocity is enough to be dismissed assertEquals( 0, // Closed position - DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + DividerPresenter.dividerPositionWithPositionOptions( 10 /* dividerPosition */, 30 /* minPosition */, 900 /* maxPosition */, 1200 /* fullyExpandedPosition */, -dismissVelocity, - displayDensity)); + displayDensity, + true /* isDraggingToFullscreenAllowed */)); // Divider position is greater than maxPosition and the velocity is enough to be dismissed assertEquals( 1200, // Fully expanded position - DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + DividerPresenter.dividerPositionWithPositionOptions( 1000 /* dividerPosition */, 30 /* minPosition */, 900 /* maxPosition */, 1200 /* fullyExpandedPosition */, dismissVelocity, - displayDensity)); + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is returned when the velocity is not fast enough for fling and is in + // between minPosition and maxPosition + assertEquals( + 500, // dividerPosition is not snapped + DividerPresenter.dividerPositionWithPositionOptions( + 500 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is snapped when the velocity is not fast enough for fling and larger + // than maxPosition + assertEquals( + 900, // Closest position is maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 950 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is snapped when the velocity is not fast enough for fling and smaller + // than minPosition + assertEquals( + 30, // Closest position is minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 20 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is in the closed to maxPosition bounds and the velocity is enough for + // backward fling + assertEquals( + 2000, // maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 2200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is not in the closed to maxPosition bounds and the velocity is enough + // for backward fling + assertEquals( + 1000, // minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is in the closed to minPosition bounds and the velocity is enough for + // forward fling + assertEquals( + 1000, // minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 500 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + flingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + + // Divider position is not in the closed to minPosition bounds and the velocity is enough + // for forward fling + assertEquals( + 2000, // maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + flingVelocity, + displayDensity, + true /* isDraggingToFullscreenAllowed */)); + } + + @Test + public void testDividerPositionWithDraggingToFullscreenNotAllowed() { + final float displayDensity = 600F; + final float nonFlingVelocity = MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity - 10f; + final float flingVelocity = MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity + 10f; // Divider position is returned when the velocity is not fast enough for fling and is in // between minPosition and maxPosition assertEquals( 500, // dividerPosition is not snapped - DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + DividerPresenter.dividerPositionWithPositionOptions( 500 /* dividerPosition */, 30 /* minPosition */, 900 /* maxPosition */, 1200 /* fullyExpandedPosition */, nonFlingVelocity, - displayDensity)); + displayDensity, + false /* isDraggingToFullscreenAllowed */)); // Divider position is snapped when the velocity is not fast enough for fling and larger // than maxPosition assertEquals( 900, // Closest position is maxPosition - DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + DividerPresenter.dividerPositionWithPositionOptions( 950 /* dividerPosition */, 30 /* minPosition */, 900 /* maxPosition */, 1200 /* fullyExpandedPosition */, nonFlingVelocity, - displayDensity)); + displayDensity, + false /* isDraggingToFullscreenAllowed */)); // Divider position is snapped when the velocity is not fast enough for fling and smaller // than minPosition assertEquals( 30, // Closest position is minPosition - DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + DividerPresenter.dividerPositionWithPositionOptions( 20 /* dividerPosition */, 30 /* minPosition */, 900 /* maxPosition */, 1200 /* fullyExpandedPosition */, nonFlingVelocity, - displayDensity)); + displayDensity, + false /* isDraggingToFullscreenAllowed */)); - // Divider position is greater than minPosition and the velocity is enough for fling + // Divider position is snapped when the velocity is not fast enough for fling and at the + // closed position assertEquals( - 30, // minPosition - DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( - 50 /* dividerPosition */, + 30, // Closest position is minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 0 /* dividerPosition */, 30 /* minPosition */, 900 /* maxPosition */, 1200 /* fullyExpandedPosition */, - -flingVelocity, - displayDensity)); + nonFlingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); - // Divider position is less than maxPosition and the velocity is enough for fling + // Divider position is snapped when the velocity is not fast enough for fling and at the + // fully expanded position assertEquals( - 900, // maxPosition - DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( - 800 /* dividerPosition */, + 900, // Closest position is maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, 30 /* minPosition */, 900 /* maxPosition */, 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is in the closed to maxPosition bounds and the velocity is enough for + // backward fling + assertEquals( + 2000, // maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 2200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is not in the closed to maxPosition bounds and the velocity is enough + // for backward fling + assertEquals( + 1000, // minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is in the closed to minPosition bounds and the velocity is enough for + // forward fling + assertEquals( + 1000, // minPosition + DividerPresenter.dividerPositionWithPositionOptions( + 500 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, + flingVelocity, + displayDensity, + false /* isDraggingToFullscreenAllowed */)); + + // Divider position is not in the closed to minPosition bounds and the velocity is enough + // for forward fling + assertEquals( + 2000, // maxPosition + DividerPresenter.dividerPositionWithPositionOptions( + 1200 /* dividerPosition */, + 1000 /* minPosition */, + 2000 /* maxPosition */, + 2500 /* fullyExpandedPosition */, flingVelocity, - displayDensity)); + displayDensity, + false /* isDraggingToFullscreenAllowed */)); } private TaskFragmentContainer createMockTaskFragmentContainer( diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 0972d40f33e3..7a0b9a0ece6b 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -409,6 +409,22 @@ public class OverlayPresentationTest { } @Test + public void testSanitizeBounds_taskInSplitScreen() { + final TaskFragmentContainer overlayContainer = + createTestOverlayContainer(TASK_ID, "test1"); + TaskContainer taskContainer = overlayContainer.getTaskContainer(); + spyOn(taskContainer); + doReturn(new Rect(TASK_BOUNDS.left + TASK_BOUNDS.width() / 2, TASK_BOUNDS.top, + TASK_BOUNDS.right, TASK_BOUNDS.bottom)).when(taskContainer).getBounds(); + final Rect taskBounds = taskContainer.getBounds(); + final Rect bounds = new Rect(taskBounds.width() / 2, 0, taskBounds.width(), + taskBounds.height()); + + assertThat(sanitizeBounds(bounds, null, overlayContainer) + .isEmpty()).isFalse(); + } + + @Test public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_createOverlay() { final Rect bounds = new Rect(0, 0, 100, 100); mSplitController.setActivityStackAttributesCalculator(params -> 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 640b1fced455..efeec82b782e 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 @@ -200,12 +200,14 @@ public class SplitControllerTest { public void testOnTaskFragmentVanished() { final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity); doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken(); + doReturn(createTestTaskContainer()).when(mSplitController).getTaskContainer(TASK_ID); // The TaskFragment has been removed in the server, we only need to cleanup the reference. - mSplitController.onTaskFragmentVanished(mTransaction, mInfo); + mSplitController.onTaskFragmentVanished(mTransaction, mInfo, TASK_ID); verify(mSplitPresenter, never()).deleteTaskFragment(any(), any()); verify(mSplitController).removeContainer(tf); + verify(mSplitController).updateDivider(any(), any(), anyBoolean()); verify(mTransaction, never()).finishActivity(any()); } @@ -1152,7 +1154,7 @@ public class SplitControllerTest { .setTaskFragmentInfo(info)); mSplitController.onTransactionReady(transaction); - verify(mSplitController).onTaskFragmentVanished(any(), eq(info)); + verify(mSplitController).onTaskFragmentVanished(any(), eq(info), anyInt()); verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), anyInt(), anyBoolean()); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java new file mode 100644 index 000000000000..ff0a82fe05d6 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.layout; + +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.content.ContextWrapper; +import android.platform.test.annotations.Presubmit; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; + +/** + * Test class for {@link WindowLayoutComponentImpl}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:WindowLayoutComponentImplTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class WindowLayoutComponentImplTest { + + private WindowLayoutComponentImpl mWindowLayoutComponent; + + @Before + public void setUp() { + mWindowLayoutComponent = new WindowLayoutComponentImpl( + ApplicationProvider.getApplicationContext(), + mock(DeviceStateManagerFoldingFeatureProducer.class)); + } + + @Test + public void testAddWindowLayoutListenerOnFakeUiContext_noCrash() { + final Context fakeUiContext = createTestContext(); + + mWindowLayoutComponent.addWindowLayoutInfoListener(fakeUiContext, info -> {}); + + mWindowLayoutComponent.onDisplayFeaturesChanged(Collections.emptyList()); + } + + private static Context createTestContext() { + return new FakeUiContext(ApplicationProvider.getApplicationContext()); + } + + /** + * A {@link android.content.Context} overrides {@link android.content.Context#isUiContext} to + * {@code true}. + */ + private static class FakeUiContext extends ContextWrapper { + + FakeUiContext(Context base) { + super(base); + } + + @Override + public boolean isUiContext() { + return true; + } + } +} diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 89781fd650a4..25d3067a34bc 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -51,6 +51,7 @@ filegroup { "src/com/android/wm/shell/common/split/SplitScreenConstants.java", "src/com/android/wm/shell/common/TransactionPool.java", "src/com/android/wm/shell/common/TriangleShape.java", + "src/com/android/wm/shell/common/desktopmode/*.kt", "src/com/android/wm/shell/draganddrop/DragAndDropConstants.java", "src/com/android/wm/shell/pip/PipContentOverlay.java", "src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java", @@ -205,6 +206,7 @@ android_library { "androidx.core_core-animation", "androidx.core_core-ktx", "androidx.arch.core_core-runtime", + "androidx.compose.material3_material3", "androidx-constraintlayout_constraintlayout", "androidx.dynamicanimation_dynamicanimation", "androidx.recyclerview_recyclerview", diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index 7a986835359a..52ae93f5ebf1 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -29,6 +29,31 @@ android:name=".desktopmode.DesktopWallpaperActivity" android:excludeFromRecents="true" android:launchMode="singleInstance" + android:showForAllUsers="true" android:theme="@style/DesktopWallpaperTheme" /> + + <activity + android:name=".bubbles.shortcut.CreateBubbleShortcutActivity" + android:exported="true" + android:excludeFromRecents="true" + android:theme="@android:style/Theme.NoDisplay" + android:label="Bubbles" + android:icon="@drawable/ic_bubbles_shortcut_widget"> + <intent-filter> + <action android:name="android.intent.action.CREATE_SHORTCUT" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + + <activity + android:name=".bubbles.shortcut.ShowBubblesActivity" + android:exported="true" + android:excludeFromRecents="true" + android:theme="@android:style/Theme.NoDisplay" > + <intent-filter> + <action android:name="com.android.wm.shell.bubbles.action.SHOW_BUBBLES"/> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> </application> </manifest> diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 15f8c328bb56..3b7eb292abc7 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -111,3 +111,23 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "animate_bubble_size_change" + namespace: "multitasking" + description: "Turns on the animation for bubble bar icons size change" + bug: "335575529" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_taskbar_on_phones" + namespace: "multitasking" + description: "Enables taskbar on phones" + bug: "348007377" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp new file mode 100644 index 000000000000..c6dbd9b25e7f --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp @@ -0,0 +1,61 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_multitasking_windowing", +} + +android_test { + name: "WMShellMultivalentScreenshotTestsOnDevice", + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "WindowManager-Shell", + "junit", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "truth", + "platform-parametric-runner-lib", + "platform-screenshot-diff-core", + ], + libs: [ + "android.test.base", + "android.test.runner", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + kotlincflags: ["-Xjvm-default=all"], + optimize: { + enabled: false, + }, + test_suites: ["device-tests"], + platform_apis: true, + certificate: "platform", + aaptflags: [ + "--extra-packages", + "com.android.wm.shell", + ], + manifest: "AndroidManifest.xml", + asset_dirs: ["goldens/onDevice"], +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml new file mode 100644 index 000000000000..467dc6a5cb81 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.wm.shell.multivalentscreenshot"> + + <application android:debuggable="true" android:supportsRtl="true" > + <uses-library android:name="android.test.runner" /> + <activity + android:name="platform.test.screenshot.ScreenshotActivity" + android:exported="true"> + </activity> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="Multivalent screenshot tests for WindowManager-Shell" + android:targetPackage="com.android.wm.shell.multivalentscreenshot"> + </instrumentation> + + <!-- this permission is required by Tuner Service in screenshot tests --> + <uses-permission android:name="android.permission.MANAGE_USERS" /> +</manifest> diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml new file mode 100644 index 000000000000..a7a3f1313a9b --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.wm.shell.multivalentscreenshot"> + <application android:debuggable="true" android:supportsRtl="true"> + <uses-library android:name="android.test.runner" /> + <activity + android:name="platform.test.screenshot.ScreenshotActivity" + android:exported="true"> + </activity> + </application> +</manifest> diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml new file mode 100644 index 000000000000..75793ae69d27 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs Tests for WindowManagerShellLib"> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="WMShellMultivalentScreenshotTestsOnDevice.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-tag" value="WMShellMultivalentScreenshotTestsOnDevice" /> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/data/user/0/com.android.wm.shell.multivalentscreenshot/files/wmshell_screenshots" /> + <option name="collect-on-run-ended-only" value="true" /> + </metrics_collector> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.wm.shell.multivalentscreenshot" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png Binary files differnew file mode 100644 index 000000000000..eb2888199ddf --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png Binary files differnew file mode 100644 index 000000000000..eb2888199ddf --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties b/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties new file mode 100644 index 000000000000..7a0527ccaafb --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties @@ -0,0 +1,2 @@ +sdk=NEWEST_SDK + diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt new file mode 100644 index 000000000000..d35f493a8f60 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles + +import android.view.LayoutInflater +import com.android.wm.shell.common.bubbles.BubblePopupView +import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager +import com.android.wm.shell.R +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays +import platform.test.screenshot.ViewScreenshotTestRule +import platform.test.screenshot.getEmulatedDevicePathConfig + +@RunWith(ParameterizedAndroidJunit4::class) +class BubbleEducationViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = DeviceEmulationSpec.forDisplays(Displays.Phone, isLandscape = false) + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + WMShellGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)) + ) + + @Test + fun bubblesEducation() { + screenshotRule.screenshotTest("bubbles_education") { activity -> + activity.actionBar?.hide() + val view = + LayoutInflater.from(activity) + .inflate(R.layout.bubble_bar_stack_education, null) as BubblePopupView + view.setup() + view + } + } +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt new file mode 100644 index 000000000000..901b79b9b1a0 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.testing.goldenpathmanager + +import android.os.Build +import androidx.test.platform.app.InstrumentationRegistry +import platform.test.screenshot.GoldenPathManager +import platform.test.screenshot.PathConfig + +/** A WM Shell specific implementation of [GoldenPathManager]. */ +class WMShellGoldenPathManager(pathConfig: PathConfig) : + GoldenPathManager( + appContext = InstrumentationRegistry.getInstrumentation().context, + assetsPathRelativeToBuildRoot = assetPath, + deviceLocalPath = deviceLocalPath, + pathConfig = pathConfig, + ) { + + private companion object { + private const val ASSETS_PATH = + "frameworks/base/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice" + private const val ASSETS_PATH_ROBO = + "frameworks/base/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/" + + "robolectric" + private val assetPath: String + get() = if (Build.FINGERPRINT.contains("robolectric")) ASSETS_PATH_ROBO else ASSETS_PATH + private val deviceLocalPath: String + get() = + InstrumentationRegistry.getInstrumentation() + .targetContext + .filesDir + .absolutePath + .toString() + "/wmshell_screenshots" + } + override fun toString(): String { + // This string is appended to all actual/expected screenshots on the device, so make sure + // it is a static value. + return "WMShellGoldenPathManager" + } +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice b/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice new file mode 120000 index 000000000000..e879efc81ec1 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice @@ -0,0 +1 @@ +multivalentScreenshotTests
\ No newline at end of file diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 0efdbdc9376c..327e2059557c 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -456,5 +456,7 @@ class BubbleStackViewTest { override fun isStackExpanded(): Boolean = false override fun isShowingAsBubbleBar(): Boolean = false + + override fun hideCurrentInputMethod() {} } } diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml index 04ad572e046f..a30cfb74bf4a 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml @@ -20,5 +20,6 @@ android:shape="rectangle"> <corners android:radius="@dimen/desktop_mode_maximize_menu_buttons_outline_radius"/> + <solid android:color="?androidprv:attr/materialColorSurfaceContainerLow"/> <stroke android:width="1dp" android:color="?androidprv:attr/materialColorOutlineVariant"/> </shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml index 50c5ca936245..b208f2fea7b2 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml +++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml @@ -13,16 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:id="@+id/backLayer"> - <shape android:shape="rectangle"> - <solid android:color="#000000" /> - </shape> - </item> - - <item android:id="@+id/frontLayer"> - <shape android:shape="rectangle"> - <solid android:color="#000000" /> - </shape> - </item> -</layer-list>
\ No newline at end of file +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_bubbles_shortcut_widget_background" /> + <foreground android:drawable="@drawable/ic_bubbles_shortcut_widget_foreground" /> +</adaptive-icon> diff --git a/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml new file mode 100644 index 000000000000..510221fb2859 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path + android:pathData="M0,0h108v108h-108z" + android:fillColor="#FFC20C"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml new file mode 100644 index 000000000000..a41b6a961bb2 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="@android:color/white"> + <group android:scaleX="0.58" + android:scaleY="0.58" + android:translateX="5.04" + android:translateY="5.04"> + <path + android:fillColor="@android:color/white" + android:pathData="M7.2,14.4m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/> + <path + android:fillColor="@android:color/white" + android:pathData="M14.8,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/> + <path + android:fillColor="@android:color/white" + android:pathData="M15.2,8.8m-4.8,0a4.8,4.8 0,1 1,9.6 0a4.8,4.8 0,1 1,-9.6 0"/> + </group> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml index 34f03c2f226b..501bedd50f55 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:layout_width="wrap_content" android:orientation="vertical" - android:id="@+id/bubble_bar_expanded_view"> + android:id="@+id/bubble_expanded_view"> <com.android.wm.shell.bubbles.bar.BubbleBarHandleView android:id="@+id/bubble_bar_handle_view" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml index c0ff1922edc8..c0ff1922edc8 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml index 84e144972f38..7b31c1420a7c 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml @@ -19,7 +19,6 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/desktop_mode_caption" - android:background="@drawable/desktop_mode_header_background" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 9599658384f0..7d5f9cdbebc8 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -31,23 +31,15 @@ android:layout_height="wrap_content" android:orientation="vertical"> - <FrameLayout - android:id="@+id/maximize_menu_maximize_button_layout" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@drawable/desktop_mode_maximize_menu_layout_background" - android:padding="4dp" + <Button + android:layout_width="94dp" + android:layout_height="60dp" + android:id="@+id/maximize_menu_maximize_button" + style="?android:attr/buttonBarButtonStyle" + android:stateListAnimator="@null" android:layout_marginRight="8dp" android:layout_marginBottom="4dp" - android:alpha="0"> - <Button - android:id="@+id/maximize_menu_maximize_button" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="86dp" - android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" - android:background="@drawable/desktop_mode_maximize_menu_button_background" - android:stateListAnimator="@null"/> - </FrameLayout> + android:alpha="0"/> <TextView android:id="@+id/maximize_menu_maximize_window_text" diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index 1c8f5e60c5c9..8b328e2c79cf 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Borrel"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Bestuur"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Borrel is toegemaak."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tik om hierdie app te herbegin vir ’n beter aansig"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Verander hierdie app se aspekverhouding in Instellings"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Verander aspekverhouding"</string> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index 81ab3ab15aad..b005a01711eb 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"አረፋ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ያቀናብሩ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"አረፋ ተሰናብቷል።"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"ለተሻለ ዕይታ ይህን መተግበሪያ እንደገና ለመጀመር መታ ያድርጉ"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"የዚህን መተግበሪያ ምጥጥነ ገፅታ በቅንብሮች ውስጥ ይለውጡ"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"የምጥጥነ ገፅታ ለውጥ"</string> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index 3974c39d4803..8c283d33eff6 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"فقاعة"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"إدارة"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"تم إغلاق الفقاعة."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"انقر لإعادة تشغيل هذا التطبيق للحصول على تجربة عرض أفضل."</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"يمكنك تغيير نسبة العرض إلى الارتفاع لهذا التطبيق من خلال \"الإعدادات\"."</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"تغيير نسبة العرض إلى الارتفاع"</string> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index a1ce1b3b9513..ef92587ad274 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"বাবল"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"পৰিচালনা কৰক"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল অগ্ৰাহ্য কৰা হৈছে"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"উন্নত ভিউ পোৱাৰ বাবে এপ্টো ৰিষ্টাৰ্ট কৰিবলৈ টিপক"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ছেটিঙলৈ গৈ এই এপ্টোৰ আকাৰৰ অনুপাত সলনি কৰক"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"আকাৰৰ অনুপাত সলনি কৰক"</string> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index 71dfe5ac6bed..04b2f1c23bc4 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Qabarcıq"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"İdarə edin"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Qabarcıqdan imtina edilib."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Yaxşı görünüş üçün toxunaraq bu tətbiqi yenidən başladın"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Ayarlarda bu tətbiqin tərəflər nisbətini dəyişin"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Tərəflər nisbətini dəyişin"</string> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml index f48360991d49..47bc105f19bc 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljajte"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Dodirnite da biste restartovali ovu aplikaciju radi boljeg prikaza"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Promenite razmeru ove aplikacije u Podešavanjima"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Promeni razmeru"</string> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index 532ecc64358f..6ad7553a8383 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Усплывальнае апавяшчэнне"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Кіраваць"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Усплывальнае апавяшчэнне адхілена."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Націсніце, каб перазапусціць гэту праграму для зручнейшага прагляду"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Змяніць суадносіны бакоў для гэтай праграмы ў наладах"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Змяніць суадносіны бакоў"</string> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 8f828badcf47..a9e0bce81376 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Балонче"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Управление"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отхвърлено."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Докоснете, за да рестартирате това приложение с цел по-добър изглед"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Променете съотношението на това приложение в „Настройки“"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Промяна на съотношението"</string> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index e0a2ea824be0..29de1007e311 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"বাবল"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ম্যানেজ করুন"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল বাতিল করা হয়েছে।"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"আরও ভাল ভিউয়ের জন্য এই অ্যাপ রিস্টার্ট করতে ট্যাপ করুন"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"সেটিংস থেকে এই অ্যাপের অ্যাস্পেক্ট রেশিও পরিবর্তন করুন"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"অ্যাস্পেক্ট রেশিও পরিবর্তন করুন"</string> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 41c72c1d3a03..5f1da7571d95 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljaj"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Dodirnite da ponovo pokrenete ovu aplikaciju radi boljeg prikaza"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Promijenite format slike aplikacije u Postavkama"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Promijenite format slike"</string> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index 679227248ea5..d70de794ba17 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bombolla"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestiona"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"La bombolla s\'ha ignorat."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Toca per reiniciar aquesta aplicació i obtenir una millor visualització"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Canvia la relació d\'aspecte d\'aquesta aplicació a Configuració"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Canvia la relació d\'aspecte"</string> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index aafb2e16b703..ca00fec12e86 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -84,7 +84,11 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovat"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina byla zavřena."</string> - <string name="restart_button_description" msgid="4564728020654658478">"Klepnutím tuto aplikaci kvůli lepšímu zobrazení restartujete"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> + <string name="restart_button_description" msgid="4564728020654658478">"Klepnutím tuto aplikaci restartujete kvůli lepšímu zobrazení"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Změnit v Nastavení poměr stran této aplikace"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Změnit poměr stran"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problémy s fotoaparátem?\nKlepnutím vyřešíte"</string> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 8878910a4d2c..d50d2f0f135d 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Boble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen blev lukket."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tryk for at genstarte denne app, så visningen forbedres"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Skift denne apps billedformat i Indstillinger"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Skift billedformat"</string> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index bcdc2a9c8539..7f44f83a1790 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -48,8 +48,8 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % oben"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % oben"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Vollbild unten"</string> - <string name="accessibility_split_left" msgid="1713683765575562458">"Links teilen"</string> - <string name="accessibility_split_right" msgid="8441001008181296837">"Rechts teilen"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Links positionieren"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Rechts positionieren"</string> <string name="accessibility_split_top" msgid="2789329702027147146">"Oben teilen"</string> <string name="accessibility_split_bottom" msgid="8694551025220868191">"Unten teilen"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Einhandmodus wird verwendet"</string> @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Verwalten"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble verworfen."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tippen, um diese App neu zu starten und die Ansicht zu verbessern"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Seitenverhältnis der App in den Einstellungen ändern"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Seitenverhältnis ändern"</string> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index 14e5e2f87ab8..a3a5ccd839e4 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Συννεφάκι"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Διαχείριση"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Το συννεφάκι παραβλέφθηκε."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Πατήστε για να επανεκκινήσετε αυτή την εφαρμογή για καλύτερη προβολή"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Αλλάξτε τον λόγο διαστάσεων αυτής της εφαρμογής στις Ρυθμίσεις"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Αλλαγή λόγου διαστάσεων"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 7427b62679be..edc4f4e25c2a 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index cb9ee4f6b6b3..e537f0a80144 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 7427b62679be..edc4f4e25c2a 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 7427b62679be..edc4f4e25c2a 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 8498807f9fdb..bdcd275d9c14 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 406c1f37c455..8653e5932a04 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Cuadro"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Se descartó el cuadro."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Presiona para reiniciar esta app y tener una mejor vista"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Cambiar la relación de aspecto de esta app en Configuración"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Cambiar relación de aspecto"</string> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 0583d79da127..8f59c9c91d20 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Burbuja"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbuja cerrada."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Toca para reiniciar esta aplicación y obtener una mejor vista"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Cambiar la relación de aspecto de esta aplicación en Ajustes"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Cambiar relación de aspecto"</string> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index 70547f566ea6..3d86eb4a91d9 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Mull"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Halda"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Mullist loobuti."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Puudutage, et see rakendus parema vaate jaoks taaskäivitada"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Muutke selle rakenduse kuvasuhet jaotises Seaded"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Muutke kuvasuhet"</string> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 4be35eac6c1f..4e7bdd246d10 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Burbuila"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Kudeatu"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Baztertu da globoa."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Hobeto ikusteko, sakatu hau, eta aplikazioa berrabiarazi egingo da"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Aldatu aplikazioaren aspektu-erlazioa ezarpenetan"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Aldatu aspektu-erlazioa"</string> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index 32d5f5f34fb8..39100425a9ac 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"حباب"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"مدیریت"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"حبابک رد شد."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"برای داشتن نمایی بهتر، ضربه بزنید تا این برنامه بازراهاندازی شود"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"نسبت ابعادی این برنامه را در «تنظیمات» تغییر دهید"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"تغییر نسبت ابعادی"</string> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 6f03545e5542..577d625ba8f8 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Kupla"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Ylläpidä"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Kupla ohitettu."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Napauta, niin sovellus käynnistyy uudelleen paremmin näytölle sopivana"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Muuta tämän sovelluksen kuvasuhdetta Asetuksissa"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Vaihda kuvasuhdetta"</string> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index 3492f136c4f9..74d822ac02c0 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle ignorée."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Touchez pour redémarrer cette application afin d\'obtenir un meilleur affichage"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Changer les proportions de cette application dans les paramètres"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Modifier les proportions"</string> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index 4002e4d04d51..4d14d0b85f3e 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle fermée."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Appuyez pour redémarrer cette appli et obtenir une meilleure vue."</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Modifiez le format de cette appli dans les Paramètres."</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Modifier le format"</string> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index c371f7f62feb..e5b67c2aaad1 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Burbulla"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Xestionar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ignorouse a burbulla."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Toca o botón para reiniciar esta aplicación e gozar dunha mellor visualización"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Cambia a proporción desta aplicación en Configuración"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Cambiar a proporción"</string> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index 7e3d7a373be4..e2a52dccd8ea 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"બબલ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"મેનેજ કરો"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"બબલ છોડી દેવાયો."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"વધુ સારા વ્યૂ માટે, આ ઍપને ફરી શરૂ કરવા ટૅપ કરો"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"સેટિંગમાં આ ઍપનો સાપેક્ષ ગુણોત્તર બદલો"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"સાપેક્ષ ગુણોત્તર બદલો"</string> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index cd0f4e3618f7..f75e0e0528e1 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"मैनेज करें"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल खारिज किया गया."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"बेहतर व्यू पाने के लिए, टैप करके ऐप्लिकेशन को रीस्टार्ट करें"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"सेटिंग में जाकर इस ऐप्लिकेशन का आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) बदलें"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) बदलें"</string> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 27d4cfcf22d5..ed80c505d756 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić odbačen."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Dodirnite da biste ponovo pokrenuli tu aplikaciju kako biste bolje vidjeli"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Promijeni omjer slike ove aplikacije u postavkama"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Promijeni omjer slike"</string> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index a8cc5c120efc..32a31063bd90 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Buborék"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Kezelés"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Buborék elvetve."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"A jobb nézet érdekében koppintson az alkalmazás újraindításához."</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Az app méretarányát a Beállításokban módosíthatja"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Méretarány módosítása"</string> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 7f372774241a..65ca704ded09 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Պղպջակ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Կառավարել"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ամպիկը փակվեց։"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Հպեք՝ հավելվածը վերագործարկելու և ավելի հարմար տեսք ընտրելու համար"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Փոխել հավելվածի կողմերի հարաբերակցությունը Կարգավորումներում"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Փոխել չափերի հարաբերակցությունը"</string> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index 3cf55fa0ede2..975dd72f67d7 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Kelola"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon ditutup."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Ketuk untuk memulai ulang aplikasi ini agar mendapatkan tampilan yang lebih baik"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Ubah rasio aspek aplikasi ini di Setelan"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Ubah rasio aspek"</string> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 6aa56f9858ad..11c47189dce0 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Blaðra"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Stjórna"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Blöðru lokað."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Ýttu til að endurræsa forritið og fá betri sýn"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Breyta myndhlutfalli þessa forrits í stillingunum"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Breyta myndhlutfalli"</string> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index 3c1d5e4dac02..168c8cc5936f 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Fumetto"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestisci"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Fumetto ignorato."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tocca per riavviare l\'app e migliorare la visualizzazione"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Cambia le proporzioni dell\'app nelle Impostazioni"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Cambia proporzioni"</string> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index a0c3b3a95ca8..fd4cd1adaa2a 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"בועה"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ניהול"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"הבועה נסגרה."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"כדי לראות טוב יותר יש להקיש ולהפעיל את האפליקציה הזו מחדש"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"אפשר לשנות את יחס הגובה-רוחב של האפליקציה הזו ב\'הגדרות\'"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"שינוי יחס גובה-רוחב"</string> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index fb726c180997..64ddec9450ae 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"バブル"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ふきだしが非表示になっています。"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"タップしてこのアプリを再起動すると、表示が適切になります"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"このアプリのアスペクト比を [設定] で変更します"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"アスペクト比を変更"</string> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index e9f620a17203..cab8807b86d1 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ბუშტი"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"მართვა"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ბუშტი დაიხურა."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"შეხებით გადატვირთეთ ეს აპი უკეთესი ხედის მისაღებად"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"შეცვალეთ ამ აპის თანაფარდობა პარამეტრებიდან"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"თანაფარდობის შეცვლა"</string> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 34e41038f285..4ff5b85b36fd 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Көпіршік"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Басқару"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Қалқыма хабар жабылды."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Көріністі жақсарту үшін осы қолданбаны түртіп, қайта ашыңыз."</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Осы қолданбаның арақатынасын параметрлерден өзгертуге болады."</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Арақатынасты өзгерту"</string> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 362bbad4ec12..ba7a32495659 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ពពុះ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"គ្រប់គ្រង"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"បានច្រានចោលសារលេចឡើង។"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"ចុចដើម្បីចាប់ផ្ដើមកម្មវិធីនេះឡើងវិញសម្រាប់ទិដ្ឋភាពកាន់តែប្រសើរ"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ផ្លាស់ប្ដូរសមាមាត្ររបស់កម្មវិធីនេះនៅក្នុងការកំណត់"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ប្ដូរសមាមាត្រ"</string> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 77cc4a44f81a..423e8d53a654 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ಬಬಲ್"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ನಿರ್ವಹಿಸಿ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ಬಬಲ್ ವಜಾಗೊಳಿಸಲಾಗಿದೆ."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"ಉತ್ತಮ ವೀಕ್ಷಣೆಗಾಗಿ ಈ ಆ್ಯಪ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ಸೆಟ್ಟಿಂಗ್ಗಳಲ್ಲಿ ಈ ಆ್ಯಪ್ನ ದೃಶ್ಯಾನುಪಾತವನ್ನು ಬದಲಾಯಿಸಿ"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ದೃಶ್ಯಾನುಪಾತವನ್ನು ಬದಲಾಯಿಸಿ"</string> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index e8b5522838b7..0d1c6216776b 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"버블"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"관리"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"대화창을 닫았습니다."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"탭하면 앱을 다시 시작하여 보기를 개선합니다."</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"설정에서 앱의 가로세로 비율을 변경합니다."</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"가로세로 비율 변경"</string> @@ -95,7 +99,7 @@ <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"앱 위치를 조정하려면 앱 외부를 두 번 탭합니다."</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"확인"</string> <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"추가 정보는 펼쳐서 확인하세요."</string> - <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"화면에 맞게 보도록 다시 시작할까요?"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"화면에 맞게 보이도록 다시 시작할까요?"</string> <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"앱을 다시 시작하면 화면에 더 잘 맞게 볼 수는 있지만 진행 상황 또는 저장되지 않은 변경사항을 잃을 수도 있습니다."</string> <string name="letterbox_restart_cancel" msgid="1342209132692537805">"취소"</string> <string name="letterbox_restart_restart" msgid="8529976234412442973">"다시 시작"</string> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index 302c0071a73a..f17e9ca891c0 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Көбүк"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Башкаруу"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Калкып чыкма билдирме жабылды."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Жакшыраак көрүү үчүн бул колдонмону өчүрүп күйгүзүңүз. Ал үчүн таптап коюңуз"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Бул колдонмонун тараптарынын катнашын параметрлерден өзгөртүү"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Тараптардын катнашын өзгөртүү"</string> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index a3519636b71f..195e4d56a1c1 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ຟອງ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ຈັດການ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ປິດ Bubble ໄສ້ແລ້ວ."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"ແຕະເພື່ອຣີສະຕາດແອັບນີ້ເພື່ອມຸມມອງທີ່ດີຂຶ້ນ"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ປ່ຽນອັດຕາສ່ວນຂອງແອັບນີ້ໃນການຕັ້ງຄ່າ"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ປ່ຽນອັດຕາສ່ວນ"</string> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index e4dd7398f679..63ad580a81cc 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Debesėlis"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Tvarkyti"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Debesėlio atsisakyta."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Palieskite, kad iš naujo paleistumėte šią programą ir matytumėte aiškiau"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Pakeiskite šios programos kraštinių santykį"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Keisti kraštinių santykį"</string> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index 99aebf626322..268d89324f54 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Burbulis"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Pārvaldīt"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbulis ir noraidīts."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Pieskarieties, lai restartētu šo lietotni un uzlabotu attēlojumu."</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Iestatījumos mainiet šīs lietotnes malu attiecību."</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Mainīt malu attiecību"</string> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index c152c60fa631..0a0027fa1bae 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Балонче"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Управувајте"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отфрлено."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Допрете за да ја рестартирате апликацијава за подобар приказ"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Промени го соодносот на апликацијава во „Поставки“"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Променување на соодносот"</string> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index 90275cdb517a..07809e1a0014 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ബബിൾ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"മാനേജ് ചെയ്യുക"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ബബിൾ ഡിസ്മിസ് ചെയ്തു."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"മികച്ച കാഴ്ചയ്ക്കായി ഈ ആപ്പ് റീസ്റ്റാർട്ട് ചെയ്യാൻ ടാപ്പ് ചെയ്യുക"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ഈ ആപ്പിന്റെ വീക്ഷണ അനുപാതം, ക്രമീകരണത്തിൽ മാറ്റുക"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"വീക്ഷണ അനുപാതം മാറ്റുക"</string> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index 5e43506ab621..99bd2dffca53 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Бөмбөлөг"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Удирдах"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Бөмбөлгийг үл хэрэгссэн."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Харагдах байдлыг сайжруулахын тулд энэ аппыг товшиж, дахин эхлүүлнэ үү"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Энэ аппын харьцааг Тохиргоонд өөрчилнө үү"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Харьцааг өөрчлөх"</string> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index 5874bffc9199..ac57e0a549b4 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापित करा"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल डिसमिस केला."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"अधिक चांगल्या दृश्यासाठी हे अॅप रीस्टार्ट करण्याकरिता टॅप करा"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"सेटिंग्ज मध्ये या ॲपचा आस्पेक्ट रेशो बदला"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"आस्पेक्ट रेशो बदला"</string> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 4de8a7b03547..6bc2fbb27c51 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Gelembung"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Urus"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Gelembung diketepikan."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Ketik untuk memulakan semula apl ini untuk mendapatkan paparan yang lebih baik"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Tukar nisbah bidang apl ini dalam Tetapan"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Tukar nisbah bidang"</string> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index 5b9e9cb7353e..12c19edcaeb1 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ပူဖောင်းဖောက်သံ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"စီမံရန်"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ပူဖောင်းကွက် ဖယ်လိုက်သည်။"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"ပိုကောင်းသောမြင်ကွင်းအတွက် ဤအက်ပ်ပြန်စရန် တို့ပါ"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ဆက်တင်များတွင် ဤအက်ပ်၏အချိုးအစားကို ပြောင်းရန်"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"အချိုးစား ပြောင်းရန်"</string> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index 9f03d8b5b178..1161eb608405 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -84,8 +84,12 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Boble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen er avvist."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Trykk for å starte denne appen på nytt og få en bedre visning"</string> - <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Endre høyde/bredde-forholdet for denne appen i innstillingene"</string> + <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Endre høyde/bredde-forholdet for denne appen i Innstillinger"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Endre høyde/bredde-forholdet"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Har du kameraproblemer?\nTrykk for å tilpasse"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ble ikke problemet løst?\nTrykk for å gå tilbake"</string> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index a5bd2ab5c10b..25d033738c86 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापन गर्नुहोस्"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल हटाइयो।"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"अझ राम्रो भ्यू प्राप्त गर्नका लागि यो एप रिस्टार्ट गर्न ट्याप गर्नुहोस्"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"सेटिङमा गई यो एपको एस्पेक्ट रेसियो परिवर्तन गर्नुहोस्"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"एस्पेक्ट रेसियो परिवर्तन गर्नुहोस्"</string> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index 0cd27c5c1457..4ad343cb1a4e 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubbel"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Beheren"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubbel gesloten."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tik om deze app opnieuw op te starten voor een betere weergave"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Wijzig de beeldverhouding van deze app in Instellingen"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Beeldverhouding wijzigen"</string> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index bf751852a255..966d40440ac7 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ବବଲ୍"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ପରିଚାଳନା କରନ୍ତୁ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ବବଲ୍ ଖାରଜ କରାଯାଇଛି।"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"ଏକ ଆହୁରି ଭଲ ଭ୍ୟୁ ପାଇଁ ଏହି ଆପ ରିଷ୍ଟାର୍ଟ କରିବାକୁ ଟାପ କରନ୍ତୁ"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ସେଟିଂସରେ ଏହି ଆପର ଚଉଡ଼ା ଓ ଉଚ୍ଚତାର ଅନୁପାତ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ଚଉଡ଼ା ଓ ଉଚ୍ଚତାର ଅନୁପାତ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index 325c1e80c433..9feaf41cda7f 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ਬੁਲਬੁਲਾ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ਪ੍ਰਬੰਧਨ ਕਰੋ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ਬਬਲ ਨੂੰ ਖਾਰਜ ਕੀਤਾ ਗਿਆ।"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"ਬਿਹਤਰ ਦ੍ਰਿਸ਼ ਵਾਸਤੇ ਇਸ ਐਪ ਨੂੰ ਮੁੜ-ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਜਾ ਕੇ ਇਸ ਐਪ ਦੇ ਆਕਾਰ ਅਨੁਪਾਤ ਨੂੰ ਬਦਲੋ"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ਆਕਾਰ ਅਨੁਪਾਤ ਬਦਲੋ"</string> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index a7648c8e323b..1c7fbf8e80d3 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Dymek"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Zarządzaj"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Zamknięto dymek"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Kliknij w celu zrestartowania aplikacji, aby lepiej się wyświetlała."</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Zmień proporcje obrazu aplikacji w Ustawieniach"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Zmień proporcje obrazu"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index e47d151337b2..5c2de2ad0d31 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bolha"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Toque para reiniciar o app e atualizar a visualização"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Mude o tamanho da janela deste app nas Configurações"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Mudar a proporção"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 1210fe8fda05..6f76525473eb 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Balão"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerir"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão ignorado."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Toque para reiniciar esta app e ficar com uma melhor visão"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Altere o formato desta app nas Definições"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Altere o formato"</string> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index e47d151337b2..5c2de2ad0d31 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bolha"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Toque para reiniciar o app e atualizar a visualização"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Mude o tamanho da janela deste app nas Configurações"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Mudar a proporção"</string> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index ae871f3dd42b..6e85e7849d95 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionează"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balonul a fost respins."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Atinge ca să repornești aplicația pentru o vizualizare mai bună"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Schimbă raportul de dimensiuni al aplicației din Setări"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Schimbă raportul de dimensiuni"</string> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 971e146ba77e..1b41983cd1a3 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Всплывающая подсказка"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Настроить"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Всплывающий чат закрыт."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Нажмите, чтобы перезапустить приложение и оптимизировать размер"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Изменить соотношение сторон приложения в настройках"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Изменить соотношение сторон"</string> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index ef1381cbe635..6fd37e91c8b0 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"බුබුළු"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"කළමනා කරන්න"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"බුබුල ඉවත දමා ඇත."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"වඩා හොඳ දසුනක් සඳහා මෙම යෙදුම යළි ඇරඹීමට තට්ටු කරන්න"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"සැකසීම් තුළ මෙම යෙදුමේ දර්ශන අනුපාතය වෙනස් කරන්න"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"දර්ශන අනුපාතය වෙනස් කරන්න"</string> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index 55a03122483b..dabbf397d38f 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovať"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina bola zavretá."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Ak chcete zlepšiť zobrazenie, klepnutím túto aplikáciu reštartujte"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Zmeniť pomer strán tejto aplikácie v Nastaveniach"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Zmeniť pomer strán"</string> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index bb123dcdbfb6..3ade33810cc8 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Mehurček"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblaček je bil opuščen."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Če želite boljši prikaz, se dotaknite za vnovični zagon te aplikacije."</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Razmerje stranic te aplikacije spremenite v nastavitvah."</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Sprememba razmerja stranic"</string> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index c74a8cd23338..ee1aa00c5cbe 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Flluskë"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Menaxho"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Flluska u hoq."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Trokit për ta rinisur këtë aplikacion për një pamje më të mirë"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Ndrysho raportin e pamjes së këtij aplikacioni te \"Cilësimet\""</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Ndrysho raportin e pamjes"</string> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index 0694a973dc1e..b2868ca84dac 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Облачић"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Управљајте"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Облачић је одбачен."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Додирните да бисте рестартовали ову апликацију ради бољег приказа"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Промените размеру ове апликације у Подешавањима"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Промени размеру"</string> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 8e0bcfe91679..66118efd5da7 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubbla"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Hantera"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubblan ignorerades."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Tryck för att starta om appen och få en bättre vy"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Ändra appens bildformat i inställningarna"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Ändra bildformat"</string> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 41180abcf712..863b49b1f010 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Kiputo"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Dhibiti"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Umeondoa kiputo."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Gusa ili uzime kisha uwashe programu hii, ili upate mwonekano bora"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Badilisha uwiano wa programu hii katika Mipangilio"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Badilisha uwiano wa kipengele"</string> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 01ac78d984f3..74e0207bf62e 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"பபிள்"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"நிர்வகி"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"குமிழ் நிராகரிக்கப்பட்டது."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"இங்கு தட்டுவதன் மூலம் இந்த ஆப்ஸை மீண்டும் தொடங்கி, ஆப்ஸ் காட்டப்படும் விதத்தை இன்னும் சிறப்பாக்கலாம்"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"அமைப்புகளில் இந்த ஆப்ஸின் தோற்ற விகிதத்தை மாற்றும்"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"தோற்ற விகிதத்தை மாற்றும்"</string> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 6224e72c19fe..35711567e760 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"బబుల్"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"మేనేజ్ చేయండి"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"బబుల్ విస్మరించబడింది."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"మెరుగైన వీక్షణ కోసం ఈ యాప్ను రీస్టార్ట్ చేయడానికి ట్యాప్ చేయండి"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"సెట్టింగ్లలో ఈ యాప్ ఆకార నిష్పత్తిని మార్చండి"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ఆకార నిష్పత్తిని మార్చండి"</string> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index fe0b74c469f4..47694164270e 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"บับเบิล"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"จัดการ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ปิดบับเบิลแล้ว"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"แตะเพื่อรีสตาร์ทแอปนี้และรับมุมมองที่ดียิ่งขึ้น"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"เปลี่ยนสัดส่วนภาพของแอปนี้ในการตั้งค่า"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"เปลี่ยนอัตราส่วนกว้างยาว"</string> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index 786e99cfe8c8..be18d88194c1 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Pamahalaan"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Na-dismiss na ang bubble."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"I-tap para i-restart ang app na ito para sa mas magandang view"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Baguhin ang aspect ratio ng app na ito sa Mga Setting"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Baguhin ang aspect ratio"</string> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index e953f5808aff..4c8c53610711 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Baloncuk"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Yönet"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon kapatıldı."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Bu uygulamayı yeniden başlatarak daha iyi bir görünüm elde etmek için dokunun"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Bu uygulamanın en boy oranını Ayarlar\'dan değiştirin"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"En boy oranını değiştir"</string> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index fbdf42e582d1..7cc1a0406f97 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Спливаюче сповіщення"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Налаштувати"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Спливаюче сповіщення закрито."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Натисніть, щоб перезапустити цей додаток для зручнішого перегляду"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Змінити формат для цього додатка в налаштуваннях"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Змінити формат"</string> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index 5562fa70bf09..8b9f29969d80 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"بلبلہ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"نظم کریں"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"بلبلہ برخاست کر دیا گیا۔"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"بہتر منظر کے لیے اس ایپ کو ری اسٹارٹ کرنے کی خاطر تھپتھپائیں"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ترتیبات میں اس ایپ کی تناسبی شرح کو تبدیل کریں"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"تناسبی شرح کو تبدیل کریں"</string> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index 50e42329a1a0..55c6b32c909b 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Pufaklar"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Boshqarish"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulutcha yopildi."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Yaxshiroq koʻrish maqsadida bu ilovani qayta ishga tushirish uchun bosing"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Sozlamalar orqali bu ilovaning tomonlar nisbatini oʻzgartiring"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Tomonlar nisbatini oʻzgartirish"</string> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 6da85881210d..07a6b6f6c2b4 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bong bóng"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Quản lý"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Đã đóng bong bóng."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Nhấn nút khởi động lại ứng dụng này để xem dễ hơn"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Thay đổi tỷ lệ khung hình của ứng dụng này thông qua phần Cài đặt"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Thay đổi tỷ lệ khung hình"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index 4318caf26199..908095a163a7 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"气泡"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已关闭消息气泡。"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"点按即可重启此应用,获得更好的视觉体验"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"在“设置”中更改此应用的宽高比"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"更改高宽比"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index 72cd39d8e00a..c8550b4e0611 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"氣泡"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"對話氣泡已關閉。"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"輕按並重新啟動此應用程式,以取得更佳的觀看體驗"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"前往「設定」變更此應用程式的長寬比"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"變更長寬比"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index c06d7b105694..67048335de64 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"泡泡"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已關閉泡泡。"</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"輕觸此按鈕重新啟動這個應用程式,即可獲得更良好的觀看體驗"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"前往「設定」變更這個應用程式的顯示比例"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"變更顯示比例"</string> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index 755414e52762..96b4faec06b8 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -84,6 +84,10 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Ibhamuza"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Phatha"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ibhamuza licashisiwe."</string> + <!-- no translation found for bubble_shortcut_label (666269077944378311) --> + <skip /> + <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> + <skip /> <string name="restart_button_description" msgid="4564728020654658478">"Thepha ukuze uqale kabusha le app ukuze ibonakale kangcono"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Shintsha ukubukeka kwesilinganiselo kwe-app kuMasethingi"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Shintsha ukubukeka kwesilinganiselo"</string> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index f27f46c07de6..595d34664cfa 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -459,6 +459,12 @@ start of this area. --> <dimen name="desktop_mode_customizable_caption_margin_end">152dp</dimen> + <!-- The default minimum allowed window width when resizing a window in desktop mode. --> + <dimen name="desktop_mode_minimum_window_width">386dp</dimen> + + <!-- The default minimum allowed window height when resizing a window in desktop mode. --> + <dimen name="desktop_mode_minimum_window_height">352dp</dimen> + <!-- The width of the maximize menu in desktop mode. --> <dimen name="desktop_mode_maximize_menu_width">228dp</dimen> @@ -476,6 +482,12 @@ <!-- The radius of the layout outline around the maximize menu buttons. --> <dimen name="desktop_mode_maximize_menu_buttons_outline_radius">6dp</dimen> + <!-- The stroke width of the outline around the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_outline_stroke">1dp</dimen> + <!-- The radius of the inner fill of the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen> + <!-- The padding between the outline and fill of the maximize menu buttons. --> + <dimen name="desktop_mode_maximize_menu_buttons_fill_padding">4dp</dimen> <!-- The corner radius of the maximize menu. --> <dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index bf654d979856..47846746b205 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -182,6 +182,12 @@ <!-- Content description to tell the user a bubble has been dismissed. --> <string name="accessibility_bubble_dismissed">Bubble dismissed.</string> + <!-- Label used to for bubbles shortcut [CHAR_LIMIT=10] --> + <string name="bubble_shortcut_label">Bubbles</string> + + <!-- Longer label used to for bubbles shortcut, shown if there is enough space [CHAR_LIMIT=25] --> + <string name="bubble_shortcut_long_label">Show Bubbles</string> + <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> <string name="restart_button_description">Tap to restart this app for a better view</string> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java index 8d8655addc65..4876f327a650 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java @@ -67,6 +67,10 @@ public class DesktopModeStatus { private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean( "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); + /** Whether the desktop density override is enabled. */ + public static final boolean DESKTOP_DENSITY_OVERRIDE_ENABLED = + SystemProperties.getBoolean("persist.wm.debug.desktop_mode_density_enabled", false); + /** Override density for tasks when they're inside the desktop. */ public static final int DESKTOP_DENSITY_OVERRIDE = SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284); @@ -157,9 +161,23 @@ public class DesktopModeStatus { } /** - * Return {@code true} if the override desktop density is set. + * Return {@code true} if the override desktop density is enabled and valid. + */ + public static boolean useDesktopOverrideDensity() { + return isDesktopDensityOverrideEnabled() && isValidDesktopDensityOverrideSet(); + } + + /** + * Return {@code true} if the override desktop density is enabled. + */ + private static boolean isDesktopDensityOverrideEnabled() { + return DESKTOP_DENSITY_OVERRIDE_ENABLED; + } + + /** + * Return {@code true} if the override desktop density is set and within a valid range. */ - public static boolean isDesktopDensityOverrideSet() { + private static boolean isValidDesktopDensityOverrideSet() { return DESKTOP_DENSITY_OVERRIDE >= DESKTOP_DENSITY_MIN && DESKTOP_DENSITY_OVERRIDE <= DESKTOP_DENSITY_MAX; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index a426b206b0cd..5a42817e839b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -31,6 +31,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_ import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; import android.util.ArraySet; @@ -45,6 +46,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationAdapter.SnapshotAdapter; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.shared.TransitionUtil; @@ -398,7 +400,15 @@ class ActivityEmbeddingAnimationRunner { // This is because the TaskFragment surface/change won't contain the Activity's before its // reparent. Animation changeAnimation = null; - Rect parentBounds = new Rect(); + final Rect parentBounds = new Rect(); + // We use a single boolean value to record the backdrop override because the override used + // for overlay and we restrict to single overlay animation. We should fix the assumption + // if we allow multiple overlay transitions. + // The backdrop logic is mainly for animations of split animations. The backdrop should be + // disabled if there is any open/close target in the same transition as the change target. + // However, the overlay change animation usually contains one change target, and shows + // backdrop unexpectedly. + Boolean overrideShowBackdrop = null; for (TransitionInfo.Change change : info.getChanges()) { if (change.getMode() != TRANSIT_CHANGE || change.getStartAbsBounds().equals(change.getEndAbsBounds())) { @@ -421,17 +431,17 @@ class ActivityEmbeddingAnimationRunner { } } - // The TaskFragment may be enter/exit split, so we take the union of both as the parent - // size. - parentBounds.union(boundsAnimationChange.getStartAbsBounds()); - parentBounds.union(boundsAnimationChange.getEndAbsBounds()); - if (boundsAnimationChange != change) { - // Union the change starting bounds in case the activity is resized and reparented - // to a TaskFragment. In that case, the TaskFragment may not cover the activity's - // starting bounds. - parentBounds.union(change.getStartAbsBounds()); + final TransitionInfo.AnimationOptions options = boundsAnimationChange + .getAnimationOptions(); + if (options != null) { + final Animation overrideAnimation = mAnimationSpec.loadCustomAnimationFromOptions( + options, TRANSIT_CHANGE); + if (overrideAnimation != null) { + overrideShowBackdrop = overrideAnimation.getShowBackdrop(); + } } + calculateParentBounds(change, boundsAnimationChange, parentBounds); // There are two animations in the array. The first one is for the start leash // (snapshot), and the second one is for the end leash (TaskFragment). final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change, @@ -466,7 +476,7 @@ class ActivityEmbeddingAnimationRunner { // If there is no corresponding open/close window with the change, we should show background // color to cover the empty part of the screen. - boolean shouldShouldBackgroundColor = true; + boolean shouldShowBackgroundColor = true; // Handle the other windows that don't have bounds change in the same transition. for (TransitionInfo.Change change : info.getChanges()) { if (handledChanges.contains(change)) { @@ -483,16 +493,18 @@ class ActivityEmbeddingAnimationRunner { animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change); } else if (TransitionUtil.isClosingType(change.getMode())) { animation = mAnimationSpec.createChangeBoundsCloseAnimation(change, parentBounds); - shouldShouldBackgroundColor = false; + shouldShowBackgroundColor = false; } else { animation = mAnimationSpec.createChangeBoundsOpenAnimation(change, parentBounds); - shouldShouldBackgroundColor = false; + shouldShowBackgroundColor = false; } adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change, TransitionUtil.getRootFor(change, info))); } - if (shouldShouldBackgroundColor && changeAnimation != null) { + shouldShowBackgroundColor = overrideShowBackdrop != null + ? overrideShowBackdrop : shouldShowBackgroundColor; + if (shouldShowBackgroundColor && changeAnimation != null) { // Change animation may leave part of the screen empty. Show background color to cover // that. changeAnimation.setShowBackdrop(true); @@ -502,6 +514,39 @@ class ActivityEmbeddingAnimationRunner { } /** + * Calculates parent bounds of the animation target by {@code change}. + */ + @VisibleForTesting + static void calculateParentBounds(@NonNull TransitionInfo.Change change, + @NonNull TransitionInfo.Change boundsAnimationChange, @NonNull Rect outParentBounds) { + if (Flags.activityEmbeddingOverlayPresentationFlag()) { + final Point endParentSize = change.getEndParentSize(); + if (endParentSize.equals(0, 0)) { + return; + } + final Point endRelPosition = change.getEndRelOffset(); + final Point endAbsPosition = new Point(change.getEndAbsBounds().left, + change.getEndAbsBounds().top); + final Point parentEndAbsPosition = new Point(endAbsPosition.x - endRelPosition.x, + endAbsPosition.y - endRelPosition.y); + outParentBounds.set(parentEndAbsPosition.x, parentEndAbsPosition.y, + parentEndAbsPosition.x + endParentSize.x, + parentEndAbsPosition.y + endParentSize.y); + } else { + // The TaskFragment may be enter/exit split, so we take the union of both as + // the parent size. + outParentBounds.union(boundsAnimationChange.getStartAbsBounds()); + outParentBounds.union(boundsAnimationChange.getEndAbsBounds()); + if (boundsAnimationChange != change) { + // Union the change starting bounds in case the activity is resized and + // reparented to a TaskFragment. In that case, the TaskFragment may not cover + // the activity's starting bounds. + outParentBounds.union(change.getStartAbsBounds()); + } + } + } + + /** * Takes a screenshot of the given {@code screenshotChange} surface if WM Core hasn't taken one. * The screenshot leash should be attached to the {@code animationChange} surface which we will * animate later. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java index 0272f1cda6ef..8d49614b021b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -18,6 +18,8 @@ package com.android.wm.shell.activityembedding; import static android.app.ActivityOptions.ANIM_CUSTOM; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.window.TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; @@ -27,6 +29,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.graphics.Rect; +import android.util.Log; +import android.view.WindowManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationSet; @@ -38,6 +42,7 @@ import android.view.animation.TranslateAnimation; import android.window.TransitionInfo; import com.android.internal.policy.TransitionAnimation; +import com.android.window.flags.Flags; import com.android.wm.shell.shared.TransitionUtil; /** Animation spec for ActivityEmbedding transition. */ @@ -202,7 +207,7 @@ class ActivityEmbeddingAnimationSpec { Animation loadOpenAnimation(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = TransitionUtil.isOpeningType(change.getMode()); - final Animation customAnimation = loadCustomAnimation(info, isEnter); + final Animation customAnimation = loadCustomAnimation(info, change); final Animation animation; if (customAnimation != null) { animation = customAnimation; @@ -229,7 +234,7 @@ class ActivityEmbeddingAnimationSpec { Animation loadCloseAnimation(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = TransitionUtil.isOpeningType(change.getMode()); - final Animation customAnimation = loadCustomAnimation(info, isEnter); + final Animation customAnimation = loadCustomAnimation(info, change); final Animation animation; if (customAnimation != null) { animation = customAnimation; @@ -261,13 +266,41 @@ class ActivityEmbeddingAnimationSpec { } @Nullable - private Animation loadCustomAnimation(@NonNull TransitionInfo info, boolean isEnter) { - final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + private Animation loadCustomAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change) { + final TransitionInfo.AnimationOptions options; + if (Flags.moveAnimationOptionsToChange()) { + options = change.getAnimationOptions(); + } else { + options = info.getAnimationOptions(); + } + return loadCustomAnimationFromOptions(options, change.getMode()); + } + + @Nullable + Animation loadCustomAnimationFromOptions(@Nullable TransitionInfo.AnimationOptions options, + @WindowManager.TransitionType int mode) { if (options == null || options.getType() != ANIM_CUSTOM) { return null; } + final int resId; + if (TransitionUtil.isOpeningType(mode)) { + resId = options.getEnterResId(); + } else if (TransitionUtil.isClosingType(mode)) { + resId = options.getExitResId(); + } else if (mode == TRANSIT_CHANGE) { + resId = options.getChangeResId(); + } else { + Log.w(TAG, "Unknown transit type:" + mode); + resId = DEFAULT_ANIMATION_RESOURCES_ID; + } + // Use the default animation if the resources ID is not specified. + if (resId == DEFAULT_ANIMATION_RESOURCES_ID) { + return null; + } + final Animation anim = mTransitionAnimation.loadAnimationRes(options.getPackageName(), - isEnter ? options.getEnterResId() : options.getExitResId()); + resId); if (anim != null) { return anim; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java index d6b9d34c5ab3..b4ef9f0fc2ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java @@ -32,6 +32,7 @@ import android.os.IBinder; import android.util.ArrayMap; import android.view.SurfaceControl; import android.window.TransitionInfo; +import android.window.TransitionInfo.AnimationOptions; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -39,6 +40,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -117,24 +119,39 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle return false; } - final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); - if (options != null) { - // Scene-transition should be handled by app side. - if (options.getType() == ANIM_SCENE_TRANSITION) { + return shouldAnimateAnimationOptions(info); + } + + private boolean shouldAnimateAnimationOptions(@NonNull TransitionInfo info) { + if (!Flags.moveAnimationOptionsToChange()) { + return shouldAnimateAnimationOptions(info.getAnimationOptions()); + } + for (TransitionInfo.Change change : info.getChanges()) { + if (!shouldAnimateAnimationOptions(change.getAnimationOptions())) { + // If any of override animation is not supported, don't animate the transition. return false; } - // The case of ActivityOptions#makeCustomAnimation, Activity#overridePendingTransition, - // and Activity#overrideActivityTransition are supported. - if (options.getType() == ANIM_CUSTOM) { - return true; - } - // Use default transition handler to animate other override animation. - return !isSupportedOverrideAnimation(options); } - return true; } + private boolean shouldAnimateAnimationOptions(@Nullable AnimationOptions options) { + if (options == null) { + return true; + } + // Scene-transition should be handled by app side. + if (options.getType() == ANIM_SCENE_TRANSITION) { + return false; + } + // The case of ActivityOptions#makeCustomAnimation, Activity#overridePendingTransition, + // and Activity#overrideActivityTransition are supported. + if (options.getType() == ANIM_CUSTOM) { + return true; + } + // Use default transition handler to animate other override animation. + return !isSupportedOverrideAnimation(options); + } + @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, 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 5600664a8f47..7041ea307b0f 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 @@ -18,7 +18,6 @@ package com.android.wm.shell.back; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; import static com.android.window.flags.Flags.predictiveBackSystemAnims; -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; @@ -31,6 +30,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Configuration; import android.database.ContentObserver; +import android.graphics.Rect; import android.hardware.input.InputManager; import android.net.Uri; import android.os.Bundle; @@ -41,7 +41,6 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings.Global; -import android.util.DisplayMetrics; import android.util.Log; import android.view.IRemoteAnimationRunner; import android.view.InputDevice; @@ -49,6 +48,7 @@ import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; +import android.view.WindowManager; import android.window.BackAnimationAdapter; import android.window.BackEvent; import android.window.BackMotionEvent; @@ -63,7 +63,6 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.LatencyTracker; import com.android.internal.view.AppearanceRegion; import com.android.wm.shell.R; -import com.android.wm.shell.animation.FlingAnimationUtils; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; @@ -88,15 +87,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont public static final boolean IS_ENABLED = SystemProperties.getInt("persist.wm.debug.predictive_back", SETTING_VALUE_ON) == SETTING_VALUE_ON; - public static final float FLING_MAX_LENGTH_SECONDS = 0.1f; // 100ms - public static final float FLING_SPEED_UP_FACTOR = 0.6f; - - /** - * The maximum additional progress in case of fling gesture. - * The end animation starts after the user lifts the finger from the screen, we continue to - * fire {@link BackEvent}s until the velocity reaches 0. - */ - private static final float MAX_FLING_PROGRESS = 0.3f; /* 30% of the screen */ /** Predictive back animation developer option */ private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); @@ -119,8 +109,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private boolean mPointersPilfered = false; private final boolean mRequirePointerPilfer; - private final FlingAnimationUtils mFlingAnimationUtils; - /** Registry for the back animations */ private final ShellBackAnimationRegistry mShellBackAnimationRegistry; @@ -133,6 +121,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mShellExecutor; private final Handler mBgHandler; + private final WindowManager mWindowManager; + @VisibleForTesting + final Rect mTouchableArea = new Rect(); /** * Tracks the current user back gesture. @@ -233,14 +224,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mBgHandler = bgHandler; shellInit.addInitCallback(this::onInit, this); mAnimationBackground = backAnimationBackground; - DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - mFlingAnimationUtils = new FlingAnimationUtils.Builder(displayMetrics) - .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS) - .setSpeedUpFactor(FLING_SPEED_UP_FACTOR) - .build(); mShellBackAnimationRegistry = shellBackAnimationRegistry; mLatencyTracker = LatencyTracker.getInstance(mContext); mShellCommandHandler = shellCommandHandler; + mWindowManager = context.getSystemService(WindowManager.class); + updateTouchableArea(); } private void onInit() { @@ -302,6 +290,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Override public void onConfigurationChanged(Configuration newConfig) { mShellBackAnimationRegistry.onConfigurationChanged(newConfig); + updateTouchableArea(); + } + + private void updateTouchableArea() { + mTouchableArea.set(mWindowManager.getCurrentWindowMetrics().getBounds()); } @Override @@ -435,11 +428,19 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!shouldDispatchToAnimator && mActiveCallback != null) { mCurrentTracker.updateStartLocation(); tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null)); + if (mBackNavigationInfo != null && !isAppProgressGenerationAllowed()) { + tryPilferPointers(); + } } else if (shouldDispatchToAnimator) { tryPilferPointers(); } } + private boolean isAppProgressGenerationAllowed() { + return mBackNavigationInfo.isAppProgressGenerationAllowed() + && mBackNavigationInfo.getTouchableRegion().equals(mTouchableArea); + } + /** * Called when a new motion event needs to be transferred to this * {@link BackAnimationController} @@ -555,6 +556,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // App is handling back animation. Cancel system animation latency tracking. cancelLatencyTracking(); tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null)); + if (!isAppProgressGenerationAllowed()) { + tryPilferPointers(); + } } } @@ -661,7 +665,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void dispatchOnBackProgressed(IOnBackInvokedCallback callback, BackMotionEvent backEvent) { - if (callback == null || !shouldDispatchToAnimator()) { + if (callback == null || (!shouldDispatchToAnimator() && mBackNavigationInfo != null + && isAppProgressGenerationAllowed())) { return; } try { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt index 9114c7adb6d8..c9d3dbdcae05 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -324,6 +324,7 @@ abstract class CrossActivityBackAnimation( enteringHasSameLetterbox = false lastPostCommitFlingScale = SPRING_SCALE gestureProgress = 0f + triggerBack = false } protected fun applyTransform( @@ -354,7 +355,7 @@ abstract class CrossActivityBackAnimation( matrix.postScale(scale, scale, scalePivotX, 0f) matrix.postTranslate(tempRectF.left, tempRectF.top) transaction - .setAlpha(leash, keepMinimumAlpha(alpha)) + .setAlpha(leash, alpha) .setMatrix(leash, matrix, tmpFloat9) .setCrop(leash, cropRect) .setCornerRadius(leash, cornerRadius) @@ -499,10 +500,12 @@ abstract class CrossActivityBackAnimation( } override fun onBackCancelled() { + triggerBack = false progressAnimator.onBackCancelled { finishAnimation() } } override fun onBackInvoked() { + triggerBack = true progressAnimator.reset() onGestureCommitted(progressAnimator.velocity) } @@ -562,9 +565,6 @@ abstract class CrossActivityBackAnimation( } } -// The target will loose focus when alpha == 0, so keep a minimum value for it. -private fun keepMinimumAlpha(transAlpha: Float) = max(transAlpha, 0.005f) - private fun isDarkMode(context: Context): Boolean { return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index da530d740d48..2aefc64a3ebb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -305,6 +305,7 @@ public class Bubble implements BubbleViewProvider { getUser().getIdentifier(), getPackageName(), getTitle(), + getAppName(), isImportantConversation()); } @@ -893,11 +894,22 @@ public class Bubble implements BubbleViewProvider { } @Nullable - Intent getAppBubbleIntent() { + @VisibleForTesting + public Intent getAppBubbleIntent() { return mAppIntent; } /** + * Sets the intent for a bubble that is an app bubble (one for which {@link #mIsAppBubble} is + * true). + * + * @param appIntent The intent to set for the app bubble. + */ + void setAppBubbleIntent(Intent appIntent) { + mAppIntent = appIntent; + } + + /** * Returns whether this bubble is from an app versus a notification. */ public boolean isAppBubble() { 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 317e00a44bce..c853301519e9 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 @@ -86,6 +86,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.util.CollectionUtils; import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.Flags; import com.android.wm.shell.R; @@ -93,6 +94,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.bubbles.properties.BubbleProperties; +import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.FloatingContentCoordinator; @@ -511,6 +513,10 @@ public class BubbleController implements ConfigurationChangeListener, } mCurrentProfiles = userProfiles; + if (Flags.enableRetrievableBubbles()) { + registerShortcutBroadcastReceiver(); + } + mShellController.addConfigurationChangeListener(this); mShellController.addExternalInterface(KEY_EXTRA_SHELL_BUBBLES, this::createExternalInterface, this); @@ -518,7 +524,7 @@ public class BubbleController implements ConfigurationChangeListener, } private ExternalInterfaceBinder createExternalInterface() { - return new BubbleController.IBubblesImpl(this); + return new IBubblesImpl(this); } @VisibleForTesting @@ -592,11 +598,12 @@ public class BubbleController implements ConfigurationChangeListener, * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. */ void hideCurrentInputMethod() { + mBubblePositioner.setImeVisible(false /* visible */, 0 /* height */); int displayId = mWindowManager.getDefaultDisplay().getDisplayId(); try { mBarService.hideCurrentInputMethodForBubbles(displayId); } catch (RemoteException e) { - e.printStackTrace(); + Log.e(TAG, "Failed to hide IME", e); } } @@ -986,6 +993,25 @@ public class BubbleController implements ConfigurationChangeListener, } }; + private void registerShortcutBroadcastReceiver() { + IntentFilter shortcutFilter = new IntentFilter(); + shortcutFilter.addAction(BubbleShortcutHelper.ACTION_SHOW_BUBBLES); + ProtoLog.d(WM_SHELL_BUBBLES, "register broadcast receive for bubbles shortcut"); + mContext.registerReceiver(mShortcutBroadcastReceiver, shortcutFilter, + Context.RECEIVER_NOT_EXPORTED); + } + + private final BroadcastReceiver mShortcutBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + ProtoLog.v(WM_SHELL_BUBBLES, "receive broadcast to show bubbles %s", + intent.getAction()); + if (BubbleShortcutHelper.ACTION_SHOW_BUBBLES.equals(intent.getAction())) { + mMainExecutor.execute(() -> showBubblesFromShortcut()); + } + } + }; + /** * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been * added in the meantime. @@ -1424,6 +1450,8 @@ public class BubbleController implements ConfigurationChangeListener, if (b != null) { // It's in the overflow, so remove it & reinflate mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); + // Update the bubble entry in the overflow with the latest intent. + b.setAppBubbleIntent(intent); } else { // App bubble does not exist, lets add and expand it b = Bubble.createAppBubble(intent, user, icon, mMainExecutor); @@ -1457,8 +1485,9 @@ public class BubbleController implements ConfigurationChangeListener, SynchronousScreenCaptureListener screenCaptureListener) { try { ScreenCapture.CaptureArgs args = null; - if (mStackView != null) { - ViewRootImpl viewRoot = mStackView.getViewRootImpl(); + View viewToUse = mStackView != null ? mStackView : mLayerView; + if (viewToUse != null) { + ViewRootImpl viewRoot = viewToUse.getViewRootImpl(); if (viewRoot != null) { SurfaceControl bubbleLayer = viewRoot.getSurfaceControl(); if (bubbleLayer != null) { @@ -1550,6 +1579,12 @@ public class BubbleController implements ConfigurationChangeListener, Log.w(TAG, "Tried to add a bubble to the stack but the stack is null"); } }; + } else if (mBubbleData.isExpanded() && mBubbleData.getSelectedBubble() != null) { + callback = b -> { + if (b.getKey().equals(mBubbleData.getSelectedBubbleKey())) { + mLayerView.showExpandedView(b); + } + }; } for (int i = mBubbleData.getBubbles().size() - 1; i >= 0; i--) { Bubble bubble = mBubbleData.getBubbles().get(i); @@ -2221,6 +2256,34 @@ public class BubbleController implements ConfigurationChangeListener, } /** + * Show bubbles UI when triggered via shortcut. + * + * <p>When there are bubbles visible, expands the top-most bubble. When there are no bubbles + * visible, opens the bubbles overflow UI. + */ + public void showBubblesFromShortcut() { + if (isStackExpanded()) { + ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: stack visible, skip"); + return; + } + if (mBubbleData.getSelectedBubble() != null) { + ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: open selected bubble"); + expandStackWithSelectedBubble(); + return; + } + BubbleViewProvider bubbleToSelect = CollectionUtils.firstOrNull(mBubbleData.getBubbles()); + if (bubbleToSelect == null) { + ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: no bubbles"); + // make sure overflow bubbles are loaded + loadOverflowBubblesFromDisk(); + bubbleToSelect = mBubbleData.getOverflow(); + } + ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: select and open %s", + bubbleToSelect.getKey()); + mBubbleData.setSelectedBubbleAndExpandStack(bubbleToSelect); + } + + /** * Description of current bubble state. */ private void dump(PrintWriter pw, String prefix) { @@ -2354,6 +2417,8 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void invalidate() { mController = null; + // Unregister the listeners to ensure any binder death recipients are unlinked + mListener.unregister(); } @Override @@ -2531,17 +2596,6 @@ public class BubbleController implements ConfigurationChangeListener, private CachedState mCachedState = new CachedState(); - private IBubblesImpl mIBubbles; - - @Override - public IBubbles createExternalInterface() { - if (mIBubbles != null) { - mIBubbles.invalidate(); - } - mIBubbles = new IBubblesImpl(BubbleController.this); - return mIBubbles; - } - @Override public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) { return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 874102c20925..761e02598460 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -256,11 +256,15 @@ public class BubbleData { } /** - * Returns a bubble bar update populated with the current list of active bubbles. + * Returns a bubble bar update populated with the current list of active bubbles, expanded, + * and selected state. */ public BubbleBarUpdate getInitialStateForBubbleBar() { BubbleBarUpdate initialState = mStateChange.getInitialState(); initialState.bubbleBarLocation = mPositioner.getBubbleBarLocation(); + initialState.expanded = mExpanded; + initialState.expandedChanged = mExpanded; // only matters if we're expanded + initialState.selectedBubbleKey = getSelectedBubbleKey(); return initialState; } @@ -598,7 +602,7 @@ public class BubbleData { List<Bubble> removedBubbles = filterAllBubbles(bubble -> userId == bubble.getUser().getIdentifier()); for (Bubble b : removedBubbles) { - doRemove(b.getKey(), Bubbles.DISMISS_USER_REMOVED); + doRemove(b.getKey(), Bubbles.DISMISS_USER_ACCOUNT_REMOVED); } if (!removedBubbles.isEmpty()) { dispatchPendingChanges(); @@ -674,7 +678,7 @@ public class BubbleData { || reason == Bubbles.DISMISS_SHORTCUT_REMOVED || reason == Bubbles.DISMISS_PACKAGE_REMOVED || reason == Bubbles.DISMISS_USER_CHANGED - || reason == Bubbles.DISMISS_USER_REMOVED; + || reason == Bubbles.DISMISS_USER_ACCOUNT_REMOVED; int indexToRemove = indexForKey(key); if (indexToRemove == -1) { @@ -912,6 +916,9 @@ public class BubbleData { ((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); } mSelectedBubble = bubble; + if (isOverflow) { + mShowingOverflow = true; + } mStateChange.selectedBubble = bubble; mStateChange.selectionChanged = true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 4e8afccee40f..c7ccd50af550 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -482,31 +482,38 @@ public class BubbleExpandedView extends LinearLayout { mPointerWidth, mPointerHeight, true /* pointLeft */)); mRightPointer = new ShapeDrawable(TriangleShape.createHorizontal( mPointerWidth, mPointerHeight, false /* pointLeft */)); - if (mPointerView != null) { - updatePointerView(); - } + updatePointerViewIfExists(); + updateManageButtonIfExists(); + } - if (mManageButton != null) { - int visibility = mManageButton.getVisibility(); - removeView(mManageButton); - ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(), - com.android.internal.R.style.Theme_DeviceDefault_DayNight); - mManageButton = (AlphaOptimizedButton) LayoutInflater.from(ctw).inflate( - R.layout.bubble_manage_button, this /* parent */, false /* attach */); - addView(mManageButton); - mManageButton.setVisibility(visibility); - post(() -> { - int touchAreaHeight = - getResources().getDimensionPixelSize( - R.dimen.bubble_manage_button_touch_area_height); - Rect r = new Rect(); - mManageButton.getHitRect(r); - int extraTouchArea = (touchAreaHeight - r.height()) / 2; - r.top -= extraTouchArea; - r.bottom += extraTouchArea; - setTouchDelegate(new TouchDelegate(r, mManageButton)); - }); + + /** + * Reinflate manage button if {@link #mManageButton} is initialized. + * Does nothing otherwise. + */ + private void updateManageButtonIfExists() { + if (mManageButton == null) { + return; } + int visibility = mManageButton.getVisibility(); + removeView(mManageButton); + ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(), + com.android.internal.R.style.Theme_DeviceDefault_DayNight); + mManageButton = (AlphaOptimizedButton) LayoutInflater.from(ctw).inflate( + R.layout.bubble_manage_button, this /* parent */, false /* attach */); + addView(mManageButton); + mManageButton.setVisibility(visibility); + post(() -> { + int touchAreaHeight = + getResources().getDimensionPixelSize( + R.dimen.bubble_manage_button_touch_area_height); + Rect r = new Rect(); + mManageButton.getHitRect(r); + int extraTouchArea = (touchAreaHeight - r.height()) / 2; + r.top -= extraTouchArea; + r.bottom += extraTouchArea; + setTouchDelegate(new TouchDelegate(r, mManageButton)); + }); } void updateFontSize() { @@ -548,11 +555,18 @@ public class BubbleExpandedView extends LinearLayout { if (mTaskView != null) { mTaskView.setCornerRadius(mCornerRadius); } - updatePointerView(); + updatePointerViewIfExists(); + updateManageButtonIfExists(); } - /** Updates the size and visuals of the pointer. **/ - private void updatePointerView() { + /** + * Updates the size and visuals of the pointer if {@link #mPointerView} is initialized. + * Does nothing otherwise. + */ + private void updatePointerViewIfExists() { + if (mPointerView == null) { + return; + } LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams(); if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) { lp.width = mPointerHeight; @@ -1055,7 +1069,7 @@ public class BubbleExpandedView extends LinearLayout { // Post because we need the width of the view post(() -> { mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; - updatePointerView(); + updatePointerViewIfExists(); if (showVertically) { mPointerPos.y = bubbleCenter - (mPointerWidth / 2f); if (!isRtl) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index b0d3cc4a5d5c..3d9bf032c1b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -29,6 +29,7 @@ interface BubbleExpandedViewManager { fun setAppBubbleTaskId(key: String, taskId: Int) fun isStackExpanded(): Boolean fun isShowingAsBubbleBar(): Boolean + fun hideCurrentInputMethod() companion object { /** @@ -73,6 +74,10 @@ interface BubbleExpandedViewManager { override fun isStackExpanded(): Boolean = controller.isStackExpanded override fun isShowingAsBubbleBar(): Boolean = controller.isShowingAsBubbleBar + + override fun hideCurrentInputMethod() { + controller.hideCurrentInputMethod() + } } } } 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 fac9bf6e2a4b..09bec8c37b9a 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 @@ -134,6 +134,8 @@ public class BubbleStackView extends FrameLayout private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f; + private static final float OPEN_OVERFLOW_ANIMATE_SCALE_AMOUNT = 0.5f; + private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; /** Minimum alpha value for scrim when alpha is being changed via drag */ @@ -1549,10 +1551,20 @@ public class BubbleStackView extends FrameLayout } private void updateOverflowVisibility() { - mBubbleOverflow.setVisible(mShowingOverflow - && (mIsExpanded || mBubbleData.isShowingOverflow()) - ? VISIBLE - : GONE); + int visibility = GONE; + if (mShowingOverflow) { + if (mIsExpanded || mBubbleData.isShowingOverflow()) { + visibility = VISIBLE; + } + } + if (Flags.enableRetrievableBubbles()) { + if (BubbleOverflow.KEY.equals(mBubbleData.getSelectedBubbleKey()) + && !mBubbleData.hasBubbles()) { + // Hide overflow bubble icon if it is the only bubble + visibility = GONE; + } + } + mBubbleOverflow.setVisible(visibility); } private void updateOverflowDotVisibility(boolean expanding) { @@ -2147,6 +2159,13 @@ public class BubbleStackView extends FrameLayout if (mIsExpanded) { hideCurrentInputMethod(); + if (Flags.enableRetrievableBubbles()) { + if (mBubbleData.getBubbles().size() == 1) { + // First bubble, check if overflow visibility needs to change + updateOverflowVisibility(); + } + } + // Make the container of the expanded view transparent before removing the expanded view // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the // expanded view becomes visible on the screen. See b/126856255 @@ -2215,6 +2234,16 @@ public class BubbleStackView extends FrameLayout } /** + * Check if we only have overflow expanded. Which is the case when we are launching bubbles from + * background. + */ + private boolean isOnlyOverflowExpanded() { + boolean overflowExpanded = mExpandedBubble != null && BubbleOverflow.KEY.equals( + mExpandedBubble.getKey()); + return overflowExpanded && !mBubbleData.hasBubbles(); + } + + /** * Monitor for swipe up gesture that is used to collapse expanded view */ void startMonitoringSwipeUpGesture() { @@ -2324,7 +2353,6 @@ public class BubbleStackView extends FrameLayout * not. */ void hideCurrentInputMethod() { - mPositioner.setImeVisible(false, 0); mManager.hideCurrentInputMethod(); } @@ -2434,7 +2462,7 @@ public class BubbleStackView extends FrameLayout ProtoLog.d(WM_SHELL_BUBBLES, "animateExpansion, expandedBubble=%s", mExpandedBubble != null ? mExpandedBubble.getKey() : "null"); cancelDelayedExpandCollapseSwitchAnimations(); - final boolean showVertically = mPositioner.showBubblesVertically(); + mIsExpanded = true; if (isStackEduVisible()) { mStackEduView.hide(true /* fromExpansion */); @@ -2444,8 +2472,17 @@ public class BubbleStackView extends FrameLayout showScrim(true, null /* runnable */); updateBubbleShadows(mIsExpanded); mBubbleContainer.setActiveController(mExpandedAnimationController); - updateBadges(false /* setBadgeForCollapsedStack */); updateOverflowVisibility(); + + if (Flags.enableRetrievableBubbles() && isOnlyOverflowExpanded()) { + animateOverflowExpansion(); + } else { + animateBubbleExpansion(); + } + } + + private void animateBubbleExpansion() { + updateBadges(false /* setBadgeForCollapsedStack */); updatePointerPosition(false /* forIme */); if (Flags.enableBubbleStashing()) { mBubbleContainer.animate().translationX(0).start(); @@ -2469,6 +2506,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setTranslationY(translationY); mExpandedViewContainer.setAlpha(1f); + final boolean showVertically = mPositioner.showBubblesVertically(); // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles // that are animating farther, so that the expanded view doesn't move as much. final float relevantStackPosition = showVertically @@ -2561,6 +2599,47 @@ public class BubbleStackView extends FrameLayout mMainExecutor.executeDelayed(mDelayedAnimation, startDelay); } + /** + * Animate expansion of overflow view when it is shown from the bubble shortcut. + * <p> + * Animates the view with a scale originating from the center of the view. + */ + private void animateOverflowExpansion() { + PointF bubbleXY = mPositioner.getExpandedBubbleXY(0, getState()); + final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, + mPositioner.showBubblesVertically() ? bubbleXY.y : bubbleXY.x); + mExpandedViewContainer.setTranslationX(0f); + mExpandedViewContainer.setTranslationY(translationY); + mExpandedViewContainer.setAlpha(1f); + + boolean stackOnLeft = mPositioner.isStackOnLeft(getStackPosition()); + float width = mPositioner.getTaskViewContentWidth(stackOnLeft); + float height = mPositioner.getExpandedViewHeight(mExpandedBubble); + float scale = 1f - OPEN_OVERFLOW_ANIMATE_SCALE_AMOUNT; + // Scale from the center of the view + mExpandedViewContainerMatrix.setScale(scale, scale, width / 2f, height / 2f); + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); + mExpandedViewAlphaAnimator.start(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) + .spring(AnimatableScaleMatrix.SCALE_X, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .addUpdateListener((target, values) -> { + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); + }).withEndActions(() -> { + mExpandedViewContainer.setAnimationMatrix(null); + afterExpandedViewAnimation(); + BubbleExpandedView expandedView = getExpandedView(); + if (expandedView != null) { + expandedView.setSurfaceZOrderedOnTop(false); + } + }).start(); + } + private void animateCollapse() { cancelDelayedExpandCollapseSwitchAnimations(); ProtoLog.d(WM_SHELL_BUBBLES, "animateCollapse"); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 21b70b8e32da..0b66bcb6930e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -161,6 +161,11 @@ public class BubbleTaskViewHelper { // The taskId is saved to use for removeTask, preventing appearance in recent tasks. mTaskId = taskId; + if (mBubble != null && mBubble.isAppBubble()) { + // Let the controller know sooner what the taskId is. + mExpandedViewManager.setAppBubbleTaskId(mBubble.getKey(), mTaskId); + } + // With the task org, the taskAppeared callback will only happen once the task has // already drawn mListener.onTaskCreated(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 1d053f9aab35..82af88d03b19 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -61,7 +61,7 @@ public interface Bubbles { DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, - DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_REMOVED, + DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_ACCOUNT_REMOVED, DISMISS_SWITCH_TO_STACK}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason { @@ -82,7 +82,7 @@ public interface Bubbles { int DISMISS_PACKAGE_REMOVED = 13; int DISMISS_NO_BUBBLE_UP = 14; int DISMISS_RELOAD_FROM_DISK = 15; - int DISMISS_USER_REMOVED = 16; + int DISMISS_USER_ACCOUNT_REMOVED = 16; int DISMISS_SWITCH_TO_STACK = 17; /** Returns a binder that can be passed to an external process to manipulate Bubbles. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 271fb9abce6a..972dce51e02b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -82,6 +82,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private static final int INVALID_TASK_ID = -1; private BubbleExpandedViewManager mManager; + private BubblePositioner mPositioner; private boolean mIsOverflow; private BubbleTaskViewHelper mBubbleTaskViewHelper; private BubbleBarMenuViewController mMenuViewController; @@ -160,6 +161,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView boolean isOverflow, @Nullable BubbleTaskView bubbleTaskView) { mManager = expandedViewManager; + mPositioner = positioner; mIsOverflow = isOverflow; if (mIsOverflow) { @@ -207,7 +209,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView @Override public void onDismissBubble(Bubble bubble) { - mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED); + mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE); } }); mHandleView.setOnClickListener(view -> { @@ -290,15 +292,27 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView } /** - * Hides the current modal menu view or collapses the bubble stack. - * Called from {@link BubbleBarLayerView} + * Hides the current modal menu if it is visible + * @return {@code true} if menu was visible and is hidden */ - public void hideMenuOrCollapse() { + public boolean hideMenuIfVisible() { if (mMenuViewController.isMenuVisible()) { - mMenuViewController.hideMenu(/* animated = */ true); - } else { - mManager.collapseStack(); + mMenuViewController.hideMenu(true /* animated */); + return true; + } + return false; + } + + /** + * Hides the IME if it is visible + * @return {@code true} if IME was visible + */ + public boolean hideImeIfVisible() { + if (mPositioner.isImeVisible()) { + mManager.hideCurrentInputMethod(); + return true; } + return false; } /** Updates the bubble shown in the expanded view. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 123cc7e9d488..badc40997902 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -132,7 +132,7 @@ public class BubbleBarLayerView extends FrameLayout } }); - setOnClickListener(view -> hideMenuOrCollapse()); + setOnClickListener(view -> hideModalOrCollapse()); } @Override @@ -217,7 +217,7 @@ public class BubbleBarLayerView extends FrameLayout @Override public void onBackPressed() { - hideMenuOrCollapse(); + hideModalOrCollapse(); } }); @@ -344,15 +344,23 @@ public class BubbleBarLayerView extends FrameLayout addView(mDismissView); } - /** Hides the current modal education/menu view, expanded view or collapses the bubble stack */ - private void hideMenuOrCollapse() { + /** Hides the current modal education/menu view, IME or collapses the expanded view */ + private void hideModalOrCollapse() { if (mEducationViewController.isEducationVisible()) { mEducationViewController.hideEducation(/* animated = */ true); - } else if (isExpanded() && mExpandedView != null) { - mExpandedView.hideMenuOrCollapse(); - } else { - mBubbleController.collapseStack(); + return; + } + if (isExpanded() && mExpandedView != null) { + boolean menuHidden = mExpandedView.hideMenuIfVisible(); + if (menuHidden) { + return; + } + boolean imeHidden = mExpandedView.hideImeIfVisible(); + if (imeHidden) { + return; + } } + mBubbleController.collapseStack(); } /** Updates the expanded view size and position. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt new file mode 100644 index 000000000000..efa12383f188 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles.shortcut + +import android.content.Context +import android.content.pm.ShortcutInfo +import android.graphics.drawable.Icon +import com.android.wm.shell.R + +/** Helper class for creating a shortcut to open bubbles */ +object BubbleShortcutHelper { + const val SHORTCUT_ID = "bubbles_shortcut_id" + const val ACTION_SHOW_BUBBLES = "com.android.wm.shell.bubbles.action.SHOW_BUBBLES" + + /** Create a shortcut that launches [ShowBubblesActivity] */ + fun createShortcut(context: Context, icon: Icon): ShortcutInfo { + return ShortcutInfo.Builder(context, SHORTCUT_ID) + .setIntent(ShowBubblesActivity.createIntent(context)) + .setActivity(ShowBubblesActivity.createComponent(context)) + .setShortLabel(context.getString(R.string.bubble_shortcut_label)) + .setLongLabel(context.getString(R.string.bubble_shortcut_long_label)) + .setLongLived(true) + .setIcon(icon) + .build() + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt new file mode 100644 index 000000000000..a124f95d7431 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles.shortcut + +import android.app.Activity +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.os.Bundle +import com.android.wm.shell.Flags +import com.android.wm.shell.R +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES +import com.android.wm.shell.util.KtProtoLog + +/** Activity to create a shortcut to open bubbles */ +class CreateBubbleShortcutActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (Flags.enableRetrievableBubbles()) { + KtProtoLog.d(WM_SHELL_BUBBLES, "Creating a shortcut for bubbles") + createShortcut() + } + finish() + } + + private fun createShortcut() { + val icon = Icon.createWithResource(this, R.drawable.ic_bubbles_shortcut_widget) + // TODO(b/340337839): shortcut shows the sysui icon + val shortcutInfo = BubbleShortcutHelper.createShortcut(this, icon) + val shortcutManager = getSystemService(ShortcutManager::class.java) + val shortcutIntent = shortcutManager?.createShortcutResultIntent(shortcutInfo) + if (shortcutIntent != null) { + setResult(RESULT_OK, shortcutIntent) + } else { + setResult(RESULT_CANCELED) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt new file mode 100644 index 000000000000..ae7940ca1b65 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles.shortcut + +import android.app.Activity +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.android.wm.shell.Flags +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES +import com.android.wm.shell.util.KtProtoLog + +/** Activity that sends a broadcast to open bubbles */ +class ShowBubblesActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (Flags.enableRetrievableBubbles()) { + val intent = + Intent().apply { + action = BubbleShortcutHelper.ACTION_SHOW_BUBBLES + // Set the package as the receiver is not exported + `package` = packageName + } + KtProtoLog.v(WM_SHELL_BUBBLES, "Sending broadcast to show bubbles") + sendBroadcast(intent) + } + finish() + } + + companion object { + /** Create intent to launch this activity */ + fun createIntent(context: Context): Intent { + return Intent(context, ShowBubblesActivity::class.java).apply { + action = BubbleShortcutHelper.ACTION_SHOW_BUBBLES + } + } + + /** Create component for this activity */ + fun createComponent(context: Context): ComponentName { + return ComponentName(context, ShowBubblesActivity::class.java) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index ad01d0fa311a..f4ac5f260fcd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -220,6 +220,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged final int mDisplayId; final InsetsState mInsetsState = new InsetsState(); @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); + boolean mImeRequestedVisible = + (WindowInsets.Type.defaultVisible() & WindowInsets.Type.ime()) != 0; InsetsSourceControl mImeSourceControl = null; int mAnimationDirection = DIRECTION_NONE; ValueAnimator mAnimation = null; @@ -247,8 +249,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged return; } - updateImeVisibility(insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, - WindowInsets.Type.ime())); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + updateImeVisibility(insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, + WindowInsets.Type.ime())); + } final InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME); final Rect newFrame = newSource != null ? newSource.getFrame() : null; @@ -287,32 +291,63 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged dispatchImeControlTargetChanged(mDisplayId, hasImeSourceControl); } - if (hasImeSourceControl) { + boolean pendingImeStartAnimation = false; + boolean canAnimate; + if (android.view.inputmethod.Flags.refactorInsetsController()) { + canAnimate = hasImeSourceControl && imeSourceControl.getLeash() != null; + } else { + canAnimate = hasImeSourceControl; + } + + boolean positionChanged = false; + if (canAnimate) { if (mAnimation != null) { final Point lastSurfacePosition = hadImeSourceControl ? mImeSourceControl.getSurfacePosition() : null; - final boolean positionChanged = - !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition); - if (positionChanged) { - startAnimation(mImeShowing, true /* forceRestart */, - SoftInputShowHideReason.DISPLAY_CONTROLS_CHANGED); - } + positionChanged = !imeSourceControl.getSurfacePosition().equals( + lastSurfacePosition); } else { if (!haveSameLeash(mImeSourceControl, imeSourceControl)) { applyVisibilityToLeash(imeSourceControl); + + if (android.view.inputmethod.Flags.refactorInsetsController()) { + pendingImeStartAnimation = true; + } } if (!mImeShowing) { removeImeSurface(); } } - } else if (mAnimation != null) { + } else if (!android.view.inputmethod.Flags.refactorInsetsController() + && mAnimation != null) { + // we don"t want to cancel the hide animation, when the control is lost, but + // continue the bar to slide to the end (even without visible IME) mAnimation.cancel(); } + if (positionChanged) { + if (android.view.inputmethod.Flags.refactorInsetsController()) { + // For showing the IME, the leash has to be available first. Hiding + // the IME happens directly via {@link #hideInsets} (triggered by + // setImeInputTargetRequestedVisibility) while the leash is not gone + // yet. + pendingImeStartAnimation = true; + } else { + startAnimation(mImeShowing, true /* forceRestart */, + SoftInputShowHideReason.DISPLAY_CONTROLS_CHANGED); + } + } if (hadImeSourceControl && mImeSourceControl != imeSourceControl) { mImeSourceControl.release(SurfaceControl::release); } mImeSourceControl = imeSourceControl; + + if (android.view.inputmethod.Flags.refactorInsetsController()) { + if (pendingImeStartAnimation) { + startAnimation(true, true /* forceRestart */, + null /* statsToken */); + } + } } private void applyVisibilityToLeash(InsetsSourceControl imeSourceControl) { @@ -354,6 +389,20 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged // Do nothing } + @Override + // TODO(b/335404678): pass control target + public void setImeInputTargetRequestedVisibility(boolean visible) { + if (android.view.inputmethod.Flags.refactorInsetsController()) { + mImeRequestedVisible = visible; + // In the case that the IME becomes visible, but we have the control with leash + // already (e.g., when focussing an editText in activity B, while and editText in + // activity A is focussed), we will not get a call of #insetsControlChanged, and + // therefore have to start the show animation from here + startAnimation(mImeRequestedVisible /* show */, false /* forceRestart */, + null /* TODO statsToken */); + } + } + /** * Sends the local visibility state back to window manager. Needed for legacy adjustForIme. */ @@ -402,6 +451,12 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged private void startAnimation(final boolean show, final boolean forceRestart, @NonNull final ImeTracker.Token statsToken) { + if (android.view.inputmethod.Flags.refactorInsetsController()) { + if (mImeSourceControl == null || mImeSourceControl.getLeash() == null) { + if (DEBUG) Slog.d(TAG, "No leash available, not starting the animation."); + return; + } + } final InsetsSource imeSource = mInsetsState.peekSource(InsetsSource.ID_IME); if (imeSource == null || mImeSourceControl == null) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE); @@ -463,10 +518,13 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mAnimation.addUpdateListener(animation -> { SurfaceControl.Transaction t = mTransactionPool.acquire(); float value = (float) animation.getAnimatedValue(); - t.setPosition(mImeSourceControl.getLeash(), x, value); - final float alpha = (mAnimateAlpha || isFloating) - ? (value - hiddenY) / (shownY - hiddenY) : 1.f; - t.setAlpha(mImeSourceControl.getLeash(), alpha); + if (!android.view.inputmethod.Flags.refactorInsetsController() || ( + mImeSourceControl != null && mImeSourceControl.getLeash() != null)) { + t.setPosition(mImeSourceControl.getLeash(), x, value); + final float alpha = (mAnimateAlpha || isFloating) + ? (value - hiddenY) / (shownY - hiddenY) : 1.f; + t.setAlpha(mImeSourceControl.getLeash(), alpha); + } dispatchPositionChanged(mDisplayId, imeTop(value), t); t.apply(); mTransactionPool.release(t); @@ -480,8 +538,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged @Override public void onAnimationStart(Animator animation) { + ValueAnimator valueAnimator = (ValueAnimator) animation; + float value = (float) valueAnimator.getAnimatedValue(); SurfaceControl.Transaction t = mTransactionPool.acquire(); - t.setPosition(mImeSourceControl.getLeash(), x, startY); + t.setPosition(mImeSourceControl.getLeash(), x, value); if (DEBUG) { Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:" + imeTop(hiddenY) + "->" + imeTop(shownY) @@ -491,7 +551,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t); mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0; final float alpha = (mAnimateAlpha || isFloating) - ? (startY - hiddenY) / (shownY - hiddenY) + ? (value - hiddenY) / (shownY - hiddenY) : 1.f; t.setAlpha(mImeSourceControl.getLeash(), alpha); if (mAnimationDirection == DIRECTION_SHOW) { @@ -502,7 +562,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged if (DEBUG_IME_VISIBILITY) { EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START, mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE, - mDisplayId, mAnimationDirection, alpha, startY , endY, + mDisplayId, mAnimationDirection, alpha, value, endY, Objects.toString(mImeSourceControl.getLeash()), Objects.toString(mImeSourceControl.getInsetsHint()), Objects.toString(mImeSourceControl.getSurfacePosition()), @@ -525,17 +585,25 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged @Override public void onAnimationEnd(Animator animation) { + boolean hasLeash = + mImeSourceControl != null && mImeSourceControl.getLeash() != null; if (DEBUG) Slog.d(TAG, "onAnimationEnd " + mCancelled); SurfaceControl.Transaction t = mTransactionPool.acquire(); if (!mCancelled) { - t.setPosition(mImeSourceControl.getLeash(), x, endY); - t.setAlpha(mImeSourceControl.getLeash(), 1.f); + if (!android.view.inputmethod.Flags.refactorInsetsController() + || hasLeash) { + t.setPosition(mImeSourceControl.getLeash(), x, endY); + t.setAlpha(mImeSourceControl.getLeash(), 1.f); + } } dispatchEndPositioning(mDisplayId, mCancelled, t); if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { ImeTracker.forLogging().onProgress(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); - t.hide(mImeSourceControl.getLeash()); + if (!android.view.inputmethod.Flags.refactorInsetsController() + || hasLeash) { + t.hide(mImeSourceControl.getLeash()); + } removeImeSurface(); ImeTracker.forLogging().onHidden(mStatsToken); } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) { @@ -548,9 +616,13 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END, mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE, mDisplayId, mAnimationDirection, endY, - Objects.toString(mImeSourceControl.getLeash()), - Objects.toString(mImeSourceControl.getInsetsHint()), - Objects.toString(mImeSourceControl.getSurfacePosition()), + Objects.toString( + mImeSourceControl != null ? mImeSourceControl.getLeash() + : "null"), + Objects.toString(mImeSourceControl != null + ? mImeSourceControl.getInsetsHint() : "null"), + Objects.toString(mImeSourceControl != null + ? mImeSourceControl.getSurfacePosition() : "null"), Objects.toString(mImeFrame)); } t.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java index 55dc793cc3b6..1fb0e1745e3e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java @@ -199,6 +199,16 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } } + private void setImeInputTargetRequestedVisibility(boolean visible) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.setImeInputTargetRequestedVisibility(visible); + } + } + @BinderThread private class DisplayWindowInsetsControllerImpl extends IDisplayWindowInsetsController.Stub { @@ -240,6 +250,14 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan PerDisplay.this.hideInsets(types, fromIme, statsToken); }); } + + @Override + public void setImeInputTargetRequestedVisibility(boolean visible) + throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.setImeInputTargetRequestedVisibility(visible); + }); + } } } @@ -291,5 +309,12 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan */ default void hideInsets(@InsetsType int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {} + + /** + * Called to set the requested visibility of the IME in DisplayImeController. Invoked by + * {@link com.android.server.wm.DisplayContent.RemoteInsetsControlTarget}. + * @param visible requested status of the IME + */ + default void setImeInputTargetRequestedVisibility(boolean visible) {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java deleted file mode 100644 index b29058b1f204..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.common; - -import android.Manifest; -import android.util.Slog; - -import java.util.function.Consumer; - -/** - * Helpers for working with executors - */ -public class ExecutorUtils { - - /** - * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given - * callback. - */ - public static <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance, - String log, Consumer<T> callback) { - executeRemoteCallWithTaskPermission(controllerInstance, log, callback, - false /* blocking */); - } - - /** - * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given - * callback. - */ - public static <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance, - String log, Consumer<T> callback, boolean blocking) { - if (controllerInstance == null) return; - - final RemoteCallable<T> controller = controllerInstance; - controllerInstance.getContext().enforceCallingPermission( - Manifest.permission.MANAGE_ACTIVITY_TASKS, log); - if (blocking) { - try { - controllerInstance.getRemoteCallExecutor().executeBlocking(() -> { - callback.accept((T) controller); - }); - } catch (InterruptedException e) { - Slog.e("ExecutorUtils", "Remote call failed", e); - } - } else { - controllerInstance.getRemoteCallExecutor().execute(() -> { - callback.accept((T) controller); - }); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java index aa5b0cb628e1..d6f4d81b44f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java @@ -16,7 +16,11 @@ package com.android.wm.shell.common; +import android.Manifest; import android.os.IBinder; +import android.util.Slog; + +import java.util.function.Consumer; /** * An interface for binders which can be registered to be sent to other processes. @@ -31,4 +35,40 @@ public interface ExternalInterfaceBinder { * Returns the IBinder to send. */ IBinder asBinder(); + + /** + * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given + * callback. + */ + default <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance, + String log, Consumer<T> callback) { + executeRemoteCallWithTaskPermission(controllerInstance, log, callback, + false /* blocking */); + } + + /** + * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given + * callback. + */ + default <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance, + String log, Consumer<T> callback, boolean blocking) { + if (controllerInstance == null) return; + + final RemoteCallable<T> controller = controllerInstance; + controllerInstance.getContext().enforceCallingPermission( + Manifest.permission.MANAGE_ACTIVITY_TASKS, log); + if (blocking) { + try { + controllerInstance.getRemoteCallExecutor().executeBlocking(() -> { + callback.accept((T) controller); + }); + } catch (InterruptedException e) { + Slog.e("ExternalInterfaceBinder", "Remote call failed", e); + } + } else { + controllerInstance.getRemoteCallExecutor().execute(() -> { + callback.accept((T) controller); + }); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java index 30f535ba940c..0d90fb7e60fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java @@ -19,7 +19,7 @@ package com.android.wm.shell.common; import android.content.Context; /** - * An interface for controllers that can receive remote calls. + * An interface for controllers (of type T) that can receive remote calls. */ public interface RemoteCallable<T> { /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java index 24608d651d06..829af08e612a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java @@ -45,10 +45,12 @@ public class BubbleInfo implements Parcelable { private Icon mIcon; @Nullable private String mTitle; + @Nullable + private String mAppName; private boolean mIsImportantConversation; public BubbleInfo(String key, int flags, @Nullable String shortcutId, @Nullable Icon icon, - int userId, String packageName, @Nullable String title, + int userId, String packageName, @Nullable String title, @Nullable String appName, boolean isImportantConversation) { mKey = key; mFlags = flags; @@ -57,6 +59,7 @@ public class BubbleInfo implements Parcelable { mUserId = userId; mPackageName = packageName; mTitle = title; + mAppName = appName; mIsImportantConversation = isImportantConversation; } @@ -68,6 +71,7 @@ public class BubbleInfo implements Parcelable { mUserId = source.readInt(); mPackageName = source.readString(); mTitle = source.readString(); + mAppName = source.readString(); mIsImportantConversation = source.readBoolean(); } @@ -102,6 +106,11 @@ public class BubbleInfo implements Parcelable { return mTitle; } + @Nullable + public String getAppName() { + return mAppName; + } + public boolean isImportantConversation() { return mIsImportantConversation; } @@ -161,6 +170,7 @@ public class BubbleInfo implements Parcelable { parcel.writeInt(mUserId); parcel.writeString(mPackageName); parcel.writeString(mTitle); + parcel.writeString(mAppName); parcel.writeBoolean(mIsImportantConversation); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl new file mode 100644 index 000000000000..c968e809bf61 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.desktopmode; + +parcelable DesktopModeTransitionSource;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt new file mode 100644 index 000000000000..dbbf178613b5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.desktopmode + +import android.os.Parcel +import android.os.Parcelable + +/** Transition source types for Desktop Mode. */ +enum class DesktopModeTransitionSource : Parcelable { + /** Transitions that originated as a consequence of task dragging. */ + TASK_DRAG, + /** Transitions that originated from an app from Overview. */ + APP_FROM_OVERVIEW, + /** Transitions that originated from app handle menu button */ + APP_HANDLE_MENU_BUTTON, + /** Transitions that originated as a result of keyboard shortcuts. */ + KEYBOARD_SHORTCUT, + /** Transitions with source unknown. */ + UNKNOWN; + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(name) + } + + companion object { + @JvmField + val CREATOR = + object : Parcelable.Creator<DesktopModeTransitionSource> { + override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource { + return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN + } + + override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl index b5f25433f9aa..e77987963b48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl @@ -53,9 +53,11 @@ interface IPip { * @param destinationBounds the destination bounds the PiP window lands into * @param overlay an optional overlay to fade out after entering PiP * @param appBounds the bounds used to set the buffer size of the optional content overlay + * @param sourceRectHint the bounds to show in the transition to PiP */ oneway void stopSwipePipToHome(int taskId, in ComponentName componentName, - in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds) = 2; + in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds, + in Rect sourceRectHint) = 2; /** * Notifies the swiping Activity to PiP onto home transition is aborted diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java index 6ffeb97f50fa..58007b50350b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java @@ -27,7 +27,9 @@ import android.util.DisplayMetrics; import android.util.Size; import android.view.Gravity; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.io.PrintWriter; @@ -39,6 +41,9 @@ public class PipBoundsAlgorithm { private static final String TAG = PipBoundsAlgorithm.class.getSimpleName(); private static final float INVALID_SNAP_FRACTION = -1f; + // The same value (with the same name) is used in Launcher. + private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f; + @NonNull private final PipBoundsState mPipBoundsState; @NonNull protected final PipDisplayLayoutState mPipDisplayLayoutState; @NonNull protected final SizeSpecSource mSizeSpecSource; @@ -206,9 +211,27 @@ public class PipBoundsAlgorithm { */ public static boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) { - return sourceRectHint != null - && sourceRectHint.width() > destinationBounds.width() - && sourceRectHint.height() > destinationBounds.height(); + if (sourceRectHint == null || sourceRectHint.isEmpty()) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "isSourceRectHintValidForEnterPip=false, empty hint"); + return false; + } + if (sourceRectHint.width() <= destinationBounds.width() + || sourceRectHint.height() <= destinationBounds.height()) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "isSourceRectHintValidForEnterPip=false, hint(%s) is smaller" + + " than destination(%s)", sourceRectHint, destinationBounds); + return false; + } + final float reportedRatio = destinationBounds.width() / (float) destinationBounds.height(); + final float inferredRatio = sourceRectHint.width() / (float) sourceRectHint.height(); + if (Math.abs(reportedRatio - inferredRatio) > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "isSourceRectHintValidForEnterPip=false, hint(%s) does not match" + + " destination(%s) aspect ratio", sourceRectHint, destinationBounds); + return false; + } + return true; } public float getDefaultAspectRatio() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt index 579a7943829e..3e9366fd6459 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager +import android.graphics.Rect import android.os.RemoteException import android.os.SystemProperties import android.util.DisplayMetrics @@ -33,6 +34,7 @@ import com.android.internal.protolog.common.ProtoLog import com.android.wm.shell.Flags import com.android.wm.shell.protolog.ShellProtoLogGroup import kotlin.math.abs +import kotlin.math.roundToInt /** A class that includes convenience methods. */ object PipUtils { @@ -138,6 +140,30 @@ object PipUtils { } } + + /** + * Returns a fake source rect hint for animation purposes when app-provided one is invalid. + * Resulting adjusted source rect hint lets the app icon in the content overlay to stay visible. + */ + @JvmStatic + fun getEnterPipWithOverlaySrcRectHint(appBounds: Rect, aspectRatio: Float): Rect { + val appBoundsAspRatio = appBounds.width().toFloat() / appBounds.height() + val width: Int + val height: Int + var left = appBounds.left + var top = appBounds.top + if (appBoundsAspRatio < aspectRatio) { + width = appBounds.width() + height = (width / aspectRatio).roundToInt() + top = appBounds.top + (appBounds.height() - height) / 2 + } else { + height = appBounds.height() + width = (height * aspectRatio).roundToInt() + left = appBounds.left + (appBounds.width() - width) / 2 + } + return Rect(left, top, left + width, top + height) + } + private var isPip2ExperimentEnabled: Boolean? = null /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 8fb9bda539a0..5d121c23c6e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -143,6 +143,8 @@ public final class SplitWindowManager extends WindowlessWindowManager { /** * Releases the surface control of the current {@link DividerView} and tear down the view * hierarchy. + * @param t If supplied, the surface removal will be bundled with this Transaction. If + * called with null, removes the surface immediately. */ void release(@Nullable SurfaceControl.Transaction t) { if (mDividerView != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java index 835f1af85c51..07082a558744 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java @@ -53,7 +53,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { private final ShellExecutor mMainExecutor; - private boolean mIsActivityLetterboxed; + private boolean mIsLetterboxDoubleTapEnabled; private int mLetterboxVerticalPosition; @@ -91,7 +91,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { Function<Integer, Integer> disappearTimeSupplier) { super(context, taskInfo, syncQueue, taskListener, displayLayout); final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; - mIsActivityLetterboxed = appCompatTaskInfo.isLetterboxDoubleTapEnabled; + mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled; mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition; mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition; mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth; @@ -119,7 +119,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { @Override protected boolean eligibleToShowLayout() { - return mIsActivityLetterboxed + return mIsLetterboxDoubleTapEnabled && (mLetterboxVerticalPosition != -1 || mLetterboxHorizontalPosition != -1); } @@ -142,13 +142,13 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { @Override public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { - final boolean prevIsActivityLetterboxed = mIsActivityLetterboxed; + final boolean prevIsLetterboxDoubleTapEnabled = mIsLetterboxDoubleTapEnabled; final int prevLetterboxVerticalPosition = mLetterboxVerticalPosition; final int prevLetterboxHorizontalPosition = mLetterboxHorizontalPosition; final int prevTopActivityLetterboxWidth = mTopActivityLetterboxWidth; final int prevTopActivityLetterboxHeight = mTopActivityLetterboxHeight; final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; - mIsActivityLetterboxed = appCompatTaskInfo.isLetterboxDoubleTapEnabled; + mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled; mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition; mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition; mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth; @@ -162,7 +162,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { mHasLetterboxSizeChanged = prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth || prevTopActivityLetterboxHeight != mTopActivityLetterboxHeight; - if (mHasUserDoubleTapped || prevIsActivityLetterboxed != mIsActivityLetterboxed + if (mHasUserDoubleTapped || prevIsLetterboxDoubleTapEnabled != mIsLetterboxDoubleTapEnabled || prevLetterboxVerticalPosition != mLetterboxVerticalPosition || prevLetterboxHorizontalPosition != mLetterboxHorizontalPosition || prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth @@ -249,7 +249,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { && (mLetterboxVerticalPosition == REACHABILITY_LEFT_OR_UP_POSITION || mLetterboxVerticalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION)); - if (mIsActivityLetterboxed && (eligibleForDisplayHorizontalEducation + if (mIsLetterboxDoubleTapEnabled && (eligibleForDisplayHorizontalEducation || eligibleForDisplayVerticalEducation)) { int availableWidth = getTaskBounds().width() - mTopActivityLetterboxWidth; int availableHeight = getTaskBounds().height() - mTopActivityLetterboxHeight; 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 991fbafed296..609e5af5c5b0 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 @@ -87,6 +87,7 @@ import com.android.wm.shell.performance.PerfHintController; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; +import com.android.wm.shell.recents.TaskStackTransitionObserver; import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.shared.ShellTransitions; import com.android.wm.shell.shared.annotations.ShellAnimationThread; @@ -619,12 +620,13 @@ public abstract class WMShellBaseModule { TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + TaskStackTransitionObserver taskStackTransitionObserver, @ShellMainThread ShellExecutor mainExecutor ) { return Optional.ofNullable( RecentTasksController.create(context, shellInit, shellController, shellCommandHandler, taskStackListener, activityTaskManager, - desktopModeTaskRepository, mainExecutor)); + desktopModeTaskRepository, taskStackTransitionObserver, mainExecutor)); } @BindsOptionalOf @@ -924,6 +926,19 @@ public abstract class WMShellBaseModule { } // + // Task Stack + // + + @WMSingleton + @Provides + static TaskStackTransitionObserver provideTaskStackTransitionObserver( + Lazy<Transitions> transitions, + ShellInit shellInit + ) { + return new TaskStackTransitionObserver(transitions, shellInit); + } + + // // Misc // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 12bbd51b968d..1fcfa7fcf350 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -23,6 +23,7 @@ import android.os.Handler; import android.os.UserManager; import android.view.Choreographer; import android.view.IWindowManager; +import android.view.SurfaceControl; import android.view.WindowManager; import com.android.internal.jank.InteractionJankMonitor; @@ -121,9 +122,9 @@ import java.util.Optional; */ @Module( includes = { - WMShellBaseModule.class, - PipModule.class, - ShellBackAnimationModule.class, + WMShellBaseModule.class, + PipModule.class, + ShellBackAnimationModule.class, }) public abstract class WMShellModule { @@ -400,7 +401,8 @@ public abstract class WMShellModule { Optional<RecentTasksController> recentTasksController, HomeTransitionObserver homeTransitionObserver) { return new RecentsTransitionHandler(shellInit, transitions, - recentTasksController.orElse(null), homeTransitionObserver); + recentTasksController.orElse(null), homeTransitionObserver, + SurfaceControl.Transaction::new); } // @@ -664,7 +666,8 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, - Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) { + Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional + ) { return new Object(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index df1b06225fda..31c8f1e45007 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode; import android.graphics.Region; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.annotations.ExternalThread; import java.util.concurrent.Executor; @@ -49,10 +50,10 @@ public interface DesktopMode { /** Called when requested to go to desktop mode from the current focused app. */ - void moveFocusedTaskToDesktop(int displayId); + void moveFocusedTaskToDesktop(int displayId, DesktopModeTransitionSource transitionSource); /** Called when requested to go to fullscreen from the current focused desktop app. */ - void moveFocusedTaskToFullscreen(int displayId); + void moveFocusedTaskToFullscreen(int displayId, DesktopModeTransitionSource transitionSource); /** Called when requested to go to split screen from the current focused desktop app. */ void moveFocusedTaskToStageSplit(int displayId, boolean leftOrTop); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index 109868daae7d..fbc11c19a5a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -159,13 +159,24 @@ class DesktopModeEventLogger { } companion object { + /** + * Describes a task position and dimensions. + * + * @property instanceId instance id of the task + * @property uid uid of the app associated with the task + * @property taskHeight height of the task in px + * @property taskWidth width of the task in px + * @property taskX x-coordinate of the top-left corner + * @property taskY y-coordinate of the top-left corner + * + */ data class TaskUpdate( val instanceId: Int, val uid: Int, - val taskHeight: Int = Int.MIN_VALUE, - val taskWidth: Int = Int.MIN_VALUE, - val taskX: Int = Int.MIN_VALUE, - val taskY: Int = Int.MIN_VALUE, + val taskHeight: Int, + val taskWidth: Int, + val taskX: Int, + val taskY: Int, ) /** @@ -187,7 +198,10 @@ class DesktopModeEventLogger { KEYBOARD_SHORTCUT_ENTER( FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER ), - SCREEN_ON(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__SCREEN_ON) + SCREEN_ON(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__SCREEN_ON), + APP_FROM_OVERVIEW( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__APP_FROM_OVERVIEW + ), } /** @@ -204,7 +218,7 @@ class DesktopModeEventLogger { FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__KEYBOARD_SHORTCUT_EXIT ), RETURN_HOME_OR_OVERVIEW( - FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__RETURN_HOME_OR_OVERVIEW ), TASK_FINISHED(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED), SCREEN_OFF(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index 5d8e34022841..e71056043d5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -35,9 +35,16 @@ import androidx.core.util.plus import androidx.core.util.putAll import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceIdSequence +import com.android.internal.protolog.common.ProtoLog import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.DesktopModeStatus import com.android.wm.shell.shared.TransitionUtil @@ -74,6 +81,9 @@ class DesktopModeLoggerTransitionObserver( // animation was cancelled, we restore these tasks to calculate the post-Transition state private val tasksSavedForRecents: SparseArray<TaskInfo> = SparseArray() + // Caching whether the previous transition was exit to overview. + private var wasPreviousTransitionExitToOverview: Boolean = false + // The instanceId for the current logging session private var loggerInstanceId: InstanceId? = null @@ -95,7 +105,7 @@ class DesktopModeLoggerTransitionObserver( finishTransaction: SurfaceControl.Transaction ) { // this was a new recents animation - if (info.isRecentsTransition() && tasksSavedForRecents.isEmpty()) { + if (info.isExitToRecentsTransition() && tasksSavedForRecents.isEmpty()) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Recents animation running, saving tasks for later" @@ -137,6 +147,7 @@ class DesktopModeLoggerTransitionObserver( preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos, postTransitionVisibleFreeformTasks = postTransitionVisibleFreeformTasks ) + wasPreviousTransitionExitToOverview = info.isExitToRecentsTransition() } override fun onTransitionStarting(transition: IBinder) {} @@ -271,17 +282,23 @@ class DesktopModeLoggerTransitionObserver( visibleFreeformTaskInfos.putAll(postTransitionVisibleFreeformTasks) } - // TODO(b/326231724) - Add logging around taskInfoChanges Updates /** Compare the old and new state of taskInfos and identify and log the changes */ private fun identifyAndLogTaskUpdates( sessionId: Int, preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, postTransitionVisibleFreeformTasks: SparseArray<TaskInfo> ) { - // find new tasks that were added postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> - if (!preTransitionVisibleFreeformTasks.containsKey(taskId)) { - desktopModeEventLogger.logTaskAdded(sessionId, buildTaskUpdateForTask(taskInfo)) + val currentTaskUpdate = buildTaskUpdateForTask(taskInfo) + val previousTaskInfo = preTransitionVisibleFreeformTasks[taskId] + when { + // new tasks added + previousTaskInfo == null -> + desktopModeEventLogger.logTaskAdded(sessionId, currentTaskUpdate) + // old tasks that were resized or repositioned + // TODO(b/347935387): Log changes only once they are stable. + buildTaskUpdateForTask(previousTaskInfo) != currentTaskUpdate -> + desktopModeEventLogger.logTaskInfoChanged(sessionId, currentTaskUpdate) } } @@ -293,38 +310,71 @@ class DesktopModeLoggerTransitionObserver( } } - // TODO(b/326231724: figure out how to get taskWidth and taskHeight from TaskInfo private fun buildTaskUpdateForTask(taskInfo: TaskInfo): TaskUpdate { - val taskUpdate = TaskUpdate(taskInfo.taskId, taskInfo.userId) - // add task x, y if available - taskInfo.positionInParent?.let { taskUpdate.copy(taskX = it.x, taskY = it.y) } - - return taskUpdate + val screenBounds = taskInfo.configuration.windowConfiguration.bounds + val positionInParent = taskInfo.positionInParent + return TaskUpdate( + instanceId = taskInfo.taskId, + uid = taskInfo.userId, + taskHeight = screenBounds.height(), + taskWidth = screenBounds.width(), + taskX = positionInParent.x, + taskY = positionInParent.y, + ) } /** Get [EnterReason] for this session enter */ - private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason { - // TODO(b/326231756) - Add support for missing enter reasons - return when (transitionInfo.type) { - WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON - Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> EnterReason.APP_HANDLE_DRAG - Transitions.TRANSIT_MOVE_TO_DESKTOP -> EnterReason.APP_HANDLE_MENU_BUTTON - WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT - else -> EnterReason.UNKNOWN_ENTER + private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason = + when { + transitionInfo.type == WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON + transitionInfo.type == Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> + EnterReason.APP_HANDLE_DRAG + transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON -> + EnterReason.APP_HANDLE_MENU_BUTTON + transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW -> + EnterReason.APP_FROM_OVERVIEW + transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT -> + EnterReason.KEYBOARD_SHORTCUT_ENTER + // NOTE: the below condition also applies for EnterReason quickswitch + transitionInfo.type == WindowManager.TRANSIT_TO_FRONT -> EnterReason.OVERVIEW + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows. + // TODO(b/346564416): Modify logging for cancelled recents once it transition is + // changed. Also see how to account to time difference between actual enter time and + // time of this log. Also account for the missed session when exit happens just after + // a cancelled recents. + wasPreviousTransitionExitToOverview -> EnterReason.OVERVIEW + transitionInfo.type == WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT + else -> { + ProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "Unknown enter reason for transition type ${transitionInfo.type}", + transitionInfo.type + ) + EnterReason.UNKNOWN_ENTER + } } - } /** Get [ExitReason] for this session exit */ - private fun getExitReason(transitionInfo: TransitionInfo): ExitReason { - // TODO(b/326231756) - Add support for missing exit reasons - return when { + private fun getExitReason(transitionInfo: TransitionInfo): ExitReason = + when { transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED - transitionInfo.type == Transitions.TRANSIT_EXIT_DESKTOP_MODE -> ExitReason.DRAG_TO_EXIT - transitionInfo.isRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW - else -> ExitReason.UNKNOWN_EXIT + transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT + transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON -> + ExitReason.APP_HANDLE_MENU_BUTTON_EXIT + transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT -> + ExitReason.KEYBOARD_SHORTCUT_EXIT + transitionInfo.isExitToRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW + else -> { + ProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "Unknown exit reason for transition type ${transitionInfo.type}", + transitionInfo.type + ) + ExitReason.UNKNOWN_EXIT + } } - } /** Adds tasks to the saved copy of freeform taskId, taskInfo. Only used for testing. */ @VisibleForTesting @@ -347,7 +397,7 @@ class DesktopModeLoggerTransitionObserver( return this.windowingMode == WINDOWING_MODE_FREEFORM } - private fun TransitionInfo.isRecentsTransition(): Boolean { + private fun TransitionInfo.isExitToRecentsTransition(): Boolean { return this.type == WindowManager.TRANSIT_TO_FRONT && this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index bc27f341b566..1a6ca0efa748 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.desktopmode -import android.window.WindowContainerTransaction +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.sysui.ShellCommandHandler import java.io.PrintWriter @@ -64,7 +64,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl return false } - return controller.moveToDesktop(taskId, WindowContainerTransaction()) + return controller.moveToDesktop(taskId, transitionSource = UNKNOWN) } private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 7d01580ecb6e..81891ce91e04 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -46,6 +46,9 @@ class DesktopModeTaskRepository { val activeTasks: ArraySet<Int> = ArraySet(), val visibleTasks: ArraySet<Int> = ArraySet(), val minimizedTasks: ArraySet<Int> = ArraySet(), + // Tasks that are closing, but are still visible + // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver + val closingTasks: ArraySet<Int> = ArraySet(), // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), ) @@ -169,6 +172,42 @@ class DesktopModeTaskRepository { return result } + /** + * Mark a task with given [taskId] as closing on given [displayId] + * + * @return `true` if the task was not closing on given [displayId] + */ + fun addClosingTask(displayId: Int, taskId: Int): Boolean { + val added = displayData.getOrCreate(displayId).closingTasks.add(taskId) + if (added) { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: added closing task=%d displayId=%d", + taskId, + displayId + ) + } + return added + } + + /** + * Remove task with given [taskId] from closing tasks. + * + * @return `true` if the task was closing + */ + fun removeClosingTask(taskId: Int): Boolean { + var removed = false + displayData.forEach { _, data -> + if (data.closingTasks.remove(taskId)) { + removed = true + } + } + if (removed) { + KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remove closing task=%d", taskId) + } + return removed + } + /** Check if a task with the given [taskId] was marked as an active task */ fun isActiveTask(taskId: Int): Boolean { return displayData.valueIterator().asSequence().any { data -> @@ -176,6 +215,10 @@ class DesktopModeTaskRepository { } } + /** Check if a task with the given [taskId] was marked as a closing task */ + fun isClosingTask(taskId: Int): Boolean = + displayData.valueIterator().asSequence().any { data -> taskId in data.closingTasks } + /** Whether a task is visible. */ fun isVisibleTask(taskId: Int): Boolean { return displayData.valueIterator().asSequence().any { data -> @@ -190,12 +233,17 @@ class DesktopModeTaskRepository { } } - /** Check if a task with the given [taskId] is the only active task on its display */ - fun isOnlyActiveTask(taskId: Int): Boolean { - return displayData.valueIterator().asSequence().any { data -> - data.activeTasks.singleOrNull() == taskId + /** + * Check if a task with the given [taskId] is the only visible, non-closing, not-minimized task + * on its display + */ + fun isOnlyVisibleNonClosingTask(taskId: Int): Boolean = + displayData.valueIterator().asSequence().any { data -> + data.visibleTasks + .subtract(data.closingTasks) + .subtract(data.minimizedTasks) + .singleOrNull() == taskId } - } /** Get a set of the active tasks for given [displayId] */ fun getActiveTasks(displayId: Int): ArraySet<Int> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt new file mode 100644 index 000000000000..b24bd10eaa0d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.view.WindowManager.TransitionType +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource +import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TYPES + +/** + * Contains desktop mode [TransitionType]s (extended from [TRANSIT_DESKTOP_MODE_TYPES]) and helper + * methods. + */ +object DesktopModeTransitionTypes { + + const val TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON = TRANSIT_DESKTOP_MODE_TYPES + 1 + const val TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW = TRANSIT_DESKTOP_MODE_TYPES + 2 + const val TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT = TRANSIT_DESKTOP_MODE_TYPES + 3 + const val TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN = TRANSIT_DESKTOP_MODE_TYPES + 4 + const val TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG = TRANSIT_DESKTOP_MODE_TYPES + 5 + const val TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON = TRANSIT_DESKTOP_MODE_TYPES + 6 + const val TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT = TRANSIT_DESKTOP_MODE_TYPES + 7 + const val TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN = TRANSIT_DESKTOP_MODE_TYPES + 8 + + /** Return whether the [TransitionType] corresponds to a transition to enter desktop mode. */ + @JvmStatic + fun @receiver:TransitionType Int.isEnterDesktopModeTransition(): Boolean { + return this in + listOf( + TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, + TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, + TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, + TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN + ) + } + + /** + * Returns corresponding desktop mode enter [TransitionType] for a + * [DesktopModeTransitionSource]. + */ + @JvmStatic + @TransitionType + fun DesktopModeTransitionSource.getEnterTransitionType(): Int { + return when (this) { + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON -> + TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON + DesktopModeTransitionSource.APP_FROM_OVERVIEW -> + TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW + DesktopModeTransitionSource.KEYBOARD_SHORTCUT -> + TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT + else -> TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN + } + } + + /** Return whether the [TransitionType] corresponds to a transition to exit desktop mode. */ + @JvmStatic + fun @receiver:TransitionType Int.isExitDesktopModeTransition(): Boolean { + return this in + listOf( + TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG, + TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON, + TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT, + TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN + ) + } + + /** + * Returns corresponding desktop mode exit [TransitionType] for a [DesktopModeTransitionSource]. + */ + @JvmStatic + @TransitionType + fun DesktopModeTransitionSource.getExitTransitionType(): Int { + return when (this) { + DesktopModeTransitionSource.TASK_DRAG -> TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON -> + TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON + DesktopModeTransitionSource.KEYBOARD_SHORTCUT -> + TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT + else -> TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index ef384c74cb5e..196538248709 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -40,7 +40,6 @@ import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN -import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.RemoteTransition import android.window.TransitionInfo @@ -54,7 +53,6 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout -import com.android.wm.shell.common.ExecutorUtils import com.android.wm.shell.common.ExternalInterfaceBinder import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.common.MultiInstanceHelper @@ -63,6 +61,7 @@ import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.compatui.isSingleTopActivityTranslucent @@ -75,7 +74,8 @@ import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.shared.DesktopModeStatus import com.android.wm.shell.shared.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE -import com.android.wm.shell.shared.DesktopModeStatus.isDesktopDensityOverrideSet +import com.android.wm.shell.shared.DesktopModeStatus.useDesktopOverrideDensity +import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.splitscreen.SplitScreenController @@ -155,6 +155,8 @@ class DesktopTasksController( visualIndicator = null } } + private val sysUIPackageName = context.resources.getString( + com.android.internal.R.string.config_systemUi) private val transitionAreaHeight get() = @@ -214,6 +216,11 @@ class DesktopTasksController( return visualIndicator } + // TODO(b/347289970): Consider replacing with API + private fun isSystemUIApplication(taskInfo: RunningTaskInfo): Boolean { + return taskInfo.baseActivity?.packageName == sysUIPackageName + } + fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) @@ -253,7 +260,7 @@ class DesktopTasksController( } /** Enter desktop by using the focused task in given `displayId` */ - fun moveFocusedTaskToDesktop(displayId: Int) { + fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) { val allFocusedTasks = shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo -> taskInfo.isFocused && @@ -272,11 +279,11 @@ class DesktopTasksController( } else { allFocusedTasks[0] } - moveToDesktop(splitFocusedTask) + moveToDesktop(splitFocusedTask, transitionSource = transitionSource) } 1 -> { // Fullscreen case where we move the current focused task. - moveToDesktop(allFocusedTasks[0].taskId) + moveToDesktop(allFocusedTasks[0].taskId, transitionSource = transitionSource) } else -> { KtProtoLog.w( @@ -293,17 +300,20 @@ class DesktopTasksController( /** Move a task with given `taskId` to desktop */ fun moveToDesktop( taskId: Int, - wct: WindowContainerTransaction = WindowContainerTransaction() + wct: WindowContainerTransaction = WindowContainerTransaction(), + transitionSource: DesktopModeTransitionSource, ): Boolean { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { - moveToDesktop(it, wct) - } ?: moveToDesktopFromNonRunningTask(taskId, wct) + moveToDesktop(it, wct, transitionSource) + } + ?: moveToDesktopFromNonRunningTask(taskId, wct, transitionSource) return true } private fun moveToDesktopFromNonRunningTask( taskId: Int, - wct: WindowContainerTransaction + wct: WindowContainerTransaction, + transitionSource: DesktopModeTransitionSource, ): Boolean { recentTasksController?.findTaskInBackground(taskId)?.let { KtProtoLog.v( @@ -316,10 +326,11 @@ class DesktopTasksController( bringDesktopAppsToFrontBeforeShowingNewTask(DEFAULT_DISPLAY, wct, taskId) addMoveToDesktopChangesNonRunningTask(wct, taskId) // TODO(343149901): Add DPI changes for task launch - val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct) + val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) addPendingMinimizeTransition(transition, taskToMinimize) return true - } ?: return false + } + ?: return false } private fun addMoveToDesktopChangesNonRunningTask( @@ -331,12 +342,11 @@ class DesktopTasksController( wct.startTask(taskId, options.toBundle()) } - /** - * Move a task to desktop - */ + /** Move a task to desktop */ fun moveToDesktop( task: RunningTaskInfo, - wct: WindowContainerTransaction = WindowContainerTransaction() + wct: WindowContainerTransaction = WindowContainerTransaction(), + transitionSource: DesktopModeTransitionSource, ) { if (Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)) { KtProtoLog.w( @@ -346,6 +356,14 @@ class DesktopTasksController( ) return } + if (isSystemUIApplication(task)) { + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: Cannot enter desktop, " + + "systemUI top activity found." + ) + return + } KtProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: moveToDesktop taskId=%d", @@ -358,7 +376,7 @@ class DesktopTasksController( addMoveToDesktopChanges(wct, task) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct) + val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) addPendingMinimizeTransition(transition, taskToMinimize) } else { shellTaskOrganizer.applyTransaction(wct) @@ -424,25 +442,34 @@ class DesktopTasksController( * active task. * * @param wct transaction to modify if the last active task is closed + * @param displayId display id of the window that's being closed * @param taskId task id of the window that's being closed */ - fun onDesktopWindowClose(wct: WindowContainerTransaction, taskId: Int) { - if (desktopModeTaskRepository.isOnlyActiveTask(taskId)) { + fun onDesktopWindowClose(wct: WindowContainerTransaction, displayId: Int, taskId: Int) { + if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskId)) { removeWallpaperActivity(wct) } + if (!desktopModeTaskRepository.addClosingTask(displayId, taskId)) { + // Could happen if the task hasn't been removed from closing list after it disappeared + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: the task with taskId=%d is already closing!", + taskId + ) + } } /** Move a task with given `taskId` to fullscreen */ - fun moveToFullscreen(taskId: Int) { + fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> - moveToFullscreenWithAnimation(task, task.positionInParent) + moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource) } } /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */ - fun enterFullscreen(displayId: Int) { + fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) { getFocusedFreeformTask(displayId)?.let { - moveToFullscreenWithAnimation(it, it.positionInParent) + moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource) } } @@ -486,10 +513,16 @@ class DesktopTasksController( "DesktopTasksController: cancelDragToDesktop taskId=%d", task.taskId ) - dragToDesktopTransitionHandler.cancelDragToDesktopTransition() + dragToDesktopTransitionHandler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) } - private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) { + private fun moveToFullscreenWithAnimation( + task: RunningTaskInfo, + position: Point, + transitionSource: DesktopModeTransitionSource + ) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: moveToFullscreen with animation taskId=%d", @@ -500,7 +533,7 @@ class DesktopTasksController( if (Transitions.ENABLE_SHELL_TRANSITIONS) { exitDesktopTaskTransitionHandler.startTransition( - Transitions.TRANSIT_EXIT_DESKTOP_MODE, + transitionSource, wct, position, mOnAnimationFinishedCallback @@ -846,8 +879,8 @@ class DesktopTasksController( reason = "recents animation is running" false } - // Handle back navigation for the last window if wallpaper available - shouldRemoveWallpaper(request) -> true + // Handle task closing for the last window if wallpaper is available + shouldHandleTaskClosing(request) -> true // Only handle open or to front transitions request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> { reason = "transition type not handled (${request.type})" @@ -885,9 +918,12 @@ class DesktopTasksController( val result = triggerTask?.let { task -> when { - request.type == TRANSIT_TO_BACK -> handleBackNavigation(task) + // Check if the closing task needs to be handled + TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task) // Check if the task has a top transparent activity - shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task) + shouldLaunchAsModal(task) -> handleIncompatibleTaskLaunch(task) + // Check if the task has a top systemUI activity + isSystemUIApplication(task) -> handleIncompatibleTaskLaunch(task) // Check if fullscreen task should be updated task.isFullscreen -> handleFullscreenTaskLaunch(task, transition) // Check if freeform task should be updated @@ -921,15 +957,14 @@ class DesktopTasksController( .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } } + // TODO(b/347289970): Consider replacing with API private fun shouldLaunchAsModal(task: TaskInfo) = Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task) - private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean { + private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean { return Flags.enableDesktopWindowingWallpaperActivity() && - request.type == TRANSIT_TO_BACK && - request.triggerTask?.let { task -> - desktopModeTaskRepository.isOnlyActiveTask(task.taskId) - } ?: false + TransitionUtil.isClosingType(request.type) && + request.triggerTask != null } private fun handleFreeformTaskLaunch( @@ -940,7 +975,7 @@ class DesktopTasksController( if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: switch freeform task to fullscreen oon transition" + + "DesktopTasksController: bring desktop tasks to front on transition" + " taskId=%d", task.taskId ) @@ -950,7 +985,7 @@ class DesktopTasksController( } } val wct = WindowContainerTransaction() - if (isDesktopDensityOverrideSet()) { + if (useDesktopOverrideDensity()) { wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE) } // Desktop Mode is showing and we're launching a new Task - we might need to minimize @@ -986,24 +1021,36 @@ class DesktopTasksController( return null } - // Always launch transparent tasks in fullscreen. - private fun handleTransparentTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + /** + * If a task is not compatible with desktop mode freeform, it should always be launched in + * fullscreen. + */ + private fun handleIncompatibleTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { // Already fullscreen, no-op. if (task.isFullscreen) return null return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) } } - /** Handle back navigation by removing wallpaper activity if it's the last active task */ - private fun handleBackNavigation(task: RunningTaskInfo): WindowContainerTransaction? { - if ( - desktopModeTaskRepository.isOnlyActiveTask(task.taskId) && + /** Handle task closing by removing wallpaper activity if it's the last active task */ + private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? { + val wct = if ( + desktopModeTaskRepository.isOnlyVisibleNonClosingTask(task.taskId) && desktopModeTaskRepository.wallpaperActivityToken != null ) { // Remove wallpaper activity when the last active task is removed - return WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) } + WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) } } else { - return null + null } + if (!desktopModeTaskRepository.addClosingTask(task.displayId, task.taskId)) { + // Could happen if the task hasn't been removed from closing list after it disappeared + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: the task with taskId=%d is already closing!", + task.taskId + ) + } + return wct } private fun addMoveToDesktopChanges( @@ -1025,7 +1072,7 @@ class DesktopTasksController( } wct.setWindowingMode(taskInfo.token, targetWindowingMode) wct.reorder(taskInfo.token, true /* onTop */) - if (isDesktopDensityOverrideSet()) { + if (useDesktopOverrideDensity()) { wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE) } } @@ -1045,7 +1092,7 @@ class DesktopTasksController( } wct.setWindowingMode(taskInfo.token, targetWindowingMode) wct.setBounds(taskInfo.token, Rect()) - if (isDesktopDensityOverrideSet()) { + if (useDesktopOverrideDensity()) { wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } } @@ -1105,20 +1152,31 @@ class DesktopTasksController( @JvmOverloads fun requestSplit( taskInfo: RunningTaskInfo, - leftOrTop: Boolean = false, + leftOrTop: Boolean = false ) { - val windowingMode = taskInfo.windowingMode - if ( - windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM - ) { - val wct = WindowContainerTransaction() - addMoveToSplitChanges(wct, taskInfo) - splitScreenController.requestEnterSplitSelect( - taskInfo, - wct, - if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT, - taskInfo.configuration.windowConfiguration.bounds - ) + // If a drag to desktop is in progress, we want to enter split select + // even if the requesting task is already in split. + val isDragging = dragToDesktopTransitionHandler.inProgress + val shouldRequestSplit = taskInfo.isFullscreen || taskInfo.isFreeform || isDragging + if (shouldRequestSplit) { + if (isDragging) { + releaseVisualIndicator() + val cancelState = if (leftOrTop) { + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT + } else { + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT + } + dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState) + } else { + val wct = WindowContainerTransaction() + addMoveToSplitChanges(wct, taskInfo) + splitScreenController.requestEnterSplitSelect( + taskInfo, + wct, + if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT, + taskInfo.configuration.windowConfiguration.bounds + ) + } } } @@ -1206,7 +1264,11 @@ class DesktopTasksController( ) when (indicatorType) { DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> { - moveToFullscreenWithAnimation(taskInfo, position) + moveToFullscreenWithAnimation( + taskInfo, + position, + DesktopModeTransitionSource.TASK_DRAG + ) } DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { releaseVisualIndicator() @@ -1247,7 +1309,10 @@ class DesktopTasksController( * @param taskInfo the task being dragged. * @param y height of drag, to be checked against status bar height. */ - fun onDragPositioningEndThroughStatusBar(inputCoordinates: PointF, taskInfo: RunningTaskInfo) { + fun onDragPositioningEndThroughStatusBar( + inputCoordinates: PointF, + taskInfo: RunningTaskInfo, + ) { val indicator = getVisualIndicator() ?: return val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode) when (indicatorType) { @@ -1264,10 +1329,10 @@ class DesktopTasksController( cancelDragToDesktop(taskInfo) } DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { - finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.LEFT)) + requestSplit(taskInfo, leftOrTop = true) } DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { - finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.RIGHT)) + requestSplit(taskInfo, leftOrTop = false) } } } @@ -1375,12 +1440,22 @@ class DesktopTasksController( } } - override fun moveFocusedTaskToDesktop(displayId: Int) { - mainExecutor.execute { this@DesktopTasksController.moveFocusedTaskToDesktop(displayId) } + override fun moveFocusedTaskToDesktop( + displayId: Int, + transitionSource: DesktopModeTransitionSource + ) { + mainExecutor.execute { + this@DesktopTasksController.moveFocusedTaskToDesktop(displayId, transitionSource) + } } - override fun moveFocusedTaskToFullscreen(displayId: Int) { - mainExecutor.execute { this@DesktopTasksController.enterFullscreen(displayId) } + override fun moveFocusedTaskToFullscreen( + displayId: Int, + transitionSource: DesktopModeTransitionSource + ) { + mainExecutor.execute { + this@DesktopTasksController.enterFullscreen(displayId, transitionSource) + } } override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) { @@ -1432,13 +1507,13 @@ class DesktopTasksController( } override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) { - ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c -> + executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c -> c.showDesktopApps(displayId, remoteTransition) } } override fun showDesktopApp(taskId: Int) { - ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c -> + executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c -> c.moveTaskToFront(taskId) } } @@ -1456,7 +1531,7 @@ class DesktopTasksController( override fun getVisibleTaskCount(displayId: Int): Int { val result = IntArray(1) - ExecutorUtils.executeRemoteCallWithTaskPermission( + executeRemoteCallWithTaskPermission( controller, "getVisibleTaskCount", { controller -> result[0] = controller.getVisibleTaskCount(displayId) }, @@ -1466,7 +1541,7 @@ class DesktopTasksController( } override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { - ExecutorUtils.executeRemoteCallWithTaskPermission( + executeRemoteCallWithTaskPermission( controller, "onDesktopSplitSelectAnimComplete" ) { c -> @@ -1480,14 +1555,14 @@ class DesktopTasksController( "IDesktopModeImpl: set task listener=%s", listener ?: "null" ) - ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ -> + executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() } } - override fun moveToDesktop(taskId: Int) { - ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "moveToDesktop") { c -> - c.moveToDesktop(taskId) + override fun moveToDesktop(taskId: Int, transitionSource: DesktopModeTransitionSource) { + executeRemoteCallWithTaskPermission(controller, "moveToDesktop") { c -> + c.moveToDesktop(taskId, transitionSource = transitionSource) } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 98c79d7174a9..d99b724c936f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -4,6 +4,7 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.RectEvaluator import android.animation.ValueAnimator +import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityOptions import android.app.ActivityOptions.SourceInfo import android.app.ActivityTaskManager.INVALID_TASK_ID @@ -12,9 +13,11 @@ import android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT import android.app.PendingIntent.FLAG_MUTABLE import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.content.Context import android.content.Intent import android.content.Intent.FILL_IN_COMPONENT +import android.graphics.PointF import android.graphics.Rect import android.os.Bundle import android.os.IBinder @@ -30,6 +33,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.splitscreen.SplitScreenController @@ -186,7 +190,7 @@ class DragToDesktopTransitionHandler( * outside the desktop drop zone and is instead dropped back into the status bar region that * means the user wants to remain in their current windowing mode. */ - fun cancelDragToDesktopTransition() { + fun cancelDragToDesktopTransition(cancelState: CancelState) { if (!inProgress) { // Don't attempt to cancel a drag to desktop transition since there is no transition in // progress which means that the drag to desktop transition was never successfully @@ -200,13 +204,32 @@ class DragToDesktopTransitionHandler( clearState() return } - state.cancelled = true - if (state.draggedTaskChange != null) { + state.cancelState = cancelState + + if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) { // Regular case, transient launch of Home happened as is waiting for the cancel // transient to start and merge. Animate the cancellation (scale back to original // bounds) first before actually starting the cancel transition so that the wallpaper // is visible behind the animating task. startCancelAnimation() + } else if ( + state.draggedTaskChange != null && + (cancelState == CancelState.CANCEL_SPLIT_LEFT || + cancelState == CancelState.CANCEL_SPLIT_RIGHT) + ) { + // We have a valid dragged task, but the animation will be handled by + // SplitScreenController; request the transition here. + @SplitPosition val splitPosition = if (cancelState == CancelState.CANCEL_SPLIT_LEFT) { + SPLIT_POSITION_TOP_OR_LEFT + } else { + SPLIT_POSITION_BOTTOM_OR_RIGHT + } + val wct = WindowContainerTransaction() + restoreWindowOrder(wct, state) + state.startTransitionFinishTransaction?.apply() + state.startTransitionFinishCb?.onTransitionFinished(null /* wct */) + requestSplitFromScaledTask(splitPosition, wct) + clearState() } else { // There's no dragged task, this can happen when the "cancel" happened too quickly // before the "start" transition is even ready (like on a fling gesture). The @@ -217,6 +240,54 @@ class DragToDesktopTransitionHandler( } } + /** Calculate the bounds of a scaled task, then use those bounds to request split select. */ + private fun requestSplitFromScaledTask( + @SplitPosition splitPosition: Int, + wct: WindowContainerTransaction + ) { + val state = requireTransitionState() + val taskInfo = state.draggedTaskChange?.taskInfo + ?: error("Expected non-null taskInfo") + val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds) + val taskScale = state.dragAnimator.scale + val scaledWidth = taskBounds.width() * taskScale + val scaledHeight = taskBounds.height() * taskScale + val dragPosition = PointF(state.dragAnimator.position) + state.dragAnimator.cancelAnimator() + val animatedTaskBounds = Rect( + dragPosition.x.toInt(), + dragPosition.y.toInt(), + (dragPosition.x + scaledWidth).toInt(), + (dragPosition.y + scaledHeight).toInt() + ) + requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds) + } + + private fun requestSplitSelect( + wct: WindowContainerTransaction, + taskInfo: RunningTaskInfo, + @SplitPosition splitPosition: Int, + taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds) + ) { + // Prepare to exit split in order to enter split select. + if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + splitScreenController.prepareExitSplitScreen( + wct, + splitScreenController.getStageOfTask(taskInfo.taskId), + SplitScreenController.EXIT_REASON_DESKTOP_MODE + ) + splitScreenController.transitionHandler.onSplitToDesktop() + } + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) + wct.setDensityDpi(taskInfo.token, context.resources.displayMetrics.densityDpi) + splitScreenController.requestEnterSplitSelect( + taskInfo, + wct, + splitPosition, + taskBounds + ) + } + override fun startAnimation( transition: IBinder, info: TransitionInfo, @@ -261,7 +332,7 @@ class DragToDesktopTransitionHandler( is TransitionState.FromSplit -> { state.splitRootChange = change val layer = - if (!state.cancelled) { + if (state.cancelState == CancelState.NO_CANCEL) { // Normal case, split root goes to the bottom behind everything // else. appLayers - i @@ -311,8 +382,18 @@ class DragToDesktopTransitionHandler( // Do not do this in the cancel-early case though, since in that case nothing should // happen on screen so the layering will remain the same as if no transition // occurred. - if (change.taskInfo?.taskId == state.draggedTaskId && !state.cancelled) { + if ( + change.taskInfo?.taskId == state.draggedTaskId && + state.cancelState != CancelState.STANDARD_CANCEL + ) { + // We need access to the dragged task's change in both non-cancel and split + // cancel cases. state.draggedTaskChange = change + } + if ( + change.taskInfo?.taskId == state.draggedTaskId && + state.cancelState == CancelState.NO_CANCEL + ) { taskDisplayAreaOrganizer.reparentToDisplayArea( change.endDisplayId, change.leash, @@ -331,11 +412,11 @@ class DragToDesktopTransitionHandler( state.startTransitionFinishTransaction = finishTransaction startTransaction.apply() - if (!state.cancelled) { + if (state.cancelState == CancelState.NO_CANCEL) { // Normal case, start animation to scale down the dragged task. It'll also be moved to // follow the finger and when released we'll start the next phase/transition. state.dragAnimator.startAnimation() - } else { + } else if (state.cancelState == CancelState.STANDARD_CANCEL) { // Cancel-early case, the state was flagged was cancelled already, which means the // gesture ended in the cancel region. This can happen even before the start transition // is ready/animate here when cancelling quickly like with a fling. There's no point @@ -343,6 +424,26 @@ class DragToDesktopTransitionHandler( // directly into starting the cancel transition to restore WM order. Surfaces should // not move as if no transition happened. startCancelDragToDesktopTransition() + } else if ( + state.cancelState == CancelState.CANCEL_SPLIT_LEFT || + state.cancelState == CancelState.CANCEL_SPLIT_RIGHT + ){ + // Cancel-early case for split-cancel. The state was flagged already as a cancel for + // requesting split select. Similar to the above, this can happen due to quick fling + // gestures. We can simply request split here without needing to calculate animated + // task bounds as the task has not shrunk at all. + val splitPosition = if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) { + SPLIT_POSITION_TOP_OR_LEFT + } else { + SPLIT_POSITION_BOTTOM_OR_RIGHT + } + val taskInfo = state.draggedTaskChange?.taskInfo + ?: error("Expected non-null task info.") + val wct = WindowContainerTransaction() + restoreWindowOrder(wct) + state.startTransitionFinishTransaction?.apply() + state.startTransitionFinishCb?.onTransitionFinished(null /* wct */) + requestSplitSelect(wct, taskInfo, splitPosition) } return true } @@ -355,6 +456,12 @@ class DragToDesktopTransitionHandler( finishCallback: Transitions.TransitionFinishCallback ) { val state = requireTransitionState() + // We don't want to merge the split select animation if that's what we requested. + if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT || + state.cancelState == CancelState.CANCEL_SPLIT_RIGHT) { + clearState() + return + } val isCancelTransition = info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP && transition == state.cancelTransitionToken && @@ -552,6 +659,17 @@ class DragToDesktopTransitionHandler( private fun startCancelDragToDesktopTransition() { val state = requireTransitionState() val wct = WindowContainerTransaction() + restoreWindowOrder(wct, state) + state.cancelTransitionToken = + transitions.startTransition( + TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this + ) + } + + private fun restoreWindowOrder( + wct: WindowContainerTransaction, + state: TransitionState = requireTransitionState() + ) { when (state) { is TransitionState.FromFullscreen -> { // There may have been tasks sent behind home that are not the dragged task (like @@ -580,9 +698,6 @@ class DragToDesktopTransitionHandler( } val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling") wct.restoreTransientOrder(homeWc) - - state.cancelTransitionToken = - transitions.startTransition(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this) } private fun clearState() { @@ -624,7 +739,7 @@ class DragToDesktopTransitionHandler( abstract var cancelTransitionToken: IBinder? abstract var homeToken: WindowContainerToken? abstract var draggedTaskChange: Change? - abstract var cancelled: Boolean + abstract var cancelState: CancelState abstract var startAborted: Boolean data class FromFullscreen( @@ -636,7 +751,7 @@ class DragToDesktopTransitionHandler( override var cancelTransitionToken: IBinder? = null, override var homeToken: WindowContainerToken? = null, override var draggedTaskChange: Change? = null, - override var cancelled: Boolean = false, + override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, var otherRootChanges: MutableList<Change> = mutableListOf() ) : TransitionState() @@ -650,13 +765,25 @@ class DragToDesktopTransitionHandler( override var cancelTransitionToken: IBinder? = null, override var homeToken: WindowContainerToken? = null, override var draggedTaskChange: Change? = null, - override var cancelled: Boolean = false, + override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, var splitRootChange: Change? = null, var otherSplitTask: Int ) : TransitionState() } + /** Enum to provide context on cancelling a drag to desktop event. */ + enum class CancelState { + /** No cancel case; this drag is not flagged for a cancel event. */ + NO_CANCEL, + /** A standard cancel event; should restore task to previous windowing mode. */ + STANDARD_CANCEL, + /** A cancel event where the task will request to enter split on the left side. */ + CANCEL_SPLIT_LEFT, + /** A cancel event where the task will request to enter split on the right side. */ + CANCEL_SPLIT_RIGHT + } + companion object { /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */ private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java index 526cf4d0295b..e5b624f91c54 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -18,7 +18,8 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_TO_DESKTOP; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isEnterDesktopModeTransition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -30,6 +31,7 @@ import android.os.IBinder; import android.util.Slog; import android.view.SurfaceControl; import android.view.WindowManager; +import android.view.WindowManager.TransitionType; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -37,6 +39,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener; @@ -82,8 +85,12 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition * @param wct WindowContainerTransaction for transition * @return the token representing the started transition */ - public IBinder moveToDesktop(@NonNull WindowContainerTransaction wct) { - final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this); + public IBinder moveToDesktop( + @NonNull WindowContainerTransaction wct, + DesktopModeTransitionSource transitionSource + ) { + final IBinder token = mTransitions.startTransition(getEnterTransitionType(transitionSource), + wct, this); mPendingTransitionTokens.add(token); return token; } @@ -117,7 +124,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition private boolean startChangeTransition( @NonNull IBinder transition, - @WindowManager.TransitionType int type, + @TransitionType int type, @NonNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @@ -127,7 +134,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition } final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (type == TRANSIT_MOVE_TO_DESKTOP + if (isEnterDesktopModeTransition(type) && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { return animateMoveToDesktop(change, startT, finishCallback); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java index 9f9e256fc2b7..891f75cfdbda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -18,6 +18,9 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -30,6 +33,7 @@ import android.os.IBinder; import android.util.DisplayMetrics; import android.view.SurfaceControl; import android.view.WindowManager; +import android.view.WindowManager.TransitionType; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -38,6 +42,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; @@ -52,6 +57,7 @@ import java.util.function.Supplier; */ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionHandler { private static final int FULLSCREEN_ANIMATION_DURATION = 336; + private final Context mContext; private final Transitions mTransitions; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); @@ -77,17 +83,18 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH /** * Starts Transition of a given type * - * @param type Transition type + * @param transitionSource DesktopModeTransitionSource for transition * @param wct WindowContainerTransaction for transition * @param position Position of the task when transition is started * @param onAnimationEndCallback to be called after animation */ - public void startTransition(@WindowManager.TransitionType int type, + public void startTransition(@NonNull DesktopModeTransitionSource transitionSource, @NonNull WindowContainerTransaction wct, Point position, Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { mPosition = position; mOnAnimationFinishedCallback = onAnimationEndCallback; - final IBinder token = mTransitions.startTransition(type, wct, this); + final IBinder token = mTransitions.startTransition(getExitTransitionType(transitionSource), + wct, this); mPendingTransitionTokens.add(token); } @@ -121,7 +128,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH @VisibleForTesting boolean startChangeTransition( @NonNull IBinder transition, - @WindowManager.TransitionType int type, + @TransitionType int type, @NonNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @@ -130,7 +137,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH return false; } final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (type == Transitions.TRANSIT_EXIT_DESKTOP_MODE + if (isExitDesktopModeTransition(type) && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { // This Transition animates a task to fullscreen after being dragged to status bar final Resources resources = mContext.getResources(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index c36f8deb6ecc..a7ec2037706d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode; import android.app.ActivityManager.RunningTaskInfo; import android.window.RemoteTransition; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.desktopmode.IDesktopTaskListener; /** @@ -47,5 +48,5 @@ interface IDesktopMode { oneway void setTaskListener(IDesktopTaskListener listener); /** Move a task with given `taskId` to desktop */ - void moveToDesktop(int taskId); + void moveToDesktop(int taskId, in DesktopModeTransitionSource transitionSource); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS index 1385f42bc676..7ad68aac62c5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS @@ -5,3 +5,4 @@ madym@google.com nmusgrave@google.com pbdr@google.com tkachenkoi@google.com +vaniadesmonda@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index 88d0554669b7..5335c0b69a24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -27,6 +27,8 @@ import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.core.animation.addListener +import com.android.internal.jank.Cuj +import com.android.wm.shell.common.InteractionJankMonitorUtils import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener @@ -103,6 +105,8 @@ class ToggleResizeDesktopTaskTransitionHandler( onTaskResizeAnimationListener.onAnimationEnd(taskId) finishCallback.onTransitionFinished(null) boundsAnimator = null + InteractionJankMonitorUtils.endTracing( + Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) } ) addUpdateListener { anim -> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md index 9aa5f4ffcd78..0acc7df98d1c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -54,8 +54,8 @@ Specifically, to support calling into a controller from an external process (lik extend `ExternalInterfaceBinder` and implement `invalidate()` to ensure it doesn't hold long references to the outer controller - Make the controller implement `RemoteCallable<T>`, and have all incoming calls use one of - the `ExecutorUtils.executeRemoteCallWithTaskPermission()` calls to verify the caller's identity - and ensure the call happens on the main shell thread and not the binder thread + the `executeRemoteCallWithTaskPermission()` calls to verify the caller's identity and ensure the + call happens on the main shell thread and not the binder thread - Inject `ShellController` and add the instance of the implementation as external interface - In Launcher, update `TouchInteractionService` to pass the interface to `SystemUIProxy`, and then call the SystemUIProxy method as needed in that code diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index 438aa768165e..b1cbe8d98397 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -73,7 +73,7 @@ stack traces when specific surface transaction calls are made, which is possible following system properties for example: ```shell # Enabling -adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha # matches the name of the SurfaceControlTransaction method +adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,setPosition # matches the name of the SurfaceControlTransaction methods adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface adb reboot adb logcat -s "SurfaceControlRegistry" @@ -87,6 +87,16 @@ adb reboot It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite noisy if unfiltered. +It can sometimes be useful to trace specific logs and when they are applied (sometimes we build +transactions that can be applied later). You can do this by adding the "merge" and "apply" calls to +the set of requested calls: +```shell +# Enabling +adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,merge,apply # apply will dump logs of each setAlpha or merge call on that tx +adb reboot +adb logcat -s "SurfaceControlRegistry" +``` + ## Tracing activity starts in the app process It's sometimes useful to know when to see a stack trace of when an activity starts in the app code diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 7e70d6a3debe..c374eb8e8f03 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -32,7 +32,6 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP; import android.app.ActivityManager; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 7d2aa275a684..b48aee5ccd5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -150,6 +150,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "Adding active freeform task: #%d", taskInfo.taskId); } + } else if (repository.isClosingTask(taskInfo.taskId) + && repository.removeClosingTask(taskInfo.taskId)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Removing closing freeform task: #%d", taskInfo.taskId); } repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, taskInfo.isVisible); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index 39b9000856f2..962309f7c534 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -19,7 +19,6 @@ package com.android.wm.shell.onehanded; import static android.os.UserHandle.myUserId; import static android.view.Display.DEFAULT_DISPLAY; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING; import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index eb845db409e3..0a3c15b6057f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -40,6 +40,7 @@ import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.Transitions; @@ -583,7 +584,7 @@ public class PipAnimationController { } static PipTransitionAnimator<Rect> ofBounds(TaskInfo taskInfo, SurfaceControl leash, - Rect baseValue, Rect startValue, Rect endValue, Rect sourceHintRect, + Rect baseValue, Rect startValue, Rect endValue, Rect sourceRectHint, @PipAnimationController.TransitionDirection int direction, float startingAngle, @Surface.Rotation int rotationDelta) { final boolean isOutPipDirection = isOutPipDirection(direction); @@ -613,14 +614,25 @@ public class PipAnimationController { initialContainerRect = initialSourceValue; } - final Rect sourceHintRectInsets; - if (sourceHintRect == null) { - sourceHintRectInsets = null; + final Rect adjustedSourceRectHint = new Rect(); + if (sourceRectHint == null || sourceRectHint.isEmpty()) { + // Crop a Rect matches the aspect ratio and pivots at the center point. + // This is done for entering case only. + if (isInPipDirection(direction)) { + final float aspectRatio = endValue.width() / (float) endValue.height(); + adjustedSourceRectHint.set(PipUtils.getEnterPipWithOverlaySrcRectHint( + startValue, aspectRatio)); + } } else { - sourceHintRectInsets = new Rect(sourceHintRect.left - initialContainerRect.left, - sourceHintRect.top - initialContainerRect.top, - initialContainerRect.right - sourceHintRect.right, - initialContainerRect.bottom - sourceHintRect.bottom); + adjustedSourceRectHint.set(sourceRectHint); + } + final Rect sourceHintRectInsets = new Rect(); + if (!adjustedSourceRectHint.isEmpty()) { + sourceHintRectInsets.set( + adjustedSourceRectHint.left - initialContainerRect.left, + adjustedSourceRectHint.top - initialContainerRect.top, + initialContainerRect.right - adjustedSourceRectHint.right, + initialContainerRect.bottom - adjustedSourceRectHint.bottom); } final Rect zeroInsets = new Rect(0, 0, 0, 0); @@ -648,7 +660,7 @@ public class PipAnimationController { } float angle = (1.0f - fraction) * startingAngle; setCurrentValue(bounds); - if (inScaleTransition() || sourceHintRect == null) { + if (inScaleTransition() || adjustedSourceRectHint.isEmpty()) { if (isOutPipDirection) { getSurfaceTransactionHelper().crop(tx, leash, end) .scale(tx, leash, end, bounds); @@ -661,7 +673,7 @@ public class PipAnimationController { } else { final Rect insets = computeInsets(fraction); getSurfaceTransactionHelper().scaleAndCrop(tx, leash, - sourceHintRect, initialSourceValue, bounds, insets, + adjustedSourceRectHint, initialSourceValue, bounds, insets, isInPipDirection, fraction); if (shouldApplyCornerRadius()) { final Rect sourceBounds = new Rect(initialContainerRect); @@ -729,9 +741,6 @@ public class PipAnimationController { } private Rect computeInsets(float fraction) { - if (sourceHintRectInsets == null) { - return zeroInsets; - } final Rect startRect = isOutPipDirection ? sourceHintRectInsets : zeroInsets; final Rect endRect = isOutPipDirection ? zeroInsets : sourceHintRectInsets; return mInsetsEvaluator.evaluate(fraction, startRect, endRect); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java index e11e8596a7fe..ff2d46e11107 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java @@ -226,11 +226,10 @@ public abstract class PipContentOverlay { appBoundsCenterX - mOverlayHalfSize, appBoundsCenterY - mOverlayHalfSize); // Scale back the bitmap with the pivot point at center. - mTmpTransform.postScale( + final float scale = Math.min( (float) mAppBounds.width() / currentBounds.width(), - (float) mAppBounds.height() / currentBounds.height(), - appBoundsCenterX, - appBoundsCenterY); + (float) mAppBounds.height() / currentBounds.height()); + mTmpTransform.postScale(scale, scale, appBoundsCenterX, appBoundsCenterY); atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9) .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index a58d94ecd19b..3d1994cac534 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -152,8 +152,14 @@ public class PipSurfaceTransactionHelper { scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), (float) destinationBounds.height() / sourceBounds.height()); } - final float left = destinationBounds.left - insets.left * scale; - final float top = destinationBounds.top - insets.top * scale; + float left = destinationBounds.left - insets.left * scale; + float top = destinationBounds.top - insets.top * scale; + if (scale == 1) { + // Work around the 1 pixel off error by rounding the position down at very beginning. + // We noticed such error from flicker tests, not visually. + left = sourceBounds.left; + top = sourceBounds.top; + } mTmpTransform.setScale(scale, scale); tx.setMatrix(leash, mTmpTransform, mTmpFloat9) .setCrop(leash, mTmpDestinationRect) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index e1657f99639d..e2e1ecde8b56 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -63,7 +63,6 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemProperties; -import android.util.Rational; import android.view.Choreographer; import android.view.Display; import android.view.Surface; @@ -128,8 +127,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, SystemProperties.getInt( "persist.wm.debug.extra_content_overlay_fade_out_delay_ms", 400); - private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.005f; - private final Context mContext; private final SyncTransactionQueue mSyncTransactionQueue; private final PipBoundsState mPipBoundsState; @@ -373,6 +370,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @NonNull final Rect mAppBounds = new Rect(); + /** The source rect hint from stopSwipePipToHome(). */ + @Nullable + private Rect mSwipeSourceRectHint; + public PipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipTransitionState pipTransitionState, @@ -504,7 +505,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards. */ public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, - SurfaceControl overlay, Rect appBounds) { + SurfaceControl overlay, Rect appBounds, Rect sourceRectHint) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "stopSwipePipToHome: %s, stat=%s", componentName, mPipTransitionState); // do nothing if there is no startSwipePipToHome being called before @@ -513,6 +514,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } mPipBoundsState.setBounds(destinationBounds); setContentOverlay(overlay, appBounds); + mSwipeSourceRectHint = sourceRectHint; if (ENABLE_SHELL_TRANSITIONS && overlay != null) { // With Shell transition, the overlay was attached to the remote transition leash, which // will be removed when the current transition is finished, so we need to reparent it @@ -529,6 +531,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } } + /** + * Returns non-null Rect if the pip is entering from swipe-to-home with a specified source hint. + * This also consumes the rect hint. + */ + @Nullable + Rect takeSwipeSourceRectHint() { + final Rect sourceRectHint = mSwipeSourceRectHint; + if (sourceRectHint == null || sourceRectHint.isEmpty()) { + return null; + } + mSwipeSourceRectHint = null; + return mPipTransitionState.getInSwipePipToHomeTransition() ? sourceRectHint : null; + } + private void mayRemoveContentOverlay(SurfaceControl overlay) { final WeakReference<SurfaceControl> overlayRef = new WeakReference<>(overlay); final long timeoutDuration = (mEnterAnimationDuration @@ -589,6 +605,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void exitPip(int animationDurationMs, boolean requestEnterSplit) { if (!mPipTransitionState.isInPip() || mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP + || mPipTransitionState.getInSwipePipToHomeTransition() || mToken == null) { ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Not allowed to exitPip in current state" @@ -603,6 +620,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // the end of the enter animation and reschedule exitPip to run after enter-PiP // has finished its transition and allowed the client to draw in PiP mode. mPipTransitionController.end(() -> { + // TODO(341627042): force set to entered state to avoid potential stack overflow. + mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); exitPip(animationDurationMs, requestEnterSplit); }); return; @@ -800,37 +819,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPictureInPictureParams.getTitle()); mPipParamsChangedForwarder.notifySubtitleChanged( mPictureInPictureParams.getSubtitle()); - - if (mPictureInPictureParams.hasSourceBoundsHint() - && mPictureInPictureParams.hasSetAspectRatio()) { - Rational sourceRectHintAspectRatio = new Rational( - mPictureInPictureParams.getSourceRectHint().width(), - mPictureInPictureParams.getSourceRectHint().height()); - if (sourceRectHintAspectRatio.compareTo( - mPictureInPictureParams.getAspectRatio()) != 0) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "Aspect ratio of source rect hint (%d/%d) does not match the provided " - + "aspect ratio value (%d/%d). Consider matching them for " - + "improved animation. Future releases might override the " - + "value to match.", - mPictureInPictureParams.getSourceRectHint().width(), - mPictureInPictureParams.getSourceRectHint().height(), - mPictureInPictureParams.getAspectRatio().getNumerator(), - mPictureInPictureParams.getAspectRatio().getDenominator()); - } - if (Math.abs(sourceRectHintAspectRatio.floatValue() - - mPictureInPictureParams.getAspectRatioFloat()) - > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "Aspect ratio of source rect hint (%f) does not match the provided " - + "aspect ratio value (%f) and is above threshold of %f. " - + "Consider matching them for improved animation. Future " - + "releases might override the value to match.", - sourceRectHintAspectRatio.floatValue(), - mPictureInPictureParams.getAspectRatioFloat(), - PIP_ASPECT_RATIO_MISMATCH_THRESHOLD); - } - } } mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); @@ -978,7 +966,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private void onEndOfSwipePipToHomeTransition() { if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mPipTransitionController.setEnterAnimationType(ANIM_TYPE_BOUNDS); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index b52b0d8dee74..3cae72d89ecc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -200,9 +200,6 @@ public class PipTransition extends PipTransitionController { animator.cancel(); } mExitTransition = mTransitions.startTransition(type, out, this); - if (mPipOrganizer.getOutPipWindowingMode() == WINDOWING_MODE_UNDEFINED) { - mHomeTransitionObserver.notifyHomeVisibilityChanged(false /* isVisible */); - } } @Override @@ -659,6 +656,9 @@ public class PipTransition extends PipTransitionController { startTransaction.remove(mPipOrganizer.mPipOverlay); mPipOrganizer.clearContentOverlay(); } + if (mPipOrganizer.getOutPipWindowingMode() == WINDOWING_MODE_UNDEFINED) { + mHomeTransitionObserver.notifyHomeVisibilityChanged(false /* isVisible */); + } if (pipChange == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: No window of exiting PIP is found. Can't play expand animation", TAG); @@ -1004,8 +1004,11 @@ public class PipTransition extends PipTransitionController { final Rect currentBounds = pipChange.getStartAbsBounds(); int rotationDelta = deltaRotation(startRotation, endRotation); - Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( - taskInfo.pictureInPictureParams, currentBounds, destinationBounds); + Rect sourceHintRect = mPipOrganizer.takeSwipeSourceRectHint(); + if (sourceHintRect == null) { + sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( + taskInfo.pictureInPictureParams, currentBounds, destinationBounds); + } if (rotationDelta != Surface.ROTATION_0 && endRotation != mPipDisplayLayoutState.getRotation()) { // Computes the destination bounds in new rotation. @@ -1080,6 +1083,8 @@ public class PipTransition extends PipTransitionController { mSurfaceTransactionHelper .crop(finishTransaction, leash, destinationBounds) .round(finishTransaction, leash, true /* applyCornerRadius */); + // Always reset to bounds animation type afterwards. + setEnterAnimationType(ANIM_TYPE_BOUNDS); } else { throw new RuntimeException("Unrecognized animation type: " + enterAnimationType); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 1d1a4e2be3e4..6eefdcfc4d93 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -311,6 +311,14 @@ public abstract class PipTransitionController implements Transitions.TransitionH } /** + * Finish the current transition if possible. + * + * @param tx transaction to be applied with a potentially new draw after finishing. + */ + public void finishTransition(@Nullable SurfaceControl.Transaction tx) { + } + + /** * End the currently-playing PiP animation. * * @param onTransitionEnd callback to run upon finishing the playing transition. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 85f9194ac804..8c4bf7620068 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -22,7 +22,6 @@ import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static android.view.WindowManager.INPUT_CONSUMER_PIP; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; @@ -1002,9 +1001,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb } private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, - SurfaceControl overlay, Rect appBounds) { + SurfaceControl overlay, Rect appBounds, Rect sourceRectHint) { mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay, - appBounds); + appBounds, sourceRectHint); } private void abortSwipePipToHome(int taskId, ComponentName componentName) { @@ -1292,13 +1291,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void stopSwipePipToHome(int taskId, ComponentName componentName, - Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { + Rect destinationBounds, SurfaceControl overlay, Rect appBounds, + Rect sourceRectHint) { if (overlay != null) { overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome"); } executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome", (controller) -> controller.stopSwipePipToHome( - taskId, componentName, destinationBounds, overlay, appBounds)); + taskId, componentName, destinationBounds, overlay, appBounds, + sourceRectHint)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java new file mode 100644 index 000000000000..895c2aeba9ef --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2.animation; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.content.Context; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Animator that handles the alpha animation for entering PIP + */ +public class PipAlphaAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener, + ValueAnimator.AnimatorListener { + @IntDef(prefix = {"FADE_"}, value = { + FADE_IN, + FADE_OUT + }) + + @Retention(RetentionPolicy.SOURCE) + public @interface Fade {} + + public static final int FADE_IN = 0; + public static final int FADE_OUT = 1; + + private final int mEnterAnimationDuration; + private final SurfaceControl mLeash; + private final SurfaceControl.Transaction mStartTransaction; + + // optional callbacks for tracking animation start and end + @Nullable private Runnable mAnimationStartCallback; + @Nullable private Runnable mAnimationEndCallback; + + private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + + public PipAlphaAnimator(Context context, + SurfaceControl leash, + SurfaceControl.Transaction tx, + @Fade int direction) { + mLeash = leash; + mStartTransaction = tx; + if (direction == FADE_IN) { + setFloatValues(0f, 1f); + } else { // direction == FADE_OUT + setFloatValues(1f, 0f); + } + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + mEnterAnimationDuration = context.getResources() + .getInteger(R.integer.config_pipEnterAnimationDuration); + setDuration(mEnterAnimationDuration); + addListener(this); + addUpdateListener(this); + } + + public void setAnimationStartCallback(@NonNull Runnable runnable) { + mAnimationStartCallback = runnable; + } + + public void setAnimationEndCallback(@NonNull Runnable runnable) { + mAnimationEndCallback = runnable; + } + + @Override + public void onAnimationStart(@NonNull Animator animation) { + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTransaction != null) { + mStartTransaction.apply(); + } + } + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final float alpha = (Float) animation.getAnimatedValue(); + mSurfaceControlTransactionFactory.getTransaction().setAlpha(mLeash, alpha).apply(); + } + + @Override + public void onAnimationEnd(@NonNull Animator animation) { + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + + @Override + public void onAnimationCancel(@NonNull Animator animation) {} + + @Override + public void onAnimationRepeat(@NonNull Animator animation) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java new file mode 100644 index 000000000000..5c561fed89c7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2.animation; + +import android.animation.Animator; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +/** + * Animator that handles any resize related animation for PIP. + */ +public class PipResizeAnimator extends ValueAnimator + implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener{ + @NonNull + private final Context mContext; + @NonNull + private final SurfaceControl mLeash; + @Nullable + private SurfaceControl.Transaction mStartTx; + @Nullable + private SurfaceControl.Transaction mFinishTx; + @Nullable + private Runnable mAnimationStartCallback; + @Nullable + private Runnable mAnimationEndCallback; + private RectEvaluator mRectEvaluator; + private final Rect mBaseBounds = new Rect(); + private final Rect mStartBounds = new Rect(); + private final Rect mEndBounds = new Rect(); + private final Rect mAnimatedRect = new Rect(); + private final float mDelta; + + private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + + public PipResizeAnimator(@NonNull Context context, + @NonNull SurfaceControl leash, + @Nullable SurfaceControl.Transaction startTransaction, + @Nullable SurfaceControl.Transaction finishTransaction, + @NonNull Rect baseBounds, + @NonNull Rect startBounds, + @NonNull Rect endBounds, + int duration, + float delta) { + mContext = context; + mLeash = leash; + mStartTx = startTransaction; + mFinishTx = finishTransaction; + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + + mBaseBounds.set(baseBounds); + mStartBounds.set(startBounds); + mAnimatedRect.set(startBounds); + mEndBounds.set(endBounds); + mDelta = delta; + + mRectEvaluator = new RectEvaluator(mAnimatedRect); + + setObjectValues(startBounds, endBounds); + addListener(this); + addUpdateListener(this); + setEvaluator(mRectEvaluator); + // TODO: change this + setDuration(duration); + } + + public void setAnimationStartCallback(@NonNull Runnable runnable) { + mAnimationStartCallback = runnable; + } + + public void setAnimationEndCallback(@NonNull Runnable runnable) { + mAnimationEndCallback = runnable; + } + + @Override + public void onAnimationStart(@NonNull Animator animation) { + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTx != null) { + setBoundsAndRotation(mStartTx, mLeash, mBaseBounds, mStartBounds, mDelta); + mStartTx.apply(); + } + } + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + final float fraction = getAnimatedFraction(); + final float degrees = (1.0f - fraction) * mDelta; + setBoundsAndRotation(tx, mLeash, mBaseBounds, mAnimatedRect, degrees); + tx.apply(); + } + + /** + * Set a proper transform matrix for a leash to move it to given bounds with a certain rotation. + * + * @param baseBounds crop/buffer size relative to which we are scaling the leash. + * @param targetBounds bounds to which we are scaling the leash. + * @param degrees degrees of rotation - counter-clockwise is positive by convention. + */ + public static void setBoundsAndRotation(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect baseBounds, Rect targetBounds, float degrees) { + Matrix transformTensor = new Matrix(); + final float[] mMatrixTmp = new float[9]; + final float scale = (float) targetBounds.width() / baseBounds.width(); + + transformTensor.setScale(scale, scale); + transformTensor.postTranslate(targetBounds.left, targetBounds.top); + transformTensor.postRotate(degrees, targetBounds.centerX(), targetBounds.centerY()); + + tx.setMatrix(leash, transformTensor, mMatrixTmp); + } + + @Override + public void onAnimationEnd(@NonNull Animator animation) { + if (mFinishTx != null) { + setBoundsAndRotation(mFinishTx, mLeash, mBaseBounds, mEndBounds, 0f); + } + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + + @Override + public void onAnimationCancel(@NonNull Animator animation) {} + + @Override + public void onAnimationRepeat(@NonNull Animator animation) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index f5afeea3eaef..fc0d36d13b2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -19,7 +19,6 @@ package com.android.wm.shell.pip2.phone; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; import android.app.ActivityManager; @@ -286,7 +285,8 @@ public class PipController implements ConfigurationChangeListener, } private void onSwipePipToHomeAnimationStart(int taskId, ComponentName componentName, - Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { + Rect destinationBounds, SurfaceControl overlay, Rect appBounds, + Rect sourceRectHint) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onSwipePipToHomeAnimationStart: %s", componentName); Bundle extra = new Bundle(); @@ -391,6 +391,7 @@ public class PipController implements ConfigurationChangeListener, @Override public void invalidate() { mController = null; + // Unregister the listener to ensure any registered binder death recipients are unlinked mListener.unregister(); } @@ -409,13 +410,15 @@ public class PipController implements ConfigurationChangeListener, @Override public void stopSwipePipToHome(int taskId, ComponentName componentName, - Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { + Rect destinationBounds, SurfaceControl overlay, Rect appBounds, + Rect sourceRectHint) { if (overlay != null) { overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome"); } executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome", (controller) -> controller.onSwipePipToHomeAnimationStart( - taskId, componentName, destinationBounds, overlay, appBounds)); + taskId, componentName, destinationBounds, overlay, appBounds, + sourceRectHint)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java index aed493f2bc8f..495cd0075494 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -731,8 +731,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); cleanUpHighPerfSessionMaybe(); - // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core. - mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + // Signal that the transition is done - should update transition state by default. + mPipScheduler.scheduleFinishResizePip(false /* configAtEnd */); break; case PipTransitionState.EXITING_PIP: // We need to force finish any local animators if about to leave PiP, to avoid diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java index 7dffe543ec9c..33e80bd80988 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -38,6 +38,7 @@ import android.view.ViewConfiguration; import androidx.annotation.VisibleForTesting; +import com.android.internal.util.Preconditions; import com.android.wm.shell.R; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -45,6 +46,7 @@ import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.pip2.animation.PipResizeAnimator; import java.io.PrintWriter; import java.util.function.Consumer; @@ -82,6 +84,7 @@ public class PipResizeGestureHandler implements private final Rect mLastResizeBounds = new Rect(); private final Rect mUserResizeBounds = new Rect(); private final Rect mDownBounds = new Rect(); + private final Rect mStartBoundsAfterRelease = new Rect(); private final Runnable mUpdateMovementBoundsRunnable; private final Consumer<Rect> mUpdateResizeBoundsCallback; @@ -418,7 +421,9 @@ public class PipResizeGestureHandler implements if (!mOngoingPinchToResize) { return; } - final Rect startBounds = new Rect(mLastResizeBounds); + + // Cache initial bounds after release for animation before mLastResizeBounds are modified. + mStartBoundsAfterRelease.set(mLastResizeBounds); // If user resize is pretty close to max size, just auto resize to max. if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x @@ -527,28 +532,39 @@ public class PipResizeGestureHandler implements int offsetY = inTopHalf ? 1 : -1; mLastResizeBounds.offset(0 /* dx */, offsetY); } - mWaitingForBoundsChangeTransition = true; - mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds); + + // Schedule PiP resize transition, but delay any config updates until very end. + mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds, true /* configAtEnd */); break; case PipTransitionState.CHANGING_PIP_BOUNDS: if (!mWaitingForBoundsChangeTransition) break; - - // If bounds change transition was scheduled from this class, handle leash updates. + // If resize transition was scheduled from this component, handle leash updates. mWaitingForBoundsChangeTransition = false; + SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + Preconditions.checkState(pipLeash != null, + "No leash cached by mPipTransitionState=" + mPipTransitionState); + SurfaceControl.Transaction startTx = extra.getParcelable( PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); - Rect destinationBounds = extra.getParcelable( - PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); - startTx.apply(); - - // All motion operations have actually finished, so make bounds cache updates. - mUpdateResizeBoundsCallback.accept(destinationBounds); - cleanUpHighPerfSessionMaybe(); - - // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core. - mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + SurfaceControl.Transaction finishTx = extra.getParcelable( + PipTransition.PIP_FINISH_TX, SurfaceControl.Transaction.class); + startTx.setWindowCrop(pipLeash, mPipBoundsState.getBounds().width(), + mPipBoundsState.getBounds().height()); + + PipResizeAnimator animator = new PipResizeAnimator(mContext, pipLeash, + startTx, finishTx, mPipBoundsState.getBounds(), mStartBoundsAfterRelease, + mLastResizeBounds, PINCH_RESIZE_SNAP_DURATION, mAngle); + animator.setAnimationEndCallback(() -> { + // All motion operations have actually finished, so make bounds cache updates. + mUpdateResizeBoundsCallback.accept(mLastResizeBounds); + cleanUpHighPerfSessionMaybe(); + + // Signal that we are done with resize transition + mPipScheduler.scheduleFinishResizePip(true /* configAtEnd */); + }); + animator.start(); break; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 49475077211f..9c1e321a1273 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -153,15 +153,46 @@ public class PipScheduler { * Animates resizing of the pinned stack given the duration. */ public void scheduleAnimateResizePip(Rect toBounds) { + scheduleAnimateResizePip(toBounds, false /* configAtEnd */); + } + + /** + * Animates resizing of the pinned stack given the duration. + * + * @param configAtEnd true if we are delaying config updates until the transition ends. + */ + public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd) { if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) { return; } WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds); + if (configAtEnd) { + wct.deferConfigToTransitionEnd(mPipTransitionState.mPipTaskToken); + } mPipTransitionController.startResizeTransition(wct); } /** + * Signals to Core to finish the PiP resize transition. + * Note that we do not allow any actual WM Core changes at this point. + * + * @param configAtEnd true if we are waiting for config updates at the end of the transition. + */ + public void scheduleFinishResizePip(boolean configAtEnd) { + SurfaceControl.Transaction tx = null; + if (configAtEnd) { + tx = new SurfaceControl.Transaction(); + tx.addTransactionCommittedListener(mMainExecutor, () -> { + mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + }); + } else { + mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + } + mPipTransitionController.finishTransition(tx); + } + + /** * Directly perform a scaled matrix transformation on the leash. This will not perform any * {@link WindowContainerTransaction}. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 7dddd2748f83..57dc5f92b2b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -52,6 +52,7 @@ import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.PipContentOverlay; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -292,37 +293,32 @@ public class PipTransition extends PipTransitionController implements return false; } + SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay(); PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; - Rect srcRectHint = params.getSourceRectHint(); - Rect startBounds = pipChange.getStartAbsBounds(); + + Rect appBounds = mPipTransitionState.getSwipePipToHomeAppBounds(); Rect destinationBounds = pipChange.getEndAbsBounds(); + float aspectRatio = pipChange.getTaskInfo().pictureInPictureParams.getAspectRatioFloat(); + + // We fake the source rect hint when the one prvided by the app is invalid for + // the animation with an app icon overlay. + Rect animationSrcRectHint = overlayLeash == null ? params.getSourceRectHint() + : PipUtils.getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio); + WindowContainerTransaction finishWct = new WindowContainerTransaction(); SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - if (PipBoundsAlgorithm.isSourceRectHintValidForEnterPip(srcRectHint, destinationBounds)) { - final float scale = (float) destinationBounds.width() / srcRectHint.width(); - startTransaction.setWindowCrop(pipLeash, srcRectHint); - startTransaction.setPosition(pipLeash, - destinationBounds.left - srcRectHint.left * scale, - destinationBounds.top - srcRectHint.top * scale); - - // Reset the scale in case we are in the multi-activity case. - // TO_FRONT transition already scales down the task in single-activity case, but - // in multi-activity case, reparenting yields new reset scales coming from pinned task. - startTransaction.setScale(pipLeash, scale, scale); - } else { - final float scaleX = (float) destinationBounds.width() / startBounds.width(); - final float scaleY = (float) destinationBounds.height() / startBounds.height(); + final float scale = (float) destinationBounds.width() / animationSrcRectHint.width(); + startTransaction.setWindowCrop(pipLeash, animationSrcRectHint); + startTransaction.setPosition(pipLeash, + destinationBounds.left - animationSrcRectHint.left * scale, + destinationBounds.top - animationSrcRectHint.top * scale); + startTransaction.setScale(pipLeash, scale, scale); + + if (overlayLeash != null) { final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize( mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds); - SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay(); - - startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top) - .setScale(pipLeash, scaleX, scaleY) - .setWindowCrop(pipLeash, startBounds) - .reparent(overlayLeash, pipLeash) - .setLayer(overlayLeash, Integer.MAX_VALUE); // Overlay needs to be adjusted once a new draw comes in resetting surface transform. tx.setScale(overlayLeash, 1f, 1f); @@ -389,11 +385,25 @@ public class PipTransition extends PipTransitionController implements if (pipChange == null) { return false; } - // cache the PiP task token and leash - WindowContainerToken pipTaskToken = pipChange.getContainer(); - startTransaction.apply(); - finishCallback.onTransitionFinished(null); + Rect destinationBounds = pipChange.getEndAbsBounds(); + SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + Preconditions.checkNotNull(pipLeash, "Leash is null for alpha transition."); + + // Start transition with 0 alpha at the entry bounds. + startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top) + .setWindowCrop(pipLeash, destinationBounds.width(), destinationBounds.height()) + .setAlpha(pipLeash, 0f); + + PipAlphaAnimator animator = new PipAlphaAnimator(mContext, pipLeash, startTransaction, + PipAlphaAnimator.FADE_IN); + animator.setAnimationEndCallback(() -> { + finishCallback.onTransitionFinished(null); + // This should update the pip transition state accordingly after we stop playing. + onClientDrawAtTransitionEnd(); + }); + + animator.start(); return true; } @@ -473,10 +483,10 @@ public class PipTransition extends PipTransitionController implements private boolean isLegacyEnter(@NonNull TransitionInfo info) { TransitionInfo.Change pipChange = getPipChange(info); - // If the only change in the changes list is a TO_FRONT mode PiP task, + // If the only change in the changes list is a opening type PiP task, // then this is legacy-enter PiP. - return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT - && info.getChanges().size() == 1; + return pipChange != null && info.getChanges().size() == 1 + && (pipChange.getMode() == TRANSIT_TO_FRONT || pipChange.getMode() == TRANSIT_OPEN); } private boolean isRemovePipTransition(@NonNull TransitionInfo info) { @@ -514,6 +524,20 @@ public class PipTransition extends PipTransitionController implements } @Override + public void finishTransition(@Nullable SurfaceControl.Transaction tx) { + WindowContainerTransaction wct = null; + if (tx != null && mPipTransitionState.mPipTaskToken != null) { + // Outside callers can only provide a transaction to be applied with the final draw. + // So no actual WM changes can be applied for this transition after this point. + wct = new WindowContainerTransaction(); + wct.setBoundsChangeTransaction(mPipTransitionState.mPipTaskToken, tx); + } + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(wct); + } + } + + @Override public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { switch (newState) { @@ -535,15 +559,6 @@ public class PipTransition extends PipTransitionController implements mPipTransitionState.mPipTaskToken = null; mPipTransitionState.mPinnedTaskLeash = null; break; - case PipTransitionState.CHANGED_PIP_BOUNDS: - // Note: this might not be the end of the animation, rather animator just finished - // adjusting startTx and finishTx and is ready to finishTransition(). The animator - // can still continue playing the leash into the destination bounds after. - if (mFinishCallback != null) { - mFinishCallback.onTransitionFinished(null); - mFinishCallback = null; - } - break; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/OWNERS new file mode 100644 index 000000000000..3f3308cfc75a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/OWNERS @@ -0,0 +1 @@ +include platform/development:/tools/winscope/OWNERS diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 19af3d544b36..497c3f704c82 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -18,6 +18,8 @@ package com.android.wm.shell.protolog; import com.android.internal.protolog.common.IProtoLogGroup; +import java.util.UUID; + /** * Defines logging groups for ProtoLog. * @@ -116,6 +118,11 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { this.mLogToLogcat = logToLogcat; } + @Override + public int getId() { + return Consts.START_ID + this.ordinal(); + } + private static class Consts { private static final String TAG_WM_SHELL = "WindowManagerShell"; private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow"; @@ -124,5 +131,9 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { private static final boolean ENABLE_DEBUG = true; private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; + + private static final int START_ID = (int) ( + UUID.nameUUIDFromBytes(ShellProtoLogGroup.class.getName().getBytes()) + .getMostSignificantBits() % Integer.MAX_VALUE); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl index 62d195efb381..245829ecafb3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl @@ -42,4 +42,7 @@ oneway interface IRecentTasksListener { * Called when a running task changes. */ void onRunningTaskChanged(in RunningTaskInfo taskInfo); -} + + /** A task has moved to front. */ + oneway void onTaskMovedToFront(in RunningTaskInfo taskInfo); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS new file mode 100644 index 000000000000..452644b05a2a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS @@ -0,0 +1,6 @@ +# WM shell sub-module task stack owners +uysalorhan@google.com +samcackett@google.com +alexchau@google.com +silvajordan@google.com +uwaisashraf@google.com
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 863202d5e1c3..9f3c519b441b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -20,7 +20,7 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.pm.PackageManager.FEATURE_PC; import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.window.flags.Flags.enableTaskStackObserverInShell; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; import android.app.ActivityManager; @@ -58,6 +58,7 @@ import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.util.GroupedRecentTaskInfo; import com.android.wm.shell.util.SplitBounds; @@ -74,7 +75,8 @@ import java.util.function.Consumer; * Manages the recent task list from the system, caching it as necessary. */ public class RecentTasksController implements TaskStackListenerCallback, - RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener { + RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener, + TaskStackTransitionObserver.TaskStackTransitionObserverListener { private static final String TAG = RecentTasksController.class.getSimpleName(); private final Context mContext; @@ -85,6 +87,7 @@ public class RecentTasksController implements TaskStackListenerCallback, private final TaskStackListenerImpl mTaskStackListener; private final RecentTasksImpl mImpl = new RecentTasksImpl(); private final ActivityTaskManager mActivityTaskManager; + private final TaskStackTransitionObserver mTaskStackTransitionObserver; private RecentsTransitionHandler mTransitionHandler = null; private IRecentTasksListener mListener; private final boolean mPcFeatureEnabled; @@ -113,13 +116,15 @@ public class RecentTasksController implements TaskStackListenerCallback, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + TaskStackTransitionObserver taskStackTransitionObserver, @ShellMainThread ShellExecutor mainExecutor ) { if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) { return null; } return new RecentTasksController(context, shellInit, shellController, shellCommandHandler, - taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor); + taskStackListener, activityTaskManager, desktopModeTaskRepository, + taskStackTransitionObserver, mainExecutor); } RecentTasksController(Context context, @@ -129,6 +134,7 @@ public class RecentTasksController implements TaskStackListenerCallback, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + TaskStackTransitionObserver taskStackTransitionObserver, ShellExecutor mainExecutor) { mContext = context; mShellController = shellController; @@ -137,6 +143,7 @@ public class RecentTasksController implements TaskStackListenerCallback, mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); mTaskStackListener = taskStackListener; mDesktopModeTaskRepository = desktopModeTaskRepository; + mTaskStackTransitionObserver = taskStackTransitionObserver; mMainExecutor = mainExecutor; shellInit.addInitCallback(this::onInit, this); } @@ -155,6 +162,10 @@ public class RecentTasksController implements TaskStackListenerCallback, mShellCommandHandler.addDumpCallback(this::dump, this); mTaskStackListener.addListener(this); mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this)); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this, + mMainExecutor); + } } void setTransitionHandler(RecentsTransitionHandler handler) { @@ -268,6 +279,12 @@ public class RecentTasksController implements TaskStackListenerCallback, notifyRecentTasksChanged(); } + @Override + public void onTaskMovedToFrontThroughTransition( + ActivityManager.RunningTaskInfo runningTaskInfo) { + notifyTaskMovedToFront(runningTaskInfo); + } + @VisibleForTesting void notifyRecentTasksChanged() { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed"); @@ -329,6 +346,19 @@ public class RecentTasksController implements TaskStackListenerCallback, } } + private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { + if (mListener == null + || !enableTaskStackObserverInShell() + || taskInfo.realActivity == null) { + return; + } + try { + mListener.onTaskMovedToFront(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onTaskMovedToFront", e); + } + } + private boolean shouldEnableRunningTasksForDesktopMode() { return mPcFeatureEnabled || (DesktopModeStatus.canEnterDesktopMode(mContext) @@ -379,10 +409,6 @@ public class RecentTasksController implements TaskStackListenerCallback, if (DesktopModeStatus.canEnterDesktopMode(mContext) && mDesktopModeTaskRepository.isPresent() && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) { - if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) { - // Minimized freeform tasks should not be shown at all. - continue; - } // Freeform tasks will be added as a separate entry if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) { mostRecentFreeformTaskIndex = recentTasks.size(); @@ -465,6 +491,7 @@ public class RecentTasksController implements TaskStackListenerCallback, } return null; } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); @@ -548,6 +575,11 @@ public class RecentTasksController implements TaskStackListenerCallback, public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { mListener.call(l -> l.onRunningTaskChanged(taskInfo)); } + + @Override + public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { + mListener.call(l -> l.onTaskMovedToFront(taskInfo)); + } }; public IRecentTasksImpl(RecentTasksController controller) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 3a266d9bb3ef..c67cf1d85918 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -74,6 +74,7 @@ import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; import java.util.function.Consumer; +import java.util.function.Supplier; /** * Handles the Recents (overview) animation. Only one of these can run at a time. A recents @@ -84,6 +85,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { private final Transitions mTransitions; private final ShellExecutor mExecutor; + private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; @Nullable private final RecentTasksController mRecentTasksController; private IApplicationThread mAnimApp = null; @@ -101,11 +103,13 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions, @Nullable RecentTasksController recentTasksController, - HomeTransitionObserver homeTransitionObserver) { + HomeTransitionObserver homeTransitionObserver, + Supplier<SurfaceControl.Transaction> transactionSupplier) { mTransitions = transitions; mExecutor = transitions.getMainExecutor(); mRecentTasksController = recentTasksController; mHomeTransitionObserver = homeTransitionObserver; + mTransactionSupplier = transactionSupplier; if (!Transitions.ENABLE_SHELL_TRANSITIONS) return; if (recentTasksController == null) return; shellInit.addInitCallback(() -> { @@ -1056,7 +1060,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { final Transitions.TransitionFinishCallback finishCB = mFinishCB; mFinishCB = null; - final SurfaceControl.Transaction t = mFinishTransaction; + SurfaceControl.Transaction t = mFinishTransaction; final WindowContainerTransaction wct = new WindowContainerTransaction(); if (mKeyguardLocked && mRecentsTask != null) { @@ -1106,6 +1110,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " normal finish"); + if (toHome && !mOpeningTasks.isEmpty()) { + // Attempting to start a task after swipe to home, don't show it, + // move recents to top + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + " attempting to start a task after swipe to home"); + t = mTransactionSupplier.get(); + wct.reorder(mRecentsTask, true /*onTop*/); + mClosingTasks.addAll(mOpeningTasks); + mOpeningTasks.clear(); + } // The general case: committing to recents, going home, or switching tasks. for (int i = 0; i < mOpeningTasks.size(); ++i) { t.show(mOpeningTasks.get(i).mTaskSurface); @@ -1174,6 +1188,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { mPipTransaction = null; } } + if (t != mFinishTransaction) { + // apply after merges because these changes are accounting for finishWCT changes. + mTransitions.setAfterMergeFinishTransaction(mTransition, t); + } cleanUp(); finishCB.onTransitionFinished(wct.isEmpty() ? null : wct); if (runnerFinishCb != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt new file mode 100644 index 000000000000..7c5f10a5bcca --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.recents + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.os.IBinder +import android.util.ArrayMap +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import com.android.window.flags.Flags.enableTaskStackObserverInShell +import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import dagger.Lazy +import java.util.concurrent.Executor + +/** + * A [Transitions.TransitionObserver] that observes shell transitions and sends updates to listeners + * about task stack changes. + * + * TODO(346588978) Move split/pip signals here as well so that launcher don't need to handle it + */ +class TaskStackTransitionObserver( + private val transitions: Lazy<Transitions>, + shellInit: ShellInit +) : Transitions.TransitionObserver { + + private val transitionToTransitionChanges: MutableMap<IBinder, TransitionChanges> = + mutableMapOf() + private val taskStackTransitionObserverListeners = + ArrayMap<TaskStackTransitionObserverListener, Executor>() + + init { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + shellInit.addInitCallback(::onInit, this) + } + } + + fun onInit() { + transitions.get().registerObserver(this) + } + + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + if (enableTaskStackObserverInShell()) { + val taskInfoList = mutableListOf<RunningTaskInfo>() + val transitionTypeList = mutableListOf<Int>() + + for (change in info.changes) { + if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) { + continue + } + + val taskInfo = change.taskInfo + if (taskInfo == null || taskInfo.taskId == -1) { + continue + } + + if (change.mode == WindowManager.TRANSIT_OPEN) { + change.taskInfo?.let { taskInfoList.add(it) } + transitionTypeList.add(change.mode) + } + } + transitionToTransitionChanges.put( + transition, + TransitionChanges(taskInfoList, transitionTypeList) + ) + } + } + + override fun onTransitionStarting(transition: IBinder) {} + + override fun onTransitionMerged(merged: IBinder, playing: IBinder) {} + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + val taskInfoList = + transitionToTransitionChanges.getOrDefault(transition, TransitionChanges()).taskInfoList + val typeList = + transitionToTransitionChanges + .getOrDefault(transition, TransitionChanges()) + .transitionTypeList + transitionToTransitionChanges.remove(transition) + + for ((index, taskInfo) in taskInfoList.withIndex()) { + if ( + TransitionUtil.isOpeningType(typeList[index]) && + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM + ) { + notifyTaskStackTransitionObserverListeners(taskInfo) + } + } + } + + fun addTaskStackTransitionObserverListener( + taskStackTransitionObserverListener: TaskStackTransitionObserverListener, + executor: Executor + ) { + taskStackTransitionObserverListeners[taskStackTransitionObserverListener] = executor + } + + fun removeTaskStackTransitionObserverListener( + taskStackTransitionObserverListener: TaskStackTransitionObserverListener + ) { + taskStackTransitionObserverListeners.remove(taskStackTransitionObserverListener) + } + + private fun notifyTaskStackTransitionObserverListeners(taskInfo: RunningTaskInfo) { + taskStackTransitionObserverListeners.forEach { (listener, executor) -> + executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) } + } + } + + /** Listener to use to get updates regarding task stack from this observer */ + interface TaskStackTransitionObserverListener { + /** Called when a task is moved to front. */ + fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {} + } + + private data class TransitionChanges( + val taskInfoList: MutableList<RunningTaskInfo> = ArrayList(), + val transitionTypeList: MutableList<Int> = ArrayList() + ) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index b9d70e1a599d..dd219d32bbaa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -24,7 +24,6 @@ import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_OPENING; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.common.MultiInstanceHelper.getComponent; import static com.android.wm.shell.common.MultiInstanceHelper.getShortcutComponent; import static com.android.wm.shell.common.MultiInstanceHelper.samePackage; @@ -1241,8 +1240,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void invalidate() { mController = null; - // Unregister the listener to ensure any registered binder death recipients are unlinked + // Unregister the listeners to ensure any binder death recipients are unlinked mListener.unregister(); + mSelectListener.unregister(); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 82ef422f829a..45eff4a24898 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -1524,6 +1524,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct); mSplitTransitions.startDismissTransition(wct, this, mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED); + setSplitsVisible(false); } else { exitSplitScreen( mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, @@ -1846,7 +1847,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void finishEnterSplitScreen(SurfaceControl.Transaction finishT) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "finishEnterSplitScreen"); - mSplitLayout.update(finishT, true /* resetImePosition */); + mSplitLayout.update(null, true /* resetImePosition */); mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash); mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash); setDividerVisibility(true, finishT); @@ -1893,6 +1894,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // will be canceled. options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + + // TODO (b/336477473): Disallow enter PiP when launching a task in split by default; + // this might have to be changed as more split-to-pip cujs are defined. + options.setDisallowEnterPictureInPictureWhileLaunching(true); opts.putAll(options.toBundle()); } @@ -2644,7 +2649,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Nullable TransitionRequestInfo request) { final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); if (triggerTask == null) { - if (isSplitActive()) { + if (isSplitScreenVisible()) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d display rotation", request.getDebugId()); // Check if the display is rotating. @@ -2757,6 +2762,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // cases above and it is not already visible return null; } else { + if (triggerTask.parentTaskId == mMainStage.mRootTaskInfo.taskId + || triggerTask.parentTaskId == mSideStage.mRootTaskInfo.taskId) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d " + + "restoring to split", request.getDebugId()); + out = new WindowContainerTransaction(); + mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */); + } if (isOpening && getStageOfTask(triggerTask) != null) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d enter split", request.getDebugId()); @@ -3103,7 +3116,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Includes TRANSIT_CHANGE to cover reparenting top-most task to split. mainChild = change; } else if (sideChild == null && stageType == STAGE_TYPE_SIDE - && isOpeningType(change.getMode())) { + && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) { sideChild = change; } else if (stageType != STAGE_TYPE_UNDEFINED && change.getMode() == TRANSIT_TO_BACK) { // Collect all to back task's and evict them when transition finished. @@ -3114,7 +3127,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, SplitScreenTransitions.EnterSession pendingEnter = mSplitTransitions.mPendingEnter; if (pendingEnter.mExtraTransitType == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) { - // Open to side should only be used when split already active and foregorund. + // Open to side should only be used when split already active and foregorund or when + // app is restoring to split from fullscreen. if (mainChild == null && sideChild == null) { Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "Launched a task in split, but didn't receive any task in transition.")); @@ -3201,6 +3215,22 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mPausingTasks.clear(); }); + if (info.getType() == TRANSIT_CHANGE && !isSplitActive() + && pendingEnter.mExtraTransitType == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) { + if (finalMainChild != null && finalSideChild == null) { + requestEnterSplitSelect(finalMainChild.getTaskInfo(), + new WindowContainerTransaction(), + getMainStagePosition(), finalMainChild.getStartAbsBounds()); + } else if (finalSideChild != null && finalMainChild == null) { + requestEnterSplitSelect(finalSideChild.getTaskInfo(), + new WindowContainerTransaction(), + getSideStagePosition(), finalSideChild.getStartAbsBounds()); + } else { + throw new IllegalStateException( + "Attempting to restore to split but reparenting change not found"); + } + } + finishEnterSplitScreen(finishT); addDividerBarToTransition(info, true /* show */); return true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java index bec4ba3bf0d1..fa084c585a59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java @@ -23,7 +23,6 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW; import android.app.ActivityManager.RunningTaskInfo; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index 66b3553bea09..8fc54edcbd4b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -21,8 +21,6 @@ import static android.graphics.Color.WHITE; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; -import static com.android.window.flags.Flags.windowSessionRelayoutInfo; - import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.Nullable; @@ -30,7 +28,6 @@ import android.app.ActivityManager; import android.app.ActivityManager.TaskDescription; import android.graphics.Paint; import android.graphics.Rect; -import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.Trace; @@ -139,16 +136,10 @@ public class TaskSnapshotWindow { } try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); - if (windowSessionRelayoutInfo()) { - final WindowRelayoutResult outRelayoutResult = new WindowRelayoutResult(tmpFrames, - tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls); - session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, - outRelayoutResult); - } else { - session.relayoutLegacy(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, - tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, - tmpControls, new Bundle()); - } + final WindowRelayoutResult outRelayoutResult = new WindowRelayoutResult(tmpFrames, + tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls); + session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, + outRelayoutResult); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java index b03daaafd70c..35427b93acea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java @@ -94,6 +94,11 @@ public class CounterRotatorHelper { return rotatedBounds; } + /** Returns true if the change is put on a surface in previous rotation. */ + public boolean isRotated(@NonNull TransitionInfo.Change change) { + return mLastRotationDelta != 0 && mRotatorMap.containsKey(change.getParent()); + } + /** * Removes the counter rotation surface in the finish transaction. No need to reparent the * children as the finish transaction should have already taken care of that. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 2d6ba6ee7217..9412b2b0b243 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -103,6 +103,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; +import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; @@ -516,7 +517,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { animRelOffset.y = Math.max(animRelOffset.y, change.getEndRelOffset().y); } - if (change.getActivityComponent() != null && !isActivityLevel) { + if (change.getActivityComponent() != null && !isActivityLevel + && !mRotator.isRotated(change)) { // At this point, this is an independent activity change in a non-activity // transition. This means that an activity transition got erroneously combined // with another ongoing transition. This then means that the animation root may @@ -543,7 +545,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { mTransactionPool, mMainExecutor, animRelOffset, cornerRadius, clipRect); - if (info.getAnimationOptions() != null) { + final TransitionInfo.AnimationOptions options; + if (Flags.moveAnimationOptionsToChange()) { + options = info.getAnimationOptions(); + } else { + options = change.getAnimationOptions(); + } + if (options != null) { attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(), cornerRadius); } @@ -725,7 +733,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final boolean isOpeningType = TransitionUtil.isOpeningType(type); final boolean enter = TransitionUtil.isOpeningType(changeMode); final boolean isTask = change.getTaskInfo() != null; - final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + final TransitionInfo.AnimationOptions options; + if (Flags.moveAnimationOptionsToChange()) { + options = change.getAnimationOptions(); + } else { + options = info.getAnimationOptions(); + } final int overrideType = options != null ? options.getType() : ANIM_NONE; final Rect endBounds = TransitionUtil.isClosingType(changeMode) ? mRotator.getEndBoundsInStartRotation(change) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java index b1a1e5999aa9..9b27e413b5e4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java @@ -133,5 +133,9 @@ public class HomeTransitionObserver implements TransitionObserver, */ public void invalidate(Transitions transitions) { transitions.unregisterObserver(this); + if (mListener != null) { + // Unregister the listener to ensure any registered binder death recipients are unlinked + mListener.unregister(); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index ad4f02d13cc6..2047b5a88604 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -55,6 +55,7 @@ import android.window.TransitionInfo; import com.android.internal.R; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; +import com.android.window.flags.Flags; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; @@ -71,7 +72,12 @@ public class TransitionAnimationHelper { final int changeFlags = change.getFlags(); final boolean enter = TransitionUtil.isOpeningType(changeMode); final boolean isTask = change.getTaskInfo() != null; - final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + final TransitionInfo.AnimationOptions options; + if (Flags.moveAnimationOptionsToChange()) { + options = change.getAnimationOptions(); + } else { + options = info.getAnimationOptions(); + } final int overrideType = options != null ? options.getType() : ANIM_NONE; int animAttr = 0; boolean translucent = false; @@ -246,7 +252,7 @@ public class TransitionAnimationHelper { if (!a.getShowBackdrop()) { return defaultColor; } - if (info.getAnimationOptions() != null + if (!Flags.moveAnimationOptionsToChange() && info.getAnimationOptions() != null && info.getAnimationOptions().getBackgroundColor() != 0) { // If available use the background color provided through AnimationOptions return info.getAnimationOptions().getBackgroundColor(); 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 6ade81c0f3a1..d2760ff88ece 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 @@ -35,7 +35,6 @@ import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; @@ -54,6 +53,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.SystemProperties; import android.provider.Settings; +import android.util.ArrayMap; import android.util.Log; import android.util.Pair; import android.view.SurfaceControl; @@ -166,9 +166,6 @@ public class Transitions implements RemoteCallable<Transitions>, public static final int TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 11; - /** Transition type to fullscreen from desktop mode. */ - public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12; - /** Transition type to cancel the drag to desktop mode. */ public static final int TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 13; @@ -177,9 +174,6 @@ public class Transitions implements RemoteCallable<Transitions>, public static final int TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 14; - /** Transition to animate task to desktop. */ - public static final int TRANSIT_MOVE_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 15; - /** Transition to resize PiP task. */ public static final int TRANSIT_RESIZE_PIP = TRANSIT_FIRST_CUSTOM + 16; @@ -190,6 +184,10 @@ public class Transitions implements RemoteCallable<Transitions>, // TRANSIT_FIRST_CUSTOM + 17 TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE; + /** Transition type for desktop mode transitions. */ + public static final int TRANSIT_DESKTOP_MODE_TYPES = + WindowManager.TRANSIT_FIRST_CUSTOM + 100; + private final ShellTaskOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; @@ -229,7 +227,8 @@ public class Transitions implements RemoteCallable<Transitions>, private boolean mDisableForceSync = false; private static final class ActiveTransition { - IBinder mToken; + final IBinder mToken; + TransitionHandler mHandler; boolean mAborted; TransitionInfo mInfo; @@ -239,6 +238,17 @@ public class Transitions implements RemoteCallable<Transitions>, /** Ordered list of transitions which have been merged into this one. */ private ArrayList<ActiveTransition> mMerged; + /** + * @deprecated DO NOT USE THIS unless absolutely necessary. It will be removed once + * everything migrates off finishWCT. + */ + @java.lang.Deprecated + SurfaceControl.Transaction mAfterMergeFinishT; + + ActiveTransition(IBinder token) { + mToken = token; + } + boolean isSync() { return (mInfo.getFlags() & TransitionInfo.FLAG_SYNC) != 0; } @@ -268,6 +278,9 @@ public class Transitions implements RemoteCallable<Transitions>, } } + /** All transitions that we have created, but not yet finished. */ + private final ArrayMap<IBinder, ActiveTransition> mKnownTransitions = new ArrayMap<>(); + /** Keeps track of transitions which have been started, but aren't ready yet. */ private final ArrayList<ActiveTransition> mPendingTransitions = new ArrayList<>(); @@ -657,8 +670,10 @@ public class Transitions implements RemoteCallable<Transitions>, } if (change.hasFlags(FLAG_NO_ANIMATION)) { hasNoAnimation = true; - } else { - // at-least one relevant participant *is* animated, so we need to animate. + } else if (!TransitionUtil.isOrderOnly(change) && !change.hasFlags(FLAG_IS_OCCLUDED)) { + // Ignore the order only or occluded changes since they shouldn't be visible during + // animation. For anything else, we need to animate if at-least one relevant + // participant *is* animated, return false; } } @@ -690,7 +705,7 @@ public class Transitions implements RemoteCallable<Transitions>, info.getDebugId(), transitionToken, info); int activeIdx = findByToken(mPendingTransitions, transitionToken); if (activeIdx < 0) { - final ActiveTransition existing = getKnownTransition(transitionToken); + final ActiveTransition existing = mKnownTransitions.get(transitionToken); if (existing != null) { Log.e(TAG, "Got duplicate transitionReady for " + transitionToken); // The transition is already somewhere else in the pipeline, so just return here. @@ -705,8 +720,8 @@ public class Transitions implements RemoteCallable<Transitions>, + transitionToken + ". expecting one of " + Arrays.toString(mPendingTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); - final ActiveTransition fallback = new ActiveTransition(); - fallback.mToken = transitionToken; + final ActiveTransition fallback = new ActiveTransition(transitionToken); + mKnownTransitions.put(transitionToken, fallback); mPendingTransitions.add(fallback); activeIdx = mPendingTransitions.size() - 1; } @@ -746,7 +761,7 @@ public class Transitions implements RemoteCallable<Transitions>, // Sleep starts a process of forcing all prior transitions to finish immediately ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Start finish-for-sync track %d", i); - finishForSync(active, i, null /* forceFinish */); + finishForSync(active.mToken, i, null /* forceFinish */); } if (hadPreceding) { return false; @@ -864,6 +879,7 @@ public class Transitions implements RemoteCallable<Transitions>, } else if (mPendingTransitions.isEmpty()) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition " + "animations finished"); + mKnownTransitions.clear(); // Run all runnables from the run-when-idle queue. for (int i = 0; i < mRunWhenIdleQueue.size(); i++) { mRunWhenIdleQueue.get(i).run(); @@ -884,7 +900,7 @@ public class Transitions implements RemoteCallable<Transitions>, ready.mStartT.apply(); } // finish now since there's nothing to animate. Calls back into processReadyQueue - onFinish(ready, null); + onFinish(ready.mToken, null); return; } playTransition(ready); @@ -943,8 +959,10 @@ public class Transitions implements RemoteCallable<Transitions>, private void playTransition(@NonNull ActiveTransition active) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Playing animation for %s", active); + final var token = active.mToken; + for (int i = 0; i < mObservers.size(); ++i) { - mObservers.get(i).onTransitionStarting(active.mToken); + mObservers.get(i).onTransitionStarting(token); } setupAnimHierarchy(active.mInfo, active.mStartT, active.mFinishT); @@ -953,8 +971,8 @@ public class Transitions implements RemoteCallable<Transitions>, if (active.mHandler != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try firstHandler %s", active.mHandler); - boolean consumed = active.mHandler.startAnimation(active.mToken, active.mInfo, - active.mStartT, active.mFinishT, (wct) -> onFinish(active, wct)); + boolean consumed = active.mHandler.startAnimation(token, active.mInfo, + active.mStartT, active.mFinishT, (wct) -> onFinish(token, wct)); if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); mTransitionTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler); @@ -962,8 +980,8 @@ public class Transitions implements RemoteCallable<Transitions>, } } // Otherwise give every other handler a chance - active.mHandler = dispatchTransition(active.mToken, active.mInfo, active.mStartT, - active.mFinishT, (wct) -> onFinish(active, wct), active.mHandler); + active.mHandler = dispatchTransition(token, active.mInfo, active.mStartT, + active.mFinishT, (wct) -> onFinish(token, wct), active.mHandler); } /** @@ -1007,6 +1025,20 @@ public class Transitions implements RemoteCallable<Transitions>, return null; } + /** @deprecated */ + @java.lang.Deprecated + public void setAfterMergeFinishTransaction(IBinder transition, + SurfaceControl.Transaction afterMergeFinishT) { + final ActiveTransition at = mKnownTransitions.get(transition); + if (at == null) return; + if (at.mAfterMergeFinishT != null) { + Log.e(TAG, "Setting after-merge-t >1 time on transition: " + at.mInfo.getDebugId()); + at.mAfterMergeFinishT.merge(afterMergeFinishT); + return; + } + at.mAfterMergeFinishT = afterMergeFinishT; + } + /** Aborts a transition. This will still queue it up to maintain order. */ private void onAbort(ActiveTransition transition) { final Track track = mTracks.get(transition.getTrack()); @@ -1039,10 +1071,15 @@ public class Transitions implements RemoteCallable<Transitions>, info.releaseAnimSurfaces(); } - private void onFinish(ActiveTransition active, + private void onFinish(IBinder token, @Nullable WindowContainerTransaction wct) { + final ActiveTransition active = mKnownTransitions.get(token); + if (active == null) { + Log.e(TAG, "Trying to finish a non-existent transition: " + token); + return; + } final Track track = mTracks.get(active.getTrack()); - if (track.mActiveTransition != active) { + if (track == null || track.mActiveTransition != active) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " + " a handler didn't properly deal with a merge. " + active, new RuntimeException()); @@ -1062,6 +1099,7 @@ public class Transitions implements RemoteCallable<Transitions>, } // Merge all associated transactions together SurfaceControl.Transaction fullFinish = active.mFinishT; + SurfaceControl.Transaction afterMergeFinish = active.mAfterMergeFinishT; if (active.mMerged != null) { for (int iM = 0; iM < active.mMerged.size(); ++iM) { final ActiveTransition toMerge = active.mMerged.get(iM); @@ -1081,6 +1119,21 @@ public class Transitions implements RemoteCallable<Transitions>, fullFinish.merge(toMerge.mFinishT); } } + if (toMerge.mAfterMergeFinishT != null) { + if (afterMergeFinish == null) { + afterMergeFinish = toMerge.mAfterMergeFinishT; + } else { + afterMergeFinish.merge(toMerge.mAfterMergeFinishT); + } + toMerge.mAfterMergeFinishT = null; + } + } + } + if (afterMergeFinish != null) { + if (fullFinish == null) { + fullFinish = afterMergeFinish; + } else { + fullFinish.merge(afterMergeFinish); } } if (fullFinish != null) { @@ -1095,54 +1148,25 @@ public class Transitions implements RemoteCallable<Transitions>, ActiveTransition merged = active.mMerged.get(iM); mOrganizer.finishTransition(merged.mToken, null /* wct */); releaseSurfaces(merged.mInfo); + mKnownTransitions.remove(merged.mToken); } active.mMerged.clear(); } + mKnownTransitions.remove(token); // Now that this is done, check the ready queue for more work. processReadyQueue(track); } - /** - * Checks to see if the transition specified by `token` is already known. If so, it will be - * returned. - */ - @Nullable - private ActiveTransition getKnownTransition(IBinder token) { - for (int i = 0; i < mPendingTransitions.size(); ++i) { - final ActiveTransition active = mPendingTransitions.get(i); - if (active.mToken == token) return active; - } - for (int i = 0; i < mReadyDuringSync.size(); ++i) { - final ActiveTransition active = mReadyDuringSync.get(i); - if (active.mToken == token) return active; - } - for (int t = 0; t < mTracks.size(); ++t) { - final Track tr = mTracks.get(t); - for (int i = 0; i < tr.mReadyTransitions.size(); ++i) { - final ActiveTransition active = tr.mReadyTransitions.get(i); - if (active.mToken == token) return active; - } - final ActiveTransition active = tr.mActiveTransition; - if (active == null) continue; - if (active.mToken == token) return active; - if (active.mMerged == null) continue; - for (int m = 0; m < active.mMerged.size(); ++m) { - final ActiveTransition merged = active.mMerged.get(m); - if (merged.mToken == token) return merged; - } - } - return null; - } - void requestStartTransition(@NonNull IBinder transitionToken, @Nullable TransitionRequestInfo request) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested (#%d): %s %s", request.getDebugId(), transitionToken, request); - if (getKnownTransition(transitionToken) != null) { + if (mKnownTransitions.containsKey(transitionToken)) { throw new RuntimeException("Transition already started " + transitionToken); } - final ActiveTransition active = new ActiveTransition(); + final ActiveTransition active = new ActiveTransition(transitionToken); + mKnownTransitions.put(transitionToken, active); WindowContainerTransaction wct = null; // If we have sleep, we use a special handler and we try to finish everything ASAP. @@ -1182,7 +1206,6 @@ public class Transitions implements RemoteCallable<Transitions>, wct.setBounds(request.getTriggerTask().token, null); } mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct); - active.mToken = transitionToken; // Currently, WMCore only does one transition at a time. If it makes a requestStart, it // is already collecting that transition on core-side, so it will be the next one to // become ready. There may already be pending transitions added as part of direct @@ -1201,9 +1224,10 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Directly starting a new transition " + "type=%d wct=%s handler=%s", type, wct, handler); - final ActiveTransition active = new ActiveTransition(); + final ActiveTransition active = + new ActiveTransition(mOrganizer.startNewTransition(type, wct)); active.mHandler = handler; - active.mToken = mOrganizer.startNewTransition(type, wct); + mKnownTransitions.put(active.mToken, active); mPendingTransitions.add(active); return active.mToken; } @@ -1243,14 +1267,14 @@ public class Transitions implements RemoteCallable<Transitions>, * * This is then repeated until there are no more pending sleep transitions. * - * @param reason The SLEEP transition that triggered this round of finishes. We will continue - * looping round finishing transitions as long as this is still waiting. + * @param reason The token for the SLEEP transition that triggered this round of finishes. + * We will continue looping round finishing transitions until this is ready. * @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge * signal to -- so it will be force-finished if it's still running. */ - private void finishForSync(ActiveTransition reason, + private void finishForSync(IBinder reason, int trackIdx, @Nullable ActiveTransition forceFinish) { - if (getKnownTransition(reason.mToken) == null) { + if (!mKnownTransitions.containsKey(reason)) { Log.d(TAG, "finishForSleep: already played sync transition " + reason); return; } @@ -1270,7 +1294,7 @@ public class Transitions implements RemoteCallable<Transitions>, forceFinish.mHandler.onTransitionConsumed( forceFinish.mToken, true /* aborted */, null /* finishTransaction */); } - onFinish(forceFinish, null); + onFinish(forceFinish.mToken, null); } } if (track.isIdle() || mReadyDuringSync.isEmpty()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 37cdbb47bfe8..e1009a0ae8bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -73,6 +73,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.Cuj; import com.android.internal.protolog.common.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.R; @@ -81,8 +82,10 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.InteractionJankMonitorUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator; import com.android.wm.shell.desktopmode.DesktopTasksController; @@ -102,6 +105,7 @@ import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionReg import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import java.io.PrintWriter; +import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; @@ -151,6 +155,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final DisplayInsetsController mDisplayInsetsController; private final Region mExclusionRegion = Region.obtain(); private boolean mInImmersiveMode; + private final String mSysUIPackageName; private final ISystemGestureExclusionListener mGestureExclusionListener = new ISystemGestureExclusionListener.Stub() { @@ -246,6 +251,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mInputManager = mContext.getSystemService(InputManager.class); mWindowDecorByTaskId = windowDecorByTaskId; + mSysUIPackageName = mContext.getResources().getString( + com.android.internal.R.string.config_systemUi); shellInit.addInitCallback(this::onInit, this); } @@ -430,7 +437,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SplitScreenController.EXIT_REASON_DESKTOP_MODE); } else { WindowContainerTransaction wct = new WindowContainerTransaction(); - mDesktopTasksController.onDesktopWindowClose(wct, mTaskId); + mDesktopTasksController.onDesktopWindowClose(wct, mDisplayId, mTaskId); mTaskOperations.closeTask(mTaskToken, wct); } } else if (id == R.id.back_button) { @@ -438,7 +445,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { moveTaskToFront(decoration.mTaskInfo); - decoration.createHandleMenu(); + decoration.createHandleMenu(mSplitScreenController); } else { decoration.closeHandleMenu(); } @@ -447,7 +454,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // App sometimes draws before the insets from WindowDecoration#relayout have // been added, so they must be added here mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct); - mDesktopTasksController.moveToDesktop(mTaskId, wct); + mDesktopTasksController.moveToDesktop(mTaskId, wct, + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON); decoration.closeHandleMenu(); } else if (id == R.id.fullscreen_button) { decoration.closeHandleMenu(); @@ -455,7 +463,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSplitScreenController.moveTaskToFullscreen(mTaskId, SplitScreenController.EXIT_REASON_DESKTOP_MODE); } else { - mDesktopTasksController.moveToFullscreen(mTaskId); + mDesktopTasksController.moveToFullscreen(mTaskId, + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON); } } else if (id == R.id.split_screen_button) { decoration.closeHandleMenu(); @@ -463,11 +472,17 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.collapse_menu_button) { decoration.closeHandleMenu(); } else if (id == R.id.maximize_window) { + InteractionJankMonitorUtils.beginTracing( + Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v, + /* tag= */ "caption_bar_button"); final RunningTaskInfo taskInfo = decoration.mTaskInfo; decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); mDesktopTasksController.toggleDesktopTaskSize(taskInfo); } else if (id == R.id.maximize_menu_maximize_button) { + InteractionJankMonitorUtils.beginTracing( + Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v, + /* tag= */ "maximize_menu_option"); final RunningTaskInfo taskInfo = decoration.mTaskInfo; mDesktopTasksController.toggleDesktopTaskSize(taskInfo); decoration.closeHandleMenu(); @@ -705,6 +720,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return false; } final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + InteractionJankMonitorUtils.beginTracing( + Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext, + /* surface= */ decoration.mTaskSurface, /* tag= */ "double_tap"); mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo); return true; } @@ -1032,10 +1050,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && taskInfo.isFocused) { return false; } + // TODO(b/347289970): Consider replacing with API if (Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(taskInfo)) { return false; } + if (isSystemUIApplication(taskInfo)) { + return false; + } return DesktopModeStatus.canEnterDesktopMode(mContext) && !DesktopWallpaperActivity.isWallpaperTask(taskInfo) && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED @@ -1106,6 +1128,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && mSplitScreenController.isTaskInSplitScreen(taskId); } + // TODO(b/347289970): Consider replacing with API + private boolean isSystemUIApplication(RunningTaskInfo taskInfo) { + if (taskInfo.baseActivity != null) { + return (Objects.equals(taskInfo.baseActivity.getPackageName(), mSysUIPackageName)); + } + return false; + } + private void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + "DesktopModeWindowDecorViewModel"); 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 eced07831ff7..4d597cac889e 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 @@ -68,10 +68,11 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; -import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder; -import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder; -import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder; +import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder; +import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; +import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder; import kotlin.Unit; @@ -90,7 +91,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; - private DesktopModeWindowDecorationViewHolder mWindowDecorViewHolder; + private WindowDecorationViewHolder mWindowDecorViewHolder; private View.OnClickListener mOnCaptionButtonClickListener; private View.OnTouchListener mOnCaptionTouchListener; private View.OnLongClickListener mOnCaptionLongClickListener; @@ -98,10 +99,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; private DragDetector mDragDetector; - + private Runnable mCurrentViewHostRunnable = null; private RelayoutParams mRelayoutParams = new RelayoutParams(); private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = new WindowDecoration.RelayoutResult<>(); + private final Runnable mViewHostRunnable = + () -> updateViewHost(mRelayoutParams, null /* onDrawTransaction */, mResult); private final Point mPositionInParent = new Point(); private HandleMenu mHandleMenu; @@ -193,17 +196,88 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // position and crop are set. final boolean shouldSetTaskPositionAndCrop = !DesktopModeStatus.isVeiledResizeEnabled() && mTaskDragResizer.isResizingOrAnimating(); - // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is - // synced with the buffer transaction (that draws the View). Both will be shown on screen - // at the same, whereas applying them independently causes flickering. See b/270202228. - relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */, - shouldSetTaskPositionAndCrop); + // For headers only (i.e. in freeform): use |applyStartTransactionOnDraw| so that the + // transaction (that applies task crop) is synced with the buffer transaction (that draws + // the View). Both will be shown on screen at the same, whereas applying them independently + // causes flickering. See b/270202228. + final boolean applyTransactionOnDraw = + taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; + relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop); + if (!applyTransactionOnDraw) { + t.apply(); + } } void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { Trace.beginSection("DesktopModeWindowDecoration#relayout"); + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + // The Task is in Freeform mode -> show its header in sync since it's an integral part + // of the window itself - a delayed header might cause bad UX. + relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw, + shouldSetTaskPositionAndCrop); + } else { + // The Task is outside Freeform mode -> allow the handle view to be delayed since the + // handle is just a small addition to the window. + relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw, + shouldSetTaskPositionAndCrop); + } + Trace.endSection(); + } + + /** Run the whole relayout phase immediately without delay. */ + private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { + // Clear the current ViewHost runnable as we will update the ViewHost here + clearCurrentViewHostRunnable(); + updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw, + shouldSetTaskPositionAndCrop); + if (mResult.mRootView != null) { + updateViewHost(mRelayoutParams, startT, mResult); + } + } + + /** + * Clear the current ViewHost runnable - to ensure it doesn't run once relayout params have been + * updated. + */ + private void clearCurrentViewHostRunnable() { + if (mCurrentViewHostRunnable != null) { + mHandler.removeCallbacks(mCurrentViewHostRunnable); + mCurrentViewHostRunnable = null; + } + } + + /** + * Relayout the window decoration but repost some of the work, to unblock the current callstack. + */ + private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { + if (applyStartTransactionOnDraw) { + throw new IllegalArgumentException( + "We cannot both sync viewhost ondraw and delay viewhost creation."); + } + // Clear the current ViewHost runnable as we will update the ViewHost here + clearCurrentViewHostRunnable(); + updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, + false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop); + if (mResult.mRootView == null) { + // This means something blocks the window decor from showing, e.g. the task is hidden. + // Nothing is set up in this case including the decoration surface. + return; + } + // Store the current runnable so it can be removed if we start a new relayout. + mCurrentViewHostRunnable = mViewHostRunnable; + mHandler.post(mCurrentViewHostRunnable); + } + + private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { + Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces"); if (isHandleMenuActive()) { mHandleMenu.relayout(startT); } @@ -215,8 +289,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - Trace.beginSection("DesktopModeWindowDecoration#relayout-inner"); - relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); + Trace.beginSection("DesktopModeWindowDecoration#relayout-updateViewsAndSurfaces"); + updateViewsAndSurfaces(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); Trace.endSection(); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo @@ -227,37 +301,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. // Nothing is set up in this case including the decoration surface. - Trace.endSection(); // DesktopModeWindowDecoration#relayout + Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces return; } if (oldRootView != mResult.mRootView) { - if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) { - mWindowDecorViewHolder = new DesktopModeFocusedWindowDecorationViewHolder( - mResult.mRootView, - mOnCaptionTouchListener, - mOnCaptionButtonClickListener - ); - } else if (mRelayoutParams.mLayoutResId - == R.layout.desktop_mode_app_controls_window_decor) { - loadAppInfoIfNeeded(); - mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder( - mResult.mRootView, - mOnCaptionTouchListener, - mOnCaptionButtonClickListener, - mOnCaptionLongClickListener, - mOnCaptionGenericMotionListener, - mAppName, - mAppIconBitmap, - () -> { - if (!isMaximizeMenuActive()) { - createMaximizeMenu(); - } - return Unit.INSTANCE; - }); - } else { - throw new IllegalArgumentException("Unexpected layout resource id"); - } + mWindowDecorViewHolder = createViewHolder(); } Trace.beginSection("DesktopModeWindowDecoration#relayout-binding"); mWindowDecorViewHolder.bindData(mTaskInfo); @@ -268,16 +317,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeMaximizeMenu(); } - final boolean isFreeform = - taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; - final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; - if (!isDragResizeable) { + updateDragResizeListener(oldDecorationSurface); + updateMaximizeMenu(startT); + Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces + } + + private void updateDragResizeListener(SurfaceControl oldDecorationSurface) { + if (!isDragResizable(mTaskInfo)) { if (!mTaskInfo.positionInParent.equals(mPositionInParent)) { // We still want to track caption bar's exclusion region on a non-resizeable task. updateExclusionRegion(); } closeDragResizeListener(); - Trace.endSection(); // DesktopModeWindowDecoration#relayout return; } @@ -311,15 +362,51 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin || !mTaskInfo.positionInParent.equals(mPositionInParent)) { updateExclusionRegion(); } + } - if (isMaximizeMenuActive()) { - if (!mTaskInfo.isVisible()) { - closeMaximizeMenu(); - } else { - mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT); - } + private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) { + final boolean isFreeform = + taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; + return isFreeform && taskInfo.isResizeable; + } + + private void updateMaximizeMenu(SurfaceControl.Transaction startT) { + if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) { + return; + } + if (!mTaskInfo.isVisible()) { + closeMaximizeMenu(); + } else { + mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT); + } + } + + private WindowDecorationViewHolder createViewHolder() { + if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) { + return new AppHandleViewHolder( + mResult.mRootView, + mOnCaptionTouchListener, + mOnCaptionButtonClickListener + ); + } else if (mRelayoutParams.mLayoutResId + == R.layout.desktop_mode_app_header) { + loadAppInfoIfNeeded(); + return new AppHeaderViewHolder( + mResult.mRootView, + mOnCaptionTouchListener, + mOnCaptionButtonClickListener, + mOnCaptionLongClickListener, + mOnCaptionGenericMotionListener, + mAppName, + mAppIconBitmap, + () -> { + if (!isMaximizeMenuActive()) { + createMaximizeMenu(); + } + return Unit.INSTANCE; + }); } - Trace.endSection(); // DesktopModeWindowDecoration#relayout + throw new IllegalArgumentException("Unexpected layout resource id"); } @VisibleForTesting @@ -331,8 +418,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean shouldSetTaskPositionAndCrop) { final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); final boolean isAppHeader = - captionLayoutId == R.layout.desktop_mode_app_controls_window_decor; - final boolean isAppHandle = captionLayoutId == R.layout.desktop_mode_focused_window_decor; + captionLayoutId == R.layout.desktop_mode_app_header; + final boolean isAppHandle = captionLayoutId == R.layout.desktop_mode_app_handle; relayoutParams.reset(); relayoutParams.mRunningTaskInfo = taskInfo; relayoutParams.mLayoutResId = captionLayoutId; @@ -385,7 +472,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // Should match the density of the task. The task may have had its density overridden // to be different that SysUI's. windowDecorConfig.setTo(taskInfo.configuration); - } else if (DesktopModeStatus.isDesktopDensityOverrideSet()) { + } else if (DesktopModeStatus.useDesktopOverrideDensity()) { // The task has had its density overridden, but keep using the system's density to // layout the header. windowDecorConfig.setTo(context.getResources().getConfiguration()); @@ -405,7 +492,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * resource. Otherwise, return ID_NULL and caption width be set to task width. */ private static int getCaptionWidthId(int layoutResId) { - if (layoutResId == R.layout.desktop_mode_focused_window_decor) { + if (layoutResId == R.layout.desktop_mode_app_handle) { return R.dimen.desktop_mode_fullscreen_decor_caption_width; } return Resources.ID_NULL; @@ -515,8 +602,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private void createResizeVeilIfNeeded() { if (mResizeVeil != null) return; loadAppInfoIfNeeded(); - mResizeVeil = new ResizeVeil(mContext, mDisplayController, mResizeVeilBitmap, mTaskInfo, - mTaskSurface, mSurfaceControlTransactionSupplier); + mResizeVeil = new ResizeVeil(mContext, mDisplayController, mResizeVeilBitmap, + mTaskSurface, mSurfaceControlTransactionSupplier, mTaskInfo); } /** @@ -524,7 +611,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ public void showResizeVeil(Rect taskBounds) { createResizeVeilIfNeeded(); - mResizeVeil.showVeil(mTaskSurface, taskBounds); + mResizeVeil.showVeil(mTaskSurface, taskBounds, mTaskInfo); } /** @@ -532,7 +619,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ public void showResizeVeil(SurfaceControl.Transaction tx, Rect taskBounds) { createResizeVeilIfNeeded(); - mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, false /* fadeIn */); + mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, mTaskInfo, false /* fadeIn */); } /** @@ -568,7 +655,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @Override @NonNull Rect calculateValidDragArea() { - final int appTextWidth = ((DesktopModeAppControlsWindowDecorationViewHolder) + final int appTextWidth = ((AppHeaderViewHolder) mWindowDecorViewHolder).getAppNameTextWidth(); final int leftButtonsWidth = loadDimensionPixelSize(mContext.getResources(), R.dimen.desktop_mode_app_details_width_minus_text) + appTextWidth; @@ -650,7 +737,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /** * Create and display handle menu window. */ - void createHandleMenu() { + void createHandleMenu(SplitScreenController splitScreenController) { loadAppInfoIfNeeded(); mHandleMenu = new HandleMenu.Builder(this) .setAppIcon(mAppIconBitmap) @@ -660,6 +747,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .setLayoutId(mRelayoutParams.mLayoutResId) .setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext)) .setCaptionHeight(mResult.mCaptionHeight) + .setDisplayController(mDisplayController) + .setSplitScreenController(splitScreenController) .build(); mWindowDecorViewHolder.onHandleMenuOpened(); mHandleMenu.show(); @@ -747,7 +836,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) { if (isHandleMenuActive() || !(mWindowDecorViewHolder - instanceof DesktopModeFocusedWindowDecorationViewHolder)) { + instanceof AppHandleViewHolder)) { return false; } @@ -815,11 +904,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // We want handle to remain pressed if the pointer moves outside of it during a drag. handle.setPressed((inHandle && action == ACTION_DOWN) || (handle.isPressed() && action != ACTION_UP && action != ACTION_CANCEL)); - if (isHandleMenuActive()) { + if (isHandleMenuActive() && !isHandleMenuAboveStatusBar()) { mHandleMenu.checkMotionEvent(ev); } } + private boolean isHandleMenuAboveStatusBar() { + return Flags.enableAdditionalWindowsAboveStatusBar() && !mTaskInfo.isFreeform(); + } + private boolean pointInView(View v, float x, float y) { return v != null && v.getLeft() <= x && v.getRight() >= x && v.getTop() <= y && v.getBottom() >= y; @@ -831,13 +924,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeHandleMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); + clearCurrentViewHostRunnable(); super.close(); } private static int getDesktopModeWindowDecorLayoutId(@WindowingMode int windowingMode) { return windowingMode == WINDOWING_MODE_FREEFORM - ? R.layout.desktop_mode_app_controls_window_decor - : R.layout.desktop_mode_focused_window_decor; + ? R.layout.desktop_mode_app_header + : R.layout.desktop_mode_app_handle; } private void updatePositionInParent() { @@ -868,6 +962,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return exclusionRegion; } + int getCaptionX() { + return mResult.mCaptionX; + } + @Override int getCaptionHeightId(@WindowingMode int windowingMode) { return getCaptionHeightIdStatic(windowingMode); @@ -889,20 +987,20 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } void setAnimatingTaskResize(boolean animatingTaskResize) { - if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) return; - ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder) + if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) return; + ((AppHeaderViewHolder) mWindowDecorViewHolder) .setAnimatingTaskResize(animatingTaskResize); } /** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */ void onMaximizeWindowHoverExit() { - ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder) + ((AppHeaderViewHolder) mWindowDecorViewHolder) .onMaximizeWindowHoverExit(); } /** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */ void onMaximizeWindowHoverEnter() { - ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder) + ((AppHeaderViewHolder) mWindowDecorViewHolder) .onMaximizeWindowHoverEnter(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java index 82c399ad8152..d48ce536f2b3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java @@ -22,12 +22,18 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED; +import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; import android.util.DisplayMetrics; import android.view.SurfaceControl; +import androidx.annotation.NonNull; + +import com.android.window.flags.Flags; +import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.shared.DesktopModeStatus; /** * Utility class that contains logic common to classes implementing {@link DragPositioningCallback} @@ -35,11 +41,11 @@ import com.android.wm.shell.common.DisplayController; * and applying that change to the task bounds when applicable. */ public class DragPositioningCallbackUtility { - /** * Determine the delta between input's current point and the input start point. - * @param inputX current input x coordinate - * @param inputY current input y coordinate + * + * @param inputX current input x coordinate + * @param inputY current input y coordinate * @param repositionStartPoint initial input coordinate * @return delta between these two points */ @@ -52,13 +58,14 @@ public class DragPositioningCallbackUtility { /** * Based on type of resize and delta provided, calculate the new bounds to display for this * task. - * @param ctrlType type of drag being performed - * @param repositionTaskBounds the bounds the task is being repositioned to + * + * @param ctrlType type of drag being performed + * @param repositionTaskBounds the bounds the task is being repositioned to * @param taskBoundsAtDragStart the bounds of the task on the first drag input event - * @param stableBounds bounds that represent the resize limit of this task - * @param delta difference between start input and current input in x/y coordinates - * @param displayController task's display controller - * @param windowDecoration window decoration of the task being dragged + * @param stableBounds bounds that represent the resize limit of this task + * @param delta difference between start input and current input in x/y + * coordinates + * @param windowDecoration window decoration of the task being dragged * @return whether this method changed repositionTaskBounds */ static boolean changeBounds(int ctrlType, Rect repositionTaskBounds, Rect taskBoundsAtDragStart, @@ -101,13 +108,15 @@ public class DragPositioningCallbackUtility { repositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom) ? candidateBottom : oldBottom; } - // If width or height are negative or less than the minimum width or height, revert the + // If width or height are negative or exceeding the width or height constraints, revert the // respective bounds to use previous bound dimensions. - if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) { + if (isExceedingWidthConstraint(repositionTaskBounds, stableBounds, displayController, + windowDecoration)) { repositionTaskBounds.right = oldRight; repositionTaskBounds.left = oldLeft; } - if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) { + if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController, + windowDecoration)) { repositionTaskBounds.top = oldTop; repositionTaskBounds.bottom = oldBottom; } @@ -142,8 +151,9 @@ public class DragPositioningCallbackUtility { /** * If task bounds are outside of provided drag area, snap the bounds to be just inside the * drag area. + * * @param repositionTaskBounds bounds determined by task positioner - * @param validDragArea the area that task must be positioned inside + * @param validDragArea the area that task must be positioned inside * @return whether bounds were modified */ public static boolean snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) { @@ -168,30 +178,80 @@ public class DragPositioningCallbackUtility { return result; } + private static boolean isExceedingWidthConstraint(@NonNull Rect repositionTaskBounds, + Rect maxResizeBounds, DisplayController displayController, + WindowDecoration windowDecoration) { + // Check if width is less than the minimum width constraint. + if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) { + return true; + } + // Check if width is more than the maximum resize bounds on desktop windowing mode. + return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) + && repositionTaskBounds.width() > maxResizeBounds.width(); + } + + private static boolean isExceedingHeightConstraint(@NonNull Rect repositionTaskBounds, + Rect maxResizeBounds, DisplayController displayController, + WindowDecoration windowDecoration) { + // Check if height is less than the minimum height constraint. + if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) { + return true; + } + // Check if height is more than the maximum resize bounds on desktop windowing mode. + return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) + && repositionTaskBounds.height() > maxResizeBounds.height(); + } + private static float getMinWidth(DisplayController displayController, WindowDecoration windowDecoration) { - return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize(displayController, + return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinWidth(displayController, windowDecoration) : windowDecoration.mTaskInfo.minWidth; } private static float getMinHeight(DisplayController displayController, WindowDecoration windowDecoration) { - return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinSize(displayController, + return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinHeight(displayController, windowDecoration) : windowDecoration.mTaskInfo.minHeight; } + private static float getDefaultMinWidth(DisplayController displayController, + WindowDecoration windowDecoration) { + if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) { + return WindowDecoration.loadDimensionPixelSize( + windowDecoration.mDecorWindowContext.getResources(), + R.dimen.desktop_mode_minimum_window_width); + } + return getDefaultMinSize(displayController, windowDecoration); + } + + private static float getDefaultMinHeight(DisplayController displayController, + WindowDecoration windowDecoration) { + if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) { + return WindowDecoration.loadDimensionPixelSize( + windowDecoration.mDecorWindowContext.getResources(), + R.dimen.desktop_mode_minimum_window_height); + } + return getDefaultMinSize(displayController, windowDecoration); + } + private static float getDefaultMinSize(DisplayController displayController, WindowDecoration windowDecoration) { - float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId) + float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId) .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE; return windowDecoration.mTaskInfo.defaultMinSize * density; } + private static boolean isSizeConstraintForDesktopModeEnabled(Context context) { + return DesktopModeStatus.canEnterDesktopMode(context) + && Flags.enableDesktopWindowingSizeConstraints(); + } + interface DragStartListener { /** * Inform the implementing class that a drag resize has started + * * @param taskId id of this positioner's {@link WindowDecoration} */ void onDragStart(int taskId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index badce6e93d67..d902444d4b15 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -16,7 +16,6 @@ package com.android.wm.shell.windowdecor; -import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; @@ -29,6 +28,8 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEdgeResizePermitted; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEventFromTouchscreen; import android.annotation.NonNull; import android.content.Context; @@ -285,6 +286,9 @@ class DragResizeInputListener implements AutoCloseable { private boolean mShouldHandleEvents; private int mLastCursorType = PointerIcon.TYPE_DEFAULT; private Rect mDragStartTaskBounds; + // The id of the particular pointer in a MotionEvent that we are listening to for drag + // resize events. For example, if multiple fingers are touching the screen, then each one + // has a separate pointer id, but we only accept drag input from one. private int mDragPointerId = -1; private TaskResizeInputEventReceiver(@NonNull Context context, @@ -389,18 +393,20 @@ class DragResizeInputListener implements AutoCloseable { boolean result = false; // Check if this is a touch event vs mouse event. // Touch events are tracked in four corners. Other events are tracked in resize edges. - boolean isTouch = isTouchEvent(e); switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { - mShouldHandleEvents = mDragResizeWindowGeometry.shouldHandleEvent(e, isTouch, + mShouldHandleEvents = mDragResizeWindowGeometry.shouldHandleEvent(e, new Point() /* offset */); if (mShouldHandleEvents) { + // Save the id of the pointer for this drag interaction; we will use the + // same pointer for all subsequent MotionEvents in this interaction. mDragPointerId = e.getPointerId(0); float x = e.getX(0); float y = e.getY(0); float rawX = e.getRawX(0); float rawY = e.getRawY(0); - int ctrlType = mDragResizeWindowGeometry.calculateCtrlType(isTouch, x, y); + final int ctrlType = mDragResizeWindowGeometry.calculateCtrlType( + isEventFromTouchscreen(e), isEdgeResizePermitted(e), x, y); ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: Handling action down, update ctrlType to %d", TAG, ctrlType); mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType, @@ -420,9 +426,16 @@ class DragResizeInputListener implements AutoCloseable { break; } mInputManager.pilferPointers(mInputChannel.getToken()); - int dragPointerIndex = e.findPointerIndex(mDragPointerId); - float rawX = e.getRawX(dragPointerIndex); - float rawY = e.getRawY(dragPointerIndex); + final int dragPointerIndex = e.findPointerIndex(mDragPointerId); + if (dragPointerIndex < 0) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "%s: Handling action move, but ignore event due to invalid " + + "pointer index", + TAG); + break; + } + final float rawX = e.getRawX(dragPointerIndex); + final float rawY = e.getRawY(dragPointerIndex); final Rect taskBounds = mCallback.onDragPositioningMove(rawX, rawY); updateInputSinkRegionForDrag(taskBounds); result = true; @@ -431,7 +444,14 @@ class DragResizeInputListener implements AutoCloseable { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { if (mShouldHandleEvents) { - int dragPointerIndex = e.findPointerIndex(mDragPointerId); + final int dragPointerIndex = e.findPointerIndex(mDragPointerId); + if (dragPointerIndex < 0) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "%s: Handling action %d, but ignore event due to invalid " + + "pointer index", + TAG, e.getActionMasked()); + break; + } final Rect taskBounds = mCallback.onDragPositioningEnd( e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); // If taskBounds has changed, setGeometry will be called and update the @@ -474,8 +494,11 @@ class DragResizeInputListener implements AutoCloseable { private void updateCursorType(int displayId, int deviceId, int pointerId, float x, float y) { + // Since we are handling cursor, we know that this is not a touchscreen event, and + // that edge resizing should always be allowed. @DragPositioningCallback.CtrlType int ctrlType = - mDragResizeWindowGeometry.calculateCtrlType(/* isTouch= */ false, x, y); + mDragResizeWindowGeometry.calculateCtrlType(/* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, x, y); int cursorType = PointerIcon.TYPE_DEFAULT; switch (ctrlType) { @@ -517,9 +540,5 @@ class DragResizeInputListener implements AutoCloseable { private boolean shouldHandleEvent(MotionEvent e, Point offset) { return mDragResizeWindowGeometry.shouldHandleEvent(e, offset); } - - private boolean isTouchEvent(MotionEvent e) { - return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; - } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java index 4f513f0a0fd8..b5d1d4a76342 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java @@ -142,40 +142,42 @@ final class DragResizeWindowGeometry { * Returns if this MotionEvent should be handled, based on its source and position. */ boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { - return shouldHandleEvent(e, isTouchEvent(e), offset); - } - - /** - * Returns if this MotionEvent should be handled, based on its source and position. - */ - boolean shouldHandleEvent(@NonNull MotionEvent e, boolean isTouch, @NonNull Point offset) { final float x = e.getX(0) + offset.x; final float y = e.getY(0) + offset.y; if (enableWindowingEdgeDragResize()) { // First check if touch falls within a corner. // Large corner bounds are used for course input like touch, otherwise fine bounds. - boolean result = isTouch + boolean result = isEventFromTouchscreen(e) ? isInCornerBounds(mLargeTaskCorners, x, y) : isInCornerBounds(mFineTaskCorners, x, y); - // Check if touch falls within the edge resize handle, since edge resizing can apply - // for any input source. - if (!result) { + // Check if touch falls within the edge resize handle. Limit edge resizing to stylus and + // mouse input. + if (!result && isEdgeResizePermitted(e)) { result = isInEdgeResizeBounds(x, y); } return result; } else { // Legacy uses only fine corners for touch, and edges only for non-touch input. - return isTouch + return isEventFromTouchscreen(e) ? isInCornerBounds(mFineTaskCorners, x, y) : isInEdgeResizeBounds(x, y); } } - private boolean isTouchEvent(@NonNull MotionEvent e) { + static boolean isEventFromTouchscreen(@NonNull MotionEvent e) { return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; } + static boolean isEdgeResizePermitted(@NonNull MotionEvent e) { + if (enableWindowingEdgeDragResize()) { + return e.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS + || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; + } else { + return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; + } + } + private boolean isInCornerBounds(TaskCorners corners, float xf, float yf) { return corners.calculateCornersCtrlType(xf, yf) != 0; } @@ -187,24 +189,29 @@ final class DragResizeWindowGeometry { /** * Returns the control type for the drag-resize, based on the touch regions and this * MotionEvent's coordinates. + * @param isTouchscreen Controls the size of the corner resize regions; touchscreen events + * (finger & stylus) are eligible for a larger area than cursor events + * @param isEdgeResizePermitted Indicates if the event is eligible for falling into an edge + * resize region. */ @DragPositioningCallback.CtrlType - int calculateCtrlType(boolean isTouch, float x, float y) { + int calculateCtrlType(boolean isTouchscreen, boolean isEdgeResizePermitted, float x, float y) { if (enableWindowingEdgeDragResize()) { // First check if touch falls within a corner. // Large corner bounds are used for course input like touch, otherwise fine bounds. - int ctrlType = isTouch + int ctrlType = isTouchscreen ? mLargeTaskCorners.calculateCornersCtrlType(x, y) : mFineTaskCorners.calculateCornersCtrlType(x, y); + // Check if touch falls within the edge resize handle, since edge resizing can apply // for any input source. - if (ctrlType == CTRL_TYPE_UNDEFINED) { + if (ctrlType == CTRL_TYPE_UNDEFINED && isEdgeResizePermitted) { ctrlType = calculateEdgeResizeCtrlType(x, y); } return ctrlType; } else { // Legacy uses only fine corners for touch, and edges only for non-touch input. - return isTouch + return isTouchscreen ? mFineTaskCorners.calculateCornersCtrlType(x, y) : calculateEdgeResizeCtrlType(x, y); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java index 65adcee1567c..df0836c1121d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java @@ -23,6 +23,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; @@ -33,7 +36,9 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.Point; import android.graphics.PointF; +import android.graphics.Rect; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; @@ -42,7 +47,15 @@ import android.widget.ImageView; import android.widget.TextView; import android.window.SurfaceSyncGroup; +import androidx.annotation.VisibleForTesting; + +import com.android.window.flags.Flags; import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; /** * Handle menu opened when the appropriate button is clicked on. @@ -56,15 +69,25 @@ class HandleMenu { private static final String TAG = "HandleMenu"; private static final boolean SHOULD_SHOW_MORE_ACTIONS_PILL = false; private final Context mContext; - private final WindowDecoration mParentDecor; - private WindowDecoration.AdditionalWindow mHandleMenuWindow; - private final PointF mHandleMenuPosition = new PointF(); + private final DesktopModeWindowDecoration mParentDecor; + @VisibleForTesting + AdditionalViewContainer mHandleMenuViewContainer; + // Position of the handle menu used for laying out the handle view. + @VisibleForTesting + final PointF mHandleMenuPosition = new PointF(); + // With the introduction of {@link AdditionalSystemViewContainer}, {@link mHandleMenuPosition} + // may be in a different coordinate space than the input coordinates. Therefore, we still care + // about the menu's coordinates relative to the display as a whole, so we need to maintain + // those as well. + final Point mGlobalMenuPosition = new Point(); private final boolean mShouldShowWindowingPill; private final Bitmap mAppIconBitmap; private final CharSequence mAppName; private final View.OnClickListener mOnClickListener; private final View.OnTouchListener mOnTouchListener; private final RunningTaskInfo mTaskInfo; + private final DisplayController mDisplayController; + private final SplitScreenController mSplitScreenController; private final int mLayoutResId; private int mMarginMenuTop; private int mMarginMenuStart; @@ -74,12 +97,16 @@ class HandleMenu { private HandleMenuAnimator mHandleMenuAnimator; - HandleMenu(WindowDecoration parentDecor, int layoutResId, View.OnClickListener onClickListener, - View.OnTouchListener onTouchListener, Bitmap appIcon, CharSequence appName, - boolean shouldShowWindowingPill, int captionHeight) { + HandleMenu(DesktopModeWindowDecoration parentDecor, int layoutResId, + View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, + Bitmap appIcon, CharSequence appName, DisplayController displayController, + SplitScreenController splitScreenController, boolean shouldShowWindowingPill, + int captionHeight) { mParentDecor = parentDecor; mContext = mParentDecor.mDecorWindowContext; mTaskInfo = mParentDecor.mTaskInfo; + mDisplayController = displayController; + mSplitScreenController = splitScreenController; mLayoutResId = layoutResId; mOnClickListener = onClickListener; mOnTouchListener = onTouchListener; @@ -95,20 +122,27 @@ class HandleMenu { final SurfaceSyncGroup ssg = new SurfaceSyncGroup(TAG); SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - createHandleMenuWindow(t, ssg); + createHandleMenuViewContainer(t, ssg); ssg.addTransaction(t); ssg.markSyncReady(); setupHandleMenu(); animateHandleMenu(); } - private void createHandleMenuWindow(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) { + private void createHandleMenuViewContainer(SurfaceControl.Transaction t, + SurfaceSyncGroup ssg) { final int x = (int) mHandleMenuPosition.x; final int y = (int) mHandleMenuPosition.y; - mHandleMenuWindow = mParentDecor.addWindow( - R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu", - t, ssg, x, y, mMenuWidth, mMenuHeight); - final View handleMenuView = mHandleMenuWindow.mWindowViewHost.getView(); + if (!mTaskInfo.isFreeform() && Flags.enableAdditionalWindowsAboveStatusBar()) { + mHandleMenuViewContainer = new AdditionalSystemViewContainer(mContext, + R.layout.desktop_mode_window_decor_handle_menu, mTaskInfo.taskId, + x, y, mMenuWidth, mMenuHeight); + } else { + mHandleMenuViewContainer = mParentDecor.addWindow( + R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu", + t, ssg, x, y, mMenuWidth, mMenuHeight); + } + final View handleMenuView = mHandleMenuViewContainer.getView(); mHandleMenuAnimator = new HandleMenuAnimator(handleMenuView, mMenuWidth, mCaptionHeight); } @@ -129,7 +163,7 @@ class HandleMenu { * pill. */ private void setupHandleMenu() { - final View handleMenu = mHandleMenuWindow.mWindowViewHost.getView(); + final View handleMenu = mHandleMenuViewContainer.getView(); handleMenu.setOnTouchListener(mOnTouchListener); setupAppInfoPill(handleMenu); if (mShouldShowWindowingPill) { @@ -147,6 +181,7 @@ class HandleMenu { final ImageView appIcon = handleMenu.findViewById(R.id.application_icon); final TextView appName = handleMenu.findViewById(R.id.application_name); collapseBtn.setOnClickListener(mOnClickListener); + collapseBtn.setTaskInfo(mTaskInfo); appIcon.setImageBitmap(mAppIconBitmap); appName.setText(mAppName); } @@ -215,33 +250,69 @@ class HandleMenu { * Updates handle menu's position variables to reflect its next position. */ private void updateHandleMenuPillPositions() { - final int menuX, menuY; - final int captionWidth = mTaskInfo.getConfiguration() - .windowConfiguration.getBounds().width(); - if (mLayoutResId - == R.layout.desktop_mode_app_controls_window_decor) { - // Align the handle menu to the left of the caption. + int menuX; + final int menuY; + final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds(); + updateGlobalMenuPosition(taskBounds); + if (mLayoutResId == R.layout.desktop_mode_app_header) { + // Align the handle menu to the left side of the caption. menuX = mMarginMenuStart; menuY = mMarginMenuTop; } else { - // Position the handle menu at the center of the caption. - menuX = (captionWidth / 2) - (mMenuWidth / 2); - menuY = mMarginMenuStart; + if (Flags.enableAdditionalWindowsAboveStatusBar()) { + // In a focused decor, we use global coordinates for handle menu. Therefore we + // need to account for other factors like split stage and menu/handle width to + // center the menu. + final DisplayLayout layout = mDisplayController + .getDisplayLayout(mTaskInfo.displayId); + menuX = mGlobalMenuPosition.x + ((mMenuWidth - layout.width()) / 2); + menuY = mGlobalMenuPosition.y + ((mMenuHeight - layout.height()) / 2); + } else { + menuX = (taskBounds.width() / 2) - (mMenuWidth / 2); + menuY = mMarginMenuTop; + } } - // Handle Menu position setup. mHandleMenuPosition.set(menuX, menuY); + } + private void updateGlobalMenuPosition(Rect taskBounds) { + if (mTaskInfo.isFreeform()) { + mGlobalMenuPosition.set(taskBounds.left + mMarginMenuStart, + taskBounds.top + mMarginMenuTop); + } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + mGlobalMenuPosition.set( + (taskBounds.width() / 2) - (mMenuWidth / 2) + mMarginMenuStart, + mMarginMenuTop + ); + } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { + final int splitPosition = mSplitScreenController.getSplitPosition(mTaskInfo.taskId); + final Rect leftOrTopStageBounds = new Rect(); + final Rect rightOrBottomStageBounds = new Rect(); + mSplitScreenController.getStageBounds(leftOrTopStageBounds, + rightOrBottomStageBounds); + // TODO(b/343561161): This needs to be calculated differently if the task is in + // top/bottom split. + if (splitPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) { + mGlobalMenuPosition.set(leftOrTopStageBounds.width() + + (rightOrBottomStageBounds.width() / 2) + - (mMenuWidth / 2) + mMarginMenuStart, + mMarginMenuTop); + } else if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { + mGlobalMenuPosition.set((leftOrTopStageBounds.width() / 2) + - (mMenuWidth / 2) + mMarginMenuStart, + mMarginMenuTop); + } + } } /** * Update pill layout, in case task changes have caused positioning to change. */ void relayout(SurfaceControl.Transaction t) { - if (mHandleMenuWindow != null) { + if (mHandleMenuViewContainer != null) { updateHandleMenuPillPositions(); - t.setPosition(mHandleMenuWindow.mWindowSurface, - mHandleMenuPosition.x, mHandleMenuPosition.y); + mHandleMenuViewContainer.setPosition(t, mHandleMenuPosition.x, mHandleMenuPosition.y); } } @@ -253,7 +324,9 @@ class HandleMenu { * @param ev the MotionEvent to compare against. */ void checkMotionEvent(MotionEvent ev) { - final View handleMenu = mHandleMenuWindow.mWindowViewHost.getView(); + // If the menu view is above status bar, we can let the views handle input directly. + if (isViewAboveStatusBar()) return; + final View handleMenu = mHandleMenuViewContainer.getView(); final HandleMenuImageButton collapse = handleMenu.findViewById(R.id.collapse_menu_button); final PointF inputPoint = translateInputToLocalSpace(ev); final boolean inputInCollapseButton = pointInView(collapse, inputPoint.x, inputPoint.y); @@ -265,6 +338,11 @@ class HandleMenu { } } + private boolean isViewAboveStatusBar() { + return Flags.enableAdditionalWindowsAboveStatusBar() + && !mTaskInfo.isFreeform(); + } + // Translate the input point from display coordinates to the same space as the handle menu. private PointF translateInputToLocalSpace(MotionEvent ev) { return new PointF(ev.getX() - mHandleMenuPosition.x, @@ -280,10 +358,33 @@ class HandleMenu { */ boolean isValidMenuInput(PointF inputPoint) { if (!viewsLaidOut()) return true; - return pointInView( - mHandleMenuWindow.mWindowViewHost.getView(), - inputPoint.x - mHandleMenuPosition.x, - inputPoint.y - mHandleMenuPosition.y); + if (!isViewAboveStatusBar()) { + return pointInView( + mHandleMenuViewContainer.getView(), + inputPoint.x - mHandleMenuPosition.x, + inputPoint.y - mHandleMenuPosition.y); + } else { + // Handle menu exists in a different coordinate space when added to WindowManager. + // Therefore we must compare the provided input coordinates to global menu coordinates. + // This includes factoring for split stage as input coordinates are relative to split + // stage position, not relative to the display as a whole. + PointF inputRelativeToMenu = new PointF( + inputPoint.x - mGlobalMenuPosition.x, + inputPoint.y - mGlobalMenuPosition.y + ); + if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId) + == SPLIT_POSITION_BOTTOM_OR_RIGHT) { + // TODO(b/343561161): This also needs to be calculated differently if + // the task is in top/bottom split. + Rect leftStageBounds = new Rect(); + mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); + inputRelativeToMenu.x += leftStageBounds.width(); + } + return pointInView( + mHandleMenuViewContainer.getView(), + inputRelativeToMenu.x, + inputRelativeToMenu.y); + } } private boolean pointInView(View v, float x, float y) { @@ -295,7 +396,7 @@ class HandleMenu { * Check if the views for handle menu can be seen. */ private boolean viewsLaidOut() { - return mHandleMenuWindow.mWindowViewHost.getView().isLaidOut(); + return mHandleMenuViewContainer.getView().isLaidOut(); } private void loadHandleMenuDimensions() { @@ -334,8 +435,8 @@ class HandleMenu { void close() { final Runnable after = () -> { - mHandleMenuWindow.releaseView(); - mHandleMenuWindow = null; + mHandleMenuViewContainer.releaseView(); + mHandleMenuViewContainer = null; }; if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { @@ -346,7 +447,7 @@ class HandleMenu { } static final class Builder { - private final WindowDecoration mParent; + private final DesktopModeWindowDecoration mParent; private CharSequence mName; private Bitmap mAppIcon; private View.OnClickListener mOnClickListener; @@ -354,9 +455,10 @@ class HandleMenu { private int mLayoutId; private boolean mShowWindowingPill; private int mCaptionHeight; + private DisplayController mDisplayController; + private SplitScreenController mSplitScreenController; - - Builder(@NonNull WindowDecoration parent) { + Builder(@NonNull DesktopModeWindowDecoration parent) { mParent = parent; } @@ -395,9 +497,20 @@ class HandleMenu { return this; } + Builder setDisplayController(DisplayController displayController) { + mDisplayController = displayController; + return this; + } + + Builder setSplitScreenController(SplitScreenController splitScreenController) { + mSplitScreenController = splitScreenController; + return this; + } + HandleMenu build() { - return new HandleMenu(mParent, mLayoutId, mOnClickListener, mOnTouchListener, - mAppIcon, mName, mShowWindowingPill, mCaptionHeight); + return new HandleMenu(mParent, mLayoutId, mOnClickListener, + mOnTouchListener, mAppIcon, mName, mDisplayController, mSplitScreenController, + mShowWindowingPill, mCaptionHeight); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt index 7898567b70e9..18757ef6ff40 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt @@ -15,6 +15,10 @@ */ package com.android.wm.shell.windowdecor +import android.app.ActivityManager.RunningTaskInfo +import com.android.window.flags.Flags +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer + import android.content.Context import android.util.AttributeSet import android.view.MotionEvent @@ -25,10 +29,20 @@ import android.widget.ImageButton * This is due to the hover events being handled by [DesktopModeWindowDecorViewModel] * in order to take the status bar layer into account. Handling it in both classes results in a * flicker when the hover moves from outside to inside status bar layer. + * TODO(b/342229481): Remove this and all uses of it once [AdditionalSystemViewContainer] is no longer + * guarded by a flag. */ -class HandleMenuImageButton(context: Context?, attrs: AttributeSet?) : - ImageButton(context, attrs) { +class HandleMenuImageButton( + context: Context?, + attrs: AttributeSet? +) : ImageButton(context, attrs) { + lateinit var taskInfo: RunningTaskInfo + override fun onHoverEvent(motionEvent: MotionEvent): Boolean { - return false + if (Flags.enableAdditionalWindowsAboveStatusBar() || taskInfo.isFreeform) { + return super.onHoverEvent(motionEvent) + } else { + return false + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 22f0adc42f5d..0470367015ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -19,17 +19,28 @@ package com.android.wm.shell.windowdecor import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.animation.ValueAnimator +import android.annotation.ColorInt import android.annotation.IdRes import android.app.ActivityManager.RunningTaskInfo import android.content.Context +import android.content.res.ColorStateList import android.content.res.Resources +import android.graphics.Paint import android.graphics.PixelFormat import android.graphics.PointF +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.StateListDrawable +import android.graphics.drawable.shapes.RoundRectShape +import android.util.StateSet import android.view.LayoutInflater import android.view.MotionEvent import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.SurfaceControlViewHost +import android.view.View import android.view.View.OnClickListener import android.view.View.OnGenericMotionListener import android.view.View.OnTouchListener @@ -39,18 +50,21 @@ import android.view.View.TRANSLATION_Z import android.view.WindowManager import android.view.WindowlessWindowManager import android.widget.Button -import android.widget.FrameLayout -import android.widget.LinearLayout import android.widget.TextView import android.window.TaskConstants -import androidx.core.content.withStyledAttributes -import com.android.internal.R.attr.colorAccentPrimary +import androidx.compose.material3.ColorScheme +import androidx.compose.ui.graphics.toArgb +import androidx.core.animation.addListener import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue -import com.android.wm.shell.windowdecor.WindowDecoration.AdditionalWindow +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.OPACITY_12 +import com.android.wm.shell.windowdecor.common.OPACITY_40 +import com.android.wm.shell.windowdecor.common.withAlpha import java.util.function.Supplier @@ -70,10 +84,10 @@ class MaximizeMenu( private val menuPosition: PointF, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } ) { - private var maximizeMenu: AdditionalWindow? = null + private var maximizeMenu: AdditionalViewHostViewContainer? = null + private var maximizeMenuView: MaximizeMenuView? = null private lateinit var viewHost: SurfaceControlViewHost private lateinit var leash: SurfaceControl - private val openMenuAnimatorSet = AnimatorSet() private val cornerRadius = loadDimensionPixelSize( R.dimen.desktop_mode_maximize_menu_corner_radius ).toFloat() @@ -81,12 +95,6 @@ class MaximizeMenu( private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height) private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding) - private lateinit var snapRightButton: Button - private lateinit var snapLeftButton: Button - private lateinit var maximizeButton: Button - private lateinit var maximizeButtonLayout: FrameLayout - private lateinit var snapButtonsLayout: LinearLayout - /** Position the menu relative to the caption's position. */ fun positionMenu(position: PointF, t: Transaction) { menuPosition.set(position) @@ -97,24 +105,20 @@ class MaximizeMenu( fun show() { if (maximizeMenu != null) return createMaximizeMenu() - setupMaximizeMenu() - animateOpenMenu() + maximizeMenuView?.animateOpenMenu() } /** Closes the maximize window and releases its view. */ fun close() { - openMenuAnimatorSet.cancel() + maximizeMenuView?.cancelAnimation() maximizeMenu?.releaseView() maximizeMenu = null + maximizeMenuView = null } /** Create a maximize menu that is attached to the display area. */ private fun createMaximizeMenu() { val t = transactionSupplier.get() - val v = LayoutInflater.from(decorWindowContext).inflate( - R.layout.desktop_mode_window_decor_maximize_menu, - null // Root - ) val builder = SurfaceControl.Builder() rootTdaOrganizer.attachToDisplayArea(taskInfo.displayId, builder) leash = builder @@ -138,14 +142,25 @@ class MaximizeMenu( viewHost = SurfaceControlViewHost(decorWindowContext, displayController.getDisplay(taskInfo.displayId), windowManager, "MaximizeMenu") - viewHost.setView(v, lp) + maximizeMenuView = MaximizeMenuView( + context = decorWindowContext, + menuHeight = menuHeight, + menuPadding = menuPadding, + onClickListener = onClickListener, + onTouchListener = onTouchListener, + onGenericMotionListener = onGenericMotionListener, + ).also { menuView -> + menuView.bind(taskInfo) + viewHost.setView(menuView.rootView, lp) + } // Bring menu to front when open t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU) .setPosition(leash, menuPosition.x, menuPosition.y) .setCornerRadius(leash, cornerRadius) .show(leash) - maximizeMenu = AdditionalWindow(leash, viewHost, transactionSupplier) + maximizeMenu = + AdditionalViewHostViewContainer(leash, viewHost, transactionSupplier) syncQueue.runInSync { transaction -> transaction.merge(t) @@ -153,77 +168,6 @@ class MaximizeMenu( } } - private fun animateOpenMenu() { - val viewHost = maximizeMenu?.mWindowViewHost - val maximizeMenuView = viewHost?.view ?: return - val maximizeWindowText = maximizeMenuView.requireViewById<TextView>( - R.id.maximize_menu_maximize_window_text) - val snapWindowText = maximizeMenuView.requireViewById<TextView>( - R.id.maximize_menu_snap_window_text) - - openMenuAnimatorSet.playTogether( - ObjectAnimator.ofFloat(maximizeMenuView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f) - .apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS - interpolator = EMPHASIZED_DECELERATE - }, - ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f) - .apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS - interpolator = EMPHASIZED_DECELERATE - addUpdateListener { - // Animate padding so that controls stay pinned to the bottom of - // the menu. - val value = animatedValue as Float - val topPadding = menuPadding - - ((1 - value) * menuHeight).toInt() - maximizeMenuView.setPadding(menuPadding, topPadding, - menuPadding, menuPadding) - } - }, - ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS - interpolator = EMPHASIZED_DECELERATE - addUpdateListener { - // Scale up the children of the maximize menu so that the menu - // scale is cancelled out and only the background is scaled. - val value = animatedValue as Float - maximizeButtonLayout.scaleY = value - snapButtonsLayout.scaleY = value - maximizeWindowText.scaleY = value - snapWindowText.scaleY = value - } - }, - ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Y, - (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS - interpolator = EMPHASIZED_DECELERATE - }, - ObjectAnimator.ofInt(maximizeMenuView.background, "alpha", - MAX_DRAWABLE_ALPHA_VALUE).apply { - duration = ALPHA_ANIMATION_DURATION_MS - }, - ValueAnimator.ofFloat(0f, 1f) - .apply { - duration = ALPHA_ANIMATION_DURATION_MS - startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS - addUpdateListener { - val value = animatedValue as Float - maximizeButtonLayout.alpha = value - snapButtonsLayout.alpha = value - maximizeWindowText.alpha = value - snapWindowText.alpha = value - } - }, - ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Z, MENU_Z_TRANSLATION) - .apply { - duration = ELEVATION_ANIMATION_DURATION_MS - startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS - } - ) - openMenuAnimatorSet.start() - } - private fun loadDimensionPixelSize(resourceId: Int): Int { return if (resourceId == Resources.ID_NULL) { 0 @@ -232,31 +176,6 @@ class MaximizeMenu( } } - private fun setupMaximizeMenu() { - val maximizeMenuView = maximizeMenu?.mWindowViewHost?.view ?: return - - maximizeMenuView.setOnGenericMotionListener(onGenericMotionListener) - maximizeMenuView.setOnTouchListener(onTouchListener) - - maximizeButtonLayout = maximizeMenuView.requireViewById( - R.id.maximize_menu_maximize_button_layout) - - maximizeButton = maximizeMenuView.requireViewById(R.id.maximize_menu_maximize_button) - maximizeButton.setOnClickListener(onClickListener) - maximizeButton.setOnGenericMotionListener(onGenericMotionListener) - - snapRightButton = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_right_button) - snapRightButton.setOnClickListener(onClickListener) - snapRightButton.setOnGenericMotionListener(onGenericMotionListener) - - snapLeftButton = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_left_button) - snapLeftButton.setOnClickListener(onClickListener) - snapLeftButton.setOnGenericMotionListener(onGenericMotionListener) - - snapButtonsLayout = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_menu_layout) - snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener) - } - /** * A valid menu input is one of the following: * An input that happens in the menu views. @@ -275,68 +194,438 @@ class MaximizeMenu( * Check if the views for maximize menu can be seen. */ private fun viewsLaidOut(): Boolean { - return maximizeMenu?.mWindowViewHost?.view?.isLaidOut ?: false + return maximizeMenu?.view?.isLaidOut ?: false } + /** + * Called when a [MotionEvent.ACTION_HOVER_ENTER] is triggered on any of the menu's views. + * + * TODO(b/346440693): this is only needed for the left/right snap options that don't support + * selector states to manage its hover state. Look into whether that can be added to avoid + * manually tracking hover enter/exit motion events. Also because those button colors/states + * aren't updating correctly for pressed, focused and selected states. + * See also [onMaximizeMenuHoverMove] and [onMaximizeMenuHoverExit]. + */ fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) { setSnapButtonsColorOnHover(viewId, ev) } + /** Called when a [MotionEvent.ACTION_HOVER_MOVE] is triggered on any of the menu's views. */ fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) { setSnapButtonsColorOnHover(viewId, ev) } + /** Called when a [MotionEvent.ACTION_HOVER_EXIT] is triggered on any of the menu's views. */ fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) { - val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapButtonsLayout.width && - ev.y >= 0 && ev.y <= snapButtonsLayout.height - val colorList = decorWindowContext.getColorStateList( - R.color.desktop_mode_maximize_menu_button_color_selector) - - if (id == R.id.maximize_menu_maximize_button) { - maximizeButton.background?.setTintList(colorList) - maximizeButtonLayout.setBackgroundResource( - R.drawable.desktop_mode_maximize_menu_layout_background) - } else if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) { + val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return + val snapOptionsHeight = maximizeMenuView?.snapOptionsHeight ?: return + val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapOptionsWidth && + ev.y >= 0 && ev.y <= snapOptionsHeight + + if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) { // After exiting the snap menu layout area, checks to see that user is not still // hovering within the snap menu layout bounds which would indicate that the user is // hovering over a snap button within the snap menu layout rather than having exited. - snapLeftButton.background?.setTintList(colorList) - snapLeftButton.background?.alpha = 255 - snapRightButton.background?.setTintList(colorList) - snapRightButton.background?.alpha = 255 - snapButtonsLayout.setBackgroundResource( - R.drawable.desktop_mode_maximize_menu_layout_background) + maximizeMenuView?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.NONE) } } private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) { - decorWindowContext.withStyledAttributes(null, intArrayOf(colorAccentPrimary), 0, 0) { - val materialColor = getColor(0, 0) - val snapMenuCenter = snapButtonsLayout.width / 2 - if (viewId == R.id.maximize_menu_maximize_button) { - // Highlight snap maximize window button - maximizeButton.background?.setTint(materialColor) - maximizeButtonLayout.setBackgroundResource( - R.drawable.desktop_mode_maximize_menu_layout_background_on_hover) - } else if (viewId == R.id.maximize_menu_snap_left_button || - (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter)) { - // Highlight snap left button - snapRightButton.background?.setTint(materialColor) - snapLeftButton.background?.setTint(materialColor) - snapButtonsLayout.setBackgroundResource( - R.drawable.desktop_mode_maximize_menu_layout_background_on_hover) - snapRightButton.background?.alpha = 102 - snapLeftButton.background?.alpha = 255 - } else if (viewId == R.id.maximize_menu_snap_right_button || - (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter)) { - // Highlight snap right button - snapRightButton.background?.setTint(materialColor) - snapLeftButton.background?.setTint(materialColor) - snapButtonsLayout.setBackgroundResource( - R.drawable.desktop_mode_maximize_menu_layout_background_on_hover) - snapRightButton.background?.alpha = 255 - snapLeftButton.background?.alpha = 102 + val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return + val snapMenuCenter = snapOptionsWidth / 2 + when { + viewId == R.id.maximize_menu_snap_left_button || + (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter) -> { + maximizeMenuView + ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.LEFT) } + viewId == R.id.maximize_menu_snap_right_button || + (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter) -> { + maximizeMenuView + ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.RIGHT) + } + } + } + + /** + * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for + * resizing a Task. + */ + class MaximizeMenuView( + context: Context, + private val menuHeight: Int, + private val menuPadding: Int, + onClickListener: OnClickListener, + onTouchListener: OnTouchListener, + onGenericMotionListener: OnGenericMotionListener, + ) { + val rootView: View = LayoutInflater.from(context) + .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) + private val maximizeText = + requireViewById(R.id.maximize_menu_maximize_window_text) as TextView + private val maximizeButton = + requireViewById(R.id.maximize_menu_maximize_button) as Button + private val snapWindowText = + requireViewById(R.id.maximize_menu_snap_window_text) as TextView + private val snapRightButton = + requireViewById(R.id.maximize_menu_snap_right_button) as Button + private val snapLeftButton = + requireViewById(R.id.maximize_menu_snap_left_button) as Button + private val snapButtonsLayout = + requireViewById(R.id.maximize_menu_snap_menu_layout) + + private val decorThemeUtil = DecorThemeUtil(context) + + private val outlineRadius = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_radius) + private val outlineStroke = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_stroke) + private val fillPadding = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_padding) + private val fillRadius = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius) + + private val openMenuAnimatorSet = AnimatorSet() + private lateinit var taskInfo: RunningTaskInfo + private lateinit var style: MenuStyle + + /** The width of the snap menu option view, including both left and right snaps. */ + val snapOptionsWidth: Int + get() = snapButtonsLayout.width + /** The height of the snap menu option view, including both left and right snaps .*/ + val snapOptionsHeight: Int + get() = snapButtonsLayout.height + + init { + // TODO(b/346441962): encapsulate menu hover enter/exit logic inside this class and + // expose only what is actually relevant to outside classes so that specific checks + // against resource IDs aren't needed outside this class. + rootView.setOnGenericMotionListener(onGenericMotionListener) + rootView.setOnTouchListener(onTouchListener) + maximizeButton.setOnClickListener(onClickListener) + maximizeButton.setOnGenericMotionListener(onGenericMotionListener) + snapRightButton.setOnClickListener(onClickListener) + snapRightButton.setOnGenericMotionListener(onGenericMotionListener) + snapLeftButton.setOnClickListener(onClickListener) + snapLeftButton.setOnGenericMotionListener(onGenericMotionListener) + snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener) + + // To prevent aliasing. + maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + + /** Bind the menu views to the new [RunningTaskInfo] data. */ + fun bind(taskInfo: RunningTaskInfo) { + this.taskInfo = taskInfo + this.style = calculateMenuStyle(taskInfo) + + rootView.background.setTint(style.backgroundColor) + + // Maximize option. + maximizeButton.background = style.maximizeOption.drawable + maximizeText.setTextColor(style.textColor) + + // Snap options. + snapWindowText.setTextColor(style.textColor) + updateSplitSnapSelection(SnapToHalfSelection.NONE) + } + + /** Animate the opening of the menu */ + fun animateOpenMenu() { + maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + openMenuAnimatorSet.playTogether( + ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f) + .apply { + duration = MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = EMPHASIZED_DECELERATE + }, + ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f) + .apply { + duration = MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = EMPHASIZED_DECELERATE + addUpdateListener { + // Animate padding so that controls stay pinned to the bottom of + // the menu. + val value = animatedValue as Float + val topPadding = menuPadding - + ((1 - value) * menuHeight).toInt() + rootView.setPadding(menuPadding, topPadding, + menuPadding, menuPadding) + } + }, + ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply { + duration = MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = EMPHASIZED_DECELERATE + addUpdateListener { + // Scale up the children of the maximize menu so that the menu + // scale is cancelled out and only the background is scaled. + val value = animatedValue as Float + maximizeButton.scaleY = value + snapButtonsLayout.scaleY = value + maximizeText.scaleY = value + snapWindowText.scaleY = value + } + }, + ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, + (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply { + duration = MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = EMPHASIZED_DECELERATE + }, + ObjectAnimator.ofInt(rootView.background, "alpha", + MAX_DRAWABLE_ALPHA_VALUE).apply { + duration = ALPHA_ANIMATION_DURATION_MS + }, + ValueAnimator.ofFloat(0f, 1f) + .apply { + duration = ALPHA_ANIMATION_DURATION_MS + startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS + addUpdateListener { + val value = animatedValue as Float + maximizeButton.alpha = value + snapButtonsLayout.alpha = value + maximizeText.alpha = value + snapWindowText.alpha = value + } + }, + ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION) + .apply { + duration = ELEVATION_ANIMATION_DURATION_MS + startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS + } + ) + openMenuAnimatorSet.addListener( + onEnd = { + maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + ) + openMenuAnimatorSet.start() + } + + /** Cancel the open menu animation. */ + fun cancelAnimation() { + openMenuAnimatorSet.cancel() + } + + /** Update the view state to a new snap to half selection. */ + fun updateSplitSnapSelection(selection: SnapToHalfSelection) { + when (selection) { + SnapToHalfSelection.NONE -> deactivateSnapOptions() + SnapToHalfSelection.LEFT -> activateSnapOption(activateLeft = true) + SnapToHalfSelection.RIGHT -> activateSnapOption(activateLeft = false) + } + } + + private fun calculateMenuStyle(taskInfo: RunningTaskInfo): MenuStyle { + val colorScheme = decorThemeUtil.getColorScheme(taskInfo) + val menuBackgroundColor = colorScheme.surfaceContainerLow.toArgb() + return MenuStyle( + backgroundColor = menuBackgroundColor, + textColor = colorScheme.onSurface.toArgb(), + maximizeOption = MenuStyle.MaximizeOption( + drawable = createMaximizeDrawable(menuBackgroundColor, colorScheme) + ), + snapOptions = MenuStyle.SnapOptions( + inactiveSnapSideColor = colorScheme.outlineVariant.toArgb(), + semiActiveSnapSideColor = colorScheme.primary.toArgb().withAlpha(OPACITY_40), + activeSnapSideColor = colorScheme.primary.toArgb(), + inactiveStrokeColor = colorScheme.outlineVariant.toArgb(), + activeStrokeColor = colorScheme.primary.toArgb(), + inactiveBackgroundColor = menuBackgroundColor, + activeBackgroundColor = colorScheme.primary.toArgb().withAlpha(OPACITY_12) + ), + ) + } + + private fun deactivateSnapOptions() { + // TODO(b/346440693): the background/colorStateList set on these buttons is overridden + // to a static resource & color on manually tracked hover events, which defeats the + // point of state lists and selector states. Look into whether changing that is + // possible, similar to the maximize option. Also to include support for the + // semi-active state (when the "other" snap option is selected). + val snapSideColorList = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_focused), + intArrayOf(android.R.attr.state_selected), + intArrayOf(), + ), + intArrayOf( + style.snapOptions.activeSnapSideColor, + style.snapOptions.activeSnapSideColor, + style.snapOptions.activeSnapSideColor, + style.snapOptions.inactiveSnapSideColor + ) + ) + snapLeftButton.background?.setTintList(snapSideColorList) + snapRightButton.background?.setTintList(snapSideColorList) + with (snapButtonsLayout) { + setBackgroundResource(R.drawable.desktop_mode_maximize_menu_layout_background) + (background as GradientDrawable).apply { + setColor(style.snapOptions.inactiveBackgroundColor) + setStroke(outlineStroke, style.snapOptions.inactiveStrokeColor) + } + } + } + + private fun activateSnapOption(activateLeft: Boolean) { + // Regardless of which side is active, the background of the snap options layout (that + // includes both sides) is considered "active". + with (snapButtonsLayout) { + setBackgroundResource( + R.drawable.desktop_mode_maximize_menu_layout_background_on_hover) + (background as GradientDrawable).apply { + setColor(style.snapOptions.activeBackgroundColor) + setStroke(outlineStroke, style.snapOptions.activeStrokeColor) + } + } + if (activateLeft) { + // Highlight snap left button, partially highlight the other side. + snapLeftButton.background.setTint(style.snapOptions.activeSnapSideColor) + snapRightButton.background.setTint(style.snapOptions.semiActiveSnapSideColor) + } else { + // Highlight snap right button, partially highlight the other side. + snapRightButton.background.setTint(style.snapOptions.activeSnapSideColor) + snapLeftButton.background.setTint(style.snapOptions.semiActiveSnapSideColor) + } + } + + private fun createMaximizeDrawable( + @ColorInt menuBackgroundColor: Int, + colorScheme: ColorScheme + ): StateListDrawable { + val activeStrokeAndFill = colorScheme.primary.toArgb() + val activeBackground = colorScheme.primary.toArgb().withAlpha(OPACITY_12) + val activeDrawable = createMaximizeButtonDrawable( + strokeAndFillColor = activeStrokeAndFill, + backgroundColor = activeBackground, + // Add a mask with the menu background's color because the active background color is + // semi transparent, otherwise the transparency will reveal the stroke/fill color + // behind it. + backgroundMask = menuBackgroundColor + ) + return StateListDrawable().apply { + addState(intArrayOf(android.R.attr.state_pressed), activeDrawable) + addState(intArrayOf(android.R.attr.state_focused), activeDrawable) + addState(intArrayOf(android.R.attr.state_selected), activeDrawable) + addState(intArrayOf(android.R.attr.state_hovered), activeDrawable) + // Inactive drawable. + addState( + StateSet.WILD_CARD, + createMaximizeButtonDrawable( + strokeAndFillColor = colorScheme.outlineVariant.toArgb(), + backgroundColor = colorScheme.surfaceContainerLow.toArgb(), + backgroundMask = null // not needed because the bg color is fully opaque + ) + ) + } + } + + private fun createMaximizeButtonDrawable( + @ColorInt strokeAndFillColor: Int, + @ColorInt backgroundColor: Int, + @ColorInt backgroundMask: Int? + ): LayerDrawable { + val layers = mutableListOf<Drawable>() + // First (bottom) layer, effectively the button's border ring once its inner shape is + // covered by the next layers. + layers.add(ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { outlineRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = strokeAndFillColor + paint.style = Paint.Style.FILL + }) + // Second layer, a mask for the next (background) layer if needed because of + // transparency. + backgroundMask?.let { color -> + layers.add( + ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { outlineRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = color + paint.style = Paint.Style.FILL + } + ) + } + // Third layer, the "background" padding between the border and the fill. + layers.add(ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { outlineRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = backgroundColor + paint.style = Paint.Style.FILL + }) + // Final layer, the inner most rounded-rect "fill". + layers.add(ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { fillRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = strokeAndFillColor + paint.style = Paint.Style.FILL + }) + return LayerDrawable(layers.toTypedArray()).apply { + when (numberOfLayers) { + 3 -> { + setLayerInset(1, outlineStroke) + setLayerInset(2, fillPadding) + } + 4 -> { + setLayerInset(intArrayOf(1, 2), outlineStroke) + setLayerInset(3, fillPadding) + } + else -> error("Unexpected number of layers: $numberOfLayers") + } + } + } + + private fun LayerDrawable.setLayerInset(index: IntArray, inset: Int) { + for (i in index) { + setLayerInset(i, inset, inset, inset, inset) + } + } + + private fun LayerDrawable.setLayerInset(index: Int, inset: Int) { + setLayerInset(index, inset, inset, inset, inset) + } + + private fun requireViewById(id: Int) = rootView.requireViewById<View>(id) + + /** The style to apply to the menu. */ + data class MenuStyle( + @ColorInt val backgroundColor: Int, + @ColorInt val textColor: Int, + val maximizeOption: MaximizeOption, + val snapOptions: SnapOptions, + ) { + data class MaximizeOption( + val drawable: StateListDrawable, + ) + data class SnapOptions( + @ColorInt val inactiveSnapSideColor: Int, + @ColorInt val semiActiveSnapSideColor: Int, + @ColorInt val activeSnapSideColor: Int, + @ColorInt val inactiveStrokeColor: Int, + @ColorInt val activeStrokeColor: Int, + @ColorInt val inactiveBackgroundColor: Int, + @ColorInt val activeBackgroundColor: Int, + ) + } + + /** The possible selection states of the half-snap menu option. */ + enum class SnapToHalfSelection { + NONE, LEFT, RIGHT } } @@ -352,7 +641,6 @@ class MaximizeMenu( fun isMaximizeMenuView(@IdRes viewId: Int): Boolean { return viewId == R.id.maximize_menu || viewId == R.id.maximize_menu_maximize_button || - viewId == R.id.maximize_menu_maximize_button_layout || viewId == R.id.maximize_menu_snap_left_button || viewId == R.id.maximize_menu_snap_right_button || viewId == R.id.maximize_menu_snap_menu_layout || diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt index 4f2d945e49f9..cd2dac806a7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt @@ -20,7 +20,6 @@ import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.app.ActivityManager.RunningTaskInfo import android.content.Context -import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color import android.graphics.PixelFormat @@ -36,10 +35,15 @@ import android.view.WindowManager import android.view.WindowlessWindowManager import android.widget.ImageView import android.window.TaskConstants +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.ui.graphics.toArgb import com.android.wm.shell.R import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.Theme import java.util.function.Supplier /** @@ -49,14 +53,18 @@ class ResizeVeil @JvmOverloads constructor( private val context: Context, private val displayController: DisplayController, private val appIcon: Bitmap, - private val taskInfo: RunningTaskInfo, private var parentSurface: SurfaceControl, private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>, private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory = object : SurfaceControlBuilderFactory {}, private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = - object : SurfaceControlViewHostFactory {} + object : SurfaceControlViewHostFactory {}, + taskInfo: RunningTaskInfo, ) { + private val decorThemeUtil = DecorThemeUtil(context) + private val lightColors = dynamicLightColorScheme(context) + private val darkColors = dynamicDarkColorScheme(context) + private val surfaceSession = SurfaceSession() private lateinit var iconView: ImageView private var iconSize = 0 @@ -86,21 +94,10 @@ class ResizeVeil @JvmOverloads constructor( return } displayController.removeDisplayWindowListener(this) - setupResizeVeil() + setupResizeVeil(taskInfo) } } - private val backgroundColorId: Int - get() { - val configuration = context.resources.configuration - return if (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - == Configuration.UI_MODE_NIGHT_YES) { - R.color.desktop_mode_resize_veil_dark - } else { - R.color.desktop_mode_resize_veil_light - } - } - /** * Whether the resize veil is ready to be shown. */ @@ -108,14 +105,14 @@ class ResizeVeil @JvmOverloads constructor( get() = viewHost != null init { - setupResizeVeil() + setupResizeVeil(taskInfo) } /** * Create the veil in its default invisible state. */ - private fun setupResizeVeil() { - if (!obtainDisplayOrRegisterListener()) { + private fun setupResizeVeil(taskInfo: RunningTaskInfo) { + if (!obtainDisplayOrRegisterListener(taskInfo.displayId)) { // Display may not be available yet, skip this until then. return } @@ -162,8 +159,8 @@ class ResizeVeil @JvmOverloads constructor( Trace.endSection() } - private fun obtainDisplayOrRegisterListener(): Boolean { - display = displayController.getDisplay(taskInfo.displayId) + private fun obtainDisplayOrRegisterListener(displayId: Int): Boolean { + display = displayController.getDisplay(displayId) if (display == null) { displayController.addDisplayWindowListener(onDisplaysChangedListener) return false @@ -184,7 +181,8 @@ class ResizeVeil @JvmOverloads constructor( t: SurfaceControl.Transaction, parent: SurfaceControl, taskBounds: Rect, - fadeIn: Boolean + taskInfo: RunningTaskInfo, + fadeIn: Boolean, ) { if (!isReady || isVisible) { t.apply() @@ -202,13 +200,15 @@ class ResizeVeil @JvmOverloads constructor( parentSurface = parent } - + val backgroundColor = when (decorThemeUtil.getAppTheme(taskInfo)) { + Theme.LIGHT -> lightColors.surfaceContainer + Theme.DARK -> darkColors.surfaceContainer + } t.show(veil) .setLayer(veil, VEIL_CONTAINER_LAYER) .setLayer(icon, VEIL_ICON_LAYER) .setLayer(background, VEIL_BACKGROUND_LAYER) - .setColor(background, - Color.valueOf(context.getColor(backgroundColorId)).components) + .setColor(background, Color.valueOf(backgroundColor.toArgb()).components) relayout(taskBounds, t) if (fadeIn) { cancelAnimation() @@ -270,12 +270,12 @@ class ResizeVeil @JvmOverloads constructor( /** * Animate veil's alpha to 1, fading it in. */ - fun showVeil(parentSurface: SurfaceControl, taskBounds: Rect) { + fun showVeil(parentSurface: SurfaceControl, taskBounds: Rect, taskInfo: RunningTaskInfo) { if (!isReady || isVisible) { return } val t = surfaceControlTransactionSupplier.get() - showVeil(t, parentSurface, taskBounds, true /* fadeIn */) + showVeil(t, parentSurface, taskBounds, taskInfo, true /* fadeIn */) } /** 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 5fce5d228d71..956d04c548f7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -18,6 +18,8 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.TRANSIT_CHANGE; +import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW; + import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; @@ -33,6 +35,7 @@ import androidx.annotation.Nullable; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.InteractionJankMonitorUtils; import com.android.wm.shell.transition.Transitions; import java.util.function.Supplier; @@ -89,6 +92,10 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); mRepositionStartPoint.set(x, y); if (isResizing()) { + // Capture CUJ for re-sizing window in DW mode. + InteractionJankMonitorUtils.beginTracing(CUJ_DESKTOP_MODE_RESIZE_WINDOW, + mDesktopWindowDecoration.mContext, mDesktopWindowDecoration.mTaskSurface, + /* tag= */ null); if (!mDesktopWindowDecoration.mTaskInfo.isFocused) { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true); @@ -146,6 +153,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, // won't be called. resetVeilIfVisible(); } + InteractionJankMonitorUtils.endTracing(CUJ_DESKTOP_MODE_RESIZE_WINDOW); } else { final WindowContainerTransaction wct = new WindowContainerTransaction(); DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds, 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 2ae3cb9ef3c0..216990c35247 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 @@ -22,6 +22,7 @@ import static android.content.res.Configuration.DENSITY_DPI_UNDEFINED; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.mandatorySystemGestures; import static android.view.WindowInsets.Type.statusBars; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import android.annotation.NonNull; import android.annotation.Nullable; @@ -56,6 +57,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement; +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer; import java.util.ArrayList; import java.util.Arrays; @@ -197,8 +199,16 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> void relayout(RelayoutParams params, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView, RelayoutResult<T> outResult) { - outResult.reset(); + updateViewsAndSurfaces(params, startT, finishT, wct, rootView, outResult); + if (outResult.mRootView != null) { + updateViewHost(params, startT, outResult); + } + } + protected void updateViewsAndSurfaces(RelayoutParams params, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, + WindowContainerTransaction wct, T rootView, RelayoutResult<T> outResult) { + outResult.reset(); if (params.mRunningTaskInfo != null) { mTaskInfo = params.mRunningTaskInfo; } @@ -211,13 +221,38 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return; } + inflateIfNeeded(params, wct, rootView, oldLayoutResId, outResult); + if (outResult.mRootView == null) { + // Didn't manage to create a root view, early out. + return; + } + rootView = null; // Clear it just in case we use it accidentally + + updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId); + + final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds(); + outResult.mWidth = taskBounds.width(); + outResult.mHeight = taskBounds.height(); + outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused); + final Resources resources = mDecorWindowContext.getResources(); + outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); + outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL + ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width(); + outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2; + + updateDecorationContainerSurface(startT, outResult); + updateCaptionContainerSurface(startT, outResult); + updateCaptionInsets(params, wct, outResult, taskBounds); + updateTaskSurface(params, startT, finishT, outResult); + } + + private void inflateIfNeeded(RelayoutParams params, WindowContainerTransaction wct, + T rootView, int oldLayoutResId, RelayoutResult<T> outResult) { if (rootView == null && params.mLayoutResId == 0) { throw new IllegalArgumentException("layoutResId and rootView can't both be invalid."); } outResult.mRootView = rootView; - rootView = null; // Clear it just in case we use it accidentally - final int oldDensityDpi = mWindowDecorConfig != null ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED; final int oldNightMode = mWindowDecorConfig != null @@ -251,25 +286,17 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) .inflate(params.mLayoutResId, null); } + } - updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId); - - final Resources resources = mDecorWindowContext.getResources(); - final Configuration taskConfig = mTaskInfo.getConfiguration(); - final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); - final boolean isFullscreen = taskConfig.windowConfiguration.getWindowingMode() - == WINDOWING_MODE_FULLSCREEN; - outResult.mWidth = taskBounds.width(); - outResult.mHeight = taskBounds.height(); - - // DecorationContainerSurface + private void updateDecorationContainerSurface( + SurfaceControl.Transaction startT, RelayoutResult<T> outResult) { if (mDecorationContainerSurface == null) { final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); mDecorationContainerSurface = builder .setName("Decor container of Task=" + mTaskInfo.taskId) .setContainerLayer() .setParent(mTaskSurface) - .setCallsite("WindowDecoration.relayout_1") + .setCallsite("WindowDecoration.updateDecorationContainerSurface") .build(); startT.setTrustedOverlay(mDecorationContainerSurface, true) @@ -279,101 +306,101 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> startT.setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) .show(mDecorationContainerSurface); + } - // CaptionContainerSurface, CaptionWindowManager + private void updateCaptionContainerSurface( + SurfaceControl.Transaction startT, RelayoutResult<T> outResult) { if (mCaptionContainerSurface == null) { final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); mCaptionContainerSurface = builder .setName("Caption container of Task=" + mTaskInfo.taskId) .setContainerLayer() .setParent(mDecorationContainerSurface) - .setCallsite("WindowDecoration.relayout_2") + .setCallsite("WindowDecoration.updateCaptionContainerSurface") .build(); } - outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); - outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL - ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width(); - outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2; - startT.setWindowCrop(mCaptionContainerSurface, outResult.mCaptionWidth, outResult.mCaptionHeight) .setPosition(mCaptionContainerSurface, outResult.mCaptionX, 0 /* y */) .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER) .show(mCaptionContainerSurface); + } - outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused); - - // Caption insets - if (mIsCaptionVisible) { - // Caption inset is the full width of the task with the |captionHeight| and - // positioned at the top of the task bounds, also in absolute coordinates. - // So just reuse the task bounds and adjust the bottom coordinate. - final Rect captionInsetsRect = new Rect(taskBounds); - captionInsetsRect.bottom = captionInsetsRect.top + outResult.mCaptionHeight; - - // Caption bounding rectangles: these are optional, and are used to present finer - // insets than traditional |Insets| to apps about where their content is occluded. - // These are also in absolute coordinates. - final Rect[] boundingRects; - final int numOfElements = params.mOccludingCaptionElements.size(); - if (numOfElements == 0) { - boundingRects = null; - } else { - // The customizable region can at most be equal to the caption bar. - if (params.hasInputFeatureSpy()) { - outResult.mCustomizableCaptionRegion.set(captionInsetsRect); - } - boundingRects = new Rect[numOfElements]; - for (int i = 0; i < numOfElements; i++) { - final OccludingCaptionElement element = - params.mOccludingCaptionElements.get(i); - final int elementWidthPx = - resources.getDimensionPixelSize(element.mWidthResId); - boundingRects[i] = - calculateBoundingRect(element, elementWidthPx, captionInsetsRect); - // Subtract the regions used by the caption elements, the rest is - // customizable. - if (params.hasInputFeatureSpy()) { - outResult.mCustomizableCaptionRegion.op(boundingRects[i], - Region.Op.DIFFERENCE); - } - } - } - - final WindowDecorationInsets newInsets = new WindowDecorationInsets( - mTaskInfo.token, mOwner, captionInsetsRect, boundingRects); - if (!newInsets.equals(mWindowDecorationInsets)) { - // Add or update this caption as an insets source. - mWindowDecorationInsets = newInsets; - mWindowDecorationInsets.addOrUpdate(wct); - } - } else { + private void updateCaptionInsets(RelayoutParams params, WindowContainerTransaction wct, + RelayoutResult<T> outResult, Rect taskBounds) { + if (!mIsCaptionVisible) { if (mWindowDecorationInsets != null) { mWindowDecorationInsets.remove(wct); mWindowDecorationInsets = null; } + return; } - - // Task surface itself - float shadowRadius; - final Point taskPosition = mTaskInfo.positionInParent; - if (isFullscreen) { - // Shadow is not needed for fullscreen tasks - shadowRadius = 0; + // Caption inset is the full width of the task with the |captionHeight| and + // positioned at the top of the task bounds, also in absolute coordinates. + // So just reuse the task bounds and adjust the bottom coordinate. + final Rect captionInsetsRect = new Rect(taskBounds); + captionInsetsRect.bottom = captionInsetsRect.top + outResult.mCaptionHeight; + + // Caption bounding rectangles: these are optional, and are used to present finer + // insets than traditional |Insets| to apps about where their content is occluded. + // These are also in absolute coordinates. + final Rect[] boundingRects; + final int numOfElements = params.mOccludingCaptionElements.size(); + if (numOfElements == 0) { + boundingRects = null; } else { - shadowRadius = loadDimension(resources, params.mShadowRadiusId); + // The customizable region can at most be equal to the caption bar. + if (params.hasInputFeatureSpy()) { + outResult.mCustomizableCaptionRegion.set(captionInsetsRect); + } + final Resources resources = mDecorWindowContext.getResources(); + boundingRects = new Rect[numOfElements]; + for (int i = 0; i < numOfElements; i++) { + final OccludingCaptionElement element = + params.mOccludingCaptionElements.get(i); + final int elementWidthPx = + resources.getDimensionPixelSize(element.mWidthResId); + boundingRects[i] = + calculateBoundingRect(element, elementWidthPx, captionInsetsRect); + // Subtract the regions used by the caption elements, the rest is + // customizable. + if (params.hasInputFeatureSpy()) { + outResult.mCustomizableCaptionRegion.op(boundingRects[i], + Region.Op.DIFFERENCE); + } + } } + final WindowDecorationInsets newInsets = new WindowDecorationInsets( + mTaskInfo.token, mOwner, captionInsetsRect, boundingRects); + if (!newInsets.equals(mWindowDecorationInsets)) { + // Add or update this caption as an insets source. + mWindowDecorationInsets = newInsets; + mWindowDecorationInsets.addOrUpdate(wct); + } + } + + private void updateTaskSurface(RelayoutParams params, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, RelayoutResult<T> outResult) { if (params.mSetTaskPositionAndCrop) { + final Point taskPosition = mTaskInfo.positionInParent; startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight); finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight) .setPosition(mTaskSurface, taskPosition.x, taskPosition.y); } - startT.setShadowRadius(mTaskSurface, shadowRadius) - .show(mTaskSurface); + float shadowRadius; + if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + // Shadow is not needed for fullscreen tasks + shadowRadius = 0; + } else { + shadowRadius = + loadDimension(mDecorWindowContext.getResources(), params.mShadowRadiusId); + } + startT.setShadowRadius(mTaskSurface, shadowRadius).show(mTaskSurface); finishT.setShadowRadius(mTaskSurface, shadowRadius); + if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { if (!DesktopModeStatus.isVeiledResizeEnabled()) { // When fluid resize is enabled, add a background to freeform tasks @@ -388,7 +415,19 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } else if (!DesktopModeStatus.isVeiledResizeEnabled()) { startT.unsetColor(mTaskSurface); } + } + /** + * Updates a {@link SurfaceControlViewHost} to connect the window decoration surfaces with our + * View hierarchy. + * + * @param params parameters to use from the last relayout + * @param onDrawTransaction a transaction to apply in sync with #onDraw + * @param outResult results to use from the last relayout + * + */ + protected void updateViewHost(RelayoutParams params, + SurfaceControl.Transaction onDrawTransaction, RelayoutResult<T> outResult) { Trace.beginSection("CaptionViewHostLayout"); if (mCaptionWindowManager == null) { // Put caption under a container surface because ViewRootImpl sets the destination frame @@ -397,12 +436,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mTaskInfo.getConfiguration(), mCaptionContainerSurface, null /* hostInputToken */); } - - // Caption view - mCaptionWindowManager.setConfiguration(taskConfig); + mCaptionWindowManager.setConfiguration(mTaskInfo.getConfiguration()); final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(outResult.mCaptionWidth, outResult.mCaptionHeight, - WindowManager.LayoutParams.TYPE_APPLICATION, + TYPE_APPLICATION, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); lp.setTitle("Caption of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); @@ -412,14 +449,20 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay, mCaptionWindowManager); if (params.mApplyStartTransactionOnDraw) { - mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT); + if (onDrawTransaction == null) { + throw new IllegalArgumentException("Trying to sync a null Transaction"); + } + mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); } mViewHost.setView(outResult.mRootView, lp); Trace.endSection(); } else { Trace.beginSection("CaptionViewHostLayout-relayout"); if (params.mApplyStartTransactionOnDraw) { - mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT); + if (onDrawTransaction == null) { + throw new IllegalArgumentException("Trying to sync a null Transaction"); + } + mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); } mViewHost.relayout(lp); Trace.endSection(); @@ -569,10 +612,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> * @param yPos y position of new window * @param width width of new window * @param height height of new window - * @return the {@link AdditionalWindow} that was added. + * @return the {@link AdditionalViewHostViewContainer} that was added. */ - AdditionalWindow addWindow(int layoutId, String namePrefix, SurfaceControl.Transaction t, - SurfaceSyncGroup ssg, int xPos, int yPos, int width, int height) { + AdditionalViewHostViewContainer addWindow(int layoutId, String namePrefix, + SurfaceControl.Transaction t, SurfaceSyncGroup ssg, int xPos, int yPos, + int width, int height) { final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); SurfaceControl windowSurfaceControl = builder .setName(namePrefix + " of Task=" + mTaskInfo.taskId) @@ -586,9 +630,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .setWindowCrop(windowSurfaceControl, width, height) .show(windowSurfaceControl); final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(width, height, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); + new WindowManager.LayoutParams(width, height, TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT); lp.setTitle("Additional window of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration, @@ -596,7 +640,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> SurfaceControlViewHost viewHost = mSurfaceControlViewHostFactory .create(mDecorWindowContext, mDisplay, windowManager); ssg.add(viewHost.getSurfacePackage(), () -> viewHost.setView(v, lp)); - return new AdditionalWindow(windowSurfaceControl, viewHost, + return new AdditionalViewHostViewContainer(windowSurfaceControl, viewHost, mSurfaceControlTransactionSupplier); } @@ -739,41 +783,4 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects)); } } - - /** - * Subclass for additional windows associated with this WindowDecoration - */ - static class AdditionalWindow { - SurfaceControl mWindowSurface; - SurfaceControlViewHost mWindowViewHost; - Supplier<SurfaceControl.Transaction> mTransactionSupplier; - - AdditionalWindow(SurfaceControl surfaceControl, - SurfaceControlViewHost surfaceControlViewHost, - Supplier<SurfaceControl.Transaction> transactionSupplier) { - mWindowSurface = surfaceControl; - mWindowViewHost = surfaceControlViewHost; - mTransactionSupplier = transactionSupplier; - } - - void releaseView() { - WindowlessWindowManager windowManager = mWindowViewHost.getWindowlessWM(); - - if (mWindowViewHost != null) { - mWindowViewHost.release(); - mWindowViewHost = null; - } - windowManager = null; - final SurfaceControl.Transaction t = mTransactionSupplier.get(); - boolean released = false; - if (mWindowSurface != null) { - t.remove(mWindowSurface); - mWindowSurface = null; - released = true; - } - if (released) { - t.apply(); - } - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt new file mode 100644 index 000000000000..6c2c8fd46bc9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.additionalviewcontainer + +import android.content.Context +import android.graphics.PixelFormat +import android.view.LayoutInflater +import android.view.SurfaceControl +import android.view.View +import android.view.WindowManager + +/** + * An [AdditionalViewContainer] that uses the system [WindowManager] instance. Intended + * for view containers that should be above the status bar layer. + */ +class AdditionalSystemViewContainer( + private val context: Context, + layoutId: Int, + taskId: Int, + x: Int, + y: Int, + width: Int, + height: Int +) : AdditionalViewContainer() { + override val view: View + + init { + view = LayoutInflater.from(context).inflate(layoutId, null) + val lp = WindowManager.LayoutParams( + width, height, x, y, + WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT + ) + lp.title = "Additional view container of Task=$taskId" + lp.setTrustedOverlay() + val wm: WindowManager? = context.getSystemService(WindowManager::class.java) + wm?.addView(view, lp) + } + + override fun releaseView() { + context.getSystemService(WindowManager::class.java)?.removeViewImmediate(view) + } + + override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) { + val lp = (view.layoutParams as WindowManager.LayoutParams).apply { + this.x = x.toInt() + this.y = y.toInt() + } + view.layoutParams = lp + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt new file mode 100644 index 000000000000..2650648a2cde --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.additionalviewcontainer + +import android.view.SurfaceControl +import android.view.View +import com.android.wm.shell.windowdecor.WindowDecoration + +/** + * Class for additional view containers associated with a [WindowDecoration]. + */ +abstract class AdditionalViewContainer internal constructor( +) { + abstract val view: View? + + /** Release the view associated with this container and perform needed cleanup. */ + abstract fun releaseView() + + /** Reposition the view container using provided coordinates. */ + abstract fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt new file mode 100644 index 000000000000..222761260289 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.additionalviewcontainer + +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import java.util.function.Supplier + +/** + * An [AdditionalViewContainer] that uses a [SurfaceControlViewHost] to show the window. + * Intended for view containers in freeform tasks that do not extend beyond task bounds. + */ +class AdditionalViewHostViewContainer( + private val windowSurface: SurfaceControl, + private val windowViewHost: SurfaceControlViewHost, + private val transactionSupplier: Supplier<SurfaceControl.Transaction>, +) : AdditionalViewContainer() { + + override val view + get() = windowViewHost.view + + override fun releaseView() { + windowViewHost.release() + val t = transactionSupplier.get() + t.remove(windowSurface) + t.apply() + } + + override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) { + t.setPosition(windowSurface, x, y) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt new file mode 100644 index 000000000000..f7cfbfa88485 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor.common + +import android.annotation.ColorInt +import android.annotation.IntRange +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_MASK +import android.graphics.Color +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme + +/** The theme of a window decoration. */ +internal enum class Theme { LIGHT, DARK } + +/** Whether a [Theme] is light. */ +internal fun Theme.isLight(): Boolean = this == Theme.LIGHT + +/** Whether a [Theme] is dark. */ +internal fun Theme.isDark(): Boolean = this == Theme.DARK + +/** Returns a copy of the color with its [alpha] component replaced with the given value. */ +@ColorInt +internal fun @receiver:ColorInt Int.withAlpha(@IntRange(from = 0, to = 255) alpha: Int): Int = + Color.argb( + alpha, + Color.red(this), + Color.green(this), + Color.blue(this) + ) + +/** Common opacity values used in window decoration views. */ +const val OPACITY_100 = 255 +const val OPACITY_11 = 28 +const val OPACITY_12 = 31 +const val OPACITY_15 = 38 +const val OPACITY_40 = 102 +const val OPACITY_55 = 140 +const val OPACITY_65 = 166 + +/** + * Utility class for determining themes based on system settings and app's [RunningTaskInfo]. + */ +internal class DecorThemeUtil(private val context: Context) { + private val lightColors = dynamicLightColorScheme(context) + private val darkColors = dynamicDarkColorScheme(context) + + private val systemTheme: Theme + get() = if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) == + Configuration.UI_MODE_NIGHT_YES) { + Theme.DARK + } else { + Theme.LIGHT + } + + /** + * Returns the [Theme] used by the app with the given [RunningTaskInfo]. + */ + fun getAppTheme(task: RunningTaskInfo): Theme { + // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the + // TaskInfo/TaskDescription. + val backgroundColor = task.taskDescription?.backgroundColor ?: return systemTheme + return if (Color.valueOf(backgroundColor).luminance() < 0.5) { + Theme.DARK + } else { + Theme.LIGHT + } + } + + /** + * Returns the [ColorScheme] to use to style window decorations based on the given + * [RunningTaskInfo]. + */ + fun getColorScheme(task: RunningTaskInfo): ColorScheme = when (getAppTheme(task)) { + Theme.LIGHT -> lightColors + Theme.DARK -> darkColors + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index 96bc4a146ebd..8d822c252288 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.wm.shell.windowdecor.viewholder import android.animation.ObjectAnimator @@ -12,14 +27,14 @@ import com.android.wm.shell.R import com.android.wm.shell.animation.Interpolators /** - * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen). It - * hosts a simple handle bar from which to initiate a drag motion to enter desktop mode. + * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split). + * It hosts a simple handle bar from which to initiate a drag motion to enter desktop mode. */ -internal class DesktopModeFocusedWindowDecorationViewHolder( +internal class AppHandleViewHolder( rootView: View, onCaptionTouchListener: View.OnTouchListener, onCaptionButtonClickListener: View.OnClickListener -) : DesktopModeWindowDecorationViewHolder(rootView) { +) : WindowDecorationViewHolder(rootView) { companion object { private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 3c12da2d6620..46127b177bc3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -1,13 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.wm.shell.windowdecor.viewholder import android.annotation.ColorInt import android.app.ActivityManager.RunningTaskInfo import android.content.res.ColorStateList import android.content.res.Configuration -import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.graphics.Bitmap import android.graphics.Color -import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable @@ -17,19 +30,27 @@ import android.view.View.OnLongClickListener import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.ui.graphics.toArgb import androidx.core.content.withStyledAttributes import androidx.core.view.isVisible import com.android.internal.R.attr.materialColorOnSecondaryContainer import com.android.internal.R.attr.materialColorOnSurface -import com.android.internal.R.attr.materialColorOnSurfaceInverse import com.android.internal.R.attr.materialColorSecondaryContainer import com.android.internal.R.attr.materialColorSurfaceContainerHigh import com.android.internal.R.attr.materialColorSurfaceContainerLow import com.android.internal.R.attr.materialColorSurfaceDim -import com.android.internal.R.attr.materialColorSurfaceInverse import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.windowdecor.MaximizeButtonView +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.OPACITY_100 +import com.android.wm.shell.windowdecor.common.OPACITY_11 +import com.android.wm.shell.windowdecor.common.OPACITY_15 +import com.android.wm.shell.windowdecor.common.OPACITY_55 +import com.android.wm.shell.windowdecor.common.OPACITY_65 +import com.android.wm.shell.windowdecor.common.Theme import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance @@ -38,7 +59,7 @@ import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppeara * finer controls such as a close window button and an "app info" section to pull up additional * controls. */ -internal class DesktopModeAppControlsWindowDecorationViewHolder( +internal class AppHeaderViewHolder( rootView: View, onCaptionTouchListener: View.OnTouchListener, onCaptionButtonClickListener: View.OnClickListener, @@ -47,7 +68,11 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( appName: CharSequence, appIconBitmap: Bitmap, onMaximizeHoverAnimationFinishedListener: () -> Unit -) : DesktopModeWindowDecorationViewHolder(rootView) { +) : WindowDecorationViewHolder(rootView) { + + private val decorThemeUtil = DecorThemeUtil(context) + private val lightColors = dynamicLightColorScheme(context) + private val darkColors = dynamicDarkColorScheme(context) /** * The corner radius to apply to the app chip, maximize and close button's background drawable. @@ -153,19 +178,12 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( val headerStyle = getHeaderStyle(header) // Caption Background - val headerBackground = captionView.background as LayerDrawable - val backLayer = headerBackground.findDrawableByLayerId(R.id.backLayer) as GradientDrawable - val frontLayer = headerBackground.findDrawableByLayerId(R.id.frontLayer) as GradientDrawable when (headerStyle.background) { is HeaderStyle.Background.Opaque -> { - backLayer.setColor(headerStyle.background.backLayerColor ?: Color.BLACK) - frontLayer.setColor(headerStyle.background.frontLayerColor) - frontLayer.alpha = headerStyle.background.frontLayerOpacity + captionView.setBackgroundColor(headerStyle.background.color) } HeaderStyle.Background.Transparent -> { - backLayer.setColor(Color.TRANSPARENT) - frontLayer.setColor(Color.TRANSPARENT) - frontLayer.alpha = OPACITY_100 + captionView.setBackgroundColor(Color.TRANSPARENT) } } @@ -189,7 +207,7 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( } // Maximize button. maximizeButtonView.setAnimationTints( - darkMode = header.appTheme == Header.Theme.DARK, + darkMode = header.appTheme == Theme.DARK, iconForegroundColor = colorStateList, baseForegroundColor = foregroundColor, rippleDrawable = createRippleDrawable( @@ -236,186 +254,88 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( ) } - private fun getHeaderBackground( - header: Header - ): HeaderStyle.Background { - when (header.type) { + private fun getHeaderBackground(header: Header): HeaderStyle.Background { + return when (header.type) { Header.Type.DEFAULT -> { - if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) { - return HeaderStyle.Background.Opaque( - frontLayerColor = attrToColor(materialColorSecondaryContainer), - frontLayerOpacity = OPACITY_100, - backLayerColor = null - ) - } - if (header.systemTheme.isLight() && header.appTheme.isLight() && - !header.isFocused) { - return HeaderStyle.Background.Opaque( - frontLayerColor = attrToColor(materialColorSurfaceContainerLow), - frontLayerOpacity = OPACITY_100, - backLayerColor = null - ) - } - if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) { - return HeaderStyle.Background.Opaque( - frontLayerColor = attrToColor(materialColorSurfaceContainerHigh), - frontLayerOpacity = OPACITY_100, - backLayerColor = null - ) - } - if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) { - return HeaderStyle.Background.Opaque( - frontLayerColor = attrToColor(materialColorSurfaceDim), - frontLayerOpacity = OPACITY_100, - backLayerColor = null - ) - } - if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) { - return HeaderStyle.Background.Opaque( - frontLayerColor = attrToColor(materialColorSurfaceInverse), - frontLayerOpacity = OPACITY_100, - backLayerColor = null - ) - } - if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) { - return HeaderStyle.Background.Opaque( - frontLayerColor = attrToColor(materialColorSurfaceInverse), - frontLayerOpacity = OPACITY_30, - backLayerColor = Color.BLACK - ) - } - if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) { - return HeaderStyle.Background.Opaque( - frontLayerColor = attrToColor(materialColorSurfaceInverse), - frontLayerOpacity = OPACITY_100, - backLayerColor = null - ) - } - if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) { - return HeaderStyle.Background.Opaque( - frontLayerColor = attrToColor(materialColorSurfaceInverse), - frontLayerOpacity = OPACITY_55, - backLayerColor = Color.WHITE - ) + when (header.appTheme) { + Theme.LIGHT -> { + if (header.isFocused) { + HeaderStyle.Background.Opaque(lightColors.secondaryContainer.toArgb()) + } else { + HeaderStyle.Background.Opaque(lightColors.surfaceContainerLow.toArgb()) + } + } + Theme.DARK -> { + if (header.isFocused) { + HeaderStyle.Background.Opaque(darkColors.surfaceContainerHigh.toArgb()) + } else { + HeaderStyle.Background.Opaque(darkColors.surfaceDim.toArgb()) + } + } } - error("No other combination expected header=$header") } - Header.Type.CUSTOM -> return HeaderStyle.Background.Transparent + Header.Type.CUSTOM -> HeaderStyle.Background.Transparent } } private fun getHeaderForeground(header: Header): HeaderStyle.Foreground { - when (header.type) { + return when (header.type) { Header.Type.DEFAULT -> { - if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSecondaryContainer), - opacity = OPACITY_100 - ) - } - if (header.systemTheme.isLight() && header.appTheme.isLight() && - !header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSecondaryContainer), - opacity = OPACITY_65 - ) - } - if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurface), - opacity = OPACITY_100 - ) - } - if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurface), - opacity = OPACITY_55 - ) - } - if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurfaceInverse), - opacity = OPACITY_100 - ) - } - if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurfaceInverse), - opacity = OPACITY_65 - ) + when (header.appTheme) { + Theme.LIGHT -> { + if (header.isFocused) { + HeaderStyle.Foreground( + color = lightColors.onSecondaryContainer.toArgb(), + opacity = OPACITY_100 + ) + } else { + HeaderStyle.Foreground( + color = lightColors.onSecondaryContainer.toArgb(), + opacity = OPACITY_65 + ) + } + } + Theme.DARK -> { + if (header.isFocused) { + HeaderStyle.Foreground( + color = darkColors.onSurface.toArgb(), + opacity = OPACITY_100 + ) + } else { + HeaderStyle.Foreground( + color = darkColors.onSurface.toArgb(), + opacity = OPACITY_55 + ) + } + } } - if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurfaceInverse), - opacity = OPACITY_100 - ) - } - if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurfaceInverse), - opacity = OPACITY_70 - ) - } - error("No other combination expected header=$header") } - Header.Type.CUSTOM -> { - if (header.systemTheme.isLight() && header.isAppearanceCaptionLight && - header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSecondaryContainer), + Header.Type.CUSTOM -> when { + header.isAppearanceCaptionLight && header.isFocused -> { + HeaderStyle.Foreground( + color = lightColors.onSecondaryContainer.toArgb(), opacity = OPACITY_100 ) } - if (header.systemTheme.isLight() && header.isAppearanceCaptionLight && - !header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSecondaryContainer), + header.isAppearanceCaptionLight && !header.isFocused -> { + HeaderStyle.Foreground( + color = lightColors.onSecondaryContainer.toArgb(), opacity = OPACITY_65 ) } - if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight && - header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurface), + !header.isAppearanceCaptionLight && header.isFocused -> { + HeaderStyle.Foreground( + color = darkColors.onSurface.toArgb(), opacity = OPACITY_100 ) } - if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight && - !header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurface), + !header.isAppearanceCaptionLight && !header.isFocused -> { + HeaderStyle.Foreground( + color = darkColors.onSurface.toArgb(), opacity = OPACITY_55 ) } - if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight && - header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurfaceInverse), - opacity = OPACITY_100 - ) - } - if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight && - !header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurfaceInverse), - opacity = OPACITY_65 - ) - } - if (header.systemTheme.isDark() && header.isAppearanceCaptionLight && - header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurfaceInverse), - opacity = OPACITY_100 - ) - } - if (header.systemTheme.isDark() && header.isAppearanceCaptionLight && - !header.isFocused) { - return HeaderStyle.Foreground( - color = attrToColor(materialColorOnSurfaceInverse), - opacity = OPACITY_70 - ) - } - error("No other combination expected header=$header") + else -> error("No other combination expected header=$header") } } } @@ -427,41 +347,12 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( } else { Header.Type.DEFAULT }, - systemTheme = getSystemTheme(), - appTheme = getAppTheme(taskInfo), + appTheme = decorThemeUtil.getAppTheme(taskInfo), isFocused = taskInfo.isFocused, isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance ) } - private fun getSystemTheme(): Header.Theme { - return if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) == - Configuration.UI_MODE_NIGHT_YES) { - Header.Theme.DARK - } else { - Header.Theme.LIGHT - } - } - - private fun getAppTheme(taskInfo: RunningTaskInfo): Header.Theme { - // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the - // TaskInfo/TaskDescription. - val backgroundColor = taskInfo.taskDescription?.backgroundColor ?: return getSystemTheme() - return if (Color.valueOf(backgroundColor).luminance() < 0.5) { - Header.Theme.DARK - } else { - Header.Theme.LIGHT - } - } - - @ColorInt - private fun attrToColor(attr: Int): Int { - context.withStyledAttributes(null, intArrayOf(attr), 0, 0) { - return getColor(0, 0) - } - return Color.WHITE - } - @ColorInt private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { return Color.argb( @@ -515,19 +406,13 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( private data class Header( val type: Type, - val systemTheme: Theme, val appTheme: Theme, val isFocused: Boolean, val isAppearanceCaptionLight: Boolean, ) { enum class Type { DEFAULT, CUSTOM } - enum class Theme { LIGHT, DARK } } - private fun Header.Theme.isLight(): Boolean = this == Header.Theme.LIGHT - - private fun Header.Theme.isDark(): Boolean = this == Header.Theme.DARK - private data class HeaderStyle( val background: Background, val foreground: Foreground @@ -539,11 +424,7 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( sealed class Background { data object Transparent : Background() - data class Opaque( - @ColorInt val frontLayerColor: Int, - val frontLayerOpacity: Int, - @ColorInt val backLayerColor: Int? - ) : Background() + data class Opaque(@ColorInt val color: Int) : Background() } } @@ -615,13 +496,5 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55% private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65% private const val FOCUSED_OPACITY = 255 - - private const val OPACITY_100 = 255 - private const val OPACITY_11 = 28 - private const val OPACITY_15 = 38 - private const val OPACITY_30 = 77 - private const val OPACITY_55 = 140 - private const val OPACITY_65 = 166 - private const val OPACITY_70 = 179 } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt index 81bc34c876b6..5ae8d252a908 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.wm.shell.windowdecor.viewholder import android.app.ActivityManager.RunningTaskInfo @@ -8,7 +23,7 @@ import android.view.View * Encapsulates the root [View] of a window decoration and its children to facilitate looking up * children (via findViewById) and updating to the latest data from [RunningTaskInfo]. */ -internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) { +internal abstract class WindowDecorationViewHolder(rootView: View) { val context: Context = rootView.context /** diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp index f813b0d3b0b7..3f2603aec86a 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp @@ -37,17 +37,51 @@ filegroup { "src/**/B*.kt", "src/**/C*.kt", "src/**/D*.kt", - "src/**/E*.kt", ], } filegroup { name: "WMShellFlickerTestsSplitScreenGroup2-src", srcs: [ + "src/**/E*.kt", + ], +} + +filegroup { + name: "WMShellFlickerTestsSplitScreenGroup3-src", + srcs: [ + "src/**/S*.kt", + ], +} + +filegroup { + name: "WMShellFlickerTestsSplitScreenGroupOther-src", + srcs: [ "src/**/*.kt", ], } +java_library { + name: "WMShellFlickerTestsSplitScreenBase", + srcs: [ + ":WMShellFlickerTestsSplitScreenBase-src", + ], + static_libs: [ + "WMShellFlickerTestsBase", + "wm-shell-flicker-utils", + "androidx.test.ext.junit", + "flickertestapplib", + "flickerlib", + "flickerlib-helpers", + "flickerlib-trace_processor_shell", + "platform-test-annotations", + "wm-flicker-common-app-helpers", + "wm-flicker-common-assertions", + "launcher-helper-lib", + "launcher-aosp-tapl", + ], +} + android_test { name: "WMShellFlickerTestsSplitScreenGroup1", defaults: ["WMShellFlickerTestsDefault"], @@ -56,25 +90,67 @@ android_test { instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", test_config_template: "AndroidTestTemplate.xml", srcs: [ - ":WMShellFlickerTestsSplitScreenBase-src", ":WMShellFlickerTestsSplitScreenGroup1-src", ], - static_libs: ["WMShellFlickerTestsBase"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellFlickerTestsSplitScreenBase", + ], data: ["trace_config/*"], } android_test { name: "WMShellFlickerTestsSplitScreenGroup2", + defaults: ["WMShellFlickerTestsDefault"], manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.splitscreen", instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", srcs: [ - ":WMShellFlickerTestsSplitScreenBase-src", - ":WMShellFlickerTestsSplitScreenGroup2-src", + ":WMShellFlickerTestsSplitScreenGroup1-src", + ], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellFlickerTestsSplitScreenBase", + ], + data: ["trace_config/*"], +} + +android_test { + name: "WMShellFlickerTestsSplitScreenGroup3", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.wm.shell.flicker.splitscreen", + instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", + srcs: [ + ":WMShellFlickerTestsSplitScreenGroup1-src", + ], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellFlickerTestsSplitScreenBase", + ], + data: ["trace_config/*"], +} + +android_test { + name: "WMShellFlickerTestsSplitScreenGroupOther", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.wm.shell.flicker.splitscreen", + instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", + srcs: [ + ":WMShellFlickerTestsSplitScreenGroupOther-src", ], exclude_srcs: [ ":WMShellFlickerTestsSplitScreenGroup1-src", + ":WMShellFlickerTestsSplitScreenGroup2-src", + ":WMShellFlickerTestsSplitScreenGroup3-src", + ], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellFlickerTestsSplitScreenBase", ], - static_libs: ["WMShellFlickerTestsBase"], data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt new file mode 100644 index 000000000000..dad5db94d062 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.splitscreen + +import android.platform.test.annotations.Presubmit +import android.tools.Rotation +import android.tools.flicker.junit.FlickerParametersRunnerFactory +import android.tools.flicker.legacy.FlickerBuilder +import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.component.ComponentNameMatcher +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitscreen.benchmark.MultipleShowImeRequestsInSplitScreenBenchmark +import com.android.wm.shell.flicker.utils.ICommonAssertions +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test quick switch between two split pairs. + * + * To run this test: `atest WMShellFlickerTestsSplitScreenGroup2:MultipleShowImeRequestsInSplitScreen` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class MultipleShowImeRequestsInSplitScreen(override val flicker: LegacyFlickerTest) : + MultipleShowImeRequestsInSplitScreenBenchmark(flicker), ICommonAssertions { + override val transition: FlickerBuilder.() -> Unit + get() = { + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @Presubmit + @Test + fun imeLayerAlwaysVisible() = + flicker.assertLayers { + this.isVisible(ComponentNameMatcher.IME) + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams() = LegacyFlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt index 90453640c91a..d34998815fca 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt @@ -47,7 +47,7 @@ import org.junit.runners.Parameterized * To run this test: `atest WMShellFlickerTestsSplitScreen:UnlockKeyguardToSplitScreen` */ @RequiresDevice -@Postsubmit +@FlakyTest(bugId = 293578017) @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -61,7 +61,6 @@ class UnlockKeyguardToSplitScreen(override val flicker: LegacyFlickerTest) : } @Test - @FlakyTest(bugId = 293578017) override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt new file mode 100644 index 000000000000..249253185607 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.splitscreen.benchmark + +import android.tools.flicker.junit.FlickerParametersRunnerFactory +import android.tools.flicker.legacy.FlickerBuilder +import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.flicker.legacy.LegacyFlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +abstract class MultipleShowImeRequestsInSplitScreenBenchmark( + override val flicker: LegacyFlickerTest +) : SplitScreenBase(flicker) { + override val primaryApp = ImeAppHelper(instrumentation) + override val defaultTeardown: FlickerBuilder.() -> Unit + get() = { + teardown { + primaryApp.closeIME(wmHelper) + super.defaultTeardown + } + } + + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + SplitScreenUtils.enterSplit( + wmHelper, + tapl, + device, + primaryApp, + secondaryApp, + flicker.scenario.startRotation + ) + // initially open the IME + primaryApp.openIME(wmHelper) + } + transitions { + for (i in 1..OPEN_IME_COUNT) { + primaryApp.openIME(wmHelper) + } + } + } + + companion object { + const val OPEN_IME_COUNT = 30 + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams() = LegacyFlickerTestFactory.nonRotationTests() + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt index 4b106034b2b5..51074f634e30 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt @@ -25,7 +25,7 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils abstract class SplitScreenBase(flicker: LegacyFlickerTest) : BaseBenchmarkTest(flicker) { protected val context: Context = instrumentation.context - protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation) + protected open val primaryApp = SplitScreenUtils.getPrimary(instrumentation) protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation) protected open val defaultSetup: FlickerBuilder.() -> Unit = { diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index 13f95ccea640..92be4f9f0374 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -46,6 +46,7 @@ android_test { "androidx.dynamicanimation_dynamicanimation", "dagger2", "frameworks-base-testutils", + "kotlin-test", "kotlinx-coroutines-android", "kotlinx-coroutines-core", "mockito-kotlin2", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java index ea522cdf2509..731f75bf9f5d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -20,9 +20,11 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; +import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationRunner.calculateParentBounds; import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; @@ -32,14 +34,22 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import android.animation.Animator; +import android.annotation.NonNull; +import android.graphics.Point; +import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.window.TransitionInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.transition.TransitionInfoBuilder; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -56,12 +66,16 @@ import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase { + @Rule + public SetFlagsRule mRule = new SetFlagsRule(); + @Before public void setup() { super.setUp(); doNothing().when(mController).onAnimationFinished(any()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) @@ -87,6 +101,7 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim verify(mController).onAnimationFinished(mTransition); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testChangesBehindStartingWindow() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) @@ -101,6 +116,7 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim assertEquals(0, animator.getDuration()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testTransitionTypeDragResize() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TASK_FRAGMENT_DRAG_RESIZE, 0) @@ -115,10 +131,11 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim assertEquals(0, animator.getDuration()); } + @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test - public void testInvalidCustomAnimation() { + public void testInvalidCustomAnimation_disableAnimationOptionsPerChange() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) - .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) + .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, TRANSIT_OPEN)) .build(); info.setAnimationOptions(TransitionInfo.AnimationOptions .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */, @@ -131,4 +148,146 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim // An invalid custom animation is equivalent to jump-cut. assertEquals(0, animator.getDuration()); } + + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testInvalidCustomAnimation_enableAnimationOptionsPerChange() { + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, TRANSIT_OPEN)) + .build(); + info.getChanges().getFirst().setAnimationOptions(TransitionInfo.AnimationOptions + .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */, + 0 /* backgroundColor */, false /* overrideTaskTransition */)); + final Animator animator = mAnimRunner.createAnimator( + info, mStartTransaction, mFinishTransaction, + () -> mFinishCallback.onTransitionFinished(null /* wct */), + new ArrayList<>()); + + // An invalid custom animation is equivalent to jump-cut. + assertEquals(0, animator.getDuration()); + } + + @DisableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG) + @Test + public void testCalculateParentBounds_flagDisabled() { + final Rect parentBounds = new Rect(0, 0, 2000, 2000); + final Rect primaryBounds = new Rect(); + final Rect secondaryBounds = new Rect(); + parentBounds.splitVertically(primaryBounds, secondaryBounds); + + final TransitionInfo.Change change = createChange(0 /* flags */); + change.setStartAbsBounds(secondaryBounds); + + final TransitionInfo.Change boundsAnimationChange = createChange(0 /* flags */); + boundsAnimationChange.setStartAbsBounds(primaryBounds); + boundsAnimationChange.setEndAbsBounds(primaryBounds); + final Rect actualParentBounds = new Rect(); + + calculateParentBounds(change, boundsAnimationChange, actualParentBounds); + + assertEquals(parentBounds, actualParentBounds); + + actualParentBounds.setEmpty(); + + boundsAnimationChange.setStartAbsBounds(secondaryBounds); + boundsAnimationChange.setEndAbsBounds(primaryBounds); + + calculateParentBounds(boundsAnimationChange, boundsAnimationChange, actualParentBounds); + + assertEquals(parentBounds, actualParentBounds); + } + + // TODO(b/243518738): Rewrite with TestParameter + @EnableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG) + @Test + public void testCalculateParentBounds_flagEnabled() { + TransitionInfo.Change change; + final TransitionInfo.Change stubChange = createChange(0 /* flags */); + final Rect actualParentBounds = new Rect(); + Rect parentBounds = new Rect(0, 0, 2000, 2000); + Rect endAbsBounds = new Rect(0, 0, 2000, 2000); + change = prepareChangeForParentBoundsCalculationTest( + new Point(0, 0) /* endRelOffset */, + endAbsBounds, + new Point() /* endParentSize */ + ); + + calculateParentBounds(change, stubChange, actualParentBounds); + + assertTrue("Parent bounds must be empty because end parent size is not set.", + actualParentBounds.isEmpty()); + + String testString = "Parent start with (0, 0)"; + change = prepareChangeForParentBoundsCalculationTest( + new Point(endAbsBounds.left - parentBounds.left, + endAbsBounds.top - parentBounds.top), + endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); + + calculateParentBounds(change, stubChange, actualParentBounds); + + assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, + actualParentBounds); + + testString = "Container not start with (0, 0)"; + parentBounds = new Rect(0, 0, 2000, 2000); + endAbsBounds = new Rect(1000, 500, 2000, 1500); + change = prepareChangeForParentBoundsCalculationTest( + new Point(endAbsBounds.left - parentBounds.left, + endAbsBounds.top - parentBounds.top), + endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); + + calculateParentBounds(change, stubChange, actualParentBounds); + + assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, + actualParentBounds); + + testString = "Parent container on the right"; + parentBounds = new Rect(1000, 0, 2000, 2000); + endAbsBounds = new Rect(1000, 500, 1500, 1500); + change = prepareChangeForParentBoundsCalculationTest( + new Point(endAbsBounds.left - parentBounds.left, + endAbsBounds.top - parentBounds.top), + endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); + + calculateParentBounds(change, stubChange, actualParentBounds); + + assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, + actualParentBounds); + + testString = "Parent container on the bottom"; + parentBounds = new Rect(0, 1000, 2000, 2000); + endAbsBounds = new Rect(500, 1500, 1500, 2000); + change = prepareChangeForParentBoundsCalculationTest( + new Point(endAbsBounds.left - parentBounds.left, + endAbsBounds.top - parentBounds.top), + endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); + + calculateParentBounds(change, stubChange, actualParentBounds); + + assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, + actualParentBounds); + + testString = "Parent container in the middle"; + parentBounds = new Rect(500, 500, 1500, 1500); + endAbsBounds = new Rect(1000, 500, 1500, 1000); + change = prepareChangeForParentBoundsCalculationTest( + new Point(endAbsBounds.left - parentBounds.left, + endAbsBounds.top - parentBounds.top), + endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); + + calculateParentBounds(change, stubChange, actualParentBounds); + + assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, + actualParentBounds); + } + + @NonNull + private static TransitionInfo.Change prepareChangeForParentBoundsCalculationTest( + @NonNull Point endRelOffset, @NonNull Rect endAbsBounds, @NonNull Point endParentSize) { + final TransitionInfo.Change change = createChange(0 /* flags */); + change.setEndRelOffset(endRelOffset.x, endRelOffset.y); + change.setEndAbsBounds(endAbsBounds); + change.setEndParentSize(endParentSize.x, endParentSize.y); + return change; + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java index 0b2265d4ce9c..c18d7ec821b6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java @@ -16,6 +16,7 @@ package com.android.wm.shell.activityembedding; +import static android.view.WindowManager.TRANSIT_NONE; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; @@ -31,6 +32,7 @@ import android.annotation.NonNull; import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; +import android.view.WindowManager; import android.window.TransitionInfo; import android.window.WindowContainerToken; @@ -82,11 +84,27 @@ abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase { spyOn(mFinishCallback); } - /** Creates a mock {@link TransitionInfo.Change}. */ + /** + * Creates a mock {@link TransitionInfo.Change}. + * + * @param flags the {@link TransitionInfo.ChangeFlags} of the change + */ static TransitionInfo.Change createChange(@TransitionInfo.ChangeFlags int flags) { + return createChange(flags, TRANSIT_NONE); + } + + /** + * Creates a mock {@link TransitionInfo.Change}. + * + * @param flags the {@link TransitionInfo.ChangeFlags} of the change + * @param mode the transition mode of the change + */ + static TransitionInfo.Change createChange(@TransitionInfo.ChangeFlags int flags, + @WindowManager.TransitionType int mode) { TransitionInfo.Change c = new TransitionInfo.Change(mock(WindowContainerToken.class), mock(SurfaceControl.class)); c.setFlags(flags); + c.setMode(mode); return c; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java index 974d69b2ac5d..39d55079ca3a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java @@ -32,6 +32,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import android.animation.Animator; import android.animation.ValueAnimator; import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -39,9 +42,11 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.transition.TransitionInfoBuilder; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -59,6 +64,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation private static final Rect EMBEDDED_LEFT_BOUNDS = new Rect(0, 0, 500, 500); private static final Rect EMBEDDED_RIGHT_BOUNDS = new Rect(500, 0, 1000, 500); + @Rule + public SetFlagsRule mRule = new SetFlagsRule(); + @Before public void setup() { super.setUp(); @@ -66,11 +74,13 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation any()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testInstantiate() { verify(mShellInit).addInitCallback(any(), any()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOnInit() { mController.onInit(); @@ -78,6 +88,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verify(mTransitions).addHandler(mController); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testSetAnimScaleSetting() { mController.setAnimScaleSetting(1.0f); @@ -86,6 +97,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verify(mAnimSpec).setAnimScaleSetting(1.0f); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsNonActivityEmbeddingChange() { final TransitionInfo.Change nonEmbeddedOpen = createChange(0 /* flags */); @@ -122,6 +134,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation assertFalse(info2.getChanges().contains(nonEmbeddedClose)); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsOnlyFillTaskActivityEmbeddingChange() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) @@ -138,6 +151,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verifyNoMoreInteractions(mFinishCallback); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsActivityEmbeddingSplitChange() { // Change that occupies only part of the Task. @@ -155,6 +169,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verifyNoMoreInteractions(mFinishTransaction); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsChangeEnterActivityEmbeddingSplit() { // Change that is entering ActivityEmbedding split. @@ -171,6 +186,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verifyNoMoreInteractions(mFinishTransaction); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testStartAnimation_containsChangeExitActivityEmbeddingSplit() { // Change that is exiting ActivityEmbedding split. @@ -187,8 +203,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verifyNoMoreInteractions(mFinishTransaction); } + @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test - public void testShouldAnimate_containsAnimationOptions() { + public void testShouldAnimate_containsAnimationOptions_disableAnimOptionsPerChange() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) .addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS)) .build(); @@ -206,6 +223,28 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation assertFalse(mController.shouldAnimate(info)); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testShouldAnimate_containsAnimationOptions_enableAnimOptionsPerChange() { + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS)) + .build(); + final TransitionInfo.Change change = info.getChanges().getFirst(); + + change.setAnimationOptions(TransitionInfo.AnimationOptions + .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */, + 0 /* backgroundColor */, false /* overrideTaskTransition */)); + assertTrue(mController.shouldAnimate(info)); + + change.setAnimationOptions(TransitionInfo.AnimationOptions + .makeSceneTransitionAnimOptions()); + assertFalse(mController.shouldAnimate(info)); + + change.setAnimationOptions(TransitionInfo.AnimationOptions.makeCrossProfileAnimOptions()); + assertFalse(mController.shouldAnimate(info)); + } + + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @UiThreadTest @Test public void testMergeAnimation() { @@ -242,6 +281,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verify(mFinishCallback).onTransitionFinished(any()); } + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOnAnimationFinished() { // Should not call finish when there is no transition. 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 f6f3aa49bc6e..57e469d5cbd2 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 @@ -123,6 +123,7 @@ public class BackAnimationControllerTest extends ShellTestCase { private DefaultCrossActivityBackAnimation mDefaultCrossActivityBackAnimation; private CrossTaskBackAnimation mCrossTaskBackAnimation; private ShellBackAnimationRegistry mShellBackAnimationRegistry; + private Rect mTouchableRegion; @Before public void setUp() throws Exception { @@ -158,6 +159,8 @@ public class BackAnimationControllerTest extends ShellTestCase { mShellCommandHandler); mShellInit.init(); mShellExecutor.flushAll(); + mTouchableRegion = new Rect(0, 0, 100, 100); + mController.mTouchableArea.set(mTouchableRegion); } private void createNavigationInfo(int backType, @@ -169,7 +172,8 @@ public class BackAnimationControllerTest extends ShellTestCase { .setOnBackNavigationDone(new RemoteCallback((bundle) -> {})) .setOnBackInvokedCallback(mAppCallback) .setPrepareRemoteAnimation(enableAnimation) - .setAnimationCallback(isAnimationCallback); + .setAnimationCallback(isAnimationCallback) + .setTouchableRegion(mTouchableRegion); createNavigationInfo(builder); } @@ -234,7 +238,8 @@ public class BackAnimationControllerTest extends ShellTestCase { .setType(type) .setOnBackInvokedCallback(mAppCallback) .setPrepareRemoteAnimation(true) - .setOnBackNavigationDone(new RemoteCallback(result))); + .setOnBackNavigationDone(new RemoteCallback(result)) + .setTouchableRegion(mTouchableRegion)); triggerBackGesture(); simulateRemoteAnimationStart(); mShellExecutor.flushAll(); @@ -512,7 +517,8 @@ public class BackAnimationControllerTest extends ShellTestCase { .setType(type) .setOnBackInvokedCallback(mAppCallback) .setPrepareRemoteAnimation(true) - .setOnBackNavigationDone(new RemoteCallback(result))); + .setOnBackNavigationDone(new RemoteCallback(result)) + .setTouchableRegion(mTouchableRegion)); triggerBackGesture(); simulateRemoteAnimationStart(); mShellExecutor.flushAll(); @@ -543,7 +549,9 @@ public class BackAnimationControllerTest extends ShellTestCase { createNavigationInfo(new BackNavigationInfo.Builder() .setType(type) .setOnBackInvokedCallback(mAppCallback) - .setOnBackNavigationDone(new RemoteCallback(result))); + .setOnBackNavigationDone(new RemoteCallback(result)) + .setTouchableRegion(mTouchableRegion) + .setAppProgressAllowed(true)); triggerBackGesture(); mShellExecutor.flushAll(); releaseBackGesture(); @@ -570,7 +578,8 @@ public class BackAnimationControllerTest extends ShellTestCase { createNavigationInfo(new BackNavigationInfo.Builder() .setType(type) .setOnBackInvokedCallback(mAppCallback) - .setOnBackNavigationDone(new RemoteCallback(result))); + .setOnBackNavigationDone(new RemoteCallback(result)) + .setTouchableRegion(mTouchableRegion)); doMotionEvent(MotionEvent.ACTION_CANCEL, 0); mShellExecutor.flushAll(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java index 8932e60048e6..4d0348b4f470 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java @@ -19,6 +19,7 @@ package com.android.wm.shell.back; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import android.os.Handler; import android.os.Looper; @@ -95,16 +96,33 @@ public class BackProgressAnimatorTest { // Trigger animation cancel, the target progress should be 0. mTargetProgress = 0; mTargetProgressCalled = new CountDownLatch(1); - CountDownLatch cancelCallbackCalled = new CountDownLatch(1); + CountDownLatch finishCallbackCalled = new CountDownLatch(1); mMainThreadHandler.post( - () -> mProgressAnimator.onBackCancelled(() -> cancelCallbackCalled.countDown())); - cancelCallbackCalled.await(1, TimeUnit.SECONDS); + () -> mProgressAnimator.onBackCancelled(finishCallbackCalled::countDown)); + finishCallbackCalled.await(1, TimeUnit.SECONDS); mTargetProgressCalled.await(1, TimeUnit.SECONDS); assertNotNull(mReceivedBackEvent); assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */); } @Test + public void testBackInvoked() throws InterruptedException { + // Give the animator some progress. + final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress); + mMainThreadHandler.post( + () -> mProgressAnimator.onBackProgressed(backEvent)); + mTargetProgressCalled.await(1, TimeUnit.SECONDS); + assertNotNull(mReceivedBackEvent); + + // Trigger back invoked animation + CountDownLatch finishCallbackCalled = new CountDownLatch(1); + mMainThreadHandler.post( + () -> mProgressAnimator.onBackInvoked(finishCallbackCalled::countDown)); + assertTrue("onBackInvoked finishCallback never called", + finishCallbackCalled.await(1, TimeUnit.SECONDS)); + } + + @Test public void testResetCallsCancelCallbackImmediately() throws InterruptedException { // Give the animator some progress. final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 0f433770777e..93e405131a58 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -1170,9 +1170,9 @@ public class BubbleDataTest extends ShellTestCase { // Verify the update has the removals. BubbleData.Update update = mUpdateCaptor.getValue(); assertThat(update.removedBubbles.get(0)).isEqualTo( - Pair.create(mBubbleA2, Bubbles.DISMISS_USER_REMOVED)); + Pair.create(mBubbleA2, Bubbles.DISMISS_USER_ACCOUNT_REMOVED)); assertThat(update.removedBubbles.get(1)).isEqualTo( - Pair.create(mBubbleA1, Bubbles.DISMISS_USER_REMOVED)); + Pair.create(mBubbleA1, Bubbles.DISMISS_USER_ACCOUNT_REMOVED)); // Verify no A bubbles in active or overflow. assertBubbleListContains(mBubbleC1, mBubbleB3); @@ -1203,6 +1203,25 @@ public class BubbleDataTest extends ShellTestCase { assertThat(update.currentBubbleList.get(0).getKey()).isEqualTo(mEntryA2.getKey()); assertThat(update.currentBubbleList.get(1).getKey()).isEqualTo(mEntryA1.getKey()); assertThat(update.bubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT); + assertThat(update.expandedChanged).isFalse(); + assertThat(update.selectedBubbleKey).isEqualTo(mEntryA2.getKey()); + } + + @Test + public void test_getInitialStateForBubbleBar_includesExpandedState() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT); + mBubbleData.setExpanded(true); + + BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar(); + assertThat(update.currentBubbleList).hasSize(2); + assertThat(update.currentBubbleList.get(0).getKey()).isEqualTo(mEntryA2.getKey()); + assertThat(update.currentBubbleList.get(1).getKey()).isEqualTo(mEntryA1.getKey()); + assertThat(update.bubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT); + assertThat(update.expandedChanged).isTrue(); + assertThat(update.expanded).isTrue(); + assertThat(update.selectedBubbleKey).isEqualTo(mEntryA2.getKey()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt index 432909f18813..5b22eddcb6ee 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt @@ -32,7 +32,17 @@ class BubbleInfoTest : ShellTestCase() { @Test fun bubbleInfo() { val bubbleInfo = - BubbleInfo("key", 0, "shortcut id", null, 6, "com.some.package", "title", true) + BubbleInfo( + "key", + 0, + "shortcut id", + null, + 6, + "com.some.package", + "title", + "Some app", + true + ) val parcel = Parcel.obtain() bubbleInfo.writeToParcel(parcel, PARCELABLE_WRITE_RETURN_VALUE) parcel.setDataPosition(0) @@ -46,6 +56,7 @@ class BubbleInfoTest : ShellTestCase() { assertThat(bubbleInfo.userId).isEqualTo(bubbleInfoFromParcel.userId) assertThat(bubbleInfo.packageName).isEqualTo(bubbleInfoFromParcel.packageName) assertThat(bubbleInfo.title).isEqualTo(bubbleInfoFromParcel.title) + assertThat(bubbleInfo.appName).isEqualTo(bubbleInfoFromParcel.appName) assertThat(bubbleInfo.isImportantConversation) .isEqualTo(bubbleInfoFromParcel.isImportantConversation) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt index 2a2483df0792..665bed0c8a88 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -19,6 +19,8 @@ import android.app.ActivityManager import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.content.Context +import android.graphics.Point +import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner import android.view.SurfaceControl @@ -36,30 +38,41 @@ import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.WindowContainerToken import androidx.test.filters.SmallTest -import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN import com.android.wm.shell.shared.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions -import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertNotNull +import junit.framework.Assert.assertNull import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.same +import org.mockito.kotlin.spy import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.whenever /** * Test class for {@link DesktopModeLoggerTransitionObserver} @@ -68,301 +81,613 @@ import org.mockito.kotlin.times */ @SmallTest @RunWith(AndroidTestingRunner::class) -class DesktopModeLoggerTransitionObserverTest { - - @JvmField - @Rule - val extendedMockitoRule = ExtendedMockitoRule.Builder(this) - .mockStatic(DesktopModeEventLogger::class.java) - .mockStatic(DesktopModeStatus::class.java).build()!! - - @Mock - lateinit var testExecutor: ShellExecutor - @Mock - private lateinit var mockShellInit: ShellInit - @Mock - private lateinit var transitions: Transitions - @Mock - private lateinit var context: Context - - private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver - private lateinit var shellInit: ShellInit - private lateinit var desktopModeEventLogger: DesktopModeEventLogger - - @Before - fun setup() { - doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) } - shellInit = Mockito.spy(ShellInit(testExecutor)) - desktopModeEventLogger = mock(DesktopModeEventLogger::class.java) - - transitionObserver = DesktopModeLoggerTransitionObserver( +class DesktopModeLoggerTransitionObserverTest : ShellTestCase() { + + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this).mockStatic(DesktopModeStatus::class.java).build()!! + + private val testExecutor = mock<ShellExecutor>() + private val mockShellInit = mock<ShellInit>() + private val transitions = mock<Transitions>() + private val context = mock<Context>() + + private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver + private lateinit var shellInit: ShellInit + private lateinit var desktopModeEventLogger: DesktopModeEventLogger + + @Before + fun setup() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + shellInit = spy(ShellInit(testExecutor)) + desktopModeEventLogger = mock<DesktopModeEventLogger>() + + transitionObserver = + DesktopModeLoggerTransitionObserver( context, mockShellInit, transitions, desktopModeEventLogger) - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - val initRunnableCaptor = ArgumentCaptor.forClass( - Runnable::class.java) - verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), - same(transitionObserver)) - initRunnableCaptor.value.run() - } else { - transitionObserver.onInit() - } - } - - @Test - fun testRegistersObserverAtInit() { - verify(transitions) - .registerObserver(same( - transitionObserver)) - } - - @Test - fun taskCreated_notFreeformWindow_doesNotLogSessionEnterOrTaskAdded() { - val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, never()).logSessionEnter(any(), any()) - verify(desktopModeEventLogger, never()).logTaskAdded(any(), any()) - } - - @Test - fun taskCreated_FreeformWindowOpen_logSessionEnterAndTaskAdded() { - val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), - eq(EnterReason.APP_FREEFORM_INTENT)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - } - - @Test - fun taskChanged_taskMovedToDesktopByDrag_logSessionEnterAndTaskAdded() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - // task change is finalised when drag ends - val transitionInfo = TransitionInfoBuilder( - Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), - eq(EnterReason.APP_HANDLE_DRAG)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - } - - @Test - fun taskChanged_taskMovedToDesktopByButtonTap_logSessionEnterAndTaskAdded() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0) - .addChange(change).build() - - callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), - eq(EnterReason.APP_HANDLE_MENU_BUTTON)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - } - - @Test - fun taskChanged_existingFreeformTaskMadeVisible_logSessionEnterAndTaskAdded() { - val taskInfo = createTaskInfo(1, WINDOWING_MODE_FREEFORM) - taskInfo.isVisibleRequested = true - val change = createChange(TRANSIT_CHANGE, taskInfo) - val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0) - .addChange(change).build() - - callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), - eq(EnterReason.APP_HANDLE_MENU_BUTTON)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - } - - @Test - fun taskToFront_screenWake_logSessionStartedAndTaskAdded() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0) - .addChange(change).build() - - callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), - eq(EnterReason.SCREEN_ON)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - } - - @Test - fun freeformTaskVisible_screenTurnOff_logSessionExitAndTaskRemoved_sessionIdNull() { - val sessionId = 1 - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - transitionObserver.setLoggerSessionId(sessionId) - - val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), - eq(ExitReason.SCREEN_OFF)) - assertThat(transitionObserver.getLoggerSessionId()).isNull() + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java) + verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver)) + initRunnableCaptor.value.run() + } else { + transitionObserver.onInit() } + } + + @Test + fun testRegistersObserverAtInit() { + verify(transitions).registerObserver(same(transitionObserver)) + } + + @Test + fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() { + val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, never()).logSessionEnter(any(), any()) + verify(desktopModeEventLogger, never()).logTaskAdded(any(), any()) + } - @Test - fun freeformTaskVisible_exitDesktopUsingDrag_logSessionExitAndTaskRemoved_sessionIdNull() { - val sessionId = 1 - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - transitionObserver.setLoggerSessionId(sessionId) - - // window mode changing from FREEFORM to FULLSCREEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_EXIT_DESKTOP_MODE) - .addChange(change).build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), - eq(ExitReason.DRAG_TO_EXIT)) - assertThat(transitionObserver.getLoggerSessionId()).isNull() - } - - @Test - fun freeformTaskVisible_exitDesktopBySwipeUp_logSessionExitAndTaskRemoved_sessionIdNull() { - val sessionId = 1 - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - transitionObserver.setLoggerSessionId(sessionId) - - // recents transition - val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) - .addChange(change).build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), - eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) - assertThat(transitionObserver.getLoggerSessionId()).isNull() - } + @Test + fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() { + val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() - @Test - fun freeformTaskVisible_taskFinished_logSessionExitAndTaskRemoved_sessionIdNull() { - val sessionId = 1 - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - transitionObserver.setLoggerSessionId(sessionId) - - // task closing - val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), - eq(ExitReason.TASK_FINISHED)) - assertThat(transitionObserver.getLoggerSessionId()).isNull() - } - - @Test - fun sessionExitByRecents_cancelledAnimation_sessionRestored() { - val sessionId = 1 - // add a freeform task to an existing session - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - transitionObserver.setLoggerSessionId(sessionId) - - // recents transition sent freeform window to back - val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - val transitionInfo1 = - TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change) - .build() - callOnTransitionReady(transitionInfo1) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), - eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) - assertThat(transitionObserver.getLoggerSessionId()).isNull() - - val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build() - callOnTransitionReady(transitionInfo2) - - verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any()) - verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any()) - } - - @Test - fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() { - val sessionId = 1 - // add an existing freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - transitionObserver.setLoggerSessionId(sessionId) - - // new freeform task added - val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verify(desktopModeEventLogger, never()).logSessionEnter(any(), any()) - } - - @Test - fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() { - val sessionId = 1 - // add two existing freeform tasks - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM)) - transitionObserver.setLoggerSessionId(sessionId) - - // new freeform task added - val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, never()).logSessionExit(any(), any()) - } - - /** - * Simulate calling the onTransitionReady() method - */ - private fun callOnTransitionReady(transitionInfo: TransitionInfo) { - val transition = mock(IBinder::class.java) - val startT = mock( - SurfaceControl.Transaction::class.java) - val finishT = mock( - SurfaceControl.Transaction::class.java) - - transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT) - } - - companion object { - fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo { - val taskInfo = ActivityManager.RunningTaskInfo() - taskInfo.taskId = taskId - taskInfo.configuration.windowConfiguration.windowingMode = windowMode - - return taskInfo + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.APP_FREEFORM_INTENT, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + // task change is finalised when drag ends + val transitionInfo = + TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_MENU_BUTTON, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonAppFromOverview() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.KEYBOARD_SHORTCUT_ENTER, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitToFront_logTaskAddedAndEnterReasonOverview() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitToFront_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { + // previous exit to overview transition + val previousSessionId = 1 + // add a freeform task + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) + transitionObserver.setLoggerSessionId(previousSessionId) + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verifyTaskRemovedAndExitLogging( + previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows + + // TRANSIT_TO_FRONT + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitChange_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { + // previous exit to overview transition + val previousSessionId = 1 + // add a freeform task + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) + transitionObserver.setLoggerSessionId(previousSessionId) + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verifyTaskRemovedAndExitLogging( + previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows + + // TRANSIT_CHANGE + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CHANGE, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitOpen_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { + // previous exit to overview transition + val previousSessionId = 1 + // add a freeform task + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) + transitionObserver.setLoggerSessionId(previousSessionId) + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verifyTaskRemovedAndExitLogging( + previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows + + // TRANSIT_OPEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE) + } + + @Test + @Suppress("ktlint:standard:max-line-length") + fun transitEnterDesktopFromAppFromOverview_previousTransitionExitToOverview_logTaskAddedAndEnterReasonAppFromOverview() { + // Tests for AppFromOverview precedence in compared to cancelled Overview + + // previous exit to overview transition + val previousSessionId = 1 + // add a freeform task + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) + transitionObserver.setLoggerSessionId(previousSessionId) + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verifyTaskRemovedAndExitLogging( + previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + // TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.UNKNOWN_ENTER, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitWake_logTaskAddedAndEnterReasonScreenOn() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitSleep_logTaskRemovedAndExitReasonScreenOff_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.DRAG_TO_EXIT, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON) + .addChange(change) + .build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging( + sessionId, ExitReason.APP_HANDLE_MENU_BUTTON_EXIT, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging( + sessionId, ExitReason.KEYBOARD_SHORTCUT_EXIT, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.UNKNOWN_EXIT, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // recents transition + val change = createChange(TRANSIT_TO_BACK, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging( + sessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitClose_logTaskRemovedAndExitReasonTaskFinished_sessionIdNull() { + val sessionId = 1 + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // task closing + val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.TASK_FINISHED, DEFAULT_TASK_UPDATE) + } + + @Test + fun sessionExitByRecents_cancelledAnimation_sessionRestored() { + val sessionId = 1 + // add a freeform task to an existing session + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(taskInfo) + transitionObserver.setLoggerSessionId(sessionId) + + // recents transition sent freeform window to back + val change = createChange(TRANSIT_TO_BACK, taskInfo) + val transitionInfo1 = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build() + callOnTransitionReady(transitionInfo1) + + verifyTaskRemovedAndExitLogging( + sessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build() + callOnTransitionReady(transitionInfo2) + + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE) + } + + @Test + fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() { + val sessionId = 1 + // add an existing freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.setLoggerSessionId(sessionId) + + // new freeform task added + val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskAdded(eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2))) + verify(desktopModeEventLogger, never()).logSessionEnter(any(), any()) + } + + @Test + fun sessionAlreadyStarted_taskPositionChanged_logsTaskUpdate() { + val sessionId = 1 + // add an existing freeform task + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(taskInfo) + transitionObserver.setLoggerSessionId(sessionId) + + // task position changed + val newTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo)) + .build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100))) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun sessionAlreadyStarted_taskResized_logsTaskUpdate() { + val sessionId = 1 + // add an existing freeform task + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(taskInfo) + transitionObserver.setLoggerSessionId(sessionId) + + // task resized + val newTaskInfo = + createTaskInfo( + WINDOWING_MODE_FREEFORM, + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo)) + .build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(sessionId), + eq( + DEFAULT_TASK_UPDATE.copy( + taskWidth = DEFAULT_TASK_WIDTH + 100, taskHeight = DEFAULT_TASK_HEIGHT - 100))) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun sessionAlreadyStarted_multipleTasksUpdated_logsTaskUpdateForCorrectTask() { + val sessionId = 1 + // add 2 existing freeform task + val taskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM) + val taskInfo2 = createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2) + transitionObserver.addTaskInfosToCachedMap(taskInfo1) + transitionObserver.addTaskInfosToCachedMap(taskInfo2) + transitionObserver.setLoggerSessionId(sessionId) + + // task 1 position update + val newTaskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100) + val transitionInfo1 = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo1)) + .build() + callOnTransitionReady(transitionInfo1) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100))) + verifyZeroInteractions(desktopModeEventLogger) + + // task 2 resize + val newTaskInfo2 = + createTaskInfo( + WINDOWING_MODE_FREEFORM, + id = 2, + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100) + val transitionInfo2 = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo2)) + .build() + + callOnTransitionReady(transitionInfo2) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(sessionId), + eq( + DEFAULT_TASK_UPDATE.copy( + instanceId = 2, + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100))) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() { + val sessionId = 1 + // add two existing freeform tasks + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) + transitionObserver.setLoggerSessionId(sessionId) + + // new freeform task closed + val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskRemoved(eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2))) + verify(desktopModeEventLogger, never()).logSessionExit(any(), any()) + } + + /** Simulate calling the onTransitionReady() method */ + private fun callOnTransitionReady(transitionInfo: TransitionInfo) { + val transition = mock<IBinder>() + val startT = mock<SurfaceControl.Transaction>() + val finishT = mock<SurfaceControl.Transaction>() + + transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT) + } + + private fun verifyTaskAddedAndEnterLogging(enterReason: EnterReason, taskUpdate: TaskUpdate) { + val sessionId = transitionObserver.getLoggerSessionId() + assertNotNull(sessionId) + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), eq(enterReason)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), eq(taskUpdate)) + verifyZeroInteractions(desktopModeEventLogger) + } + + private fun verifyTaskRemovedAndExitLogging( + sessionId: Int, + exitReason: ExitReason, + taskUpdate: TaskUpdate + ) { + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), eq(taskUpdate)) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), eq(exitReason)) + verifyZeroInteractions(desktopModeEventLogger) + assertNull(transitionObserver.getLoggerSessionId()) + } + + private companion object { + const val DEFAULT_TASK_ID = 1 + const val DEFAULT_TASK_UID = 2 + const val DEFAULT_TASK_HEIGHT = 100 + const val DEFAULT_TASK_WIDTH = 200 + const val DEFAULT_TASK_X = 30 + const val DEFAULT_TASK_Y = 70 + val DEFAULT_TASK_UPDATE = + TaskUpdate( + DEFAULT_TASK_ID, + DEFAULT_TASK_UID, + DEFAULT_TASK_HEIGHT, + DEFAULT_TASK_WIDTH, + DEFAULT_TASK_X, + DEFAULT_TASK_Y, + ) + + fun createTaskInfo( + windowMode: Int, + id: Int = DEFAULT_TASK_ID, + uid: Int = DEFAULT_TASK_UID, + taskHeight: Int = DEFAULT_TASK_HEIGHT, + taskWidth: Int = DEFAULT_TASK_WIDTH, + taskX: Int = DEFAULT_TASK_X, + taskY: Int = DEFAULT_TASK_Y, + ) = + ActivityManager.RunningTaskInfo().apply { + taskId = id + userId = uid + configuration.windowConfiguration.apply { + windowingMode = windowMode + positionInParent = Point(taskX, taskY) + bounds.set(Rect(taskX, taskY, taskX + taskWidth, taskY + taskHeight)) + } } - fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change { - val change = Change( - WindowContainerToken(mock( - IWindowContainerToken::class.java)), - mock(SurfaceControl::class.java)) - change.mode = mode - change.taskInfo = taskInfo - return change - } + fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change { + val change = + Change(WindowContainerToken(mock<IWindowContainerToken>()), mock<SurfaceControl>()) + change.mode = mode + change.taskInfo = taskInfo + return change } -}
\ No newline at end of file + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 310ccc252469..6612aee0cd12 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -119,54 +119,91 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun isOnlyActiveTask_noActiveTasks() { - // Not an active task - assertThat(repo.isOnlyActiveTask(1)).isFalse() + fun isOnlyVisibleNonClosingTask_noTasks() { + // No visible tasks + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() + assertThat(repo.isClosingTask(1)).isFalse() } @Test - fun isOnlyActiveTask_singleActiveTask() { - repo.addActiveTask(DEFAULT_DISPLAY, 1) - // The only active task - assertThat(repo.isActiveTask(1)).isTrue() - assertThat(repo.isOnlyActiveTask(1)).isTrue() - // Not an active task - assertThat(repo.isActiveTask(99)).isFalse() - assertThat(repo.isOnlyActiveTask(99)).isFalse() + fun isOnlyVisibleNonClosingTask_singleVisibleNonClosingTask() { + repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + + // The only visible task + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isClosingTask(1)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isTrue() + // Not a visible task + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isClosingTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() + } + + @Test + fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() { + repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.addClosingTask(DEFAULT_DISPLAY, 1) + + // A visible task that's closing + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isClosingTask(1)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() + // Not a visible task + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() + } + + @Test + fun isOnlyVisibleNonClosingTask_singleVisibleMinimizedTask() { + repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.minimizeTask(DEFAULT_DISPLAY, 1) + + // The visible task that's closing + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isMinimizedTask(1)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() + // Not a visible task + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() } @Test - fun isOnlyActiveTask_multipleActiveTasks() { - repo.addActiveTask(DEFAULT_DISPLAY, 1) - repo.addActiveTask(DEFAULT_DISPLAY, 2) + fun isOnlyVisibleNonClosingTask_multipleVisibleNonClosingTasks() { + repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) + // Not the only task - assertThat(repo.isActiveTask(1)).isTrue() - assertThat(repo.isOnlyActiveTask(1)).isFalse() + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isClosingTask(1)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() // Not the only task - assertThat(repo.isActiveTask(2)).isTrue() - assertThat(repo.isOnlyActiveTask(2)).isFalse() - // Not an active task - assertThat(repo.isActiveTask(99)).isFalse() - assertThat(repo.isOnlyActiveTask(99)).isFalse() + assertThat(repo.isVisibleTask(2)).isTrue() + assertThat(repo.isClosingTask(2)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse() + // Not a visible task + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isClosingTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() } @Test - fun isOnlyActiveTask_multipleDisplays() { - repo.addActiveTask(DEFAULT_DISPLAY, 1) - repo.addActiveTask(DEFAULT_DISPLAY, 2) - repo.addActiveTask(SECOND_DISPLAY, 3) + fun isOnlyVisibleNonClosingTask_multipleDisplays() { + repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 3, visible = true) + // Not the only task on DEFAULT_DISPLAY - assertThat(repo.isActiveTask(1)).isTrue() - assertThat(repo.isOnlyActiveTask(1)).isFalse() + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() // Not the only task on DEFAULT_DISPLAY - assertThat(repo.isActiveTask(2)).isTrue() - assertThat(repo.isOnlyActiveTask(2)).isFalse() - // The only active task on SECOND_DISPLAY - assertThat(repo.isActiveTask(3)).isTrue() - assertThat(repo.isOnlyActiveTask(3)).isTrue() - // Not an active task - assertThat(repo.isActiveTask(99)).isFalse() - assertThat(repo.isOnlyActiveTask(99)).isFalse() + assertThat(repo.isVisibleTask(2)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse() + // The only visible task on SECOND_DISPLAY + assertThat(repo.isVisibleTask(3)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(3)).isTrue() + // Not a visible task + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt new file mode 100644 index 000000000000..518c00d377ad --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_FROM_OVERVIEW +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.KEYBOARD_SHORTCUT +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.TASK_DRAG +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test class for [DesktopModeTransitionTypes] + * + * Usage: atest WMShellUnitTests:DesktopModeTransitionTypesTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopModeTransitionTypesTest { + + @Test + fun testGetEnterTransitionType() { + assertThat(UNKNOWN.getEnterTransitionType()).isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN) + assertThat(APP_HANDLE_MENU_BUTTON.getEnterTransitionType()) + .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON) + assertThat(APP_FROM_OVERVIEW.getEnterTransitionType()) + .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW) + assertThat(TASK_DRAG.getEnterTransitionType()) + .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN) + assertThat(KEYBOARD_SHORTCUT.getEnterTransitionType()) + .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT) + } + + @Test + fun testGetExitTransitionType() { + assertThat(UNKNOWN.getExitTransitionType()).isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN) + assertThat(APP_HANDLE_MENU_BUTTON.getExitTransitionType()) + .isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON) + assertThat(APP_FROM_OVERVIEW.getExitTransitionType()) + .isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN) + assertThat(TASK_DRAG.getExitTransitionType()).isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG) + assertThat(KEYBOARD_SHORTCUT.getExitTransitionType()) + .isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index cf6cea2b34a7..14fa0f1a338d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -24,6 +24,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.content.ComponentName import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.CONFIG_DENSITY @@ -44,6 +45,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl import android.view.WindowManager import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT @@ -76,6 +78,7 @@ import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.common.split.SplitScreenConstants import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask @@ -94,11 +97,14 @@ import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.TestRemoteTransition import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS -import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_DESKTOP_MODE import com.android.wm.shell.transition.Transitions.TransitionHandler import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import java.util.Optional +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import kotlin.test.assertNotNull +import kotlin.test.assertNull import org.junit.After import org.junit.Assume.assumeTrue import org.junit.Before @@ -117,13 +123,11 @@ import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture import org.mockito.quality.Strictness -import junit.framework.Assert.assertFalse -import junit.framework.Assert.assertTrue -import org.mockito.Mockito.`when` as whenever /** * Test class for {@link DesktopTasksController} @@ -134,1725 +138,2057 @@ import org.mockito.Mockito.`when` as whenever @RunWith(AndroidTestingRunner::class) class DesktopTasksControllerTest : ShellTestCase() { - @JvmField - @Rule - val setFlagsRule = SetFlagsRule() - - @Mock lateinit var testExecutor: ShellExecutor - @Mock lateinit var shellCommandHandler: ShellCommandHandler - @Mock lateinit var shellController: ShellController - @Mock lateinit var displayController: DisplayController - @Mock lateinit var displayLayout: DisplayLayout - @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer - @Mock lateinit var syncQueue: SyncTransactionQueue - @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer - @Mock lateinit var transitions: Transitions - @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler - @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler - @Mock lateinit var toggleResizeDesktopTaskTransitionHandler: - ToggleResizeDesktopTaskTransitionHandler - @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler - @Mock lateinit var launchAdjacentController: LaunchAdjacentController - @Mock lateinit var splitScreenController: SplitScreenController - @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler - @Mock lateinit var dragAndDropController: DragAndDropController - @Mock lateinit var multiInstanceHelper: MultiInstanceHelper - @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver - @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator - @Mock lateinit var recentTasksController: RecentTasksController - - private lateinit var mockitoSession: StaticMockitoSession - private lateinit var controller: DesktopTasksController - private lateinit var shellInit: ShellInit - private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository - private lateinit var desktopTasksLimiter: DesktopTasksLimiter - private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener - - private val shellExecutor = TestShellExecutor() - - // Mock running tasks are registered here so we can get the list from mock shell task organizer - private val runningTasks = mutableListOf<RunningTaskInfo>() - - private val DISPLAY_DIMENSION_SHORT = 1600 - private val DISPLAY_DIMENSION_LONG = 2560 - private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400) - private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240) - private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880) - private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400) - private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861) - private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400) - - @Before - fun setUp() { - mockitoSession = mockitoSession().strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java).startMocking() - whenever(DesktopModeStatus.isEnabled()).thenReturn(true) - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - - shellInit = spy(ShellInit(testExecutor)) - desktopModeTaskRepository = DesktopModeTaskRepository() - desktopTasksLimiter = - DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer) - - whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } - whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } - whenever(enterDesktopTransitionHandler.moveToDesktop(any())).thenAnswer { Binder() } - whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) - whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> - (i.arguments.first() as Rect).set(STABLE_BOUNDS) - } - - val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) - - controller = createController() - controller.setSplitScreenController(splitScreenController) - - shellInit.init() - - val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) - verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) - recentsTransitionStateListener = captor.value - } - - private fun createController(): DesktopTasksController { - return DesktopTasksController( - context, - shellInit, - shellCommandHandler, - shellController, - displayController, - shellTaskOrganizer, - syncQueue, - rootTaskDisplayAreaOrganizer, - dragAndDropController, - transitions, - enterDesktopTransitionHandler, - exitDesktopTransitionHandler, - toggleResizeDesktopTaskTransitionHandler, - dragToDesktopTransitionHandler, - desktopModeTaskRepository, - desktopModeLoggerTransitionObserver, - launchAdjacentController, - recentsTransitionHandler, - multiInstanceHelper, - shellExecutor, - Optional.of(desktopTasksLimiter), - recentTasksController - ) - } - - @After - fun tearDown() { - mockitoSession.finishMocking() - - runningTasks.clear() - } - - @Test - fun instantiate_addInitCallback() { - verify(shellInit).addInitCallback(any(), any<DesktopTasksController>()) - } - - @Test - fun instantiate_flagOff_doNotAddInitCallback() { - whenever(DesktopModeStatus.isEnabled()).thenReturn(false) - clearInvocations(shellInit) - - createController() - - verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>()) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskHidden(task1) - markTaskHidden(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskHidden(task1) - markTaskHidden(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: wallpaper intent, task1, task2 - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskVisible(task1) - markTaskVisible(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskVisible(task1) - markTaskVisible(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: wallpaper intent, task1, task2 - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskHidden(task1) - markTaskVisible(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskHidden(task1) - markTaskVisible(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: wallpaper intent, task1, task2 - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(1) - wct.assertReorderAt(index = 0, homeTask) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { - val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) - val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) - setUpHomeTask(SECOND_DISPLAY) - val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) - markTaskHidden(taskDefaultDisplay) - markTaskHidden(taskSecondDisplay) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(2) - // Expect order to be from bottom: home, task - wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) - wct.assertReorderAt(index = 1, taskDefaultDisplay) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { - val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) - setUpHomeTask(SECOND_DISPLAY) - val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) - markTaskHidden(taskDefaultDisplay) - markTaskHidden(taskSecondDisplay) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = - getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(2) - // Expect order to be from bottom: wallpaper intent, task - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - wct.assertReorderAt(index = 1, taskDefaultDisplay) - } - - @Test - fun showDesktopApps_dontReorderMinimizedTask() { - val homeTask = setUpHomeTask() - val freeformTask = setUpFreeformTask() - val minimizedTask = setUpFreeformTask() - markTaskHidden(freeformTask) - markTaskHidden(minimizedTask) - desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct( - type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(2) - // Reorder home and freeform task to top, don't reorder the minimized task - wct.assertReorderAt(index = 0, homeTask, toTop = true) - wct.assertReorderAt(index = 1, freeformTask, toTop = true) - } - - @Test - fun getVisibleTaskCount_noTasks_returnsZero() { - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) - } - - @Test - fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { - setUpHomeTask() - setUpFreeformTask().also(::markTaskVisible) - setUpFreeformTask().also(::markTaskVisible) - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) - } - - @Test - fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() { - setUpHomeTask() - setUpFreeformTask().also(::markTaskVisible) - setUpFreeformTask().also(::markTaskHidden) - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) - } - - @Test - fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { - setUpHomeTask() - setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) - setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) - assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask() - setUpLandscapeDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + @Mock lateinit var testExecutor: ShellExecutor + @Mock lateinit var shellCommandHandler: ShellCommandHandler + @Mock lateinit var shellController: ShellController + @Mock lateinit var displayController: DisplayController + @Mock lateinit var displayLayout: DisplayLayout + @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var syncQueue: SyncTransactionQueue + @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock lateinit var transitions: Transitions + @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler + @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler + @Mock + lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler + @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler + @Mock lateinit var launchAdjacentController: LaunchAdjacentController + @Mock lateinit var splitScreenController: SplitScreenController + @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler + @Mock lateinit var dragAndDropController: DragAndDropController + @Mock lateinit var multiInstanceHelper: MultiInstanceHelper + @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver + @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator + @Mock lateinit var recentTasksController: RecentTasksController + + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var controller: DesktopTasksController + private lateinit var shellInit: ShellInit + private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository + private lateinit var desktopTasksLimiter: DesktopTasksLimiter + private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener + + private val shellExecutor = TestShellExecutor() + + // Mock running tasks are registered here so we can get the list from mock shell task organizer + private val runningTasks = mutableListOf<RunningTaskInfo>() + + private val DISPLAY_DIMENSION_SHORT = 1600 + private val DISPLAY_DIMENSION_LONG = 2560 + private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400) + private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240) + private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880) + private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400) + private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861) + private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400) + + @Before + fun setUp() { + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + whenever(DesktopModeStatus.isEnabled()).thenReturn(true) + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + shellInit = spy(ShellInit(testExecutor)) + desktopModeTaskRepository = DesktopModeTaskRepository() + desktopTasksLimiter = + DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer) + + whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } + whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() } + whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + + val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) + + controller = createController() + controller.setSplitScreenController(splitScreenController) + + shellInit.init() + + val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) + verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) + recentsTransitionStateListener = captor.value + } + + private fun createController(): DesktopTasksController { + return DesktopTasksController( + context, + shellInit, + shellCommandHandler, + shellController, + displayController, + shellTaskOrganizer, + syncQueue, + rootTaskDisplayAreaOrganizer, + dragAndDropController, + transitions, + enterDesktopTransitionHandler, + exitDesktopTransitionHandler, + toggleResizeDesktopTaskTransitionHandler, + dragToDesktopTransitionHandler, + desktopModeTaskRepository, + desktopModeLoggerTransitionObserver, + launchAdjacentController, + recentsTransitionHandler, + multiInstanceHelper, + shellExecutor, + Optional.of(desktopTasksLimiter), + recentTasksController) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + + runningTasks.clear() + } + + @Test + fun instantiate_addInitCallback() { + verify(shellInit).addInitCallback(any(), any<DesktopTasksController>()) + } + + @Test + fun instantiate_flagOff_doNotAddInitCallback() { + whenever(DesktopModeStatus.isEnabled()).thenReturn(false) + clearInvocations(shellInit) + + createController() + + verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>()) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertReorderAt(index = 0, homeTask) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { + val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) + val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(taskDefaultDisplay) + markTaskHidden(taskSecondDisplay) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Expect order to be from bottom: home, task + wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) + wct.assertReorderAt(index = 1, taskDefaultDisplay) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(taskDefaultDisplay) + markTaskHidden(taskSecondDisplay) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Expect order to be from bottom: wallpaper intent, task + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, taskDefaultDisplay) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val minimizedTask = setUpFreeformTask() + + markTaskHidden(freeformTask) + markTaskHidden(minimizedTask) + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Reorder home and freeform task to top, don't reorder the minimized task + wct.assertReorderAt(index = 0, homeTask, toTop = true) + wct.assertReorderAt(index = 1, freeformTask, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { + setUpHomeTask() + val freeformTask = setUpFreeformTask() + val minimizedTask = setUpFreeformTask() + + markTaskHidden(freeformTask) + markTaskHidden(minimizedTask) + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Add desktop wallpaper activity + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + // Reorder freeform task to top, don't reorder the minimized task + wct.assertReorderAt(index = 1, freeformTask, toTop = true) + } + + @Test + fun getVisibleTaskCount_noTasks_returnsZero() { + assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + } + + @Test + fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskVisible) + assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) + } + + @Test + fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskHidden) + assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + } + + @Test + fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { + setUpHomeTask() + setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) + setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) + assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { + val task = setUpFullscreenTask() + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { + val task = + setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { + val task = + setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { + val task = + setUpFullscreenTask( + isResizable = false, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) - setUpLandscapeDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask(isResizable = false, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask(isResizable = false, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) - setUpLandscapeDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT, + setUpLandscapeDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { + val task = + setUpFullscreenTask( + deviceOrientation = ORIENTATION_PORTRAIT, screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true) - setUpPortraitDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask(isResizable = false, + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { + val task = + setUpFullscreenTask( + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + shouldLetterbox = true) + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { + val task = + setUpFullscreenTask( + isResizable = false, deviceOrientation = ORIENTATION_PORTRAIT, screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val task = setUpFullscreenTask(isResizable = false, + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { + val task = + setUpFullscreenTask( + isResizable = false, deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true) - setUpPortraitDisplay() - - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test - fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { - val task = setUpFullscreenTask() - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() { - val task = setUpFullscreenTask() - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun moveToDesktop_nonExistentTask_doesNothing() { - controller.moveToDesktop(999) - verifyWCTNotExecuted() - } - - @Test - fun moveToDesktop_nonRunningTask_launchesInFreeform() { - whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) - - val task = createTaskInfo(1) - - whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - - controller.moveToDesktop(task.taskId) - with(getLatestMoveToDesktopWct()){ - assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) - } - } - - @Test - fun moveToDesktop_topActivityTranslucent_doesNothing() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - val task = setUpFullscreenTask().apply { - isTopActivityTransparent = true - numActivities = 1 - } - - controller.moveToDesktop(task) - verifyWCTNotExecuted() - } - - @Test - fun moveToDesktop_deviceNotSupported_doesNothing() { - val task = setUpFullscreenTask() - - // Simulate non compatible device - doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - - controller.moveToDesktop(task) - verifyWCTNotExecuted() - } - - @Test - fun moveToDesktop_deviceNotSupported_deviceRestrictionsOverridden_taskIsMovedToDesktop() { - val task = setUpFullscreenTask() - - // Simulate non compatible device - doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - - // Simulate enforce device restrictions system property overridden to false - whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false) - - controller.moveToDesktop(task) - - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - fun moveToDesktop_deviceSupported_taskIsMovedToDesktop() { - val task = setUpFullscreenTask() - - controller.moveToDesktop(task) - - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - val freeformTask = setUpFreeformTask() - val fullscreenTask = setUpFullscreenTask() - markTaskHidden(freeformTask) - - controller.moveToDesktop(fullscreenTask) - - with(getLatestMoveToDesktopWct()) { - // Operations should include home task, freeform task - assertThat(hierarchyOps).hasSize(3) - assertReorderSequence(homeTask, freeformTask, fullscreenTask) - assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { - val freeformTask = setUpFreeformTask() - val fullscreenTask = setUpFullscreenTask() - markTaskHidden(freeformTask) - - controller.moveToDesktop(fullscreenTask) - - with(getLatestMoveToDesktopWct()) { - // Operations should include wallpaper intent, freeform task, fullscreen task - assertThat(hierarchyOps).hasSize(3) - assertPendingIntentAt(index = 0, desktopWallpaperIntent) - assertReorderAt(index = 1, freeformTask) - assertReorderAt(index = 2, fullscreenTask) - assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - } - - @Test - fun moveToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { - setUpHomeTask(displayId = DEFAULT_DISPLAY) - val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) - markTaskHidden(freeformTaskDefault) - - val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY) - val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) - markTaskHidden(freeformTaskSecond) - - controller.moveToDesktop(fullscreenTaskDefault) - - with(getLatestMoveToDesktopWct()) { - // Check that hierarchy operations do not include tasks from second display - assertThat(hierarchyOps.map { it.container }) - .doesNotContain(homeTaskSecond.token.asBinder()) - assertThat(hierarchyOps.map { it.container }) - .doesNotContain(freeformTaskSecond.token.asBinder()) - } - } - - @Test - fun moveToDesktop_splitTaskExitsSplit() { - val task = setUpSplitScreenTask() - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController).prepareExitSplitScreen( - any(), - anyInt(), - eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) - ) - } - - @Test - fun moveToDesktop_fullscreenTaskDoesNotExitSplit() { - val task = setUpFullscreenTask() - controller.moveToDesktop(task) - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController, never()).prepareExitSplitScreen( - any(), - anyInt(), - eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) - ) - } - - @Test - fun moveToDesktop_bringsTasksOverLimit_dontShowBackTask() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val homeTask = setUpHomeTask() - val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } - val newTask = setUpFullscreenTask() - - controller.moveToDesktop(newTask) - - val wct = getLatestMoveToDesktopWct() - assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home - wct.assertReorderAt(0, homeTask) - for (i in 1..<taskLimit) { // Skipping freeformTasks[0] - wct.assertReorderAt(index = i, task = freeformTasks[i]) - } - wct.assertReorderAt(taskLimit, newTask) - } - - @Test - fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { - val task = setUpFreeformTask() - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveToFullscreen(task.taskId) - val wct = getLatestExitDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { - val task = setUpFreeformTask() - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - controller.moveToFullscreen(task.taskId) - val wct = getLatestExitDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - } - - @Test - fun moveToFullscreen_nonExistentTask_doesNothing() { - controller.moveToFullscreen(999) - verifyWCTNotExecuted() - } - - @Test - fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() { - val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) - - controller.moveToFullscreen(taskDefaultDisplay.taskId) - - with(getLatestExitDesktopWct()) { - assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder()) - assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder()) - } - } - - @Test - fun moveTaskToFront_postsWctWithReorderOp() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - controller.moveTaskToFront(task1) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT) - assertThat(wct.hierarchyOps).hasSize(1) - wct.assertReorderAt(index = 0, task1) - } - - @Test - fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - setUpHomeTask() - val freeformTasks = (1..taskLimit + 1).map { _ -> setUpFreeformTask() } - - controller.moveTaskToFront(freeformTasks[0]) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT) - assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize - wct.assertReorderAt(0, freeformTasks[0], toTop = true) - wct.assertReorderAt(1, freeformTasks[1], toTop = false) - } - - @Test - fun moveToNextDisplay_noOtherDisplays() { - whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) - val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - controller.moveToNextDisplay(task.taskId) - verifyWCTNotExecuted() - } - - @Test - fun moveToNextDisplay_moveFromFirstToSecondDisplay() { - // Set up two display ids - whenever(rootTaskDisplayAreaOrganizer.displayIds) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) - // Create a mock for the target display area: second display - val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) - .thenReturn(secondDisplayArea) - - val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - controller.moveToNextDisplay(task.taskId) - with(getLatestWct(type = TRANSIT_CHANGE)) { - assertThat(hierarchyOps).hasSize(1) - assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) - assertThat(hierarchyOps[0].isReparent).isTrue() - assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder()) - assertThat(hierarchyOps[0].toTop).isTrue() + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + shouldLetterbox = true) + setUpPortraitDisplay() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { + val task = setUpFullscreenTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() { + val task = setUpFullscreenTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun moveToDesktop_nonExistentTask_doesNothing() { + controller.moveToDesktop(999, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_desktopWallpaperDisabled_nonRunningTask_launchesInFreeform() { + val task = createTaskInfo(1) + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) + + controller.moveToDesktop(task.taskId, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { + val task = createTaskInfo(1) + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) + + controller.moveToDesktop(task.taskId, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Add desktop wallpaper activity + assertPendingIntentAt(index = 0, desktopWallpaperIntent) + // Launch task + assertLaunchTaskAt(index = 1, task.taskId, WINDOWING_MODE_FREEFORM) + } + } + + @Test + fun moveToDesktop_topActivityTranslucent_doesNothing() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + val task = + setUpFullscreenTask().apply { + isTopActivityTransparent = true + numActivities = 1 } - } - @Test - fun moveToNextDisplay_moveFromSecondToFirstDisplay() { - // Set up two display ids - whenever(rootTaskDisplayAreaOrganizer.displayIds) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) - // Create a mock for the target display area: default display - val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) - .thenReturn(defaultDisplayArea) - - val task = setUpFreeformTask(displayId = SECOND_DISPLAY) - controller.moveToNextDisplay(task.taskId) - - with(getLatestWct(type = TRANSIT_CHANGE)) { - assertThat(hierarchyOps).hasSize(1) - assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) - assertThat(hierarchyOps[0].isReparent).isTrue() - assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder()) - assertThat(hierarchyOps[0].toTop).isTrue() + controller.moveToDesktop(task, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + } + + @Test + fun moveToDesktop_systemUIActivity_doesNothing() { + val task = setUpFullscreenTask() + + // Set task as systemUI package + val systemUIPackageName = context.resources.getString( + com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + task.baseActivity = baseComponent + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + } + + @Test + fun moveToDesktop_deviceSupported_taskIsMovedToDesktop() { + val task = setUpFullscreenTask() + + controller.moveToDesktop(task, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveToDesktop(fullscreenTask, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Operations should include home task, freeform task + assertThat(hierarchyOps).hasSize(3) + assertReorderSequence(homeTask, freeformTask, fullscreenTask) + assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveToDesktop(fullscreenTask, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Operations should include wallpaper intent, freeform task, fullscreen task + assertThat(hierarchyOps).hasSize(3) + assertPendingIntentAt(index = 0, desktopWallpaperIntent) + assertReorderAt(index = 1, freeformTask) + assertReorderAt(index = 2, fullscreenTask) + assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + } + + @Test + fun moveToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { + setUpHomeTask(displayId = DEFAULT_DISPLAY) + val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + markTaskHidden(freeformTaskDefault) + + val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY) + val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) + markTaskHidden(freeformTaskSecond) + + controller.moveToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Check that hierarchy operations do not include tasks from second display + assertThat(hierarchyOps.map { it.container }).doesNotContain(homeTaskSecond.token.asBinder()) + assertThat(hierarchyOps.map { it.container }) + .doesNotContain(freeformTaskSecond.token.asBinder()) + } + } + + @Test + fun moveToDesktop_splitTaskExitsSplit() { + val task = setUpSplitScreenTask() + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + verify(splitScreenController) + .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)) + } + + @Test + fun moveToDesktop_fullscreenTaskDoesNotExitSplit() { + val task = setUpFullscreenTask() + controller.moveToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + verify(splitScreenController, never()) + .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() + val homeTask = setUpHomeTask() + + controller.moveToDesktop(newTask, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home + wct.assertReorderAt(0, homeTask) + wct.assertReorderSequenceInRange( + range = 1..<(taskLimit + 1), + *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] + newTask + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() + setUpHomeTask() + + controller.moveToDesktop(newTask, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + wallpaper + // Add desktop wallpaper activity + wct.assertPendingIntentAt(0, desktopWallpaperIntent) + wct.assertReorderSequenceInRange( + range = 1..<(taskLimit + 1), + *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] + newTask + ) + } + + @Test + fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + + @Test + fun moveToFullscreen_nonExistentTask_doesNothing() { + controller.moveToFullscreen(999, transitionSource = UNKNOWN) + verifyExitDesktopWCTNotExecuted() + } + + @Test + fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() { + val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) + + controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN) + + with(getLatestExitDesktopWct()) { + assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder()) + assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder()) + } + } + + @Test + fun moveTaskToFront_postsWctWithReorderOp() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + controller.moveTaskToFront(task1) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertReorderAt(index = 0, task1) + } + + @Test + fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + setUpHomeTask() + val freeformTasks = (1..taskLimit + 1).map { _ -> setUpFreeformTask() } + + controller.moveTaskToFront(freeformTasks[0]) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT) + assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize + wct.assertReorderAt(0, freeformTasks[0], toTop = true) + wct.assertReorderAt(1, freeformTasks[1], toTop = false) + } + + @Test + fun moveToNextDisplay_noOtherDisplays() { + whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + controller.moveToNextDisplay(task.taskId) + verifyWCTNotExecuted() + } + + @Test + fun moveToNextDisplay_moveFromFirstToSecondDisplay() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + controller.moveToNextDisplay(task.taskId) + with(getLatestWct(type = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(1) + assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) + assertThat(hierarchyOps[0].isReparent).isTrue() + assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder()) + assertThat(hierarchyOps[0].toTop).isTrue() + } + } + + @Test + fun moveToNextDisplay_moveFromSecondToFirstDisplay() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: default display + val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultDisplayArea) + + val task = setUpFreeformTask(displayId = SECOND_DISPLAY) + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(1) + assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) + assertThat(hierarchyOps[0].isReparent).isTrue() + assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder()) + assertThat(hierarchyOps[0].toTop).isTrue() + } + } + + @Test + fun getTaskWindowingMode() { + val fullscreenTask = setUpFullscreenTask() + val freeformTask = setUpFreeformTask() + + assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId)) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + assertThat(controller.getTaskWindowingMode(freeformTask.taskId)) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun onDesktopWindowClose_noActiveTasks() { + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = 1) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowClose_singleActiveTask_isClosing() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_isMinimized() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_multipleActiveTasks() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_multipleActiveTasks_isOnlyNonClosingTask() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowClose_multipleActiveTasks_hasMinimized() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + + val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we only reorder the new task to top (we don't reorder the old task to bottom) + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, fullscreenTask, toTop = true) + } + + @Test + fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we reorder the new task to top, and the back task to the bottom + assertThat(wct!!.hierarchyOps.size).isEqualTo(2) + wct.assertReorderAt(0, fullscreenTask, toTop = true) + wct.assertReorderAt(1, freeformTasks[0], toTop = false) + } + + @Test + fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) + createFreeformTask(displayId = SECOND_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay)) + assertThat(result).isNull() + } + + @Test + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val newFreeformTask = createFreeformTask() + + val wct = controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN)) + + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask1 = setUpFreeformTask() + val freeformTask2 = createFreeformTask() + + markTaskHidden(freeformTask1) + val result = + controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + result.assertReorderAt(1, freeformTask2, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask1 = setUpFreeformTask() + val freeformTask2 = createFreeformTask() + + markTaskHidden(freeformTask1) + val result = + controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(3) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring active desktop tasks to front + result.assertReorderAt(1, freeformTask1, toTop = true) + // Bring new task to front + result.assertReorderAt(2, freeformTask2, toTop = true) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, task, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring new task to front + result.assertReorderAt(1, task, toTop = true) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) + // Second display task + createFreeformTask(displayId = SECOND_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, taskDefaultDisplay, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) + // Second display task + createFreeformTask(displayId = SECOND_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring new task to front + result.assertReorderAt(1, taskDefaultDisplay, toTop = true) + } + + @Test + fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false) + + val freeformTask1 = setUpFreeformTask() + markTaskVisible(freeformTask1) + + val freeformTask2 = createFreeformTask() + val result = + controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2)) + assertFalse(result.anyDensityConfigChange(freeformTask2.token)) + } + + @Test + fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true) + + val freeformTask1 = setUpFreeformTask() + markTaskVisible(freeformTask1) + + val freeformTask2 = createFreeformTask() + val result = + controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2)) + assertTrue(result.anyDensityConfigChange(freeformTask2.token)) + } + + @Test + fun handleRequest_notOpenOrToFrontTransition_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .build() + val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE) + val result = controller.handleRequest(Binder(), transition) + assertThat(result).isNull() + } + + @Test + fun handleRequest_noTriggerTask_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull() + } + + @Test + fun handleRequest_triggerTaskNotStandard_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() + assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() + } + + @Test + fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .build() + assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() + } + + @Test + fun handleRequest_recentsAnimationRunning_returnNull() { + // Set up a visible freeform task so a fullscreen task should be converted to freeform + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + // Mark recents animation running + recentsTransitionStateListener.onAnimationStateChanged(true) + + // Open a fullscreen task, check that it does not result in a WCT with changes to it + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_shouldLaunchAsModal_returnSwitchToFullscreenWCT() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + val task = + setUpFreeformTask().apply { + isTopActivityTransparent = true + numActivities = 1 } - } - - @Test - fun getTaskWindowingMode() { - val fullscreenTask = setUpFullscreenTask() - val freeformTask = setUpFreeformTask() - - assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId)) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - assertThat(controller.getTaskWindowingMode(freeformTask.taskId)) - .isEqualTo(WINDOWING_MODE_FREEFORM) - assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun onDesktopWindowClose_noActiveTasks() { - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, 1 /* taskId */) - // Doesn't modify transaction - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() { - val task = setUpFreeformTask() - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, task.taskId) - // Doesn't modify transaction - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken - - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, task.taskId) - // Adds remove wallpaper operation - wct.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun onDesktopWindowClose_multipleActiveTasks() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - val wallpaperToken = MockToken().token() - desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken - - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, task1.taskId) - // Doesn't modify transaction - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - val fullscreenTask = createFullscreenTask() - - val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - val fullscreenTask = createFullscreenTask() - - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - // Make sure we only reorder the new task to top (we don't reorder the old task to bottom) - assertThat(wct?.hierarchyOps?.size).isEqualTo(1) - wct!!.assertReorderAt(0, fullscreenTask, toTop = true) - } - - @Test - fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } - freeformTasks.forEach { markTaskVisible(it) } - val fullscreenTask = createFullscreenTask() - - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - // Make sure we reorder the new task to top, and the back task to the bottom - assertThat(wct!!.hierarchyOps.size).isEqualTo(2) - wct!!.assertReorderAt(0, fullscreenTask, toTop = true) - wct!!.assertReorderAt(1, freeformTasks[0], toTop = false) - } - - @Test - fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val freeformTask = setUpFreeformTask() - markTaskHidden(freeformTask) - val fullscreenTask = createFullscreenTask() - assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() - } - @Test - fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } - val fullscreenTask = createFullscreenTask() - assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() - } - - @Test - fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) - createFreeformTask(displayId = SECOND_DISPLAY) - - val result = - controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay)) - assertThat(result).isNull() - } - - @Test - fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } - freeformTasks.forEach { markTaskVisible(it) } - val newFreeformTask = createFreeformTask() - - val wct = - controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN)) - - assertThat(wct?.hierarchyOps?.size).isEqualTo(1) - wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom - } - - @Test - fun handleRequest_freeformTask_freeformNotVisible_reorderedToTop() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val freeformTask1 = setUpFreeformTask() - markTaskHidden(freeformTask1) - - val freeformTask2 = createFreeformTask() - val result = - controller.handleRequest( - Binder(), - createTransition(freeformTask2, type = TRANSIT_TO_FRONT) - ) - - assertThat(result?.hierarchyOps?.size).isEqualTo(2) - result!!.assertReorderAt(1, freeformTask2, toTop = true) - } - - @Test - fun handleRequest_freeformTask_noOtherTasks_reorderedToTop() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val task = createFreeformTask() - val result = controller.handleRequest(Binder(), createTransition(task)) - - assertThat(result?.hierarchyOps?.size).isEqualTo(1) - result!!.assertReorderAt(0, task, toTop = true) - } - - @Test - fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_reorderedToTop() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) - val taskSecondDisplay = createFreeformTask(displayId = SECOND_DISPLAY) - - val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) - assertThat(result?.hierarchyOps?.size).isEqualTo(1) - result!!.assertReorderAt(0, taskDefaultDisplay, toTop = true) - } - - @Test - fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - whenever(DesktopModeStatus.isDesktopDensityOverrideSet()).thenReturn(false) - - val freeformTask1 = setUpFreeformTask() - markTaskVisible(freeformTask1) - - val freeformTask2 = createFreeformTask() - val result = controller.handleRequest(freeformTask2.token.asBinder(), - createTransition(freeformTask2)) - assertFalse(result.anyDensityConfigChange(freeformTask2.token)) - } - - @Test - fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - whenever(DesktopModeStatus.isDesktopDensityOverrideSet()).thenReturn(true) - - val freeformTask1 = setUpFreeformTask() - markTaskVisible(freeformTask1) - - val freeformTask2 = createFreeformTask() - val result = controller.handleRequest(freeformTask2.token.asBinder(), - createTransition(freeformTask2)) - assertTrue(result.anyDensityConfigChange(freeformTask2.token)) - } - - @Test - fun handleRequest_notOpenOrToFrontTransition_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val task = - TestRunningTaskInfoBuilder() - .setActivityType(ACTIVITY_TYPE_STANDARD) - .setWindowingMode(WINDOWING_MODE_FULLSCREEN) - .build() - val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE) - val result = controller.handleRequest(Binder(), transition) - assertThat(result).isNull() - } - - @Test - fun handleRequest_noTriggerTask_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull() - } - - @Test - fun handleRequest_triggerTaskNotStandard_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() - assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() - } - - @Test - fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val task = - TestRunningTaskInfoBuilder() - .setActivityType(ACTIVITY_TYPE_STANDARD) - .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) - .build() - assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() - } - - @Test - fun handleRequest_recentsAnimationRunning_returnNull() { - // Set up a visible freeform task so a fullscreen task should be converted to freeform - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - - // Mark recents animation running - recentsTransitionStateListener.onAnimationStateChanged(true) - - // Open a fullscreen task, check that it does not result in a WCT with changes to it - val fullscreenTask = createFullscreenTask() - assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() - } - - @Test - fun handleRequest_shouldLaunchAsModal_returnSwitchToFullscreenWCT() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - val task = setUpFreeformTask().apply { - isTopActivityTransparent = true - numActivities = 1 - } - - val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTask_noToken() { - val task = setUpFreeformTask() - val result = - controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - // Doesn't handle request - assertThat(result).isNull() - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() { - desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() - - val task = setUpFreeformTask() - val result = - controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - // Doesn't handle request - assertThat(result).isNull() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperEnabled() { - val wallpaperToken = MockToken().token() - desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken - - val task = setUpFreeformTask() - val result = - controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - assertThat(result).isNotNull() - // Creates remove wallpaper transaction - result!!.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_multipleActiveTasks() { - desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() - - val task1 = setUpFreeformTask() - setUpFreeformTask() - val result = - controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - // Doesn't handle request - assertThat(result).isNull() - } - - @Test - fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() { - val task = setUpFreeformTask() - clearInvocations(launchAdjacentController) - - markTaskVisible(task) - shellExecutor.flushAll() - verify(launchAdjacentController).launchAdjacentEnabled = false - } - - @Test - fun desktopTasksVisibilityChange_invisible_setLaunchAdjacentEnabled() { - val task = setUpFreeformTask() - markTaskVisible(task) - clearInvocations(launchAdjacentController) - - markTaskHidden(task) - shellExecutor.flushAll() - verify(launchAdjacentController).launchAdjacentEnabled = true - } - @Test - fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { - val task1 = setUpFullscreenTask() - val task2 = setUpFullscreenTask() - val task3 = setUpFullscreenTask() - - task1.isFocused = true - task2.isFocused = false - task3.isFocused = false - - controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY) - - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { - val task1 = setUpSplitScreenTask() - val task2 = setUpFullscreenTask() - val task3 = setUpFullscreenTask() - val task4 = setUpSplitScreenTask() - - task1.isFocused = true - task2.isFocused = false - task3.isFocused = false - task4.isFocused = true - - task4.parentTaskId = task1.taskId - - controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY) - - val wct = getLatestMoveToDesktopWct() - assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController).prepareExitSplitScreen( - any(), - anyInt(), - eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE) - ) - } - - @Test - fun moveFocusedTaskToFullscreen() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - - task1.isFocused = false - task2.isFocused = true - task3.isFocused = false - - controller.enterFullscreen(DEFAULT_DISPLAY) - - val wct = getLatestExitDesktopWct() - assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask() - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + @Test + fun handleRequest_systemUIActivity_returnSwitchToFullscreenWCT() { + val task = setUpFreeformTask() + + // Set task as systemUI package + val systemUIPackageName = context.resources.getString( + com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + task.baseActivity = baseComponent + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() { + val task = setUpFreeformTask() + + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() { + val task = setUpFreeformTask() + + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() { + val task = setUpFreeformTask() + + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNotNull(result, "Should handle request") + // Should create remove wallpaper transaction + .assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleActiveTasksFlagDisabled_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleActiveTasksFlagEnabled_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleActiveTasksSingleNonClosing_handlesRequest() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + assertNotNull(result, "Should handle request") + // Should create remove wallpaper transaction + .assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + assertNotNull(result, "Should handle request") + // Should create remove wallpaper transaction + .assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() { + val task = setUpFreeformTask() + + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() { + val task = setUpFreeformTask() + + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() { + val task = setUpFreeformTask() + + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNotNull(result, "Should handle request") + // Should create remove wallpaper transaction + .assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_multipleActiveTasksFlagDisabled_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_multipleActiveTasksFlagEnabled_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_multipleActiveTasksSingleNonClosing_handlesRequest() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNotNull(result, "Should handle request") + // Should create remove wallpaper transaction + .assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNotNull(result, "Should handle request") + // Should create remove wallpaper transaction + .assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() { + val task = setUpFreeformTask() + clearInvocations(launchAdjacentController) + + markTaskVisible(task) + shellExecutor.flushAll() + verify(launchAdjacentController).launchAdjacentEnabled = false + } + + @Test + fun desktopTasksVisibilityChange_invisible_setLaunchAdjacentEnabled() { + val task = setUpFreeformTask() + markTaskVisible(task) + clearInvocations(launchAdjacentController) + + markTaskHidden(task) + shellExecutor.flushAll() + verify(launchAdjacentController).launchAdjacentEnabled = true + } + + @Test + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { + val task1 = setUpFullscreenTask() + val task2 = setUpFullscreenTask() + val task3 = setUpFullscreenTask() + + task1.isFocused = true + task2.isFocused = false + task3.isFocused = false + + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { + val task1 = setUpSplitScreenTask() + val task2 = setUpFullscreenTask() + val task3 = setUpFullscreenTask() + val task4 = setUpSplitScreenTask() + + task1.isFocused = true + task2.isFocused = false + task3.isFocused = false + task4.isFocused = true + + task4.parentTaskId = task1.taskId + + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + verify(splitScreenController) + .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)) + } + + @Test + fun moveFocusedTaskToFullscreen() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask() + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + isResizable = false, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(isResizable = false, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(isResizable = false, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT, + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + deviceOrientation = ORIENTATION_PORTRAIT, screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(isResizable = false, + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + shouldLetterbox = true) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + isResizable = false, deviceOrientation = ORIENTATION_PORTRAIT, screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(isResizable = false, + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + isResizable = false, deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test - fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() { - val task = setUpFreeformTask() - val mockSurface = mock(SurfaceControl::class.java) - val mockDisplayLayout = mock(DisplayLayout::class.java) - whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) - whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) - controller.onDragPositioningMove(task, mockSurface, 200f, - Rect(100, -100, 500, 1000)) - - controller.onDragPositioningEnd(task, - Point(100, -100), /* position */ - PointF(200f, -200f), /* inputCoordinate */ - Rect(100, -100, 500, 1000), /* taskBounds */ - Rect(0, 50, 2000, 2000) /* validDragArea */ - ) - val rectAfterEnd = Rect(100, 50, 500, 1150) - verify(transitions).startTransition( - eq(TRANSIT_CHANGE), Mockito.argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - change.configuration.windowConfiguration.bounds == rectAfterEnd - } - }, eq(null)) - } - - fun enterSplit_freeformTaskIsMovedToSplit() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - - task1.isFocused = false - task2.isFocused = true - task3.isFocused = false - - controller.enterSplit(DEFAULT_DISPLAY, false) - - verify(splitScreenController).requestEnterSplitSelect( - task2, - any(), - SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT, - task2.configuration.windowConfiguration.bounds - ) - } - - @Test - fun toggleBounds_togglesToStableBounds() { - val bounds = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) - - controller.toggleDesktopTaskSize(task) - // Assert bounds set to stable bounds - val wct = getLatestToggleResizeDesktopTaskWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) - } - - @Test - fun toggleBounds_lastBoundsBeforeMaximizeSaved() { - val bounds = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) - - controller.toggleDesktopTaskSize(task) - assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)) - .isEqualTo(bounds) - } - - @Test - fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() { - val boundsBeforeMaximize = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) - - // Maximize - controller.toggleDesktopTaskSize(task) - task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) - - // Restore - controller.toggleDesktopTaskSize(task) - - // Assert bounds set to last bounds before maximize - val wct = getLatestToggleResizeDesktopTaskWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) - } - - @Test - fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() { - val boundsBeforeMaximize = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) - - // Maximize - controller.toggleDesktopTaskSize(task) - task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) - - // Restore - controller.toggleDesktopTaskSize(task) - - // Assert last bounds before maximize removed after use - assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() - } - - private val desktopWallpaperIntent: Intent - get() = Intent(context, DesktopWallpaperActivity::class.java) - - private fun setUpFreeformTask( - displayId: Int = DEFAULT_DISPLAY, - bounds: Rect? = null - ): RunningTaskInfo { - val task = createFreeformTask(displayId, bounds) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - desktopModeTaskRepository.addActiveTask(displayId, task.taskId) - desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) - runningTasks.add(task) - return task - } - - private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createHomeTask(displayId) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - runningTasks.add(task) - return task - } - - private fun setUpFullscreenTask( - displayId: Int = DEFAULT_DISPLAY, - isResizable: Boolean = true, - windowingMode: Int = WINDOWING_MODE_FULLSCREEN, - deviceOrientation: Int = ORIENTATION_LANDSCAPE, - screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, - shouldLetterbox: Boolean = false - ): RunningTaskInfo { - val task = createFullscreenTask(displayId) - val activityInfo = ActivityInfo() - activityInfo.screenOrientation = screenOrientation - with(task) { - topActivityInfo = activityInfo - isResizeable = isResizable - configuration.orientation = deviceOrientation - configuration.windowConfiguration.windowingMode = windowingMode - - if (shouldLetterbox) { - if (deviceOrientation == ORIENTATION_LANDSCAPE && - screenOrientation == SCREEN_ORIENTATION_PORTRAIT) { - // Letterbox to portrait size - appCompatTaskInfo.topActivityBoundsLetterboxed = true - appCompatTaskInfo.topActivityLetterboxWidth = 1200 - appCompatTaskInfo.topActivityLetterboxHeight = 1600 - } else if (deviceOrientation == ORIENTATION_PORTRAIT && - screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) { - // Letterbox to landscape size - appCompatTaskInfo.topActivityBoundsLetterboxed = true - appCompatTaskInfo.topActivityLetterboxWidth = 1600 - appCompatTaskInfo.topActivityLetterboxHeight = 1200 - } - } else { - appCompatTaskInfo.topActivityBoundsLetterboxed = false - } - - if (deviceOrientation == ORIENTATION_LANDSCAPE) { - configuration.windowConfiguration.appBounds = Rect(0, 0, - DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) - } else { - configuration.windowConfiguration.appBounds = Rect(0, 0, - DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) - } - } - whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - runningTasks.add(task) - return task - } - - private fun setUpLandscapeDisplay() { - whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG) - whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT) - } - - private fun setUpPortraitDisplay() { - whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT) - whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG) - } - - private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createSplitScreenTask(displayId) - whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) - whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - runningTasks.add(task) - return task - } - - private fun markTaskVisible(task: RunningTaskInfo) { - desktopModeTaskRepository.updateVisibleFreeformTasks( - task.displayId, - task.taskId, - visible = true - ) - } - - private fun markTaskHidden(task: RunningTaskInfo) { - desktopModeTaskRepository.updateVisibleFreeformTasks( - task.displayId, - task.taskId, - visible = false - ) - } - - private fun getLatestWct( - @WindowManager.TransitionType type: Int = TRANSIT_OPEN, - handlerClass: Class<out TransitionHandler>? = null - ): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - if (handlerClass == null) { - verify(transitions).startTransition(eq(type), arg.capture(), isNull()) - } else { - verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) - } - } else { - verify(shellTaskOrganizer).applyTransaction(arg.capture()) - } - return arg.value - } - - private fun getLatestToggleResizeDesktopTaskWct(): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) - .startTransition(capture(arg)) - } else { - verify(shellTaskOrganizer).applyTransaction(capture(arg)) - } - return arg.value - } - - private fun getLatestMoveToDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture()) - } else { - verify(shellTaskOrganizer).applyTransaction(arg.capture()) - } - return arg.value - } - - private fun getLatestDragToDesktopWct(): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) - } else { - verify(shellTaskOrganizer).applyTransaction(capture(arg)) - } - return arg.value - } - - private fun getLatestExitDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(exitDesktopTransitionHandler) - .startTransition(eq(TRANSIT_EXIT_DESKTOP_MODE), arg.capture(), any(), any()) - } else { - verify(shellTaskOrganizer).applyTransaction(arg.capture()) - } - return arg.value - } - - private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = - wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds - - - private fun verifyWCTNotExecuted() { - if (ENABLE_SHELL_TRANSITIONS) { - verify(transitions, never()).startTransition(anyInt(), any(), isNull()) - } else { - verify(shellTaskOrganizer, never()).applyTransaction(any()) + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + shouldLetterbox = true) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() { + val task = setUpFreeformTask() + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + controller.onDragPositioningMove(task, mockSurface, 200f, Rect(100, -100, 500, 1000)) + + controller.onDragPositioningEnd( + task, + Point(100, -100), /* position */ + PointF(200f, -200f), /* inputCoordinate */ + Rect(100, -100, 500, 1000), /* taskBounds */ + Rect(0, 50, 2000, 2000) /* validDragArea */) + val rectAfterEnd = Rect(100, 50, 500, 1150) + verify(transitions) + .startTransition( + eq(TRANSIT_CHANGE), + Mockito.argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + change.configuration.windowConfiguration.bounds == rectAfterEnd + } + }, + eq(null)) + } + + fun enterSplit_freeformTaskIsMovedToSplit() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + + controller.enterSplit(DEFAULT_DISPLAY, false) + + verify(splitScreenController) + .requestEnterSplitSelect( + task2, + any(), + SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT, + task2.configuration.windowConfiguration.bounds) + } + + @Test + fun toggleBounds_togglesToStableBounds() { + val bounds = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) + + controller.toggleDesktopTaskSize(task) + // Assert bounds set to stable bounds + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) + } + + @Test + fun toggleBounds_lastBoundsBeforeMaximizeSaved() { + val bounds = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) + + controller.toggleDesktopTaskSize(task) + assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds) + } + + @Test + fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) + + // Maximize + controller.toggleDesktopTaskSize(task) + task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) + + // Restore + controller.toggleDesktopTaskSize(task) + + // Assert bounds set to last bounds before maximize + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) + } + + @Test + fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) + + // Maximize + controller.toggleDesktopTaskSize(task) + task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) + + // Restore + controller.toggleDesktopTaskSize(task) + + // Assert last bounds before maximize removed after use + assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() + } + + private val desktopWallpaperIntent: Intent + get() = Intent(context, DesktopWallpaperActivity::class.java) + + private fun setUpFreeformTask( + displayId: Int = DEFAULT_DISPLAY, + bounds: Rect? = null + ): RunningTaskInfo { + val task = createFreeformTask(displayId, bounds) + val activityInfo = ActivityInfo() + task.topActivityInfo = activityInfo + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + desktopModeTaskRepository.addActiveTask(displayId, task.taskId) + desktopModeTaskRepository.updateVisibleFreeformTasks(displayId, task.taskId, visible = true) + desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) + runningTasks.add(task) + return task + } + + private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { + val task = createHomeTask(displayId) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + private fun setUpFullscreenTask( + displayId: Int = DEFAULT_DISPLAY, + isResizable: Boolean = true, + windowingMode: Int = WINDOWING_MODE_FULLSCREEN, + deviceOrientation: Int = ORIENTATION_LANDSCAPE, + screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, + shouldLetterbox: Boolean = false + ): RunningTaskInfo { + val task = createFullscreenTask(displayId) + val activityInfo = ActivityInfo() + activityInfo.screenOrientation = screenOrientation + with(task) { + topActivityInfo = activityInfo + isResizeable = isResizable + configuration.orientation = deviceOrientation + configuration.windowConfiguration.windowingMode = windowingMode + + if (shouldLetterbox) { + if (deviceOrientation == ORIENTATION_LANDSCAPE && + screenOrientation == SCREEN_ORIENTATION_PORTRAIT) { + // Letterbox to portrait size + appCompatTaskInfo.topActivityBoundsLetterboxed = true + appCompatTaskInfo.topActivityLetterboxWidth = 1200 + appCompatTaskInfo.topActivityLetterboxHeight = 1600 + } else if (deviceOrientation == ORIENTATION_PORTRAIT && + screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) { + // Letterbox to landscape size + appCompatTaskInfo.topActivityBoundsLetterboxed = true + appCompatTaskInfo.topActivityLetterboxWidth = 1600 + appCompatTaskInfo.topActivityLetterboxHeight = 1200 } - } - - private fun createTransition( - task: RunningTaskInfo?, - @WindowManager.TransitionType type: Int = TRANSIT_OPEN - ): TransitionRequestInfo { - return TransitionRequestInfo(type, task, null /* remoteTransition */) - } - - companion object { - const val SECOND_DISPLAY = 2 - private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) - } + } else { + appCompatTaskInfo.topActivityBoundsLetterboxed = false + } + + if (deviceOrientation == ORIENTATION_LANDSCAPE) { + configuration.windowConfiguration.appBounds = + Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) + } else { + configuration.windowConfiguration.appBounds = + Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) + } + } + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + private fun setUpLandscapeDisplay() { + whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG) + whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT) + } + + private fun setUpPortraitDisplay() { + whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT) + whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG) + } + + private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { + val task = createSplitScreenTask(displayId) + whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + private fun markTaskVisible(task: RunningTaskInfo) { + desktopModeTaskRepository.updateVisibleFreeformTasks( + task.displayId, task.taskId, visible = true) + } + + private fun markTaskHidden(task: RunningTaskInfo) { + desktopModeTaskRepository.updateVisibleFreeformTasks( + task.displayId, task.taskId, visible = false) + } + + private fun getLatestWct( + @WindowManager.TransitionType type: Int = TRANSIT_OPEN, + handlerClass: Class<out TransitionHandler>? = null + ): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + if (handlerClass == null) { + verify(transitions).startTransition(eq(type), arg.capture(), isNull()) + } else { + verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) + } + } else { + verify(shellTaskOrganizer).applyTransaction(arg.capture()) + } + return arg.value + } + + private fun getLatestToggleResizeDesktopTaskWct(): WindowContainerTransaction { + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()).startTransition(capture(arg)) + } else { + verify(shellTaskOrganizer).applyTransaction(capture(arg)) + } + return arg.value + } + + private fun getLatestEnterDesktopWct(): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) + } else { + verify(shellTaskOrganizer).applyTransaction(arg.capture()) + } + return arg.value + } + + private fun getLatestDragToDesktopWct(): WindowContainerTransaction { + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) + } else { + verify(shellTaskOrganizer).applyTransaction(capture(arg)) + } + return arg.value + } + + private fun getLatestExitDesktopWct(): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) + } else { + verify(shellTaskOrganizer).applyTransaction(arg.capture()) + } + return arg.value + } + + private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = + wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds + + private fun verifyWCTNotExecuted() { + if (ENABLE_SHELL_TRANSITIONS) { + verify(transitions, never()).startTransition(anyInt(), any(), isNull()) + } else { + verify(shellTaskOrganizer, never()).applyTransaction(any()) + } + } + + private fun verifyExitDesktopWCTNotExecuted() { + if (ENABLE_SHELL_TRANSITIONS) { + verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any()) + } else { + verify(shellTaskOrganizer, never()).applyTransaction(any()) + } + } + + private fun verifyEnterDesktopWCTNotExecuted() { + if (ENABLE_SHELL_TRANSITIONS) { + verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any()) + } else { + verify(shellTaskOrganizer, never()).applyTransaction(any()) + } + } + + private fun createTransition( + task: RunningTaskInfo?, + @WindowManager.TransitionType type: Int = TRANSIT_OPEN + ): TransitionRequestInfo { + return TransitionRequestInfo(type, task, null /* remoteTransition */) + } + + companion object { + const val SECOND_DISPLAY = 2 + private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) + } } private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { - assertWithMessage("WCT does not have a hierarchy operation at index $index") - .that(hierarchyOps.size) - .isGreaterThan(index) + assertWithMessage("WCT does not have a hierarchy operation at index $index") + .that(hierarchyOps.size) + .isGreaterThan(index) } private fun WindowContainerTransaction.assertReorderAt( - index: Int, - task: RunningTaskInfo, - toTop: Boolean? = null + index: Int, + task: RunningTaskInfo, + toTop: Boolean? = null ) { - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) - assertThat(op.container).isEqualTo(task.token.asBinder()) - toTop?.let { assertThat(op.toTop).isEqualTo(it) } + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) + assertThat(op.container).isEqualTo(task.token.asBinder()) + toTop?.let { assertThat(op.toTop).isEqualTo(it) } } private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) { - for (i in tasks.indices) { - assertReorderAt(i, tasks[i]) - } + for (i in tasks.indices) { + assertReorderAt(i, tasks[i]) + } +} + +/** Checks if the reorder hierarchy operations in [range] correspond to [tasks] list */ +private fun WindowContainerTransaction.assertReorderSequenceInRange( + range: IntRange, + vararg tasks: RunningTaskInfo +) { + assertThat(hierarchyOps.slice(range).map { it.type to it.container }) + .containsExactlyElementsIn(tasks.map { HIERARCHY_OP_TYPE_REORDER to it.token.asBinder() }) + .inOrder() } private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) { - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) - assertThat(op.container).isEqualTo(token.asBinder()) + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + assertThat(op.container).isEqualTo(token.asBinder()) } private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) { - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) - assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component) + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) + assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component) } private fun WindowContainerTransaction.assertLaunchTaskAt( @@ -1860,23 +2196,26 @@ private fun WindowContainerTransaction.assertLaunchTaskAt( taskId: Int, windowingMode: Int ) { - val keyLaunchWindowingMode = "android.activity.windowingMode" - - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_LAUNCH_TASK) - assertThat(op.launchOptions?.getInt(LAUNCH_KEY_TASK_ID)).isEqualTo(taskId) - assertThat(op.launchOptions?.getInt(keyLaunchWindowingMode, WINDOWING_MODE_UNDEFINED)) - .isEqualTo(windowingMode) + val keyLaunchWindowingMode = "android.activity.windowingMode" + + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_LAUNCH_TASK) + assertThat(op.launchOptions?.getInt(LAUNCH_KEY_TASK_ID)).isEqualTo(taskId) + assertThat(op.launchOptions?.getInt(keyLaunchWindowingMode, WINDOWING_MODE_UNDEFINED)) + .isEqualTo(windowingMode) } + private fun WindowContainerTransaction?.anyDensityConfigChange( token: WindowContainerToken ): Boolean { - return this?.changes?.any { change -> - change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0) - } ?: false -} -private fun createTaskInfo(id: Int) = RecentTaskInfo().apply { - taskId = id - token = WindowContainerToken(mock(IWindowContainerToken::class.java)) + return this?.changes?.any { change -> + change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0) + } ?: false } + +private fun createTaskInfo(id: Int) = + RecentTaskInfo().apply { + taskId = id + token = WindowContainerToken(mock(IWindowContainerToken::class.java)) + } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 2ade3fba9b08..bbf523bc40d2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -18,6 +18,8 @@ import androidx.test.filters.SmallTest import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP @@ -48,6 +50,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var transitions: Transitions @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var splitScreenController: SplitScreenController + @Mock private lateinit var dragAnimator: MoveToDesktopAnimator private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } @@ -68,7 +71,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_animateDragWhenReady() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate transition is started. val transition = startDragToDesktopTransition(task, dragAnimator) @@ -90,36 +92,36 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() { - val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() - // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) - - handler.cancelDragToDesktopTransition() + performEarlyCancel(DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL) + verify(transitions) + .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler)) + } - handler.startAnimation( - transition = transition, - info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), - startTransaction = mock(), - finishTransaction = mock(), - finishCallback = {} + @Test + fun startDragToDesktop_cancelledBeforeReady_verifySplitLeftCancel() { + performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT) + verify(splitScreenController).requestEnterSplitSelect( + any(), + any(), + eq(SPLIT_POSITION_TOP_OR_LEFT), + any() ) + } - // Don't even animate the "drag" since it was already cancelled. - verify(dragAnimator, never()).startAnimation() - // Instead, start the cancel transition. - verify(transitions) - .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler)) + @Test + fun startDragToDesktop_cancelledBeforeReady_verifySplitRightCancel() { + performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT) + verify(splitScreenController).requestEnterSplitSelect( + any(), + any(), + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), + any() + ) } @Test fun startDragToDesktop_aborted_finishDropped() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate transition is started. val transition = startDragToDesktopTransition(task, dragAnimator) // But the transition was aborted. @@ -137,14 +139,15 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_aborted_cancelDropped() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate transition is started. val transition = startDragToDesktopTransition(task, dragAnimator) // But the transition was aborted. handler.onTransitionConsumed(transition, aborted = true, mock()) // Attempt to finish the failed drag start. - handler.cancelDragToDesktopTransition() + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) // Should not be attempted and state should be reset. assertFalse(handler.inProgress) @@ -153,7 +156,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_anotherTransitionInProgress_startDropped() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate attempt to start two drag to desktop transitions. startDragToDesktopTransition(task, dragAnimator) @@ -169,39 +171,63 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_startWasReady_cancel() { - val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() - whenever(dragAnimator.position).thenReturn(PointF()) - // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) - handler.startAnimation( - transition = transition, - info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), - startTransaction = mock(), - finishTransaction = mock(), - finishCallback = {} - ) + startDrag() // Then user cancelled after it had already started. - handler.cancelDragToDesktopTransition() + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) // Cancel animation should run since it had already started. verify(dragAnimator).cancelAnimator() } @Test + fun cancelDragToDesktop_splitLeftCancelType_splitRequested() { + startDrag() + + // Then user cancelled it, requesting split. + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT + ) + + // Verify the request went through split controller. + verify(splitScreenController).requestEnterSplitSelect( + any(), + any(), + eq(SPLIT_POSITION_TOP_OR_LEFT), + any() + ) + } + + @Test + fun cancelDragToDesktop_splitRightCancelType_splitRequested() { + startDrag() + + // Then user cancelled it, requesting split. + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT + ) + + // Verify the request went through split controller. + verify(splitScreenController).requestEnterSplitSelect( + any(), + any(), + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), + any() + ) + } + + @Test fun cancelDragToDesktop_startWasNotReady_animateCancel() { val task = createTask() - val dragAnimator = mock<MoveToDesktopAnimator>() // Simulate transition is started and is ready to animate. startDragToDesktopTransition(task, dragAnimator) // Then user cancelled before the transition was ready and animated. - handler.cancelDragToDesktopTransition() + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) // No need to animate the cancel since the start animation couldn't even start. verifyZeroInteractions(dragAnimator) @@ -210,7 +236,9 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_transitionNotInProgress_dropCancel() { // Then cancel is called before the transition was started. - handler.cancelDragToDesktopTransition() + handler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) // Verify cancel is dropped. verify(transitions, never()).startTransition( @@ -233,6 +261,24 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) } + private fun startDrag() { + val task = createTask() + whenever(dragAnimator.position).thenReturn(PointF()) + // Simulate transition is started and is ready to animate. + val transition = startDragToDesktopTransition(task, dragAnimator) + handler.startAnimation( + transition = transition, + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + } + private fun startDragToDesktopTransition( task: RunningTaskInfo, dragAnimator: MoveToDesktopAnimator @@ -250,6 +296,29 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { return token } + private fun performEarlyCancel(cancelState: DragToDesktopTransitionHandler.CancelState) { + val task = createTask() + // Simulate transition is started and is ready to animate. + val transition = startDragToDesktopTransition(task, dragAnimator) + + handler.cancelDragToDesktopTransition(cancelState) + + handler.startAnimation( + transition = transition, + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + // Don't even animate the "drag" since it was already cancelled. + verify(dragAnimator, never()).startAnimation() + } + private fun createTask( @WindowingMode windowingMode: Int = WINDOWING_MODE_FULLSCREEN, isHome: Boolean = false, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java index 0d0a08cb0ffb..b2467e9a62cf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java @@ -21,6 +21,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread; +import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN; + import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -45,6 +47,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -97,18 +100,18 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { @Test public void testTransitExitDesktopModeAnimation() throws Throwable { - final int transitionType = Transitions.TRANSIT_EXIT_DESKTOP_MODE; + final int transitionType = TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN; final int taskId = 1; WindowContainerTransaction wct = new WindowContainerTransaction(); doReturn(mToken).when(mTransitions) .startTransition(transitionType, wct, mExitDesktopTaskTransitionHandler); - mExitDesktopTaskTransitionHandler.startTransition(transitionType, wct, mPoint, - null); + mExitDesktopTaskTransitionHandler.startTransition(DesktopModeTransitionSource.UNKNOWN, + wct, mPoint, null); TransitionInfo.Change change = createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FULLSCREEN); - TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_EXIT_DESKTOP_MODE, change); + TransitionInfo info = createTransitionInfo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN, change); ArrayList<Exception> exceptions = new ArrayList<>(); runOnUiThread(() -> { try { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java index 5880ffb0dce2..72950a8dc139 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java @@ -88,8 +88,11 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void getAnimator_withBounds_returnBoundsAnimator() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue1 = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mTaskInfo, mLeash, new Rect(), new Rect(), new Rect(), null, + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); assertEquals("Expect ANIM_TYPE_BOUNDS animation", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java index 974539f23b80..aa2d6f09508f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java @@ -241,16 +241,16 @@ public class TvPipGravityTest extends ShellTestCase { @Test public void updateGravity_move_expanded_valid() { - mTvPipBoundsState.setTvPipExpanded(true); - // Vertical expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, true); moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, true); // Horizontal expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, true); moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, true); @@ -281,10 +281,9 @@ public class TvPipGravityTest extends ShellTestCase { @Test public void updateGravity_move_expanded_invalid() { - mTvPipBoundsState.setTvPipExpanded(true); - // Vertical expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false); moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false); @@ -297,6 +296,7 @@ public class TvPipGravityTest extends ShellTestCase { // Horizontal expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false); moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 56c4ceacc8ab..5c5a1a26f5d2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -113,6 +113,8 @@ public class RecentTasksControllerTest extends ShellTestCase { private DisplayInsetsController mDisplayInsetsController; @Mock private IRecentTasksListener mRecentTasksListener; + @Mock + private TaskStackTransitionObserver mTaskStackTransitionObserver; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -139,7 +141,8 @@ public class RecentTasksControllerTest extends ShellTestCase { mDisplayInsetsController, mMainExecutor)); mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit, mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager, - Optional.of(mDesktopModeTaskRepository), mMainExecutor); + Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver, + mMainExecutor); mRecentTasksController = spy(mRecentTasksControllerReal); mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler, null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController), @@ -396,7 +399,7 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test - public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() { + public void testGetRecentTasks_proto2Enabled_includesMinimizedFreeformTasks() { ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); @@ -412,8 +415,7 @@ public class RecentTasksControllerTest extends ShellTestCase { ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks( MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0); - // 2 freeform tasks should be grouped into one, 1 task should be skipped, 3 total recents - // entries + // 3 freeform tasks should be grouped into one, 2 single tasks, 3 total recents entries assertEquals(3, recentTasks.size()); GroupedRecentTaskInfo freeformGroup = recentTasks.get(0); GroupedRecentTaskInfo singleGroup1 = recentTasks.get(1); @@ -425,9 +427,10 @@ public class RecentTasksControllerTest extends ShellTestCase { assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup2.getType()); // Check freeform group entries - assertEquals(2, freeformGroup.getTaskInfoList().size()); + assertEquals(3, freeformGroup.getTaskInfoList().size()); assertEquals(t1, freeformGroup.getTaskInfoList().get(0)); - assertEquals(t5, freeformGroup.getTaskInfoList().get(1)); + assertEquals(t3, freeformGroup.getTaskInfoList().get(1)); + assertEquals(t5, freeformGroup.getTaskInfoList().get(2)); // Check single entries assertEquals(t2, singleGroup1.getTaskInfo1()); @@ -557,6 +560,30 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + public void onTaskMovedToFront_TaskStackObserverEnabled_triggersOnTaskMovedToFront() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo); + + verify(mRecentTasksListener).onTaskMovedToFront(taskInfo); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + public void onTaskMovedToFront_TaskStackObserverEnabled_doesNotTriggersOnTaskMovedToFront() + throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + + mRecentTasksControllerReal.onTaskMovedToFront(taskInfo); + + verify(mRecentTasksListener, never()).onTaskMovedToFront(any()); + } + + @Test public void getNullSplitBoundsNonSplitTask() { SplitBounds sb = mRecentTasksController.getSplitBoundsForTaskId(3); assertNull("splitBounds should be null for non-split task", sb); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt new file mode 100644 index 000000000000..f9599702e763 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.recents + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.os.IBinder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.IWindowContainerToken +import android.window.TransitionInfo +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.TransitionInfoBuilder +import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat +import dagger.Lazy +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.same +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + + +/** + * Test class for {@link TaskStackTransitionObserver} + * + * Usage: atest WMShellUnitTests:TaskStackTransitionObserverTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TaskStackTransitionObserverTest { + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + @Mock private lateinit var shellInit: ShellInit + @Mock lateinit var testExecutor: ShellExecutor + @Mock private lateinit var transitionsLazy: Lazy<Transitions> + @Mock private lateinit var transitions: Transitions + @Mock private lateinit var mockTransitionBinder: IBinder + + private lateinit var transitionObserver: TaskStackTransitionObserver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + shellInit = Mockito.spy(ShellInit(testExecutor)) + whenever(transitionsLazy.get()).thenReturn(transitions) + transitionObserver = TaskStackTransitionObserver(transitionsLazy, shellInit) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java) + verify(shellInit) + .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver)) + initRunnableCaptor.value.run() + } else { + transitionObserver.onInit() + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun testRegistersObserverAtInit() { + verify(transitions).registerObserver(same(transitionObserver)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun taskCreated_freeformWindow_listenerNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + val change = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(change.taskInfo?.windowingMode) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun taskCreated_fullscreenWindow_listenerNotNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + val change = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(0) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(WindowConfiguration.WINDOWING_MODE_UNDEFINED) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun taskCreated_freeformWindowOnTopOfFreeform_listenerNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + val freeformOpenChange = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val freeformReorderChange = + createChange( + WindowManager.TRANSIT_TO_BACK, + createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0) + .addChange(freeformOpenChange) + .addChange(freeformReorderChange) + .build() + + callOnTransitionReady(transitionInfo) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId) + .isEqualTo(freeformOpenChange.taskInfo?.taskId) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(freeformOpenChange.taskInfo?.windowingMode) + } + + class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener { + var taskInfoToBeNotified = ActivityManager.RunningTaskInfo() + + override fun onTaskMovedToFrontThroughTransition( + taskInfo: ActivityManager.RunningTaskInfo + ) { + taskInfoToBeNotified = taskInfo + } + } + + /** Simulate calling the onTransitionReady() method */ + private fun callOnTransitionReady(transitionInfo: TransitionInfo) { + val startT = Mockito.mock(SurfaceControl.Transaction::class.java) + val finishT = Mockito.mock(SurfaceControl.Transaction::class.java) + + transitionObserver.onTransitionReady(mockTransitionBinder, transitionInfo, startT, finishT) + } + + /** Simulate calling the onTransitionFinished() method */ + private fun callOnTransitionFinished() { + transitionObserver.onTransitionFinished(mockTransitionBinder, false) + } + + companion object { + fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo { + val taskInfo = ActivityManager.RunningTaskInfo() + taskInfo.taskId = taskId + taskInfo.configuration.windowConfiguration.windowingMode = windowingMode + + return taskInfo + } + + fun createChange( + mode: Int, + taskInfo: ActivityManager.RunningTaskInfo + ): TransitionInfo.Change { + val change = + TransitionInfo.Change( + WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)), + Mockito.mock(SurfaceControl::class.java) + ) + change.mode = mode + change.taskInfo = taskInfo + return change + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 964d86e8bd35..69a61eadf61d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -1192,7 +1192,8 @@ public class ShellTransitionTests extends ShellTestCase { mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); final RecentsTransitionHandler recentsHandler = new RecentsTransitionHandler(shellInit, transitions, - mock(RecentTasksController.class), mock(HomeTransitionObserver.class)); + mock(RecentTasksController.class), mock(HomeTransitionObserver.class), + () -> mock(SurfaceControl.Transaction.class)); transitions.replaceDefaultHandlerForTest(mDefaultHandler); shellInit.init(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 9c1dc22bcef2..ca1e3f173e24 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -22,7 +22,9 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.content.ComponentName import android.content.Context +import android.content.pm.ActivityInfo import android.graphics.Rect import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay @@ -341,7 +343,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test - fun testDescorationIsNotCreatedForTopTranslucentActivities() { + fun testDecorationIsNotCreatedForTopTranslucentActivities() { setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply { isTopActivityTransparent = true @@ -354,6 +356,22 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + fun testDecorationIsNotCreatedForSystemUIActivities() { + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) + + // Set task as systemUI package + val systemUIPackageName = context.resources.getString( + com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + task.baseActivity = baseComponent + + onTaskOpening(task) + + verify(mockDesktopModeWindowDecorFactory, never()) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + } + + @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) @@ -522,7 +540,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { displayId: Int = DEFAULT_DISPLAY, @WindowConfiguration.WindowingMode windowingMode: Int, activityType: Int = ACTIVITY_TYPE_STANDARD, - focused: Boolean = true + focused: Boolean = true, + activityInfo: ActivityInfo = ActivityInfo() ): RunningTaskInfo { return TestRunningTaskInfoBuilder() .setDisplayId(displayId) @@ -530,6 +549,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { .setVisible(true) .setActivityType(activityType) .build().apply { + topActivityInfo = activityInfo isFocused = focused } } 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 a731e5394bdf..46c158908226 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 @@ -22,11 +22,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,7 +40,7 @@ import android.app.ActivityManager; import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; -import android.content.res.Configuration; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Handler; @@ -45,15 +50,22 @@ import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; +import android.view.AttachedSurfaceControl; import android.view.Choreographer; import android.view.Display; +import android.view.GestureDetector; +import android.view.InsetsState; +import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.View; import android.view.WindowManager; import android.window.WindowContainerTransaction; +import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.R; import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; @@ -62,14 +74,18 @@ import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; +import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.quality.Strictness; import java.util.function.Supplier; @@ -106,18 +122,26 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private Supplier<SurfaceControl.Transaction> mMockTransactionSupplier; @Mock - private SurfaceControl.Transaction mMockTransaction; - @Mock private SurfaceControl mMockSurfaceControl; @Mock private SurfaceControlViewHost mMockSurfaceControlViewHost; @Mock + private AttachedSurfaceControl mMockRootSurfaceControl; + @Mock private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory; @Mock private TypedArray mMockRoundedCornersRadiusArray; - private final Configuration mConfiguration = new Configuration(); + @Mock + private TestTouchEventListener mMockTouchEventListener; + @Mock + private DesktopModeWindowDecoration.ExclusionRegionListener mMockExclusionRegionListener; + @Mock + private PackageManager mMockPackageManager; + private final InsetsState mInsetsState = new InsetsState(); + private SurfaceControl.Transaction mMockTransaction; + private StaticMockitoSession mMockitoSession; private TestableContext mTestableContext; /** Set up run before test class. */ @@ -131,11 +155,29 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Before public void setUp() { + mMockitoSession = mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus.class) + .startMocking(); + when(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false); doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory).create( any(), any(), any()); + when(mMockSurfaceControlViewHost.getRootSurfaceControl()) + .thenReturn(mMockRootSurfaceControl); + mMockTransaction = createMockSurfaceControlTransaction(); doReturn(mMockTransaction).when(mMockTransactionSupplier).get(); mTestableContext = new TestableContext(mContext); mTestableContext.ensureTestableResources(); + mContext.setMockPackageManager(mMockPackageManager); + when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel"); + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); + doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt()); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); } @Test @@ -206,6 +248,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Test @DisableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY) public void updateRelayoutParams_appHeader_usesSystemDensity() { + when(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true); final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources() .getConfiguration().densityDpi; final int customTaskDensity = systemDensity + 300; @@ -323,6 +366,99 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue(); } + @Test + public void relayout_fullscreenTask_appliesTransactionImmediately() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + spyWindowDecor.relayout(taskInfo); + + verify(mMockTransaction).apply(); + verify(mMockRootSurfaceControl, never()).applyTransactionOnDraw(any()); + } + + @Test + public void relayout_freeformTask_appliesTransactionOnDraw() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) + taskInfo.isResizeable = false; + + spyWindowDecor.relayout(taskInfo); + + verify(mMockTransaction, never()).apply(); + verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction); + } + + @Test + public void relayout_fullscreenTask_doesNotCreateViewHostImmediately() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + spyWindowDecor.relayout(taskInfo); + + verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any()); + } + + @Test + public void relayout_fullscreenTask_postsViewHostCreation() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + spyWindowDecor.relayout(taskInfo); + + verify(mMockHandler).post(runnableArgument.capture()); + runnableArgument.getValue().run(); + verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); + } + + @Test + public void relayout_freeformTask_createsViewHostImmediately() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) + taskInfo.isResizeable = false; + + spyWindowDecor.relayout(taskInfo); + + verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); + verify(mMockHandler, never()).post(any()); + } + + @Test + public void relayout_removesExistingHandlerCallback() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + spyWindowDecor.relayout(taskInfo); + verify(mMockHandler).post(runnableArgument.capture()); + + spyWindowDecor.relayout(taskInfo); + + verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); + } + + @Test + public void close_removesExistingHandlerCallback() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + spyWindowDecor.relayout(taskInfo); + verify(mMockHandler).post(runnableArgument.capture()); + + spyWindowDecor.close(); + + verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); + } + private void fillRoundedCornersResources(int fillValue) { when(mMockRoundedCornersRadiusArray.getDimensionPixelSize(anyInt(), anyInt())) .thenReturn(fillValue); @@ -343,12 +479,16 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private DesktopModeWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo) { - return new DesktopModeWindowDecoration(mContext, mMockDisplayController, - mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, + DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, + mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, mMockSurfaceControlViewHostFactory); + windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, + mMockTouchEventListener, mMockTouchEventListener); + windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); + return windowDecor; } private ActivityManager.RunningTaskInfo createTaskInfo(boolean visible) { @@ -373,4 +513,32 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { return (params.mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) != 0; } + + private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener + implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, + View.OnGenericMotionListener, DragDetector.MotionEventHandler { + + @Override + public void onClick(View v) {} + + @Override + public boolean onGenericMotion(View v, MotionEvent event) { + return false; + } + + @Override + public boolean onLongClick(View v) { + return false; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + return false; + } + + @Override + public boolean handleMotionEvent(@Nullable View v, MotionEvent ev) { + return false; + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt index e6fabcfec58a..86aded76c0f3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt @@ -16,25 +16,34 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager +import android.content.Context +import android.content.res.Resources import android.graphics.PointF import android.graphics.Rect import android.os.IBinder +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display import android.window.WindowContainerToken +import com.android.window.flags.Flags +import com.android.wm.shell.R import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.shared.DesktopModeStatus 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.google.common.truth.Truth.assertThat import junit.framework.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.any +import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations /** @@ -47,17 +56,32 @@ import org.mockito.MockitoAnnotations class DragPositioningCallbackUtilityTest { @Mock private lateinit var mockWindowDecoration: WindowDecoration<*> + @Mock private lateinit var taskToken: WindowContainerToken + @Mock private lateinit var taskBinder: IBinder + @Mock private lateinit var mockDisplayController: DisplayController + @Mock private lateinit var mockDisplayLayout: DisplayLayout + @Mock private lateinit var mockDisplay: Display + @Mock + private lateinit var mockContext: Context + + @Mock + private lateinit var mockResources: Resources + + @JvmField + @Rule + val setFlagsRule = SetFlagsRule() + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -69,16 +93,15 @@ class DragPositioningCallbackUtilityTest { (i.arguments.first() as Rect).set(STABLE_BOUNDS) } - mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { - taskId = TASK_ID - token = taskToken - minWidth = MIN_WIDTH - minHeight = MIN_HEIGHT - defaultMinSize = DEFAULT_MIN - displayId = DISPLAY_ID - configuration.windowConfiguration.setBounds(STARTING_BOUNDS) - } + initializeTaskInfo() mockWindowDecoration.mDisplay = mockDisplay + mockWindowDecoration.mDecorWindowContext = mockContext + whenever(mockContext.getResources()).thenReturn(mockResources) + whenever(mockWindowDecoration.mDecorWindowContext.resources).thenReturn(mockResources) + whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_width)) + .thenReturn(DESKTOP_MODE_MIN_WIDTH) + whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_height)) + .thenReturn(DESKTOP_MODE_MIN_HEIGHT) whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } } @@ -93,8 +116,8 @@ class DragPositioningCallbackUtilityTest { val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) @@ -113,8 +136,8 @@ class DragPositioningCallbackUtilityTest { val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 5) @@ -127,14 +150,14 @@ class DragPositioningCallbackUtilityTest { val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) val repositionTaskBounds = Rect(STARTING_BOUNDS) - // Resize to width of 95px and width of -5px with minimum of 10px + // Resize to width of 95px and height of -5px with minimum of 10px val newX = STARTING_BOUNDS.right.toFloat() - 5 val newY = STARTING_BOUNDS.top.toFloat() + 105 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) @@ -153,8 +176,8 @@ class DragPositioningCallbackUtilityTest { val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 80) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80) @@ -172,8 +195,8 @@ class DragPositioningCallbackUtilityTest { val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) @@ -196,14 +219,13 @@ class DragPositioningCallbackUtilityTest { assertThat(repositionTaskBounds.left).isEqualTo(validDragArea.left) assertThat(repositionTaskBounds.top).isEqualTo(validDragArea.bottom) assertThat(repositionTaskBounds.right) - .isEqualTo(validDragArea.left + STARTING_BOUNDS.width()) + .isEqualTo(validDragArea.left + STARTING_BOUNDS.width()) assertThat(repositionTaskBounds.bottom) - .isEqualTo(validDragArea.bottom + STARTING_BOUNDS.height()) + .isEqualTo(validDragArea.bottom + STARTING_BOUNDS.height()) } @Test fun testChangeBounds_toDisallowedBounds_freezesAtLimit() { - var hasMoved = false val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) val repositionTaskBounds = Rect(STARTING_BOUNDS) @@ -212,32 +234,177 @@ class DragPositioningCallbackUtilityTest { var newY = STARTING_BOUNDS.bottom.toFloat() + 10 var delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration)) - hasMoved = true + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration)) // Resize width to 120px, height to disallowed area which should not result in a change. newX += 10 newY = DISALLOWED_RESIZE_AREA.top.toFloat() delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, - mockDisplayController, mockWindowDecoration)) + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration)) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right + 20) assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom + 10) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeLessThanMin_shouldNotChangeBounds() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true) + initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) + val startingPoint = + PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + // Shrink height and width to 1px. The default allowed width and height are defined in + // R.dimen.desktop_mode_minimum_window_width and R.dimen.desktop_mode_minimum_window_height + val newX = STARTING_BOUNDS.right.toFloat() - 99 + val newY = STARTING_BOUNDS.bottom.toFloat() - 99 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeAllowedSize_shouldChangeBounds() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true) + initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) + val startingPoint = + PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + // Shrink height and width to 20px. The default allowed width and height are defined in + // R.dimen.desktop_mode_minimum_window_width and R.dimen.desktop_mode_minimum_window_height + val newX = STARTING_BOUNDS.right.toFloat() - 80 + val newY = STARTING_BOUNDS.bottom.toFloat() - 80 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 80) + } + + @Test + fun taskMinWidthHeightUndefined_changeBoundsLessThanDefaultMinSize_shouldNotChangeBounds() { + initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) + val startingPoint = + PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + // Shrink height and width to 1px. The default allowed width and height are defined in the + // defaultMinSize of the TaskInfo. + val newX = STARTING_BOUNDS.right.toFloat() - 99 + val newY = STARTING_BOUNDS.bottom.toFloat() - 99 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + fun taskMinWidthHeightUndefined_changeBoundsToAnAllowedSize_shouldChangeBounds() { + initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) + val startingPoint = + PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + // Shrink height and width to 50px. The default allowed width and height are defined in the + // defaultMinSize of the TaskInfo. + val newX = STARTING_BOUNDS.right.toFloat() - 50 + val newY = STARTING_BOUNDS.bottom.toFloat() - 50 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 50) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 50) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testChangeBounds_windowSizeExceedsStableBounds_shouldBeAllowedToChangeBounds() { + val startingPoint = + PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(), + OFF_CENTER_STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS) + // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach + // the disallowed drag area. + val offset = 5 + val newX = STABLE_BOUNDS.right.toFloat() - offset + val newY = STABLE_BOUNDS.bottom.toFloat() - offset + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration) + assertThat(repositionTaskBounds.width()).isGreaterThan(STABLE_BOUNDS.right) + assertThat(repositionTaskBounds.height()).isGreaterThan(STABLE_BOUNDS.bottom) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testChangeBoundsInDesktopMode_windowSizeExceedsStableBounds_shouldBeLimitedToDisplaySize() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true) + val startingPoint = + PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(), + OFF_CENTER_STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS) + // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach + // the disallowed drag area. + val offset = 5 + val newX = STABLE_BOUNDS.right.toFloat() - offset + val newY = STABLE_BOUNDS.bottom.toFloat() - offset + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration) + assertThat(repositionTaskBounds.width()).isLessThan(STABLE_BOUNDS.right) + assertThat(repositionTaskBounds.height()).isLessThan(STABLE_BOUNDS.bottom) + } + + private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) { + mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + minWidth = taskMinWidth + minHeight = taskMinHeight + defaultMinSize = DEFAULT_MIN + displayId = DISPLAY_ID + configuration.windowConfiguration.setBounds(STARTING_BOUNDS) + } + } + companion object { private const val TASK_ID = 5 private const val MIN_WIDTH = 10 private const val MIN_HEIGHT = 10 + private const val DESKTOP_MODE_MIN_WIDTH = 20 + private const val DESKTOP_MODE_MIN_HEIGHT = 20 private const val DENSITY_DPI = 20 private const val DEFAULT_MIN = 40 private const val DISPLAY_ID = 1 private const val NAVBAR_HEIGHT = 50 private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val OFF_CENTER_STARTING_BOUNDS = Rect(-100, -100, 10, 10) private val DISALLOWED_RESIZE_AREA = Rect( DISPLAY_BOUNDS.left, DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java index 54645083eca8..4dea5a75a0e8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java @@ -27,10 +27,9 @@ import static com.google.common.truth.Truth.assertThat; import android.annotation.NonNull; import android.graphics.Point; import android.graphics.Region; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.Size; @@ -74,7 +73,7 @@ public class DragResizeWindowGeometryTests { TASK_SIZE.getHeight() + EDGE_RESIZE_THICKNESS / 2); @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); /** * Check that both groups of objects satisfy equals/hashcode within each group, and that each @@ -144,10 +143,11 @@ public class DragResizeWindowGeometryTests { /** * Validate that with the flag enabled, the corner resize regions are the largest size, to - * capture all eligible input regardless of source (touch or cursor). + * capture all eligible input regardless of source (touchscreen or cursor). + * <p>Note that capturing input does not necessarily mean that the event will be handled. */ @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testRegionUnion_edgeDragResizeEnabled_containsLargeCorners() { Region region = new Region(); GEOMETRY.union(region); @@ -164,7 +164,7 @@ public class DragResizeWindowGeometryTests { * size. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testRegionUnion_edgeDragResizeDisabled_containsFineCorners() { Region region = new Region(); GEOMETRY.union(region); @@ -176,74 +176,114 @@ public class DragResizeWindowGeometryTests { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeEnabled_edges() { - // The input source (touch or cursor) shouldn't impact the edge resize size. - validateCtrlTypeForEdges(/* isTouch= */ false); - validateCtrlTypeForEdges(/* isTouch= */ true); + // The input source (touchscreen or cursor) shouldn't impact the edge resize size. + validateCtrlTypeForEdges(/* isTouchscreen= */ false, /* isEdgeResizePermitted= */ false); + validateCtrlTypeForEdges(/* isTouchscreen= */ true, /* isEdgeResizePermitted= */ false); + validateCtrlTypeForEdges(/* isTouchscreen= */ false, /* isEdgeResizePermitted= */ true); + validateCtrlTypeForEdges(/* isTouchscreen= */ true, /* isEdgeResizePermitted= */ true); } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeDisabled_edges() { - // Edge resizing is not supported when the flag is disabled. - validateCtrlTypeForEdges(/* isTouch= */ false); - validateCtrlTypeForEdges(/* isTouch= */ false); + // Edge resizing is not supported for touchscreen input when the flag is disabled. + validateCtrlTypeForEdges(/* isTouchscreen= */ false, /* isEdgeResizePermitted= */ true); + validateCtrlTypeForEdges(/* isTouchscreen= */ true, /* isEdgeResizePermitted= */ false); } - private void validateCtrlTypeForEdges(boolean isTouch) { - assertThat(GEOMETRY.calculateCtrlType(isTouch, LEFT_EDGE_POINT.x, - LEFT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_LEFT); - assertThat(GEOMETRY.calculateCtrlType(isTouch, TOP_EDGE_POINT.x, - TOP_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_TOP); - assertThat(GEOMETRY.calculateCtrlType(isTouch, RIGHT_EDGE_POINT.x, - RIGHT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_RIGHT); - assertThat(GEOMETRY.calculateCtrlType(isTouch, BOTTOM_EDGE_POINT.x, - BOTTOM_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_BOTTOM); + private void validateCtrlTypeForEdges(boolean isTouchscreen, boolean isEdgeResizePermitted) { + assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + LEFT_EDGE_POINT.x, LEFT_EDGE_POINT.y)).isEqualTo( + isEdgeResizePermitted ? CTRL_TYPE_LEFT : CTRL_TYPE_UNDEFINED); + assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + TOP_EDGE_POINT.x, TOP_EDGE_POINT.y)).isEqualTo( + isEdgeResizePermitted ? CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + RIGHT_EDGE_POINT.x, RIGHT_EDGE_POINT.y)).isEqualTo( + isEdgeResizePermitted ? CTRL_TYPE_RIGHT : CTRL_TYPE_UNDEFINED); + assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + BOTTOM_EDGE_POINT.x, BOTTOM_EDGE_POINT.y)).isEqualTo( + isEdgeResizePermitted ? CTRL_TYPE_BOTTOM : CTRL_TYPE_UNDEFINED); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeEnabled_corners() { final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); // When the flag is enabled, points within fine corners should pass regardless of touch or // not. Points outside fine corners should not pass when using a course input (non-touch). - fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true); - fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, true); - fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, true); - fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false); + // Edge resizing permitted (events from stylus/cursor) should have no impact on corners. + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, true); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ false, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ false, true); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ false, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ false, false); // When the flag is enabled, points near the large corners should only pass when the point // is within the corner for large touch inputs. - largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true); - largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, - false); - largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false); - largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, - false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, true); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeDisabled_corners() { final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); - // When the flag is disabled, points within fine corners should pass only when touch. - fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true); - fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, false); - fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false); - fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false); + // When the flag is disabled, points within fine corners should pass only from touchscreen. + // Edge resize permitted (indicating the event is from a cursor/stylus) should have no + // impact. + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, false); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ false, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ false, false); + + // Points within fine corners should never pass when not from touchscreen; expect edge + // resizing only. + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ false, false); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ false, false); // When the flag is disabled, points near the large corners should never pass. - largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, false); - largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, - false); - largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false); - largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, - false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + true, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */ + false, /* isEdgeResizePermitted= */ true, false); } /** @@ -306,19 +346,20 @@ public class DragResizeWindowGeometryTests { * {@code @DragPositioningCallback.CtrlType}. */ public void validateCtrlTypeForInnerPoints(@NonNull DragResizeWindowGeometry geometry, - boolean isTouch, boolean expectedWithinGeometry) { - assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPoint.x, - mTopLeftPoint.y)).isEqualTo( + boolean isTouchscreen, boolean isEdgeResizePermitted, + boolean expectedWithinGeometry) { + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mTopLeftPoint.x, mTopLeftPoint.y)).isEqualTo( expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); - assertThat(geometry.calculateCtrlType(isTouch, mTopRightPoint.x, - mTopRightPoint.y)).isEqualTo( + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mTopRightPoint.x, mTopRightPoint.y)).isEqualTo( expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); - assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPoint.x, - mBottomLeftPoint.y)).isEqualTo( + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mBottomLeftPoint.x, mBottomLeftPoint.y)).isEqualTo( expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM : CTRL_TYPE_UNDEFINED); - assertThat(geometry.calculateCtrlType(isTouch, mBottomRightPoint.x, - mBottomRightPoint.y)).isEqualTo( + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mBottomRightPoint.x, mBottomRightPoint.y)).isEqualTo( expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM : CTRL_TYPE_UNDEFINED); } @@ -328,19 +369,20 @@ public class DragResizeWindowGeometryTests { * {@code @DragPositioningCallback.CtrlType}. */ public void validateCtrlTypeForOutsidePoints(@NonNull DragResizeWindowGeometry geometry, - boolean isTouch, boolean expectedWithinGeometry) { - assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPointOutside.x, - mTopLeftPointOutside.y)).isEqualTo( + boolean isTouchscreen, boolean isEdgeResizePermitted, + boolean expectedWithinGeometry) { + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mTopLeftPointOutside.x, mTopLeftPointOutside.y)).isEqualTo( expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); - assertThat(geometry.calculateCtrlType(isTouch, mTopRightPointOutside.x, - mTopRightPointOutside.y)).isEqualTo( + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mTopRightPointOutside.x, mTopRightPointOutside.y)).isEqualTo( expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); - assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPointOutside.x, - mBottomLeftPointOutside.y)).isEqualTo( + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mBottomLeftPointOutside.x, mBottomLeftPointOutside.y)).isEqualTo( expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM : CTRL_TYPE_UNDEFINED); - assertThat(geometry.calculateCtrlType(isTouch, mBottomRightPointOutside.x, - mBottomRightPointOutside.y)).isEqualTo( + assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + mBottomRightPointOutside.x, mBottomRightPointOutside.y)).isEqualTo( expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM : CTRL_TYPE_UNDEFINED); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt index 9174556d091b..666750485ef2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt @@ -2,6 +2,8 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.content.Context +import android.content.res.Resources import android.graphics.Point import android.graphics.Rect import android.os.IBinder @@ -17,6 +19,7 @@ import android.window.WindowContainerToken import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING import androidx.test.filters.SmallTest +import com.android.wm.shell.R import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController @@ -83,7 +86,10 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { private lateinit var mockTransaction: SurfaceControl.Transaction @Mock private lateinit var mockTransitionBinder: IBinder - + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockResources: Resources private lateinit var taskPositioner: FluidResizeTaskPositioner @Before @@ -119,6 +125,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { } `when`(mockWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA) mockWindowDecoration.mDisplay = mockDisplay + mockWindowDecoration.mDecorWindowContext = mockContext + whenever(mockWindowDecoration.mDecorWindowContext.resources).thenReturn(mockResources) + whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_width)) + .thenReturn(DESKTOP_MODE_MIN_WIDTH) + whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_height)) + .thenReturn(DESKTOP_MODE_MIN_HEIGHT) whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } whenever(mockTransitions.startTransition(anyInt(), any(), any())) .doReturn(mockTransitionBinder) @@ -788,6 +800,8 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { private const val TASK_ID = 5 private const val MIN_WIDTH = 10 private const val MIN_HEIGHT = 10 + private const val DESKTOP_MODE_MIN_WIDTH = 20 + private const val DESKTOP_MODE_MIN_HEIGHT = 20 private const val DENSITY_DPI = 20 private const val DEFAULT_MIN = 40 private const val DISPLAY_ID = 1 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt new file mode 100644 index 000000000000..5582e0f46321 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Rect +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.Display +import android.view.LayoutInflater +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.View +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.wm.shell.R +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT +import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED +import com.android.wm.shell.splitscreen.SplitScreenController +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +/** + * Tests for [HandleMenu]. + * + * Build/Install/Run: + * atest WMShellUnitTests:HandleMenuTest + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class HandleMenuTest : ShellTestCase() { + @JvmField + @Rule + val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Mock + private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration + @Mock + private lateinit var onClickListener: View.OnClickListener + @Mock + private lateinit var onTouchListener: View.OnTouchListener + @Mock + private lateinit var appIcon: Bitmap + @Mock + private lateinit var appName: CharSequence + @Mock + private lateinit var displayController: DisplayController + @Mock + private lateinit var splitScreenController: SplitScreenController + @Mock + private lateinit var displayLayout: DisplayLayout + @Mock + private lateinit var mockSurfaceControlViewHost: SurfaceControlViewHost + + private lateinit var handleMenu: HandleMenu + + @Before + fun setUp() { + val mockAdditionalViewHostViewContainer = AdditionalViewHostViewContainer( + mock(SurfaceControl::class.java), + mockSurfaceControlViewHost, + ) { + SurfaceControl.Transaction() + } + val menuView = LayoutInflater.from(context).inflate( + R.layout.desktop_mode_window_decor_handle_menu, null) + whenever(mockDesktopWindowDecoration.addWindow( + anyInt(), any(), any(), any(), anyInt(), anyInt(), anyInt(), anyInt()) + ).thenReturn(mockAdditionalViewHostViewContainer) + whenever(mockAdditionalViewHostViewContainer.view).thenReturn(menuView) + whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) + whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) + whenever(displayLayout.isLandscape).thenReturn(true) + mockDesktopWindowDecoration.mDecorWindowContext = context + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + fun testFullscreenMenuUsesSystemViewContainer() { + createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED) + val handleMenu = createAndShowHandleMenu() + assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + // Verify menu is created at coordinates that, when added to WindowManager, + // show at the top-center of display. + assertTrue(handleMenu.mHandleMenuPosition.equals(16f, -512f)) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + fun testFreeformMenu_usesViewHostViewContainer() { + createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED) + handleMenu = createAndShowHandleMenu() + assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalViewHostViewContainer) + // Verify menu is created near top-left of task. + assertTrue(handleMenu.mHandleMenuPosition.equals(12f, 8f)) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + fun testSplitLeftMenu_usesSystemViewContainer() { + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT) + handleMenu = createAndShowHandleMenu() + assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + // Verify menu is created at coordinates that, when added to WindowManager, + // show at the top of split left task. + assertTrue(handleMenu.mHandleMenuPosition.equals(-624f, -512f)) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + fun testSplitRightMenu_usesSystemViewContainer() { + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT) + handleMenu = createAndShowHandleMenu() + assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + // Verify menu is created at coordinates that, when added to WindowManager, + // show at the top of split right task. + assertTrue(handleMenu.mHandleMenuPosition.equals(656f, -512f)) + } + + private fun createTaskInfo(windowingMode: Int, splitPosition: Int) { + val taskDescriptionBuilder = ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.YELLOW) + val bounds = when (windowingMode) { + WINDOWING_MODE_FULLSCREEN -> DISPLAY_BOUNDS + WINDOWING_MODE_FREEFORM -> FREEFORM_BOUNDS + WINDOWING_MODE_MULTI_WINDOW -> { + if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { + SPLIT_LEFT_BOUNDS + } else { + SPLIT_RIGHT_BOUNDS + } + } + else -> error("Unsupported windowing mode") + } + mockDesktopWindowDecoration.mTaskInfo = TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setWindowingMode(windowingMode) + .setBounds(bounds) + .setVisible(true) + .build() + // Calculate captionX similar to how WindowDecoration calculates it. + whenever(mockDesktopWindowDecoration.captionX).thenReturn( + (mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration + .bounds.width() - context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_decor_caption_width)) / 2) + whenever(splitScreenController.getSplitPosition(any())).thenReturn(splitPosition) + whenever(splitScreenController.getStageBounds(any(), any())).thenAnswer { + (it.arguments.first() as Rect).set(SPLIT_LEFT_BOUNDS) + } + } + + private fun createAndShowHandleMenu(): HandleMenu { + val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) { + R.layout.desktop_mode_app_header + } else { + R.layout.desktop_mode_app_header + } + val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId, + onClickListener, onTouchListener, appIcon, appName, displayController, + splitScreenController, true /* shouldShowWindowingPill */, + 50 /* captionHeight */ ) + handleMenu.show() + return handleMenu + } + + companion object { + private val DISPLAY_BOUNDS = Rect(0, 0, 2560, 1600) + private val FREEFORM_BOUNDS = Rect(500, 500, 2000, 1200) + private val SPLIT_LEFT_BOUNDS = Rect(0, 0, 1280, 1600) + private val SPLIT_RIGHT_BOUNDS = Rect(1280, 0, 2560, 1600) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt index 5da57c50e6c1..a07be79579eb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt @@ -150,7 +150,7 @@ class ResizeVeilTest : ShellTestCase() { fun showVeil() { val veil = createResizeVeil() - veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) verify(mockTransaction).show(mockResizeVeilSurface) verify(mockTransaction).show(mockBackgroundSurface) @@ -162,7 +162,7 @@ class ResizeVeilTest : ShellTestCase() { fun showVeil_displayUnavailable_doesNotShow() { val veil = createResizeVeil(withDisplayAvailable = false) - veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) verify(mockTransaction, never()).show(mockResizeVeilSurface) verify(mockTransaction, never()).show(mockBackgroundSurface) @@ -174,8 +174,8 @@ class ResizeVeilTest : ShellTestCase() { fun showVeil_alreadyVisible_doesNotShowAgain() { val veil = createResizeVeil() - veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) - veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) verify(mockTransaction, times(1)).show(mockResizeVeilSurface) verify(mockTransaction, times(1)).show(mockBackgroundSurface) @@ -188,7 +188,13 @@ class ResizeVeilTest : ShellTestCase() { val veil = createResizeVeil(parent = mock()) val newParent = mock<SurfaceControl>() - veil.showVeil(mockTransaction, newParent, Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil( + mockTransaction, + newParent, + Rect(0, 0, 100, 100), + taskInfo, + false /* fadeIn */ + ) verify(mockTransaction).reparent(mockResizeVeilSurface, newParent) } @@ -212,11 +218,11 @@ class ResizeVeilTest : ShellTestCase() { context, mockDisplayController, mockAppIcon, - taskInfo, parent, { mockTransaction }, mockSurfaceControlBuilderFactory, - mockSurfaceControlViewHostFactory + mockSurfaceControlViewHostFactory, + taskInfo ) } } 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 48ac1e5717aa..901ca90b573e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -17,6 +17,8 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.content.Context +import android.content.res.Resources import android.graphics.Point import android.graphics.Rect import android.os.IBinder @@ -98,6 +100,10 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { private lateinit var mockFinishCallback: TransitionFinishCallback @Mock private lateinit var mockTransitions: Transitions + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockResources: Resources private lateinit var taskPositioner: VeiledResizeTaskPositioner @@ -105,6 +111,9 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + mockDesktopWindowDecoration.mDisplay = mockDisplay + mockDesktopWindowDecoration.mDecorWindowContext = mockContext + whenever(mockContext.getResources()).thenReturn(mockResources) whenever(taskToken.asBinder()).thenReturn(taskBinder) whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) 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 48310810e8c9..f3603e1d9b46 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 @@ -32,6 +32,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.any; @@ -76,6 +77,7 @@ import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.tests.R; +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; import org.junit.Before; import org.junit.Test; @@ -371,7 +373,7 @@ public class WindowDecorationTests extends ShellTestCase { } @Test - public void testAddWindow() { + public void testAddViewHostViewContainer() { final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController) .getDisplay(Display.DEFAULT_DISPLAY); @@ -393,6 +395,7 @@ public class WindowDecorationTests extends ShellTestCase { final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() .setDisplayId(Display.DEFAULT_DISPLAY) .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setWindowingMode(WINDOWING_MODE_FREEFORM) .setBounds(TASK_BOUNDS) .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y) .setVisible(true) @@ -407,7 +410,7 @@ public class WindowDecorationTests extends ShellTestCase { createMockSurfaceControlBuilder(additionalWindowSurface); mMockSurfaceControlBuilders.add(additionalWindowSurfaceBuilder); - WindowDecoration.AdditionalWindow additionalWindow = windowDecor.addTestWindow(); + windowDecor.addTestViewContainer(); verify(additionalWindowSurfaceBuilder).setContainerLayer(); verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface); @@ -421,12 +424,6 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface); verify(mMockSurfaceControlViewHostFactory, Mockito.times(2)) .create(any(), eq(defaultDisplay), any()); - assertThat(additionalWindow.mWindowViewHost).isNotNull(); - - additionalWindow.releaseView(); - - assertThat(additionalWindow.mWindowViewHost).isNull(); - assertThat(additionalWindow.mWindowSurface).isNull(); } @Test @@ -832,6 +829,36 @@ public class WindowDecorationTests extends ShellTestCase { eq(mMockTaskSurface), anyInt(), anyInt()); } + @Test + public void updateViewHost_applyTransactionOnDrawIsTrue_surfaceControlIsUpdated() { + final TestWindowDecoration windowDecor = createWindowDecoration( + new TestRunningTaskInfoBuilder().build()); + mRelayoutParams.mApplyStartTransactionOnDraw = true; + + windowDecor.updateViewHost(mRelayoutParams, mMockSurfaceControlStartT, mRelayoutResult); + + verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT); + } + + @Test + public void updateViewHost_nullDrawTransaction_applyTransactionOnDrawIsTrue_throwsException() { + final TestWindowDecoration windowDecor = createWindowDecoration( + new TestRunningTaskInfoBuilder().build()); + mRelayoutParams.mApplyStartTransactionOnDraw = true; + + assertThrows(IllegalArgumentException.class, + () -> windowDecor.updateViewHost( + mRelayoutParams, null /* onDrawTransaction */, mRelayoutResult)); + } + + @Test + public void updateViewHost_nullDrawTransaction_applyTransactionOnDrawIsFalse_doesNotThrow() { + final TestWindowDecoration windowDecor = createWindowDecoration( + new TestRunningTaskInfoBuilder().build()); + mRelayoutParams.mApplyStartTransactionOnDraw = false; + + windowDecor.updateViewHost(mRelayoutParams, null /* onDrawTransaction */, mRelayoutResult); + } private TestWindowDecoration createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo) { return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, @@ -905,16 +932,16 @@ public class WindowDecorationTests extends ShellTestCase { mMockWindowContainerTransaction, mMockView, mRelayoutResult); } - private WindowDecoration.AdditionalWindow addTestWindow() { + private AdditionalViewContainer addTestViewContainer() { final Resources resources = mDecorWindowContext.getResources(); - int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId); - int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); - String name = "Test Window"; - WindowDecoration.AdditionalWindow additionalWindow = + final int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId); + final int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + final String name = "Test Window"; + final AdditionalViewContainer additionalViewContainer = addWindow(R.layout.desktop_mode_window_decor_handle_menu, name, mMockSurfaceControlAddWindowT, mMockSurfaceSyncGroup, 0 /* x */, 0 /* y */, width, height); - return additionalWindow; + return additionalViewContainer; } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt new file mode 100644 index 000000000000..d3e996b12e1f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.additionalviewcontainer + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import androidx.test.filters.SmallTest +import com.android.wm.shell.R +import com.android.wm.shell.ShellTestCase +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** + * Tests for [AdditionalSystemViewContainer]. + * + * Build/Install/Run: + * atest WMShellUnitTests:AdditionalSystemViewContainerTest + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class AdditionalSystemViewContainerTest : ShellTestCase() { + @Mock + private lateinit var mockView: View + @Mock + private lateinit var mockLayoutInflater: LayoutInflater + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockWindowManager: WindowManager + private lateinit var viewContainer: AdditionalSystemViewContainer + + @Before + fun setUp() { + whenever(mockContext.getSystemService(WindowManager::class.java)) + .thenReturn(mockWindowManager) + whenever(mockContext.getSystemService(Context + .LAYOUT_INFLATER_SERVICE)).thenReturn(mockLayoutInflater) + whenever(mockLayoutInflater.inflate( + R.layout.desktop_mode_window_decor_handle_menu, null)).thenReturn(mockView) + } + + @Test + fun testReleaseView_ViewRemoved() { + viewContainer = AdditionalSystemViewContainer( + mockContext, + R.layout.desktop_mode_window_decor_handle_menu, + TASK_ID, + X, + Y, + WIDTH, + HEIGHT + ) + verify(mockWindowManager).addView(eq(mockView), any()) + viewContainer.releaseView() + verify(mockWindowManager).removeViewImmediate(mockView) + } + + companion object { + private const val X = 500 + private const val Y = 50 + private const val WIDTH = 400 + private const val HEIGHT = 600 + private const val TASK_ID = 5 + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt new file mode 100644 index 000000000000..82d557a28f52 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.additionalviewcontainer + +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.function.Supplier + +/** + * Tests for [AdditionalViewHostViewContainer]. + * + * Build/Install/Run: + * atest WMShellUnitTests:AdditionalViewHostViewContainerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class AdditionalViewHostViewContainerTest : ShellTestCase() { + @Mock + private lateinit var mockTransactionSupplier: Supplier<SurfaceControl.Transaction> + @Mock + private lateinit var mockTransaction: SurfaceControl.Transaction + @Mock + private lateinit var mockSurface: SurfaceControl + @Mock + private lateinit var mockViewHost: SurfaceControlViewHost + private lateinit var viewContainer: AdditionalViewHostViewContainer + + @Before + fun setUp() { + whenever(mockTransactionSupplier.get()).thenReturn(mockTransaction) + } + + @Test + fun testReleaseView_ViewRemoved() { + viewContainer = AdditionalViewHostViewContainer( + mockSurface, + mockViewHost, + mockTransactionSupplier + ) + viewContainer.releaseView() + verify(mockViewHost).release() + verify(mockTransaction).remove(mockSurface) + verify(mockTransaction).apply() + } +} diff --git a/libs/androidfw/AssetManager.cpp b/libs/androidfw/AssetManager.cpp index 68befffecf2f..e6182454ad8a 100644 --- a/libs/androidfw/AssetManager.cpp +++ b/libs/androidfw/AssetManager.cpp @@ -926,8 +926,8 @@ Asset* AssetManager::openAssetFromZipLocked(const ZipFileRO* pZipFile, //printf("USING Zip '%s'\n", pEntry->getFileName()); - if (!pZipFile->getEntryInfo(entry, &method, &uncompressedLen, NULL, NULL, - NULL, NULL)) + if (!pZipFile->getEntryInfo(entry, &method, &uncompressedLen, nullptr, nullptr, + nullptr, nullptr, nullptr)) { ALOGW("getEntryInfo failed\n"); return NULL; diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp index b68143d82090..94351182871a 100644 --- a/libs/androidfw/LocaleDataTables.cpp +++ b/libs/androidfw/LocaleDataTables.cpp @@ -2451,10 +2451,10 @@ const struct { const char script[4]; const std::unordered_map<uint32_t, uint32_t>* map; } SCRIPT_PARENTS[] = { + {{'L', 'a', 't', 'n'}, &LATN_PARENTS}, {{'A', 'r', 'a', 'b'}, &ARAB_PARENTS}, {{'D', 'e', 'v', 'a'}, &DEVA_PARENTS}, {{'H', 'a', 'n', 't'}, &HANT_PARENTS}, - {{'L', 'a', 't', 'n'}, &LATN_PARENTS}, {{'~', '~', '~', 'B'}, &___B_PARENTS}, }; diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index a3dd9833219e..de9991a8be5e 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -2650,8 +2650,9 @@ bool ResTable_config::isBetterThan(const ResTable_config& o, return (mnc); } } - - if (isLocaleBetterThan(o, requested)) { + // Cheaper to check for the empty locales here before calling the function + // as we often can skip it completely. + if (requested->locale && (locale || o.locale) && isLocaleBetterThan(o, requested)) { return true; } @@ -7237,27 +7238,11 @@ void DynamicRefTable::addMapping(uint8_t buildPackageId, uint8_t runtimePackageI status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const { uint32_t res = *resId; - size_t packageId = Res_GETPACKAGE(res) + 1; - if (!Res_VALIDID(res)) { // Cannot look up a null or invalid id, so no lookup needs to be done. return NO_ERROR; } - - const auto alias_it = std::lower_bound(mAliasId.begin(), mAliasId.end(), res, - [](const AliasMap::value_type& pair, uint32_t val) { return pair.first < val; }); - if (alias_it != mAliasId.end() && alias_it->first == res) { - // Rewrite the resource id to its alias resource id. Since the alias resource id is a - // compile-time id, it still needs to be resolved further. - res = alias_it->second; - } - - if (packageId == SYS_PACKAGE_ID || (packageId == APP_PACKAGE_ID && !mAppAsLib)) { - // No lookup needs to be done, app and framework package IDs are absolute. - *resId = res; - return NO_ERROR; - } - + const size_t packageId = Res_GETPACKAGE(res) + 1; if (packageId == 0 || (packageId == APP_PACKAGE_ID && mAppAsLib)) { // The package ID is 0x00. That means that a shared library is accessing // its own local resource. @@ -7267,6 +7252,24 @@ status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const { *resId = (0xFFFFFF & (*resId)) | (((uint32_t) mAssignedPackageId) << 24); return NO_ERROR; } + // All aliases are coming from the framework, and usually have their own separate ID range, + // skipping the whole binary search is much more efficient than not finding anything. + if (packageId == SYS_PACKAGE_ID && !mAliasId.empty() && + res >= mAliasId.front().first && res <= mAliasId.back().first) { + const auto alias_it = std::lower_bound(mAliasId.begin(), mAliasId.end(), res, + [](const AliasMap::value_type& pair, + uint32_t val) { return pair.first < val; }); + if (alias_it != mAliasId.end() && alias_it->first == res) { + // Rewrite the resource id to its alias resource id. Since the alias resource id is a + // compile-time id, it still needs to be resolved further. + res = alias_it->second; + } + } + if (packageId == SYS_PACKAGE_ID || (packageId == APP_PACKAGE_ID && !mAppAsLib)) { + // No lookup needs to be done, app and framework package IDs are absolute. + *resId = res; + return NO_ERROR; + } // Do a proper lookup. uint8_t translatedId = mLookupTable[packageId]; diff --git a/libs/androidfw/ZipFileRO.cpp b/libs/androidfw/ZipFileRO.cpp index 839c7b6fef37..10651cdaff33 100644 --- a/libs/androidfw/ZipFileRO.cpp +++ b/libs/androidfw/ZipFileRO.cpp @@ -119,14 +119,6 @@ ZipEntryRO ZipFileRO::findEntryByName(const char* entryName) const * appear to be bogus. */ bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, - uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, - uint32_t* pModWhen, uint32_t* pCrc32) const -{ - return getEntryInfo(entry, pMethod, pUncompLen, pCompLen, pOffset, pModWhen, - pCrc32, nullptr); -} - -bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const { diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp index 829a39617012..a218a1ff1eb6 100644 --- a/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp +++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp @@ -52,10 +52,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { // Populate the DynamicRefTable with fuzzed data populateDynamicRefTableWithFuzzedData(*dynamic_ref_table, fuzzedDataProvider); + std::vector<uint8_t> xmlData = fuzzedDataProvider.ConsumeRemainingBytes<uint8_t>(); + // Make sure the object here outlives the vector it's set to, otherwise it will try + // accessing an already freed buffer and crash. auto tree = android::ResXMLTree(std::move(dynamic_ref_table)); - - std::vector<uint8_t> xmlData = fuzzedDataProvider.ConsumeRemainingBytes<uint8_t>(); if (tree.setTo(xmlData.data(), xmlData.size()) != android::NO_ERROR) { return 0; // Exit early if unable to parse XML data } diff --git a/libs/androidfw/include/androidfw/ZipFileRO.h b/libs/androidfw/include/androidfw/ZipFileRO.h index f7c5007c80d2..0f3f19c91ed1 100644 --- a/libs/androidfw/include/androidfw/ZipFileRO.h +++ b/libs/androidfw/include/androidfw/ZipFileRO.h @@ -147,10 +147,6 @@ public: * Returns "false" if "entry" is bogus or if the data in the Zip file * appears to be bad. */ - bool getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, uint32_t* pUncompLen, - uint32_t* pCompLen, off64_t* pOffset, uint32_t* pModWhen, - uint32_t* pCrc32) const; - bool getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const; diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 7c1c5b4e7e5f..341599e79662 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -115,6 +115,7 @@ cc_defaults { "libharfbuzz_ng", "libminikin", "server_configurable_flags", + "libaconfig_storage_read_api_cc" ], static_libs: [ diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 325bdd63ab22..5d3bc89b40dd 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -39,6 +39,9 @@ constexpr bool clip_surfaceviews() { constexpr bool hdr_10bit_plus() { return false; } +constexpr bool initialize_gl_always() { + return false; +} } // namespace hwui_flags #endif @@ -257,5 +260,9 @@ bool Properties::isDrawingEnabled() { return drawingEnabled == DrawingEnabled::On; } +bool Properties::initializeGlAlways() { + return base::GetBoolProperty(PROPERTY_INITIALIZE_GL_ALWAYS, hwui_flags::initialize_gl_always()); +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index c1510d96461f..d3176f6879d2 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -229,6 +229,11 @@ enum DebugLevel { #define PROPERTY_8BIT_HDR_HEADROOM "debug.hwui.8bit_hdr_headroom" +/** + * Whether to initialize GL even when HWUI is running Vulkan. + */ +#define PROPERTY_INITIALIZE_GL_ALWAYS "debug.hwui.initialize_gl_always" + /////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////// @@ -368,6 +373,8 @@ public: static bool isDrawingEnabled(); static void setDrawingEnabled(bool enable); + static bool initializeGlAlways(); + private: static StretchEffectBehavior stretchEffectBehavior; static ProfileType sProfileType; diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 50f8b3929e1e..cd3ae5342f4e 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -90,3 +90,10 @@ flag { description: "Add canvas#drawRegion API" bug: "318612129" } + +flag { + name: "initialize_gl_always" + namespace: "core_graphics" + description: "Initialize GL even when HWUI is set to use Vulkan. This improves app startup time for apps using GL." + bug: "335172671" +} diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index 6ace3967ecf3..15b2bac50c79 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -192,5 +192,14 @@ void zygote_preload_graphics() { // Preload Vulkan driver if HWUI renders with Vulkan backend. uint32_t apiVersion; vkEnumerateInstanceVersion(&apiVersion); + + if (Properties::initializeGlAlways()) { + // Even though HWUI is rendering with Vulkan, some apps still use + // GL. Preload GL driver just in case. Since this happens prior to + // forking from the zygote, apps that do not use GL are unaffected. + // Any memory that (E)GL uses for this call is in shared memory, + // and this call only happens once. + eglGetDisplay(EGL_DEFAULT_DISPLAY); + } } } diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp index 0a30c6c14c4c..eac03609d72f 100644 --- a/libs/hwui/effects/GainmapRenderer.cpp +++ b/libs/hwui/effects/GainmapRenderer.cpp @@ -96,6 +96,7 @@ void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkR #ifdef __ANDROID__ static constexpr char gGainmapSKSL[] = R"SKSL( + uniform shader linearBase; uniform shader base; uniform shader gainmap; uniform colorFilter workingSpaceToLinearSrgb; @@ -117,7 +118,11 @@ static constexpr char gGainmapSKSL[] = R"SKSL( } half4 main(float2 coord) { - half4 S = base.eval(coord); + if (W == 0.0) { + return base.eval(coord); + } + + half4 S = linearBase.eval(coord); half4 G = gainmap.eval(coord); if (gainmapIsAlpha == 1) { G = half4(G.a, G.a, G.a, 1.0); @@ -186,8 +191,10 @@ private: SkColorFilterPriv::MakeColorSpaceXform(baseColorSpace, gainmapMathColorSpace); // The base image shader will convert into the color space in which the gainmap is applied. - auto baseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions) - ->makeWithColorFilter(colorXformSdrToGainmap); + auto linearBaseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions) + ->makeWithColorFilter(colorXformSdrToGainmap); + + auto baseImageShader = baseImage->makeShader(tileModeX, tileModeY, samplingOptions); // The gainmap image shader will ignore any color space that the gainmap has. const SkMatrix gainmapRectToDstRect = @@ -201,6 +208,7 @@ private: auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform( gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear()); + mBuilder.child("linearBase") = std::move(linearBaseImageShader); mBuilder.child("base") = std::move(baseImageShader); mBuilder.child("gainmap") = std::move(gainmapImageShader); mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst); diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index 07e97f85d588..a88139d6b5d6 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -583,6 +583,16 @@ jobject GraphicsJNI::getColorSpace(JNIEnv* env, SkColorSpace* decodeColorSpace, transferParams.a, transferParams.b, transferParams.c, transferParams.d, transferParams.e, transferParams.f, transferParams.g); + // Some transfer functions that are considered valid by Skia are not + // accepted by android.graphics. + if (hasException(env)) { + // Callers (e.g. Bitmap#getColorSpace) are not expected to throw an + // Exception, so clear it and return null, which is a documented + // possibility. + env->ExceptionClear(); + return nullptr; + } + jfloatArray xyzArray = env->NewFloatArray(9); jfloat xyz[9] = { xyzMatrix.vals[0][0], diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index 0d0af1110ca4..4d185c69c172 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -28,8 +28,8 @@ #include <include/gpu/ganesh/vk/GrVkBackendSemaphore.h> #include <include/gpu/ganesh/vk/GrVkBackendSurface.h> #include <include/gpu/ganesh/vk/GrVkDirectContext.h> +#include <include/gpu/vk/VulkanBackendContext.h> #include <ui/FatVector.h> -#include <vk/GrVkExtensions.h> #include <vk/GrVkTypes.h> #include <sstream> @@ -141,7 +141,8 @@ VulkanManager::~VulkanManager() { mPhysicalDeviceFeatures2 = {}; } -void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFeatures2& features) { +void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, + VkPhysicalDeviceFeatures2& features) { VkResult err; constexpr VkApplicationInfo app_info = { @@ -506,7 +507,7 @@ sk_sp<GrDirectContext> VulkanManager::createContext(GrContextOptions& options, return vkGetInstanceProcAddr(instance, proc_name); }; - GrVkBackendContext backendContext; + skgpu::VulkanBackendContext backendContext; backendContext.fInstance = mInstance; backendContext.fPhysicalDevice = mPhysicalDevice; backendContext.fDevice = mDevice; diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index b92ebb3cdf71..08f9d4253d7e 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -24,8 +24,7 @@ #include <SkSurface.h> #include <android-base/unique_fd.h> #include <utils/StrongPointer.h> -#include <vk/GrVkBackendContext.h> -#include <vk/GrVkExtensions.h> +#include <vk/VulkanExtensions.h> #include <vulkan/vulkan.h> // VK_ANDROID_frame_boundary is a bespoke extension defined by AGI @@ -127,7 +126,7 @@ private: // Sets up the VkInstance and VkDevice objects. Also fills out the passed in // VkPhysicalDeviceFeatures struct. - void setupDevice(GrVkExtensions&, VkPhysicalDeviceFeatures2&); + void setupDevice(skgpu::VulkanExtensions&, VkPhysicalDeviceFeatures2&); // simple wrapper class that exists only to initialize a pointer to NULL template <typename FNPTR_TYPE> @@ -206,7 +205,7 @@ private: BufferAge, }; SwapBehavior mSwapBehavior = SwapBehavior::Discard; - GrVkExtensions mExtensions; + skgpu::VulkanExtensions mExtensions; uint32_t mDriverVersion = 0; std::once_flag mInitFlag; diff --git a/libs/input/Android.bp b/libs/input/Android.bp index 7b7ccf51aa1a..7a82938435af 100644 --- a/libs/input/Android.bp +++ b/libs/input/Android.bp @@ -46,7 +46,6 @@ cc_library_shared { "liblog", "libutils", "libgui", - "libui", "libinput", ], diff --git a/libs/nativehelper_jvm/Android.bp b/libs/nativehelper_jvm/Android.bp new file mode 100644 index 000000000000..b5b70283551a --- /dev/null +++ b/libs/nativehelper_jvm/Android.bp @@ -0,0 +1,19 @@ +package { + default_applicable_licenses: ["frameworks_base_license"], +} + +cc_library_host_static { + name: "libnativehelper_jvm", + srcs: [ + "JNIPlatformHelp.c", + "JniConstants.c", + "file_descriptor_jni.c", + ], + whole_static_libs: ["libnativehelper_any_vm"], + export_static_lib_headers: ["libnativehelper_any_vm"], + target: { + windows: { + enabled: true, + }, + }, +} diff --git a/libs/nativehelper_jvm/JNIPlatformHelp.c b/libs/nativehelper_jvm/JNIPlatformHelp.c new file mode 100644 index 000000000000..9df31a8caa7f --- /dev/null +++ b/libs/nativehelper_jvm/JNIPlatformHelp.c @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <nativehelper/JNIPlatformHelp.h> + +#include <stddef.h> + +#include "JniConstants.h" + +static int GetBufferPosition(JNIEnv* env, jobject nioBuffer) { + return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_position(env)); +} + +static int GetBufferLimit(JNIEnv* env, jobject nioBuffer) { + return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_limit(env)); +} + +static int GetBufferElementSizeShift(JNIEnv* env, jobject nioBuffer) { + jclass byteBufferClass = JniConstants_NioByteBufferClass(env); + jclass shortBufferClass = JniConstants_NioShortBufferClass(env); + jclass charBufferClass = JniConstants_NioCharBufferClass(env); + jclass intBufferClass = JniConstants_NioIntBufferClass(env); + jclass floatBufferClass = JniConstants_NioFloatBufferClass(env); + jclass longBufferClass = JniConstants_NioLongBufferClass(env); + jclass doubleBufferClass = JniConstants_NioDoubleBufferClass(env); + + // Check the type of the Buffer + if ((*env)->IsInstanceOf(env, nioBuffer, byteBufferClass)) { + return 0; + } else if ((*env)->IsInstanceOf(env, nioBuffer, shortBufferClass) || + (*env)->IsInstanceOf(env, nioBuffer, charBufferClass)) { + return 1; + } else if ((*env)->IsInstanceOf(env, nioBuffer, intBufferClass) || + (*env)->IsInstanceOf(env, nioBuffer, floatBufferClass)) { + return 2; + } else if ((*env)->IsInstanceOf(env, nioBuffer, longBufferClass) || + (*env)->IsInstanceOf(env, nioBuffer, doubleBufferClass)) { + return 3; + } + return 0; +} + +jarray jniGetNioBufferBaseArray(JNIEnv* env, jobject nioBuffer) { + jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env); + jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod); + if (hasArray) { + jmethodID arrayMethod = JniConstants_NioBuffer_array(env); + return (*env)->CallObjectMethod(env, nioBuffer, arrayMethod); + } else { + return NULL; + } +} + +int jniGetNioBufferBaseArrayOffset(JNIEnv* env, jobject nioBuffer) { + jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env); + jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod); + if (hasArray) { + jmethodID arrayOffsetMethod = JniConstants_NioBuffer_arrayOffset(env); + jint arrayOffset = (*env)->CallIntMethod(env, nioBuffer, arrayOffsetMethod); + const int position = GetBufferPosition(env, nioBuffer); + jint elementSizeShift = GetBufferElementSizeShift(env, nioBuffer); + return (arrayOffset + position) << elementSizeShift; + } else { + return 0; + } +} + +jlong jniGetNioBufferPointer(JNIEnv* env, jobject nioBuffer) { + // in Java 11, the address field of a HeapByteBuffer contains a non-zero value despite + // HeapByteBuffer being a non-direct buffer. In that case, this should still return 0. + jmethodID isDirectMethod = JniConstants_NioBuffer_isDirect(env); + jboolean isDirect = (*env)->CallBooleanMethod(env, nioBuffer, isDirectMethod); + if (isDirect == JNI_FALSE) { + return 0L; + } + jlong baseAddress = (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env)); + if (baseAddress != 0) { + const int position = GetBufferPosition(env, nioBuffer); + const int shift = GetBufferElementSizeShift(env, nioBuffer); + baseAddress += position << shift; + } + return baseAddress; +} + +jlong jniGetNioBufferFields(JNIEnv* env, jobject nioBuffer, + jint* position, jint* limit, jint* elementSizeShift) { + *position = GetBufferPosition(env, nioBuffer); + *limit = GetBufferLimit(env, nioBuffer); + *elementSizeShift = GetBufferElementSizeShift(env, nioBuffer); + return (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env)); +} diff --git a/libs/nativehelper_jvm/JniConstants.c b/libs/nativehelper_jvm/JniConstants.c new file mode 100644 index 000000000000..ca58f61070ba --- /dev/null +++ b/libs/nativehelper_jvm/JniConstants.c @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "JniConstants.h" + +#include <pthread.h> +#include <stdbool.h> +#include <stddef.h> +#include <string.h> + +#define LOG_TAG "JniConstants" +#include <log/log.h> + +// jclass constants list: +// <class, signature, androidOnly> +#define JCLASS_CONSTANTS_LIST(V) \ + V(FileDescriptor, "java/io/FileDescriptor", false) \ + V(NioBuffer, "java/nio/Buffer", false) \ + V(NioByteBuffer, "java/nio/ByteBuffer", false) \ + V(NioShortBuffer, "java/nio/ShortBuffer", false) \ + V(NioCharBuffer, "java/nio/CharBuffer", false) \ + V(NioIntBuffer, "java/nio/IntBuffer", false) \ + V(NioFloatBuffer, "java/nio/FloatBuffer", false) \ + V(NioLongBuffer, "java/nio/LongBuffer", false) \ + V(NioDoubleBuffer, "java/nio/DoubleBuffer", false) + +// jmethodID's of public methods constants list: +// <Class, method, method-string, signature, is_static> +#define JMETHODID_CONSTANTS_LIST(V) \ + V(FileDescriptor, init, "<init>", "()V", false) \ + V(NioBuffer, array, "array", "()Ljava/lang/Object;", false) \ + V(NioBuffer, hasArray, "hasArray", "()Z", false) \ + V(NioBuffer, isDirect, "isDirect", "()Z", false) \ + V(NioBuffer, arrayOffset, "arrayOffset", "()I", false) + +// jfieldID constants list: +// <Class, field, signature, is_static> +#define JFIELDID_CONSTANTS_LIST(V) \ + V(FileDescriptor, fd, "I", false) \ + V(NioBuffer, address, "J", false) \ + V(NioBuffer, limit, "I", false) \ + V(NioBuffer, position, "I", false) + +#define CLASS_NAME(cls) g_ ## cls +#define METHOD_NAME(cls, method) g_ ## cls ## _ ## method +#define FIELD_NAME(cls, field) g_ ## cls ## _ ## field + +// +// Declare storage for cached classes, methods and fields. +// + +#define JCLASS_DECLARE_STORAGE(cls, ...) \ + static jclass CLASS_NAME(cls) = NULL; +JCLASS_CONSTANTS_LIST(JCLASS_DECLARE_STORAGE) +#undef JCLASS_DECLARE_STORAGE + +#define JMETHODID_DECLARE_STORAGE(cls, method, ...) \ + static jmethodID METHOD_NAME(cls, method) = NULL; +JMETHODID_CONSTANTS_LIST(JMETHODID_DECLARE_STORAGE) +#undef JMETHODID_DECLARE_STORAGE + +#define JFIELDID_DECLARE_STORAGE(cls, field, ...) \ + static jfieldID FIELD_NAME(cls, field) = NULL; +JFIELDID_CONSTANTS_LIST(JFIELDID_DECLARE_STORAGE) +#undef JFIELDID_DECLARE_STORAGE + +// +// Helper methods +// + +static jclass FindClass(JNIEnv* env, const char* signature, bool androidOnly) { + jclass cls = (*env)->FindClass(env, signature); + if (cls == NULL) { + LOG_ALWAYS_FATAL_IF(!androidOnly, "Class not found: %s", signature); + return NULL; + } + return (*env)->NewGlobalRef(env, cls); +} + +static jmethodID FindMethod(JNIEnv* env, jclass cls, + const char* name, const char* signature, bool isStatic) { + jmethodID method; + if (isStatic) { + method = (*env)->GetStaticMethodID(env, cls, name, signature); + } else { + method = (*env)->GetMethodID(env, cls, name, signature); + } + LOG_ALWAYS_FATAL_IF(method == NULL, "Method not found: %s:%s", name, signature); + return method; +} + +static jfieldID FindField(JNIEnv* env, jclass cls, + const char* name, const char* signature, bool isStatic) { + jfieldID field; + if (isStatic) { + field = (*env)->GetStaticFieldID(env, cls, name, signature); + } else { + field = (*env)->GetFieldID(env, cls, name, signature); + } + LOG_ALWAYS_FATAL_IF(field == NULL, "Field not found: %s:%s", name, signature); + return field; +} + +static pthread_once_t g_initialized = PTHREAD_ONCE_INIT; +static JNIEnv* g_init_env; + +static void InitializeConstants() { + // Initialize cached classes. +#define JCLASS_INITIALIZE(cls, signature, androidOnly) \ + CLASS_NAME(cls) = FindClass(g_init_env, signature, androidOnly); + JCLASS_CONSTANTS_LIST(JCLASS_INITIALIZE) +#undef JCLASS_INITIALIZE + + // Initialize cached methods. +#define JMETHODID_INITIALIZE(cls, method, name, signature, isStatic) \ + METHOD_NAME(cls, method) = \ + FindMethod(g_init_env, CLASS_NAME(cls), name, signature, isStatic); + JMETHODID_CONSTANTS_LIST(JMETHODID_INITIALIZE) +#undef JMETHODID_INITIALIZE + + // Initialize cached fields. +#define JFIELDID_INITIALIZE(cls, field, signature, isStatic) \ + FIELD_NAME(cls, field) = \ + FindField(g_init_env, CLASS_NAME(cls), #field, signature, isStatic); + JFIELDID_CONSTANTS_LIST(JFIELDID_INITIALIZE) +#undef JFIELDID_INITIALIZE +} + +void EnsureInitialized(JNIEnv* env) { + // This method has to be called in every cache accesses because library can be built + // 2 different ways and existing usage for compat version doesn't have a good hook for + // initialization and is widely used. + g_init_env = env; + pthread_once(&g_initialized, InitializeConstants); +} + +// API exported by libnativehelper_api.h. + +void jniUninitializeConstants() { + // Uninitialize cached classes, methods and fields. + // + // NB we assume the runtime is stopped at this point and do not delete global + // references. +#define JCLASS_INVALIDATE(cls, ...) CLASS_NAME(cls) = NULL; + JCLASS_CONSTANTS_LIST(JCLASS_INVALIDATE); +#undef JCLASS_INVALIDATE + +#define JMETHODID_INVALIDATE(cls, method, ...) METHOD_NAME(cls, method) = NULL; + JMETHODID_CONSTANTS_LIST(JMETHODID_INVALIDATE); +#undef JMETHODID_INVALIDATE + +#define JFIELDID_INVALIDATE(cls, field, ...) FIELD_NAME(cls, field) = NULL; + JFIELDID_CONSTANTS_LIST(JFIELDID_INVALIDATE); +#undef JFIELDID_INVALIDATE + + // If jniConstantsUninitialize is called, runtime has shutdown. Reset + // state as some tests re-start the runtime. + pthread_once_t o = PTHREAD_ONCE_INIT; + memcpy(&g_initialized, &o, sizeof(o)); +} + +// +// Accessors +// + +#define JCLASS_ACCESSOR_IMPL(cls, ...) \ +jclass JniConstants_ ## cls ## Class(JNIEnv* env) { \ + EnsureInitialized(env); \ + return CLASS_NAME(cls); \ +} +JCLASS_CONSTANTS_LIST(JCLASS_ACCESSOR_IMPL) +#undef JCLASS_ACCESSOR_IMPL + +#define JMETHODID_ACCESSOR_IMPL(cls, method, ...) \ +jmethodID JniConstants_ ## cls ## _ ## method(JNIEnv* env) { \ + EnsureInitialized(env); \ + return METHOD_NAME(cls, method); \ +} +JMETHODID_CONSTANTS_LIST(JMETHODID_ACCESSOR_IMPL) + +#define JFIELDID_ACCESSOR_IMPL(cls, field, ...) \ +jfieldID JniConstants_ ## cls ## _ ## field(JNIEnv* env) { \ + EnsureInitialized(env); \ + return FIELD_NAME(cls, field); \ +} +JFIELDID_CONSTANTS_LIST(JFIELDID_ACCESSOR_IMPL) diff --git a/libs/nativehelper_jvm/JniConstants.h b/libs/nativehelper_jvm/JniConstants.h new file mode 100644 index 000000000000..e7a266d72509 --- /dev/null +++ b/libs/nativehelper_jvm/JniConstants.h @@ -0,0 +1,63 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <sys/cdefs.h> + +#include <jni.h> + +__BEGIN_DECLS + +// +// Classes in constants cache. +// +// NB The implementations of these methods are generated by the JCLASS_ACCESSOR_IMPL macro in +// JniConstants.c. +// +jclass JniConstants_FileDescriptorClass(JNIEnv* env); +jclass JniConstants_NioByteBufferClass(JNIEnv* env); +jclass JniConstants_NioShortBufferClass(JNIEnv* env); +jclass JniConstants_NioCharBufferClass(JNIEnv* env); +jclass JniConstants_NioIntBufferClass(JNIEnv* env); +jclass JniConstants_NioFloatBufferClass(JNIEnv* env); +jclass JniConstants_NioLongBufferClass(JNIEnv* env); +jclass JniConstants_NioDoubleBufferClass(JNIEnv* env); + +// +// Methods in the constants cache. +// +// NB The implementations of these methods are generated by the JMETHODID_ACCESSOR_IMPL macro in +// JniConstants.c. +// +jmethodID JniConstants_FileDescriptor_init(JNIEnv* env); +jmethodID JniConstants_NioBuffer_array(JNIEnv* env); +jmethodID JniConstants_NioBuffer_arrayOffset(JNIEnv* env); +jmethodID JniConstants_NioBuffer_hasArray(JNIEnv* env); +jmethodID JniConstants_NioBuffer_isDirect(JNIEnv* env); + +// +// Fields in the constants cache. +// +// NB The implementations of these methods are generated by the JFIELDID_ACCESSOR_IMPL macro in +// JniConstants.c. +// +jfieldID JniConstants_FileDescriptor_fd(JNIEnv* env); +jfieldID JniConstants_NioBuffer_address(JNIEnv* env); +jfieldID JniConstants_NioBuffer_limit(JNIEnv* env); +jfieldID JniConstants_NioBuffer_position(JNIEnv* env); + +__END_DECLS diff --git a/libs/nativehelper_jvm/OWNERS b/libs/nativehelper_jvm/OWNERS new file mode 100644 index 000000000000..5d55f6e4319b --- /dev/null +++ b/libs/nativehelper_jvm/OWNERS @@ -0,0 +1,7 @@ +# Bug component: 326772 + +include /libs/hwui/OWNERS +include platform/libnativehelper:/OWNERS + +diegoperez@google.com +jgaillard@google.com diff --git a/libs/nativehelper_jvm/README b/libs/nativehelper_jvm/README new file mode 100644 index 000000000000..755c42261f43 --- /dev/null +++ b/libs/nativehelper_jvm/README @@ -0,0 +1,2 @@ +libnativehelper_jvm is a JVM-compatible version of libnativehelper. +It should be used instead of libnativehelper whenever a host library is meant to run on a JVM.
\ No newline at end of file diff --git a/libs/nativehelper_jvm/file_descriptor_jni.c b/libs/nativehelper_jvm/file_descriptor_jni.c new file mode 100644 index 000000000000..36880cd586ca --- /dev/null +++ b/libs/nativehelper_jvm/file_descriptor_jni.c @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <android/file_descriptor_jni.h> + +#include <stddef.h> + +#define LOG_TAG "file_descriptor_jni" +#include <log/log.h> + +#include "JniConstants.h" + +static void EnsureArgumentIsFileDescriptor(JNIEnv* env, jobject instance) { + LOG_ALWAYS_FATAL_IF(instance == NULL, "FileDescriptor is NULL"); + jclass jifd = JniConstants_FileDescriptorClass(env); + LOG_ALWAYS_FATAL_IF(!(*env)->IsInstanceOf(env, instance, jifd), + "Argument is not a FileDescriptor"); +} + +JNIEXPORT _Nullable jobject AFileDescriptor_create(JNIEnv* env) { + return (*env)->NewObject(env, + JniConstants_FileDescriptorClass(env), + JniConstants_FileDescriptor_init(env)); +} + +JNIEXPORT int AFileDescriptor_getFd(JNIEnv* env, jobject fileDescriptor) { + EnsureArgumentIsFileDescriptor(env, fileDescriptor); + return (*env)->GetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env)); +} + +JNIEXPORT void AFileDescriptor_setFd(JNIEnv* env, jobject fileDescriptor, int fd) { + EnsureArgumentIsFileDescriptor(env, fileDescriptor); + (*env)->SetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env), fd); +} |