diff options
Diffstat (limited to 'libs')
49 files changed, 1440 insertions, 473 deletions
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 1fbaeeac8608..29936cc2cac3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -33,7 +33,9 @@ import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSI import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; -import android.annotation.DimenRes; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityThread; @@ -53,9 +55,11 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.VelocityTracker; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; +import android.view.animation.PathInterpolator; import android.widget.FrameLayout; import android.widget.ImageButton; import android.window.InputTransferToken; @@ -97,6 +101,16 @@ class DividerPresenter implements View.OnTouchListener { @VisibleForTesting static final int DEFAULT_DIVIDER_WIDTH_DP = 24; + @VisibleForTesting + static final PathInterpolator FLING_ANIMATION_INTERPOLATOR = + new PathInterpolator(0.4f, 0f, 0.2f, 1f); + @VisibleForTesting + static final int FLING_ANIMATION_DURATION = 250; + @VisibleForTesting + static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600; + @VisibleForTesting + static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400; + private final int mTaskId; @NonNull @@ -109,6 +123,14 @@ class DividerPresenter implements View.OnTouchListener { private final Executor mCallbackExecutor; /** + * The VelocityTracker of the divider, used to track the dragging velocity. This field is + * {@code null} until dragging starts. + */ + @GuardedBy("mLock") + @Nullable + VelocityTracker mVelocityTracker; + + /** * The {@link Properties} of the divider. This field is {@code null} when no divider should be * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface * is not available. @@ -370,13 +392,11 @@ class DividerPresenter implements View.OnTouchListener { applicationContext.getResources().getDisplayMetrics()); } - private static int getDimensionDp(@DimenRes int resId) { - final Context context = ActivityThread.currentActivityThread().getApplication(); - final int px = context.getResources().getDimensionPixelSize(resId); - return (int) TypedValue.convertPixelsToDimension( - COMPLEX_UNIT_DIP, - px, - context.getResources().getDisplayMetrics()); + private static float getDisplayDensity() { + // TODO(b/329193115) support divider on secondary display + final Context applicationContext = + ActivityThread.currentActivityThread().getApplication(); + return applicationContext.getResources().getDisplayMetrics().density; } /** @@ -487,24 +507,27 @@ class DividerPresenter implements View.OnTouchListener { @Override public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) { synchronized (mLock) { - final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); - mDividerPosition = calculateDividerPosition( - event, taskBounds, mRenderer.mDividerWidthPx, mProperties.mDividerAttributes, - mProperties.mIsVerticalSplit, calculateMinPosition(), calculateMaxPosition()); - mRenderer.setDividerPosition(mDividerPosition); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - onStartDragging(); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - onFinishDragging(); - break; - case MotionEvent.ACTION_MOVE: - onDrag(); - break; - default: - break; + if (mProperties != null && mRenderer != null) { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + mDividerPosition = calculateDividerPosition( + event, taskBounds, mRenderer.mDividerWidthPx, + mProperties.mDividerAttributes, mProperties.mIsVerticalSplit, + calculateMinPosition(), calculateMaxPosition()); + mRenderer.setDividerPosition(mDividerPosition); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + onStartDragging(event); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onFinishDragging(event); + break; + case MotionEvent.ACTION_MOVE: + onDrag(event); + break; + default: + break; + } } } @@ -514,7 +537,10 @@ class DividerPresenter implements View.OnTouchListener { } @GuardedBy("mLock") - private void onStartDragging() { + private void onStartDragging(@NonNull MotionEvent event) { + mVelocityTracker = VelocityTracker.obtain(); + mVelocityTracker.addMovement(event); + mRenderer.mIsDragging = true; mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); mRenderer.updateSurface(); @@ -536,16 +562,81 @@ class DividerPresenter implements View.OnTouchListener { } @GuardedBy("mLock") - private void onDrag() { + private void onDrag(@NonNull MotionEvent event) { + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + } mRenderer.updateSurface(); } @GuardedBy("mLock") - private void onFinishDragging() { - mDividerPosition = adjustDividerPositionForSnapPoints(mDividerPosition); - mRenderer.setDividerPosition(mDividerPosition); + private void onFinishDragging(@NonNull MotionEvent event) { + float velocity = 0.0f; + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + mVelocityTracker.computeCurrentVelocity(1000 /* units */); + velocity = mProperties.mIsVerticalSplit + ? mVelocityTracker.getXVelocity() + : mVelocityTracker.getYVelocity(); + mVelocityTracker.recycle(); + } + + final int prevDividerPosition = mDividerPosition; + mDividerPosition = dividerPositionForSnapPoints(mDividerPosition, velocity); + if (mDividerPosition != prevDividerPosition) { + ValueAnimator animator = getFlingAnimator(prevDividerPosition, mDividerPosition); + animator.start(); + } else { + onDraggingEnd(); + } + } + + @GuardedBy("mLock") + @NonNull + @VisibleForTesting + ValueAnimator getFlingAnimator(int prevDividerPosition, int snappedDividerPosition) { + final ValueAnimator animator = + getValueAnimator(prevDividerPosition, snappedDividerPosition); + animator.addUpdateListener(animation -> { + synchronized (mLock) { + updateDividerPosition((int) animation.getAnimatedValue()); + } + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + synchronized (mLock) { + onDraggingEnd(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + synchronized (mLock) { + onDraggingEnd(); + } + } + }); + return animator; + } + + @VisibleForTesting + static ValueAnimator getValueAnimator(int prevDividerPosition, int snappedDividerPosition) { + ValueAnimator animator = ValueAnimator + .ofInt(prevDividerPosition, snappedDividerPosition) + .setDuration(FLING_ANIMATION_DURATION); + animator.setInterpolator(FLING_ANIMATION_INTERPOLATOR); + return animator; + } + + @GuardedBy("mLock") + private void updateDividerPosition(int position) { + mRenderer.setDividerPosition(position); mRenderer.updateSurface(); + } + @GuardedBy("mLock") + private void onDraggingEnd() { // Veil visibility change should be applied together with the surface boost transaction in // the wct. final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); @@ -570,36 +661,76 @@ class DividerPresenter implements View.OnTouchListener { /** * Returns the divider position adjusted for the min max ratio and fullscreen expansion. - * - * If the dragging position is above the {@link DividerAttributes#getPrimaryMaxRatio()} or below - * {@link DividerAttributes#getPrimaryMinRatio()} and - * {@link DividerAttributes#isDraggingToFullscreenAllowed} is {@code true}, the system will - * choose a snap algorithm to adjust the ending position to either fully expand one container or - * move the divider back to the specified min/max ratio. - * - * TODO(b/327067596) implement snap algorithm - * * The adjusted divider position is in the range of [minPosition, maxPosition] for a split, 0 * for expanded right (bottom) container, or task width (height) minus the divider width for * expanded left (top) container. */ @GuardedBy("mLock") - private int adjustDividerPositionForSnapPoints(int dividerPosition) { + private int dividerPositionForSnapPoints(int dividerPosition, float velocity) { final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); final int minPosition = calculateMinPosition(); final int maxPosition = calculateMaxPosition(); final int fullyExpandedPosition = mProperties.mIsVerticalSplit ? taskBounds.right - mRenderer.mDividerWidthPx : taskBounds.bottom - mRenderer.mDividerWidthPx; + if (isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)) { - if (dividerPosition < minPosition) { - return 0; + final float displayDensity = getDisplayDensity(); + return dividerPositionWithDraggingToFullscreenAllowed( + dividerPosition, + minPosition, + maxPosition, + fullyExpandedPosition, + velocity, + displayDensity); + } + return Math.clamp(dividerPosition, minPosition, maxPosition); + } + + /** + * 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. + */ + @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; + final float minFlingVelocityPxPerSecond = + MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity; + if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) { + return 0; + } + if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) { + return fullyExpandedPosition; + } + if (Math.abs(velocity) < minFlingVelocityPxPerSecond) { + if (dividerPosition >= minPosition && dividerPosition <= maxPosition) { + return dividerPosition; } - if (dividerPosition > maxPosition) { - return fullyExpandedPosition; + int[] possiblePositions = {0, minPosition, maxPosition, fullyExpandedPosition}; + return snap(dividerPosition, possiblePositions); + } + if (velocity < 0) { + return 0; + } else { + return fullyExpandedPosition; + } + } + + /** Calculates the snapped divider position based on the possible positions and distance. */ + private static int snap(int dividerPosition, int[] possiblePositions) { + int snappedPosition = dividerPosition; + float minDistance = Float.MAX_VALUE; + for (int position : possiblePositions) { + float distance = Math.abs(dividerPosition - position); + if (distance < minDistance) { + snappedPosition = position; + minDistance = distance; } } - return Math.clamp(dividerPosition, minPosition, maxPosition); + return snappedPosition; } private static void setDecorSurfaceBoosted( 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 13c2d1f73461..b764b6ee065f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -50,6 +50,7 @@ import static androidx.window.extensions.embedding.SplitPresenter.getActivityInt import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; +import static androidx.window.extensions.embedding.TaskFragmentContainer.OverlayContainerRestoreParams; import android.annotation.CallbackExecutor; import android.app.Activity; @@ -133,6 +134,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); /** + * Stores the token of the associated Activity that maps to the + * {@link OverlayContainerRestoreParams} of the most recent created overlay container. + */ + @GuardedBy("mLock") + final ArrayMap<IBinder, OverlayContainerRestoreParams> mOverlayRestoreParams = new ArrayMap<>(); + + /** * A developer-defined {@link SplitAttributes} calculator to compute the current * {@link SplitAttributes} with the current device and window states. * It is registered via {@link #setSplitAttributesCalculator(Function)} @@ -686,11 +694,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen exception); break; case TYPE_ACTIVITY_REPARENTED_TO_TASK: + final IBinder candidateAssociatedActToken, lastOverlayToken; + if (Flags.fixPipRestoreToOverlay()) { + candidateAssociatedActToken = change.getOtherActivityToken(); + lastOverlayToken = change.getTaskFragmentToken(); + } else { + candidateAssociatedActToken = lastOverlayToken = null; + } onActivityReparentedToTask( wct, taskId, change.getActivityIntent(), - change.getActivityToken()); + change.getActivityToken(), + candidateAssociatedActToken, + lastOverlayToken); break; default: throw new IllegalArgumentException( @@ -917,11 +934,28 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * different process, the server will generate a temporary token that * the organizer can use to reparent the activity through * {@link WindowContainerTransaction} if needed. + * @param candidateAssociatedActToken The token of the candidate associated-activity. + * @param lastOverlayToken The last parent overlay container token. */ @VisibleForTesting @GuardedBy("mLock") void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct, - int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) { + int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken, + @Nullable IBinder candidateAssociatedActToken, @Nullable IBinder lastOverlayToken) { + // Reparent the activity to an overlay container if needed. + final OverlayContainerRestoreParams params = getOverlayContainerRestoreParams( + candidateAssociatedActToken, lastOverlayToken); + if (params != null) { + final Activity associatedActivity = getActivity(candidateAssociatedActToken); + final TaskFragmentContainer targetContainer = createOrUpdateOverlayTaskFragmentIfNeeded( + wct, params.mOptions, params.mIntent, associatedActivity); + if (targetContainer != null) { + wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), + activityToken); + return; + } + } + // If the activity belongs to the current app process, we treat it as a new activity // launch. final Activity activity = getActivity(activityToken); @@ -966,6 +1000,43 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** + * Returns the {@link OverlayContainerRestoreParams} that stored last time the {@code + * associatedActivityToken} associated with and only if data matches the {@code overlayToken}. + * Otherwise, return {@code null}. + */ + @VisibleForTesting + @GuardedBy("mLock") + @Nullable + OverlayContainerRestoreParams getOverlayContainerRestoreParams( + @Nullable IBinder associatedActivityToken, @Nullable IBinder overlayToken) { + if (!Flags.fixPipRestoreToOverlay()) { + return null; + } + + if (associatedActivityToken == null || overlayToken == null) { + return null; + } + + final TaskFragmentContainer.OverlayContainerRestoreParams params = + mOverlayRestoreParams.get(associatedActivityToken); + if (params == null) { + return null; + } + + if (params.mOverlayToken != overlayToken) { + // Not the same overlay container, no need to restore. + return null; + } + + final Activity associatedActivity = getActivity(associatedActivityToken); + if (associatedActivity == null || associatedActivity.isFinishing()) { + return null; + } + + return params; + } + + /** * Called when the {@link WindowContainerTransaction} created with * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side. * @@ -1433,6 +1504,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen for (int i = mTaskContainers.size() - 1; i >= 0; i--) { mTaskContainers.valueAt(i).onFinishingActivityPaused(wct, activityToken); } + + mOverlayRestoreParams.remove(activity.getActivityToken()); updateCallbackIfNecessary(); } @@ -1450,6 +1523,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen for (int i = mTaskContainers.size() - 1; i >= 0; i--) { mTaskContainers.valueAt(i).onActivityDestroyed(wct, activityToken); } + + mOverlayRestoreParams.remove(activity.getActivityToken()); // We didn't trigger the callback if there were any pending appeared activities, so check // again after the pending is removed. updateCallbackIfNecessary(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 482554351b97..d0b6a01bb51e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -36,6 +36,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.Collections; @@ -274,6 +275,15 @@ class TaskFragmentContainer { addPendingAppearedActivity(pendingAppearedActivity); } mPendingAppearedIntent = pendingAppearedIntent; + + // Save the information necessary for restoring the overlay when needed. + if (Flags.fixPipRestoreToOverlay() && overlayTag != null && pendingAppearedIntent != null + && associatedActivity != null && !associatedActivity.isFinishing()) { + final IBinder associatedActivityToken = associatedActivity.getActivityToken(); + final OverlayContainerRestoreParams params = new OverlayContainerRestoreParams(mToken, + launchOptions, pendingAppearedIntent); + mController.mOverlayRestoreParams.put(associatedActivityToken, params); + } } /** @@ -1105,4 +1115,25 @@ class TaskFragmentContainer { } return sb.append("]").toString(); } + + static class OverlayContainerRestoreParams { + /** The token of the overlay container */ + @NonNull + final IBinder mOverlayToken; + + /** The launch options to create this container. */ + @NonNull + final Bundle mOptions; + + /** The Intent that used to be started in the overlay container. */ + @NonNull + final Intent mIntent; + + OverlayContainerRestoreParams(@NonNull IBinder overlayToken, @NonNull Bundle options, + @NonNull Intent intent) { + mOverlayToken = overlayToken; + mOptions = options; + mIntent = intent; + } + } } 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 b0a45e285896..746607c8094c 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 @@ -19,6 +19,10 @@ package androidx.window.extensions.embedding; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; +import static androidx.window.extensions.embedding.DividerPresenter.FLING_ANIMATION_DURATION; +import static androidx.window.extensions.embedding.DividerPresenter.FLING_ANIMATION_INTERPOLATOR; +import static androidx.window.extensions.embedding.DividerPresenter.MIN_DISMISS_VELOCITY_DP_PER_SECOND; +import static androidx.window.extensions.embedding.DividerPresenter.MIN_FLING_VELOCITY_DP_PER_SECOND; import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider; import static androidx.window.extensions.embedding.DividerPresenter.getInitialDividerPosition; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; @@ -35,6 +39,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.animation.ValueAnimator; import android.app.Activity; import android.content.res.Configuration; import android.graphics.Color; @@ -637,6 +642,105 @@ public class DividerPresenterTest { DividerPresenter.getContainerBackgroundColor(container, defaultColor)); } + @Test + public void testGetValueAnimator() { + ValueAnimator animator = + DividerPresenter.getValueAnimator( + 375 /* prevDividerPosition */, + 500 /* snappedDividerPosition */); + + assertEquals(animator.getDuration(), FLING_ANIMATION_DURATION); + assertEquals(animator.getInterpolator(), FLING_ANIMATION_INTERPOLATOR); + } + + @Test + public void testDividerPositionWithDraggingToFullscreenAllowed() { + final float displayDensity = 600F; + final float dismissVelocity = MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity + 10f; + 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 less than minPosition and the velocity is enough to be dismissed + assertEquals( + 0, // Closed position + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 10 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + -dismissVelocity, + displayDensity)); + + // Divider position is greater than maxPosition and the velocity is enough to be dismissed + assertEquals( + 1200, // Fully expanded position + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 1000 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + dismissVelocity, + displayDensity)); + + // 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( + 500 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity)); + + // 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( + 950 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity)); + + // 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( + 20 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + nonFlingVelocity, + displayDensity)); + + // Divider position is greater than minPosition and the velocity is enough for fling + assertEquals( + 0, // Closed position + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 50 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + -flingVelocity, + displayDensity)); + + // Divider position is less than maxPosition and the velocity is enough for fling + assertEquals( + 1200, // Fully expanded position + DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed( + 800 /* dividerPosition */, + 30 /* minPosition */, + 900 /* maxPosition */, + 1200 /* fullyExpandedPosition */, + flingVelocity, + displayDensity)); + } + private TaskFragmentContainer createMockTaskFragmentContainer( @NonNull IBinder token, @NonNull Rect bounds) { final TaskFragmentContainer container = mock(TaskFragmentContainer.class); 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 9ebcb759115d..f3222572a3e2 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 @@ -836,6 +836,30 @@ public class OverlayPresentationTest { any()); } + @Test + public void testOnActivityReparentedToTask_overlayRestoration() { + mSetFlagRule.enableFlags(Flags.FLAG_FIX_PIP_RESTORE_TO_OVERLAY); + + // Prepares and mock the data necessary for the test. + final IBinder activityToken = mActivity.getActivityToken(); + final Intent intent = new Intent(); + final IBinder fillTaskActivityToken = new Binder(); + final IBinder lastOverlayToken = new Binder(); + final TaskFragmentContainer overlayContainer = mSplitController.newContainer(intent, + mActivity, TASK_ID); + final TaskFragmentContainer.OverlayContainerRestoreParams params = mock( + TaskFragmentContainer.OverlayContainerRestoreParams.class); + doReturn(params).when(mSplitController).getOverlayContainerRestoreParams(any(), any()); + doReturn(overlayContainer).when(mSplitController).createOrUpdateOverlayTaskFragmentIfNeeded( + any(), any(), any(), any()); + + // Verify the activity should be reparented to the overlay container. + mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken, + fillTaskActivityToken, lastOverlayToken); + verify(mTransaction).reparentActivityToTaskFragment( + eq(overlayContainer.getTaskFragmentToken()), eq(activityToken)); + } + /** * A simplified version of {@link SplitController#createOrUpdateOverlayTaskFragmentIfNeeded} */ 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 7d86ec2272af..35353dbe36be 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 @@ -397,7 +397,8 @@ public class SplitControllerTest { @Test public void testOnActivityReparentedToTask_sameProcess() { mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, new Intent(), - mActivity.getActivityToken()); + mActivity.getActivityToken(), null /* fillTaskActivityToken */, + null /* lastOverlayToken */); // Treated as on activity created, but allow to split as primary. verify(mSplitController).resolveActivityToContainer(mTransaction, @@ -413,7 +414,8 @@ public class SplitControllerTest { final IBinder activityToken = new Binder(); final Intent intent = new Intent(); - mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken); + mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken, + null /* fillTaskActivityToken */, null /* lastOverlayToken */); // Treated as starting new intent verify(mSplitController, never()).resolveActivityToContainer(any(), any(), anyBoolean()); @@ -1210,7 +1212,7 @@ public class SplitControllerTest { mSplitController.onTransactionReady(transaction); verify(mSplitController).onActivityReparentedToTask(any(), eq(TASK_ID), eq(intent), - eq(activityToken)); + eq(activityToken), any(), any()); verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), anyInt(), anyBoolean()); } diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 8977d5cad780..66d48799204c 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -78,3 +78,10 @@ flag { description: "Allow opening bubbles overflow UI without bubbles being visible" bug: "340337839" } + +flag { + name: "enable_bubble_stashing" + namespace: "multitasking" + description: "Allow the floating bubble stack to stash on the edge of the screen" + bug: "341361249" +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt index 8487e3792993..9e1440d5716b 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -218,11 +218,10 @@ class BubblePositionerTest { insets = Insets.of(10, 20, 5, 15), windowBounds = Rect(0, 0, 1800, 2600) ) - val bubbleBarBounds = Rect(1700, 2500, 1780, 2600) positioner.setShowingInBubbleBar(true) positioner.update(deviceConfig) - positioner.bubbleBarBounds = bubbleBarBounds + positioner.bubbleBarTopOnScreen = 2500 val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680 val expandedViewVerticalSpacing = @@ -246,10 +245,9 @@ class BubblePositionerTest { insets = Insets.of(10, 20, 5, 15), windowBounds = Rect(0, 0, screenWidth, 2600) ) - val bubbleBarBounds = Rect(100, 2500, 280, 2550) positioner.setShowingInBubbleBar(true) positioner.update(deviceConfig) - positioner.bubbleBarBounds = bubbleBarBounds + positioner.bubbleBarTopOnScreen = 2500 val spaceBetweenTopInsetAndBubbleBarInLandscape = 180 val expandedViewSpacing = @@ -597,16 +595,19 @@ class BubblePositionerTest { private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) { positioner.setShowingInBubbleBar(true) + val windowBounds = Rect(0, 0, 2000, 2600) + val insets = Insets.of(10, 20, 5, 15) val deviceConfig = defaultDeviceConfig.copy( isLargeScreen = true, isLandscape = true, - insets = Insets.of(10, 20, 5, 15), - windowBounds = Rect(0, 0, 2000, 2600) + insets = insets, + windowBounds = windowBounds ) positioner.update(deviceConfig) - positioner.bubbleBarBounds = getBubbleBarBounds(onLeft, deviceConfig) + val bubbleBarHeight = 100 + positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight val expandedViewPadding = context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) @@ -624,7 +625,7 @@ class BubblePositionerTest { left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow) } // Above the bubble bar - val bottom = positioner.bubbleBarBounds.top - expandedViewPadding + val bottom = positioner.bubbleBarTopOnScreen - expandedViewPadding // Calculate right and top based on size val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow) val expectedBounds = Rect(left, top, right, bottom) @@ -666,21 +667,4 @@ class BubblePositionerTest { positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent } - - private fun getBubbleBarBounds(onLeft: Boolean, deviceConfig: DeviceConfig): Rect { - val width = 200 - val height = 100 - val bottom = deviceConfig.windowBounds.bottom - deviceConfig.insets.bottom - val top = bottom - height - val left: Int - val right: Int - if (onLeft) { - left = deviceConfig.insets.left - right = left + width - } else { - right = deviceConfig.windowBounds.right - deviceConfig.insets.right - left = right - width - } - return Rect(left, top, right, bottom) - } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt index 076414132e27..12d19279111a 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -53,7 +53,6 @@ class BubbleExpandedViewPinControllerTest { const val SCREEN_WIDTH = 2000 const val SCREEN_HEIGHT = 1000 - const val BUBBLE_BAR_WIDTH = 100 const val BUBBLE_BAR_HEIGHT = 50 } @@ -84,14 +83,8 @@ class BubbleExpandedViewPinControllerTest { insets = Insets.of(10, 20, 30, 40) ) positioner.update(deviceConfig) - positioner.bubbleBarBounds = - Rect( - SCREEN_WIDTH - deviceConfig.insets.right - BUBBLE_BAR_WIDTH, - SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT, - SCREEN_WIDTH - deviceConfig.insets.right, - SCREEN_HEIGHT - deviceConfig.insets.bottom - ) - + positioner.bubbleBarTopOnScreen = + SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT controller = BubbleExpandedViewPinController(context, container, positioner) testListener = TestLocationChangeListener() controller.setListener(testListener) @@ -247,9 +240,10 @@ class BubbleExpandedViewPinControllerTest { private val dropTargetView: View? get() = container.findViewById(R.id.bubble_bar_drop_target) - private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect = Rect().also { - positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, it) - } + private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect = + Rect().also { + positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, it) + } private fun runOnMainSync(runnable: Runnable) { InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable) diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 8d24c161e3e4..aa4fb4448ac7 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -247,13 +247,11 @@ <!-- Padding for the bubble popup view contents. --> <dimen name="bubble_popup_padding">24dp</dimen> <!-- The size of the caption bar inset at the top of bubble bar expanded view. --> - <dimen name="bubble_bar_expanded_view_caption_height">32dp</dimen> + <dimen name="bubble_bar_expanded_view_caption_height">36dp</dimen> <!-- The width of the caption bar at the top of bubble bar expanded view. --> - <dimen name="bubble_bar_expanded_view_caption_width">128dp</dimen> - <!-- The height of the dots shown for the caption menu in the bubble bar expanded view.. --> - <dimen name="bubble_bar_expanded_view_caption_dot_size">4dp</dimen> - <!-- The spacing between the dots for the caption menu in the bubble bar expanded view.. --> - <dimen name="bubble_bar_expanded_view_caption_dot_spacing">4dp</dimen> + <dimen name="bubble_bar_expanded_view_caption_width">80dp</dimen> + <!-- The height of the handle shown for the caption menu in the bubble bar expanded view. --> + <dimen name="bubble_bar_expanded_view_handle_height">4dp</dimen> <!-- Width of the expanded bubble bar view shown when the bubble is expanded. --> <dimen name="bubble_bar_expanded_view_width">412dp</dimen> <!-- Minimum width of the bubble bar manage menu. --> 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 bdd89c0e1ac9..8d8655addc65 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,16 @@ public class DesktopModeStatus { private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean( "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); + /** 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); + + /** The minimum override density allowed for tasks inside the desktop. */ + private static final int DESKTOP_DENSITY_MIN = 100; + + /** The maximum override density allowed for tasks inside the desktop. */ + private static final int DESKTOP_DENSITY_MAX = 1000; + /** * Default value for {@code MAX_TASK_LIMIT}. */ @@ -145,4 +155,12 @@ public class DesktopModeStatus { public static boolean canEnterDesktopMode(@NonNull Context context) { return (!enforceDeviceRestrictions() || isDesktopModeSupported(context)) && isEnabled(); } + + /** + * Return {@code true} if the override desktop density is set. + */ + public static boolean isDesktopDensityOverrideSet() { + return DESKTOP_DENSITY_OVERRIDE >= DESKTOP_DENSITY_MIN + && DESKTOP_DENSITY_OVERRIDE <= DESKTOP_DENSITY_MAX; + } } 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 ddf58fb43d6a..42a4ab2e352d 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 @@ -87,6 +87,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.BubbleIconFactory; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; @@ -1188,10 +1189,12 @@ public class BubbleController implements ConfigurationChangeListener, * A bubble is no longer being dragged in Launcher. And was released in given location. * Will be called only when bubble bar is expanded. * - * @param location location where bubble was released + * @param location location where bubble was released + * @param topOnScreen top coordinate of the bubble bar on the screen after release */ - public void stopBubbleDrag(BubbleBarLocation location) { + public void stopBubbleDrag(BubbleBarLocation location, int topOnScreen) { mBubblePositioner.setBubbleBarLocation(location); + mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); if (mBubbleData.getSelectedBubble() != null) { mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true); } @@ -1247,8 +1250,8 @@ public class BubbleController implements ConfigurationChangeListener, * <p>This is used by external callers (launcher). */ @VisibleForTesting - public void expandStackAndSelectBubbleFromLauncher(String key, Rect bubbleBarBounds) { - mBubblePositioner.setBubbleBarBounds(bubbleBarBounds); + public void expandStackAndSelectBubbleFromLauncher(String key, int topOnScreen) { + mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); if (BubbleOverflow.KEY.equals(key)) { mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow()); @@ -1866,7 +1869,11 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void bubbleOverflowChanged(boolean hasBubbles) { - // TODO (b/334175587): tell stack view to hide / show the overflow + if (Flags.enableOptionalBubbleOverflow()) { + if (mStackView != null) { + mStackView.showOverflow(hasBubbles); + } + } } }; @@ -2359,10 +2366,9 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void showBubble(String key, Rect bubbleBarBounds) { + public void showBubble(String key, int topOnScreen) { mMainExecutor.execute( - () -> mController.expandStackAndSelectBubbleFromLauncher( - key, bubbleBarBounds)); + () -> mController.expandStackAndSelectBubbleFromLauncher(key, topOnScreen)); } @Override @@ -2381,8 +2387,8 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void stopBubbleDrag(BubbleBarLocation location) { - mMainExecutor.execute(() -> mController.stopBubbleDrag(location)); + public void stopBubbleDrag(BubbleBarLocation location, int topOnScreen) { + mMainExecutor.execute(() -> mController.stopBubbleDrag(location, topOnScreen)); } @Override @@ -2403,9 +2409,9 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void setBubbleBarBounds(Rect bubbleBarBounds) { + public void updateBubbleBarTopOnScreen(int topOnScreen) { mMainExecutor.execute(() -> { - mBubblePositioner.setBubbleBarBounds(bubbleBarBounds); + mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); if (mLayerView != null) mLayerView.updateExpandedView(); }); } @@ -2719,6 +2725,15 @@ public class BubbleController implements ConfigurationChangeListener, () -> BubbleController.this.onSensitiveNotificationProtectionStateChanged( sensitiveNotificationProtectionActive)); } + + @Override + public boolean canShowBubbleNotification() { + // in bubble bar mode, when the IME is visible we can't animate new bubbles. + if (BubbleController.this.isShowingAsBubbleBar()) { + return !BubbleController.this.mBubblePositioner.getIsImeVisible(); + } + return true; + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java index 633b01bde4ca..18e04d14c71b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java @@ -44,6 +44,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ContrastColorUtil; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import java.util.ArrayList; @@ -195,7 +196,9 @@ public class BubbleOverflowContainerView extends LinearLayout { } void updateEmptyStateVisibility() { - mEmptyState.setVisibility(mOverflowBubbles.isEmpty() ? View.VISIBLE : View.GONE); + boolean showEmptyState = mOverflowBubbles.isEmpty() + && !Flags.enableOptionalBubbleOverflow(); + mEmptyState.setVisibility(showEmptyState ? View.VISIBLE : View.GONE); mRecyclerView.setVisibility(mOverflowBubbles.isEmpty() ? View.GONE : View.VISIBLE); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index c4bbe32e3205..1e482cac0b46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -98,7 +98,7 @@ public class BubblePositioner { private boolean mShowingInBubbleBar; private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT; - private final Rect mBubbleBarBounds = new Rect(); + private int mBubbleBarTopOnScreen; public BubblePositioner(Context context, WindowManager windowManager) { mContext = context; @@ -324,6 +324,11 @@ public class BubblePositioner { return 0; } + /** Returns whether the IME is visible. */ + public boolean getIsImeVisible() { + return mImeVisible; + } + /** Sets whether the IME is visible. **/ public void setImeVisible(boolean visible, int height) { mImeVisible = visible; @@ -841,17 +846,17 @@ public class BubblePositioner { } /** - * Sets the position of the bubble bar in display coordinates. + * Set top coordinate of bubble bar on screen */ - public void setBubbleBarBounds(Rect bubbleBarBounds) { - mBubbleBarBounds.set(bubbleBarBounds); + public void setBubbleBarTopOnScreen(int topOnScreen) { + mBubbleBarTopOnScreen = topOnScreen; } /** - * Returns the display coordinates of the bubble bar. + * Returns the top coordinate of bubble bar on screen */ - public Rect getBubbleBarBounds() { - return mBubbleBarBounds; + public int getBubbleBarTopOnScreen() { + return mBubbleBarTopOnScreen; } /** @@ -903,7 +908,7 @@ public class BubblePositioner { /** The bottom position of the expanded view when showing above the bubble bar. */ public int getExpandedViewBottomForBubbleBar() { - return mBubbleBarBounds.top - mExpandedViewPadding; + return mBubbleBarTopOnScreen - mExpandedViewPadding; } /** 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 be88b3497000..9fabd4247670 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 @@ -80,6 +80,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener; @@ -863,6 +864,7 @@ public class BubbleStackView extends FrameLayout } }; + private boolean mShowingOverflow; private BubbleOverflow mBubbleOverflow; private StackEducationView mStackEduView; private StackEducationView.Manager mStackEducationViewManager; @@ -992,18 +994,12 @@ public class BubbleStackView extends FrameLayout mBubbleOverflow = mBubbleData.getOverflow(); - resetOverflowView(); - mBubbleContainer.addView(mBubbleOverflow.getIconView(), - mBubbleContainer.getChildCount() /* index */, - new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), - mPositioner.getBubbleSize())); - updateOverflow(); - mBubbleOverflow.getIconView().setOnClickListener((View v) -> { - mBubbleData.setShowingOverflow(true); - mBubbleData.setSelectedBubble(mBubbleOverflow); - mBubbleData.setExpanded(true); - }); - + if (Flags.enableOptionalBubbleOverflow()) { + showOverflow(mBubbleData.hasOverflowBubbles()); + } else { + mShowingOverflow = true; // if the flags not on this is always true + setUpOverflow(); + } mScrim = new View(getContext()); mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); mScrim.setBackgroundDrawable(new ColorDrawable( @@ -1220,6 +1216,19 @@ public class BubbleStackView extends FrameLayout } }; + private void setUpOverflow() { + resetOverflowView(); + mBubbleContainer.addView(mBubbleOverflow.getIconView(), + mBubbleContainer.getChildCount() /* index */, + new FrameLayout.LayoutParams(mBubbleSize, mBubbleSize)); + updateOverflow(); + mBubbleOverflow.getIconView().setOnClickListener((View v) -> { + mBubbleData.setShowingOverflow(true); + mBubbleData.setSelectedBubble(mBubbleOverflow); + mBubbleData.setExpanded(true); + }); + } + private void setUpDismissView() { if (mDismissView != null) { removeView(mDismissView); @@ -1458,24 +1467,56 @@ public class BubbleStackView extends FrameLayout b.getExpandedView().updateFontSize(); } } - if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) { + if (mShowingOverflow && mBubbleOverflow != null + && mBubbleOverflow.getExpandedView() != null) { mBubbleOverflow.getExpandedView().updateFontSize(); } } void updateLocale() { - if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) { + if (mShowingOverflow && mBubbleOverflow != null + && mBubbleOverflow.getExpandedView() != null) { mBubbleOverflow.getExpandedView().updateLocale(); } } private void updateOverflow() { mBubbleOverflow.update(); - mBubbleContainer.reorderView(mBubbleOverflow.getIconView(), - mBubbleContainer.getChildCount() - 1 /* index */); + if (mShowingOverflow) { + mBubbleContainer.reorderView(mBubbleOverflow.getIconView(), + mBubbleContainer.getChildCount() - 1 /* index */); + } updateOverflowVisibility(); } + private void updateOverflowVisibility() { + mBubbleOverflow.setVisible(mShowingOverflow + && (mIsExpanded || mBubbleData.isShowingOverflow()) + ? VISIBLE + : GONE); + } + + private void updateOverflowDotVisibility(boolean expanding) { + if (mShowingOverflow && mBubbleOverflow.showDot()) { + mBubbleOverflow.getIconView().animateDotScale(expanding ? 1 : 0f, () -> { + mBubbleOverflow.setVisible(expanding ? VISIBLE : GONE); + }); + } + } + + /** Sets whether the overflow should be visible or not. */ + public void showOverflow(boolean showOverflow) { + if (!Flags.enableOptionalBubbleOverflow()) return; + if (mShowingOverflow != showOverflow) { + mShowingOverflow = showOverflow; + if (showOverflow) { + setUpOverflow(); + } else if (mBubbleOverflow != null) { + resetOverflowView(); + } + } + } + /** * Handle theme changes. */ @@ -1535,7 +1576,10 @@ public class BubbleStackView extends FrameLayout b.getExpandedView().updateDimensions(); } } - mBubbleOverflow.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize)); + if (mShowingOverflow) { + mBubbleOverflow.getIconView().setLayoutParams( + new LayoutParams(mBubbleSize, mBubbleSize)); + } mExpandedAnimationController.updateResources(); mStackAnimationController.updateResources(); mDismissView.updateResources(); @@ -1699,7 +1743,7 @@ public class BubbleStackView extends FrameLayout bubble.getIconView().setContentDescription(getResources().getString( R.string.bubble_content_description_single, titleStr, appName)); } else { - final int moreCount = mBubbleContainer.getChildCount() - 1; + final int moreCount = getBubbleCount(); bubble.getIconView().setContentDescription(getResources().getString( R.string.bubble_content_description_stack, titleStr, appName, moreCount)); @@ -1752,7 +1796,8 @@ public class BubbleStackView extends FrameLayout View bubbleOverflowIconView = mBubbleOverflow != null ? mBubbleOverflow.getIconView() : null; - if (bubbleOverflowIconView != null && !mBubbleData.getBubbles().isEmpty()) { + if (mShowingOverflow && bubbleOverflowIconView != null + && !mBubbleData.getBubbles().isEmpty()) { Bubble lastBubble = mBubbleData.getBubbles().get(mBubbleData.getBubbles().size() - 1); View lastBubbleIconView = lastBubble.getIconView(); @@ -1928,20 +1973,6 @@ public class BubbleStackView extends FrameLayout } } - private void updateOverflowVisibility() { - mBubbleOverflow.setVisible((mIsExpanded || mBubbleData.isShowingOverflow()) - ? VISIBLE - : GONE); - } - - private void updateOverflowDotVisibility(boolean expanding) { - if (mBubbleOverflow.showDot()) { - mBubbleOverflow.getIconView().animateDotScale(expanding ? 1 : 0f, () -> { - mBubbleOverflow.setVisible(expanding ? VISIBLE : GONE); - }); - } - } - // via BubbleData.Listener void updateBubble(Bubble bubble) { animateInFlyoutForBubble(bubble); @@ -3428,8 +3459,9 @@ public class BubbleStackView extends FrameLayout * @return the number of bubbles in the stack view. */ public int getBubbleCount() { - // Subtract 1 for the overflow button that is always in the bubble container. - return mBubbleContainer.getChildCount() - 1; + final int childCount = mBubbleContainer.getChildCount(); + // Subtract 1 for the overflow button if it's showing. + return mShowingOverflow ? childCount - 1 : childCount; } /** 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 322088b17e63..1d053f9aab35 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 @@ -297,6 +297,15 @@ public interface Bubbles { boolean sensitiveNotificationProtectionActive); /** + * Determines whether Bubbles can show notifications. + * + * <p>Normally bubble notifications are shown by Bubbles, but in some cases the bubble + * notification is suppressed and should be shown by the Notifications pipeline as regular + * notifications. + */ + boolean canShowBubbleNotification(); + + /** * A listener to be notified of bubble state changes, used by launcher to render bubbles in * its process. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 1eff149f2e91..1db556c04180 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -31,7 +31,7 @@ interface IBubbles { oneway void unregisterBubbleListener(in IBubblesListener listener) = 2; - oneway void showBubble(in String key, in Rect bubbleBarBounds) = 3; + oneway void showBubble(in String key, in int topOnScreen) = 3; oneway void dragBubbleToDismiss(in String key) = 4; @@ -45,7 +45,7 @@ interface IBubbles { oneway void setBubbleBarLocation(in BubbleBarLocation location) = 9; - oneway void setBubbleBarBounds(in Rect bubbleBarBounds) = 10; + oneway void updateBubbleBarTopOnScreen(in int topOnScreen) = 10; - oneway void stopBubbleDrag(in BubbleBarLocation location) = 11; + oneway void stopBubbleDrag(in BubbleBarLocation location, in int topOnScreen) = 11; }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 45ad6319bbf8..8e58db198b13 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -237,12 +237,10 @@ public class BubbleBarAnimationHelper { private void setScaleFromBubbleBar(AnimatableScaleMatrix matrix, float scale) { // Set the pivot point for the scale, so the view animates out from the bubble bar. - Rect bubbleBarBounds = mPositioner.getBubbleBarBounds(); - matrix.setScale( - scale, - scale, - bubbleBarBounds.centerX(), - bubbleBarBounds.top); + Rect availableRect = mPositioner.getAvailableRect(); + float pivotX = mPositioner.isBubbleBarOnLeft() ? availableRect.left : availableRect.right; + float pivotY = mPositioner.getBubbleBarTopOnScreen(); + matrix.setScale(scale, scale, pivotX, pivotY); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java index 2b7a0706b4de..d54a6b002e43 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java @@ -37,15 +37,11 @@ import com.android.wm.shell.R; */ public class BubbleBarHandleView extends View { private static final long COLOR_CHANGE_DURATION = 120; - - // The handle view is currently rendered as 3 evenly spaced dots. - private int mDotSize; - private int mDotSpacing; // Path used to draw the dots private final Path mPath = new Path(); - private @ColorInt int mHandleLightColor; - private @ColorInt int mHandleDarkColor; + private final @ColorInt int mHandleLightColor; + private final @ColorInt int mHandleDarkColor; private @Nullable ObjectAnimator mColorChangeAnim; public BubbleBarHandleView(Context context) { @@ -63,10 +59,8 @@ public class BubbleBarHandleView extends View { public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mDotSize = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_caption_dot_size); - mDotSpacing = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_caption_dot_spacing); + final int handleHeight = getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_handle_height); mHandleLightColor = ContextCompat.getColor(getContext(), R.color.bubble_bar_expanded_view_handle_light); mHandleDarkColor = ContextCompat.getColor(getContext(), @@ -76,27 +70,13 @@ public class BubbleBarHandleView extends View { setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { - final int handleCenterX = view.getWidth() / 2; final int handleCenterY = view.getHeight() / 2; - final int handleTotalWidth = mDotSize * 3 + mDotSpacing * 2; - final int handleLeft = handleCenterX - handleTotalWidth / 2; - final int handleTop = handleCenterY - mDotSize / 2; - final int handleBottom = handleTop + mDotSize; - RectF dot1 = new RectF( - handleLeft, handleTop, - handleLeft + mDotSize, handleBottom); - RectF dot2 = new RectF( - dot1.right + mDotSpacing, handleTop, - dot1.right + mDotSpacing + mDotSize, handleBottom - ); - RectF dot3 = new RectF( - dot2.right + mDotSpacing, handleTop, - dot2.right + mDotSpacing + mDotSize, handleBottom - ); + final int handleTop = handleCenterY - handleHeight / 2; + final int handleBottom = handleTop + handleHeight; + final int radius = handleHeight / 2; + RectF handle = new RectF(/* left = */ 0, handleTop, view.getWidth(), handleBottom); mPath.reset(); - mPath.addOval(dot1, Path.Direction.CW); - mPath.addOval(dot2, Path.Direction.CW); - mPath.addOval(dot3, Path.Direction.CW); + mPath.addRoundRect(handle, radius, radius, Path.Direction.CW); outline.setPath(mPath); } }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index 607a3b5423d1..2234041b8c9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -347,7 +347,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { if (mMoving) { final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos; mLastDraggingPosition = position; - mSplitLayout.updateDividerBounds(position); + mSplitLayout.updateDividerBounds(position, true /* shouldUseParallaxEffect */); } break; case MotionEvent.ACTION_UP: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 30eb8b5d2f05..de016d3ae400 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -31,7 +31,6 @@ import android.animation.ValueAnimator; import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; -import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -57,7 +56,13 @@ import com.android.wm.shell.common.SurfaceUtils; import java.util.function.Consumer; /** - * Handles split decor like showing resizing hint for a specific split. + * Handles additional layers over a running task in a split pair, for example showing a veil with an + * app icon when the task is being resized (usually to hide weird layouts while the app is being + * stretched). One SplitDecorManager is initialized on each window. + * <br> + * Currently, we show a veil when: + * a) Task is resizing down from a fullscreen window. + * b) Task is being stretched past its original bounds. */ public class SplitDecorManager extends WindowlessWindowManager { private static final String TAG = SplitDecorManager.class.getSimpleName(); @@ -78,7 +83,11 @@ public class SplitDecorManager extends WindowlessWindowManager { private boolean mShown; private boolean mIsResizing; - private final Rect mOldBounds = new Rect(); + /** The original bounds of the main task, captured at the beginning of a resize transition. */ + private final Rect mOldMainBounds = new Rect(); + /** The original bounds of the side task, captured at the beginning of a resize transition. */ + private final Rect mOldSideBounds = new Rect(); + /** The current bounds of the main task, mid-resize. */ private final Rect mResizingBounds = new Rect(); private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; @@ -184,29 +193,38 @@ public class SplitDecorManager extends WindowlessWindowManager { mResizingIconView = null; mIsResizing = false; mShown = false; - mOldBounds.setEmpty(); + mOldMainBounds.setEmpty(); + mOldSideBounds.setEmpty(); mResizingBounds.setEmpty(); } /** Showing resizing hint. */ public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, - boolean immediately) { + boolean immediately, float[] veilColor) { if (mResizingIconView == null) { return; } if (!mIsResizing) { mIsResizing = true; - mOldBounds.set(newBounds); + mOldMainBounds.set(newBounds); + mOldSideBounds.set(sideBounds); } mResizingBounds.set(newBounds); mOffsetX = offsetX; mOffsetY = offsetY; - final boolean show = - newBounds.width() > mOldBounds.width() || newBounds.height() > mOldBounds.height(); - final boolean update = show != mShown; + // Show a veil when: + // a) Task is resizing down from a fullscreen window. + // b) Task is being stretched past its original bounds. + final boolean isResizingDownFromFullscreen = + mOldSideBounds.width() <= 1 || mOldSideBounds.height() <= 1; + final boolean isStretchingPastOriginalBounds = + newBounds.width() > mOldMainBounds.width() + || newBounds.height() > mOldMainBounds.height(); + final boolean showVeil = isResizingDownFromFullscreen || isStretchingPastOriginalBounds; + final boolean update = showVeil != mShown; if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) { // If we need to animate and animator still running, cancel it before we ensure both // background and icon surfaces are non null for next animation. @@ -216,18 +234,18 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mBackgroundLeash == null) { mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); - t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) + t.setColor(mBackgroundLeash, veilColor) .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); } if (mGapBackgroundLeash == null && !immediately) { final boolean isLandscape = newBounds.height() == sideBounds.height(); - final int left = isLandscape ? mOldBounds.width() : 0; - final int top = isLandscape ? 0 : mOldBounds.height(); + final int left = isLandscape ? mOldMainBounds.width() : 0; + final int top = isLandscape ? 0 : mOldMainBounds.height(); mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession); // Fill up another side bounds area. - t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask)) + t.setColor(mGapBackgroundLeash, veilColor) .setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2) .setPosition(mGapBackgroundLeash, left, top) .setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height()); @@ -251,12 +269,12 @@ public class SplitDecorManager extends WindowlessWindowManager { if (update) { if (immediately) { - t.setVisibility(mBackgroundLeash, show); - t.setVisibility(mIconLeash, show); + t.setVisibility(mBackgroundLeash, showVeil); + t.setVisibility(mIconLeash, showVeil); } else { - startFadeAnimation(show, false, null); + startFadeAnimation(showVeil, false, null); } - mShown = show; + mShown = showVeil; } } @@ -309,7 +327,8 @@ public class SplitDecorManager extends WindowlessWindowManager { mIsResizing = false; mOffsetX = 0; mOffsetY = 0; - mOldBounds.setEmpty(); + mOldMainBounds.setEmpty(); + mOldSideBounds.setEmpty(); mResizingBounds.setEmpty(); if (mFadeAnimator != null && mFadeAnimator.isRunning()) { if (!mShown) { @@ -346,14 +365,14 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Screenshot host leash and attach on it if meet some conditions */ public void screenshotIfNeeded(SurfaceControl.Transaction t) { - if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { t.remove(mScreenshot); } - mTempRect.set(mOldBounds); + mTempRect.set(mOldMainBounds); mTempRect.offsetTo(0, 0); mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect, Integer.MAX_VALUE - 1); @@ -364,7 +383,7 @@ public class SplitDecorManager extends WindowlessWindowManager { public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) { if (screenshot == null || !screenshot.isValid()) return; - if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { @@ -465,9 +484,4 @@ public class SplitDecorManager extends WindowlessWindowManager { mIcon = null; } } - - private static float[] getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { - final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); - return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).getComponents(); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 2ea32f44a78b..8331654839c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -496,10 +496,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Updates bounds with the passing position. Usually used to update recording bounds while * performing animation or dragging divider bar to resize the splits. */ - void updateDividerBounds(int position) { + void updateDividerBounds(int position, boolean shouldUseParallaxEffect) { updateBounds(position); mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x, - mSurfaceEffectPolicy.mParallaxOffset.y); + mSurfaceEffectPolicy.mParallaxOffset.y, shouldUseParallaxEffect); } void setDividerPosition(int position, boolean applyLayoutChange) { @@ -647,7 +647,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange .setDuration(duration); mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); mDividerFlingAnimator.addUpdateListener( - animation -> updateDividerBounds((int) animation.getAnimatedValue())); + animation -> updateDividerBounds( + (int) animation.getAnimatedValue(), false /* shouldUseParallaxEffect */) + ); mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -897,7 +899,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, * SurfaceControl, SurfaceControl, boolean) */ - void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY); + void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, + boolean shouldUseParallaxEffect); /** * Calls when finish resizing the split bounds. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java index f9259e79472e..e8226051b672 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -16,8 +16,6 @@ package com.android.wm.shell.common.split; -import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED; - import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -26,25 +24,18 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import android.app.ActivityManager; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.Intent; -import android.content.pm.LauncherApps; -import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Rect; -import android.os.UserHandle; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.util.ArrayUtils; import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; -import java.util.Arrays; -import java.util.List; - /** Helper utility class for split screen components to use. */ public class SplitScreenUtils { /** Reverse the split position. */ @@ -137,4 +128,10 @@ public class SplitScreenUtils { return isLandscape; } } + + /** Returns the specified background color that matches a RunningTaskInfo. */ + public static Color getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { + final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); + return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor); + } } 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 fb0a1ab3062e..4e9e8f97620c 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 @@ -100,6 +100,7 @@ import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition; import com.android.wm.shell.unfold.qualifier.UnfoldTransition; import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; +import com.android.wm.shell.windowdecor.ResizeHandleSizeRepository; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import dagger.Binds; @@ -220,7 +221,8 @@ public abstract class WMShellModule { SyncTransactionQueue syncQueue, Transitions transitions, Optional<DesktopTasksController> desktopTasksController, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + ResizeHandleSizeRepository resizeHandleSizeRepository) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return new DesktopModeWindowDecorViewModel( context, @@ -237,7 +239,8 @@ public abstract class WMShellModule { syncQueue, transitions, desktopTasksController, - rootTaskDisplayAreaOrganizer); + rootTaskDisplayAreaOrganizer, + resizeHandleSizeRepository); } return new CaptionWindowDecorViewModel( context, @@ -247,7 +250,8 @@ public abstract class WMShellModule { displayController, rootTaskDisplayAreaOrganizer, syncQueue, - transitions); + transitions, + resizeHandleSizeRepository); } // @@ -529,7 +533,8 @@ public abstract class WMShellModule { exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopModeTaskRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, - recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter); + recentsTransitionHandler, multiInstanceHelper, + mainExecutor, desktopTasksLimiter); } @WMSingleton @@ -622,6 +627,12 @@ public abstract class WMShellModule { return new DesktopModeEventLogger(); } + @WMSingleton + @Provides + static ResizeHandleSizeRepository provideResizeHandleSizeRepository() { + return new ResizeHandleSizeRepository(); + } + // // Drag and drop // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 414a9d1151ac..01364d1de279 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -133,6 +133,7 @@ public abstract class Pip2Module { PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, @NonNull PipTransitionState pipTransitionState, + @NonNull PipScheduler pipScheduler, @NonNull SizeSpecSource sizeSpecSource, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, @@ -140,7 +141,7 @@ public abstract class Pip2Module { @ShellMainThread ShellExecutor mainExecutor, Optional<PipPerfHintController> pipPerfHintControllerOptional) { return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm, - pipBoundsState, pipTransitionState, sizeSpecSource, pipMotionHelper, + pipBoundsState, pipTransitionState, pipScheduler, sizeSpecSource, pipMotionHelper, floatingContentCoordinator, pipUiEventLogger, mainExecutor, pipPerfHintControllerOptional); } 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 2dc4573b4921..e5bf53a4afdb 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 @@ -73,6 +73,8 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 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.annotations.ExternalThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.splitscreen.SplitScreenController @@ -906,18 +908,22 @@ class DesktopTasksController( task.taskId ) return WindowContainerTransaction().also { wct -> - addMoveToFullscreenChanges(wct, task) + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) + wct.reorder(task.token, true) } } + val wct = WindowContainerTransaction() + if (isDesktopDensityOverrideSet()) { + wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE) + } // Desktop Mode is showing and we're launching a new Task - we might need to minimize // a Task. - val wct = WindowContainerTransaction() val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) if (taskToMinimize != null) { addPendingMinimizeTransition(transition, taskToMinimize) return wct } - return null + return if (wct.isEmpty) null else wct } private fun handleFullscreenTaskLaunch( @@ -985,7 +991,7 @@ class DesktopTasksController( wct.setWindowingMode(taskInfo.token, targetWindowingMode) wct.reorder(taskInfo.token, true /* onTop */) if (isDesktopDensityOverrideSet()) { - wct.setDensityDpi(taskInfo.token, getDesktopDensityDpi()) + wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE) } } @@ -1089,10 +1095,6 @@ class DesktopTasksController( return context.resources.displayMetrics.densityDpi } - private fun getDesktopDensityDpi(): Int { - return DESKTOP_DENSITY_OVERRIDE - } - /** Creates a new instance of the external interface to pass to another process. */ private fun createExternalInterface(): ExternalInterfaceBinder { return IDesktopModeImpl(this) @@ -1466,21 +1468,9 @@ class DesktopTasksController( } companion object { - private val DESKTOP_DENSITY_OVERRIDE = - SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284) - private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000) - @JvmField val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f - - /** - * Check if desktop density override is enabled - */ - @JvmStatic - fun isDesktopDensityOverrideSet(): Boolean { - return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE - } } /** The positions on a screen that a task can snap to. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index 59d696918448..4bb10dfdf8c6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -22,11 +22,11 @@ import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS; import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenUtils.getResizingBackgroundColor; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; @@ -41,7 +41,6 @@ import android.app.StatusBarManager; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Color; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; @@ -278,7 +277,7 @@ public class DragLayout extends LinearLayout final int activityType = taskInfo1.getActivityType(); if (activityType == ACTIVITY_TYPE_STANDARD) { Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo); - int bgColor1 = getResizingBackgroundColor(taskInfo1); + int bgColor1 = getResizingBackgroundColor(taskInfo1).toArgb(); mDropZoneView1.setAppInfo(bgColor1, icon1); mDropZoneView2.setAppInfo(bgColor1, icon1); updateDropZoneSizes(null, null); // passing null splits the views evenly @@ -298,10 +297,10 @@ public class DragLayout extends LinearLayout mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); if (topOrLeftTask != null && bottomOrRightTask != null) { Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo); - int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask); + int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask).toArgb(); Drawable bottomOrRightIcon = mIconProvider.getIcon( bottomOrRightTask.topActivityInfo); - int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask); + int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask).toArgb(); mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon); mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon); } @@ -556,11 +555,6 @@ public class DragLayout extends LinearLayout } } - private static int getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { - final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); - return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb(); - } - /** * Dumps information about this drag layout. */ 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 a097a0ffa47d..be10151ca5aa 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 @@ -58,7 +58,8 @@ import java.util.function.Consumer; * A helper to animate and manipulate the PiP. */ public class PipMotionHelper implements PipAppOpsListener.Callback, - FloatingContentCoordinator.FloatingContent { + FloatingContentCoordinator.FloatingContent, + PipTransitionState.PipTransitionStateChangedListener { private static final String TAG = "PipMotionHelper"; private static final String FLING_BOUNDS_CHANGE = "fling_bounds_change"; private static final boolean DEBUG = false; @@ -181,7 +182,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, } }; mPipTransitionState = pipTransitionState; - mPipTransitionState.addPipTransitionStateChangedListener(this::onPipTransitionStateChanged); + mPipTransitionState.addPipTransitionStateChangedListener(this); } void init() { @@ -687,7 +688,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, // setAnimatingToBounds(toBounds); } - private void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { switch (newState) { 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 04cf350ddd3e..b55a41d8808f 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 @@ -24,6 +24,7 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.hardware.input.InputManager; +import android.os.Bundle; import android.os.Looper; import android.view.BatchedInputEventReceiver; import android.view.Choreographer; @@ -32,6 +33,7 @@ import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InputMonitor; import android.view.MotionEvent; +import android.view.SurfaceControl; import android.view.ViewConfiguration; import androidx.annotation.VisibleForTesting; @@ -51,16 +53,20 @@ import java.util.function.Consumer; * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to * trigger dynamic resize. */ -public class PipResizeGestureHandler { +public class PipResizeGestureHandler implements + PipTransitionState.PipTransitionStateChangedListener { private static final String TAG = "PipResizeGestureHandler"; private static final int PINCH_RESIZE_SNAP_DURATION = 250; private static final float PINCH_RESIZE_AUTO_MAX_RATIO = 0.9f; + private static final String RESIZE_BOUNDS_CHANGE = "resize_bounds_change"; private final Context mContext; private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final PipBoundsState mPipBoundsState; private final PipTouchState mPipTouchState; + private final PipScheduler mPipScheduler; + private final PipTransitionState mPipTransitionState; private final PhonePipMenuController mPhonePipMenuController; private final PipUiEventLogger mPipUiEventLogger; private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; @@ -88,6 +94,7 @@ public class PipResizeGestureHandler { private boolean mIsSysUiStateValid; private boolean mThresholdCrossed; private boolean mOngoingPinchToResize = false; + private boolean mWaitingForBoundsChangeTransition = false; private float mAngle = 0; int mFirstIndex = -1; int mSecondIndex = -1; @@ -104,11 +111,17 @@ public class PipResizeGestureHandler { private int mCtrlType; private int mOhmOffset; - public PipResizeGestureHandler(Context context, PipBoundsAlgorithm pipBoundsAlgorithm, - PipBoundsState pipBoundsState, PipTouchState pipTouchState, + public PipResizeGestureHandler(Context context, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipBoundsState pipBoundsState, + PipTouchState pipTouchState, + PipScheduler pipScheduler, + PipTransitionState pipTransitionState, Runnable updateMovementBoundsRunnable, - PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, - ShellExecutor mainExecutor, @Nullable PipPerfHintController pipPerfHintController) { + PipUiEventLogger pipUiEventLogger, + PhonePipMenuController menuActivityController, + ShellExecutor mainExecutor, + @Nullable PipPerfHintController pipPerfHintController) { mContext = context; mDisplayId = context.getDisplayId(); mMainExecutor = mainExecutor; @@ -116,6 +129,11 @@ public class PipResizeGestureHandler { mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipBoundsState = pipBoundsState; mPipTouchState = pipTouchState; + mPipScheduler = pipScheduler; + + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); + mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; @@ -125,6 +143,7 @@ public class PipResizeGestureHandler { mUserResizeBounds.set(rect); // mMotionHelper.synchronizePinnedStackBounds(); mUpdateMovementBoundsRunnable.run(); + mPipBoundsState.setBounds(rect); resetState(); }; } @@ -202,7 +221,7 @@ public class PipResizeGestureHandler { @VisibleForTesting void onInputEvent(InputEvent ev) { if (!mEnablePinchResize) { - // No need to handle anything if neither form of resizing is enabled. + // No need to handle anything if resizing isn't enabled. return; } @@ -227,7 +246,7 @@ public class PipResizeGestureHandler { } } - if (mEnablePinchResize && mOngoingPinchToResize) { + if (mOngoingPinchToResize) { onPinchResize(mv); } } @@ -249,13 +268,11 @@ public class PipResizeGestureHandler { } boolean willStartResizeGesture(MotionEvent ev) { - if (isInValidSysUiState()) { - if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { - if (mEnablePinchResize && ev.getPointerCount() == 2) { - onPinchResize(ev); - mOngoingPinchToResize = mAllowGesture; - return mAllowGesture; - } + if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { + if (mEnablePinchResize && ev.getPointerCount() == 2) { + onPinchResize(ev); + mOngoingPinchToResize = mAllowGesture; + return mAllowGesture; } } return false; @@ -284,7 +301,6 @@ public class PipResizeGestureHandler { mSecondIndex = -1; mAllowGesture = false; finishResize(); - cleanUpHighPerfSessionMaybe(); } if (ev.getPointerCount() != 2) { @@ -347,10 +363,7 @@ public class PipResizeGestureHandler { mDownSecondPoint, mLastPoint, mLastSecondPoint, mMinSize, mMaxSize, mDownBounds, mLastResizeBounds); - /* - mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds, - mAngle, null); - */ + mPipScheduler.scheduleUserResizePip(mLastResizeBounds, mAngle); mPipBoundsState.setHasUserResizedPip(true); } } @@ -399,57 +412,43 @@ public class PipResizeGestureHandler { } private void finishResize() { - if (!mLastResizeBounds.isEmpty()) { - // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped - // position correctly. Drag-resize does not need to move, so just finalize resize. - if (mOngoingPinchToResize) { - final Rect startBounds = new Rect(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 - || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { - resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y); - } + if (mLastResizeBounds.isEmpty()) { + resetState(); + } + if (!mOngoingPinchToResize) { + return; + } + final Rect startBounds = new Rect(mLastResizeBounds); - // If user resize is smaller than min size, auto resize to min - if (mLastResizeBounds.width() < mMinSize.x - || mLastResizeBounds.height() < mMinSize.y) { - resizeRectAboutCenter(mLastResizeBounds, mMinSize.x, mMinSize.y); - } + // If user resize is pretty close to max size, just auto resize to max. + if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x + || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { + resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y); + } - // get the current movement bounds - final Rect movementBounds = mPipBoundsAlgorithm - .getMovementBounds(mLastResizeBounds); - - // snap mLastResizeBounds to the correct edge based on movement bounds - snapToMovementBoundsEdge(mLastResizeBounds, movementBounds); - - final float snapFraction = mPipBoundsAlgorithm.getSnapFraction( - mLastResizeBounds, movementBounds); - mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); - - // disable any touch events beyond resizing too - mPipTouchState.setAllowInputEvents(false); - - /* - mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, - PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback, () -> { - // enable touch events - mPipTouchState.setAllowInputEvents(true); - }); - */ - } else { - /* - mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, - TRANSITION_DIRECTION_USER_RESIZE, - mUpdateResizeBoundsCallback); - */ - } - final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f; - mPipUiEventLogger.log( - PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); - } else { - resetState(); + // If user resize is smaller than min size, auto resize to min + if (mLastResizeBounds.width() < mMinSize.x + || mLastResizeBounds.height() < mMinSize.y) { + resizeRectAboutCenter(mLastResizeBounds, mMinSize.x, mMinSize.y); } + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm + .getMovementBounds(mLastResizeBounds); + + // snap mLastResizeBounds to the correct edge based on movement bounds + snapToMovementBoundsEdge(mLastResizeBounds, movementBounds); + + final float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mLastResizeBounds, movementBounds); + mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); + + // Update the transition state to schedule a resize transition. + Bundle extra = new Bundle(); + extra.putBoolean(RESIZE_BOUNDS_CHANGE, true); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); + + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); } private void resetState() { @@ -509,6 +508,40 @@ public class PipResizeGestureHandler { rect.set(l, t, r, b); } + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: + if (!extra.getBoolean(RESIZE_BOUNDS_CHANGE)) break; + mWaitingForBoundsChangeTransition = true; + mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds); + break; + case PipTransitionState.CHANGING_PIP_BOUNDS: + if (!mWaitingForBoundsChangeTransition) break; + + // If bounds change transition was scheduled from this class, handle leash updates. + mWaitingForBoundsChangeTransition = false; + + SurfaceControl.Transaction startTx = extra.getParcelable( + PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); + Rect destinationBounds = extra.getParcelable( + PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); + startTx.setPosition(mPipTransitionState.mPinnedTaskLeash, + destinationBounds.left, destinationBounds.top); + startTx.apply(); + + // All motion operations have actually finished, so make bounds cache updates. + cleanUpHighPerfSessionMaybe(); + + // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core. + mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + + mUpdateResizeBoundsCallback.accept(destinationBounds); + break; + } + } + /** * Dumps the {@link PipResizeGestureHandler} state. */ 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 c5b0de31f104..49475077211f 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 @@ -24,6 +24,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Matrix; import android.graphics.Rect; import android.view.SurfaceControl; import android.window.WindowContainerTransaction; @@ -165,6 +166,16 @@ public class PipScheduler { * {@link WindowContainerTransaction}. */ public void scheduleUserResizePip(Rect toBounds) { + scheduleUserResizePip(toBounds, 0f /* degrees */); + } + + /** + * Directly perform a scaled matrix transformation on the leash. This will not perform any + * {@link WindowContainerTransaction}. + * + * @param degrees the angle to rotate the bounds to. + */ + public void scheduleUserResizePip(Rect toBounds, float degrees) { if (toBounds.isEmpty()) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Attempted to user resize PIP to empty bounds, aborting.", TAG); @@ -172,7 +183,16 @@ public class PipScheduler { } SurfaceControl leash = mPipTransitionState.mPinnedTaskLeash; final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.setPosition(leash, toBounds.left, toBounds.top); + + Matrix transformTensor = new Matrix(); + final float[] mMatrixTmp = new float[9]; + final float scale = (float) toBounds.width() / mPipBoundsState.getBounds().width(); + + transformTensor.setScale(scale, scale); + transformTensor.postTranslate(toBounds.left, toBounds.top); + transformTensor.postRotate(degrees, toBounds.centerX(), toBounds.centerY()); + + tx.setMatrix(leash, transformTensor, mMatrixTmp); tx.apply(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index 9c6e3ea494fa..319d1999a272 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -73,7 +73,7 @@ import java.util.Optional; * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding * the PIP. */ -public class PipTouchHandler { +public class PipTouchHandler implements PipTransitionState.PipTransitionStateChangedListener { private static final String TAG = "PipTouchHandler"; private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; @@ -84,6 +84,7 @@ public class PipTouchHandler { private final PipBoundsAlgorithm mPipBoundsAlgorithm; @NonNull private final PipBoundsState mPipBoundsState; @NonNull private final PipTransitionState mPipTransitionState; + @NonNull private final PipScheduler mPipScheduler; @NonNull private final SizeSpecSource mSizeSpecSource; private final PipUiEventLogger mPipUiEventLogger; private final PipDismissTargetHandler mPipDismissTargetHandler; @@ -173,6 +174,7 @@ public class PipTouchHandler { PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, @NonNull PipTransitionState pipTransitionState, + @NonNull PipScheduler pipScheduler, @NonNull SizeSpecSource sizeSpecSource, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, @@ -188,6 +190,7 @@ public class PipTouchHandler { mPipTransitionState = pipTransitionState; mPipTransitionState.addPipTransitionStateChangedListener(this::onPipTransitionStateChanged); + mPipScheduler = pipScheduler; mSizeSpecSource = sizeSpecSource; mMenuController = menuController; mPipUiEventLogger = pipUiEventLogger; @@ -213,10 +216,10 @@ public class PipTouchHandler { }, menuController::hideMenu, mainExecutor); - mPipResizeGestureHandler = - new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, - mTouchState, this::updateMovementBounds, pipUiEventLogger, - menuController, mainExecutor, mPipPerfHintController); + mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm, + pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState, + this::updateMovementBounds, pipUiEventLogger, menuController, mainExecutor, + mPipPerfHintController); mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize); if (PipUtils.isPip2ExperimentEnabled()) { @@ -1075,7 +1078,8 @@ public class PipTouchHandler { mPipResizeGestureHandler.setOhmOffset(offset); } - private void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { switch (newState) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 6e5b7673e206..0541a0287179 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -501,10 +501,11 @@ class SplitScreenTransitions { mAnimatingTransition = null; mOnFinish.run(); - if (mFinishCallback != null) { - mFinishCallback.onTransitionFinished(wct /* wct */); - mFinishCallback = null; - } + if (mFinishCallback != null) { + Transitions.TransitionFinishCallback currentFinishCallback = mFinishCallback; + mFinishCallback = null; + currentFinishCallback.onTransitionFinished(wct /* wct */); + } } private void startFadeAnimation(@NonNull SurfaceControl leash, boolean show) { 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 8e97068dce84..4299088a51f0 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 @@ -43,6 +43,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString; +import static com.android.wm.shell.common.split.SplitScreenUtils.getResizingBackgroundColor; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; @@ -2388,14 +2389,20 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY) { + public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, + boolean shouldUseParallaxEffect) { final SurfaceControl.Transaction t = mTransactionPool.acquire(); t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); - updateSurfaceBounds(layout, t, true /* applyResizingOffset */); + updateSurfaceBounds(layout, t, shouldUseParallaxEffect); getMainStageBounds(mTempRect1); getSideStageBounds(mTempRect2); - mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately); - mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately); + // TODO (b/307490004): "commonColor" below is a temporary fix to ensure the colors on both + // sides match. When b/307490004 is fixed, this code can be reverted. + float[] commonColor = getResizingBackgroundColor(mSideStage.mRootTaskInfo).getComponents(); + mMainStage.onResizing( + mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately, commonColor); + mSideStage.onResizing( + mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately, commonColor); t.apply(); mTransactionPool.release(t); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index f77c80d2ff3b..0f3d6cade95a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -314,10 +314,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, - int offsetY, boolean immediately) { + int offsetY, boolean immediately, float[] veilColor) { if (mSplitDecorManager != null && mRootTaskInfo != null) { mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t, offsetX, - offsetY, immediately); + offsetY, immediately, veilColor); } } 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 4d3c76322fa8..6224543516fa 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 @@ -31,7 +31,6 @@ import static android.view.WindowManager.fixScale; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; -import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; @@ -567,15 +566,15 @@ public class Transitions implements RemoteCallable<Transitions>, final int mode = change.getMode(); // Put all the OPEN/SHOW on top if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { - if (isOpening - // This is for when an activity launches while a different transition is - // collecting. - || change.hasFlags(FLAG_MOVED_TO_TOP)) { + if (isOpening) { // put on top return zSplitLine + numChanges - i; - } else { + } else if (isClosing) { // put on bottom return zSplitLine - i; + } else { + // maintain relative ordering (put all changes in the animating layer) + return zSplitLine + numChanges - i; } } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { if (isOpening) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index e85cb6400000..bfa163cb2860 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -63,6 +63,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final SyncTransactionQueue mSyncQueue; private final Transitions mTransitions; + private final ResizeHandleSizeRepository mResizeHandleSizeRepository; private TaskOperations mTaskOperations; private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); @@ -75,7 +76,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { DisplayController displayController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SyncTransactionQueue syncQueue, - Transitions transitions) { + Transitions transitions, + ResizeHandleSizeRepository resizeHandleSizeRepository) { mContext = context; mMainHandler = mainHandler; mMainChoreographer = mainChoreographer; @@ -84,6 +86,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mSyncQueue = syncQueue; mTransitions = transitions; + mResizeHandleSizeRepository = resizeHandleSizeRepository; if (!Transitions.ENABLE_SHELL_TRANSITIONS) { mTaskOperations = new TaskOperations(null, mContext, mSyncQueue); } @@ -231,7 +234,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { taskSurface, mMainHandler, mMainChoreographer, - mSyncQueue); + mSyncQueue, + mResizeHandleSizeRepository); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); final FluidResizeTaskPositioner taskPositioner = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 6671391efdeb..1be3b02e1465 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -16,11 +16,11 @@ package com.android.wm.shell.windowdecor; -import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; -import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; -import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; +import static com.android.wm.shell.windowdecor.ResizeHandleSizeRepository.getFineResizeCornerPixels; +import static com.android.wm.shell.windowdecor.ResizeHandleSizeRepository.getLargeResizeCornerPixels; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; @@ -45,6 +45,8 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; +import java.util.function.Consumer; + /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with * {@link CaptionWindowDecorViewModel}. The caption bar contains a back button, minimize button, @@ -58,12 +60,28 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private View.OnClickListener mOnCaptionButtonClickListener; private View.OnTouchListener mOnCaptionTouchListener; private DragPositioningCallback mDragPositioningCallback; + // Listener for handling drag resize events. Will be null if the task cannot be resized. + @Nullable private DragResizeInputListener mDragResizeListener; private DragDetector mDragDetector; private RelayoutParams mRelayoutParams = new RelayoutParams(); private final RelayoutResult<WindowDecorLinearLayout> mResult = new RelayoutResult<>(); + private final ResizeHandleSizeRepository mResizeHandleSizeRepository; + private final Consumer<ResizeHandleSizeRepository> mResizeHandleSizeChangedFunction = + (ResizeHandleSizeRepository sizeRepository) -> { + if (mDragResizeListener == null) { + return; + } + final Resources res = mResult.mRootView.getResources(); + mDragResizeListener.setGeometry( + new DragResizeWindowGeometry(0 /* taskCornerRadius */, + new Size(mResult.mWidth, mResult.mHeight), + sizeRepository.getResizeEdgeHandlePixels(res), + getFineResizeCornerPixels(res), getLargeResizeCornerPixels(res)), + mDragDetector.getTouchSlop()); + }; CaptionWindowDecoration( Context context, @@ -73,13 +91,16 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL SurfaceControl taskSurface, Handler handler, Choreographer choreographer, - SyncTransactionQueue syncQueue) { + SyncTransactionQueue syncQueue, + ResizeHandleSizeRepository resizeHandleSizeRepository) { super(context, displayController, taskOrganizer, taskInfo, taskSurface, taskInfo.getConfiguration()); mHandler = handler; mChoreographer = choreographer; mSyncQueue = syncQueue; + mResizeHandleSizeRepository = resizeHandleSizeRepository; + mResizeHandleSizeRepository.registerSizeChangeFunction(mResizeHandleSizeChangedFunction); } void setCaptionListeners( @@ -238,10 +259,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL .getScaledTouchSlop(); mDragDetector.setTouchSlop(touchSlop); - final Resources res = mResult.mRootView.getResources(); - mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */, - new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res), - getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop); + mResizeHandleSizeChangedFunction.accept(mResizeHandleSizeRepository); } /** 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 9afb057ffbe5..10ab13a74042 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 @@ -149,6 +149,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new DesktopModeKeyguardChangeListener(); private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final DisplayInsetsController mDisplayInsetsController; + private final ResizeHandleSizeRepository mResizeHandleSizeRepository; private final Region mExclusionRegion = Region.obtain(); private boolean mInImmersiveMode; @@ -181,7 +182,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SyncTransactionQueue syncQueue, Transitions transitions, Optional<DesktopTasksController> desktopTasksController, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + ResizeHandleSizeRepository resizeHandleSizeRepository ) { this( context, @@ -202,7 +204,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new InputMonitorFactory(), SurfaceControl.Transaction::new, rootTaskDisplayAreaOrganizer, - new SparseArray<>()); + new SparseArray<>(), + resizeHandleSizeRepository); } @VisibleForTesting @@ -225,7 +228,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { InputMonitorFactory inputMonitorFactory, Supplier<SurfaceControl.Transaction> transactionFactory, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId) { + SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId, + ResizeHandleSizeRepository resizeHandleSizeRepository) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -246,6 +250,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mInputManager = mContext.getSystemService(InputManager.class); mWindowDecorByTaskId = windowDecorByTaskId; + mResizeHandleSizeRepository = resizeHandleSizeRepository; shellInit.addInitCallback(this::onInit, this); } @@ -1060,7 +1065,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mMainHandler, mMainChoreographer, mSyncQueue, - mRootTaskDisplayAreaOrganizer); + mRootTaskDisplayAreaOrganizer, + mResizeHandleSizeRepository); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); final DragPositioningCallback dragPositioningCallback; 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 4d4dc3c72420..bb89adfeac1b 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 @@ -24,11 +24,11 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; -import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; -import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; -import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; +import static com.android.wm.shell.windowdecor.ResizeHandleSizeRepository.getFineResizeCornerPixels; +import static com.android.wm.shell.windowdecor.ResizeHandleSizeRepository.getLargeResizeCornerPixels; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.WindowConfiguration.WindowingMode; import android.content.Context; @@ -60,13 +60,13 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.IconProvider; +import com.android.window.flags.Flags; import com.android.wm.shell.R; 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.SyncTransactionQueue; -import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder; @@ -75,6 +75,7 @@ import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationVi import kotlin.Unit; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -96,6 +97,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private View.OnLongClickListener mOnCaptionLongClickListener; private View.OnGenericMotionListener mOnCaptionGenericMotionListener; private DragPositioningCallback mDragPositioningCallback; + // Listener for handling drag resize events. Will be null if the task cannot be resized. + @Nullable private DragResizeInputListener mDragResizeListener; private DragDetector mDragDetector; @@ -118,6 +121,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final ResizeHandleSizeRepository mResizeHandleSizeRepository; + private final Function<ResizeHandleSizeRepository, Boolean> mResizeHandleSizeChangedFunction = + (ResizeHandleSizeRepository sizeRepository) -> { + final Resources res = mResult.mRootView.getResources(); + return mDragResizeListener == null || mDragResizeListener.setGeometry( + new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius, + new Size(mResult.mWidth, mResult.mHeight), + sizeRepository.getResizeEdgeHandlePixels(res), + getFineResizeCornerPixels(res), + getLargeResizeCornerPixels(res)), + mDragDetector.getTouchSlop()); + }; + DesktopModeWindowDecoration( Context context, DisplayController displayController, @@ -128,12 +144,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + ResizeHandleSizeRepository resizeHandleSizeRepository) { this (context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig, handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer, - SurfaceControl.Builder::new, SurfaceControl.Transaction::new, - WindowContainerTransaction::new, SurfaceControl::new, - new SurfaceControlViewHostFactory() {}); + resizeHandleSizeRepository, SurfaceControl.Builder::new, + SurfaceControl.Transaction::new, WindowContainerTransaction::new, + SurfaceControl::new, new SurfaceControlViewHostFactory() {}); } DesktopModeWindowDecoration( @@ -147,6 +164,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + ResizeHandleSizeRepository resizeHandleSizeRepository, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, @@ -160,6 +178,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mChoreographer = choreographer; mSyncQueue = syncQueue; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mResizeHandleSizeRepository = resizeHandleSizeRepository; + mResizeHandleSizeRepository.registerSizeChangeFunction( + mResizeHandleSizeChangedFunction::apply); } void setCaptionListeners( @@ -305,11 +326,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // If either task geometry or position have changed, update this task's // exclusion region listener - final Resources res = mResult.mRootView.getResources(); - if (mDragResizeListener.setGeometry( - new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius, - new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res), - getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop) + if (mResizeHandleSizeChangedFunction.apply(mResizeHandleSizeRepository) || !mTaskInfo.positionInParent.equals(mPositionInParent)) { updateExclusionRegion(); } @@ -332,13 +349,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean applyStartTransactionOnDraw, 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; relayoutParams.reset(); relayoutParams.mRunningTaskInfo = taskInfo; relayoutParams.mLayoutResId = captionLayoutId; relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId); - if (captionLayoutId == R.layout.desktop_mode_app_controls_window_decor) { + if (isAppHeader) { if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { // If the app is requesting to customize the caption bar, allow input to fall // through to the windows below so that the app can respond to input events on @@ -359,7 +379,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin controlsElement.mWidthResId = R.dimen.desktop_mode_customizable_caption_margin_end; controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; relayoutParams.mOccludingCaptionElements.add(controlsElement); - } else if (captionLayoutId == R.layout.desktop_mode_focused_window_decor) { + } else if (isAppHandle) { // The focused decor (fullscreen/split) does not need to handle input because input in // the App Handle is handled by the InputMonitor in DesktopModeWindowDecorViewModel. relayoutParams.mInputFeatures @@ -372,19 +392,25 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; relayoutParams.mSetTaskPositionAndCrop = shouldSetTaskPositionAndCrop; - // The configuration used to lay out the window decoration. The system context's config is - // used when the task density has been overridden to a custom density so that the resources - // and views of the decoration aren't affected and match the rest of the System UI, if not - // then just use the task's configuration. A copy is made instead of using the original - // reference so that the configuration isn't mutated on config changes and diff checks can - // be made in WindowDecoration#relayout using the pre/post-relayout configuration. - // See b/301119301. + + // The configuration used to layout the window decoration. A copy is made instead of using + // the original reference so that the configuration isn't mutated on config changes and + // diff checks can be made in WindowDecoration#relayout using the pre/post-relayout + // configuration. See b/301119301. // TODO(b/301119301): consider moving the config data needed for diffs to relayout params // instead of using a whole Configuration as a parameter. final Configuration windowDecorConfig = new Configuration(); - windowDecorConfig.setTo(DesktopTasksController.isDesktopDensityOverrideSet() - ? context.getResources().getConfiguration() // Use system context. - : taskInfo.configuration); // Use task configuration. + if (Flags.enableAppHeaderWithTaskDensity() && isAppHeader) { + // 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()) { + // The task has had its density overridden, but keep using the system's density to + // layout the header. + windowDecorConfig.setTo(context.getResources().getConfiguration()); + } else { + windowDecorConfig.setTo(taskInfo.configuration); + } relayoutParams.mWindowDecorConfig = windowDecorConfig; if (DesktopModeStatus.useRoundedCorners()) { @@ -936,9 +962,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + ResizeHandleSizeRepository resizeHandleSizeRepository) { final Configuration windowDecorConfig = - DesktopTasksController.isDesktopDensityOverrideSet() + DesktopModeStatus.isDesktopDensityOverrideSet() ? context.getResources().getConfiguration() // Use system context : taskInfo.configuration; // Use task configuration return new DesktopModeWindowDecoration( @@ -951,7 +978,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin handler, choreographer, syncQueue, - rootTaskDisplayAreaOrganizer); + rootTaskDisplayAreaOrganizer, + resizeHandleSizeRepository); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java index da268988bac7..a3b0a7122752 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java @@ -119,6 +119,10 @@ class DragDetector { mTouchSlop = touchSlop; } + int getTouchSlop() { + return mTouchSlop; + } + private void resetState() { mIsDragEvent = false; mInputDownPoint.set(0, 0); 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..80d60d4fbdd0 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 @@ -26,7 +26,6 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED; import android.annotation.NonNull; -import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; @@ -36,8 +35,6 @@ import android.view.MotionEvent; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.android.wm.shell.R; - import java.util.Objects; /** @@ -63,6 +60,10 @@ final class DragResizeWindowGeometry { // Extra-large edge bounds for logging to help debug when an edge resize is ignored. private final @Nullable TaskEdges mDebugTaskEdges; + /** + * Constructs an instance representing the drag resize touch input regions, where all sizes + * are represented in pixels. + */ DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize, int resizeHandleThickness, int fineCornerSize, int largeCornerSize) { mTaskCornerRadius = taskCornerRadius; @@ -82,31 +83,6 @@ final class DragResizeWindowGeometry { } /** - * Returns the resource value to use for the resize handle on the edge of the window. - */ - static int getResizeEdgeHandleSize(@NonNull Resources res) { - return enableWindowingEdgeDragResize() - ? res.getDimensionPixelSize(R.dimen.desktop_mode_edge_handle) - : res.getDimensionPixelSize(R.dimen.freeform_resize_handle); - } - - /** - * Returns the resource value to use for course input, such as touch, that benefits from a large - * square on each of the window's corners. - */ - static int getLargeResizeCornerSize(@NonNull Resources res) { - return res.getDimensionPixelSize(R.dimen.desktop_mode_corner_resize_large); - } - - /** - * Returns the resource value to use for fine input, such as stylus, that can use a smaller - * square on each of the window's corners. - */ - static int getFineResizeCornerSize(@NonNull Resources res) { - return res.getDimensionPixelSize(R.dimen.freeform_resize_corner); - } - - /** * Returns the size of the task this geometry is calculated for. */ @NonNull Size getTaskSize() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeHandleSizeRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeHandleSizeRepository.kt new file mode 100644 index 000000000000..be7a301ec4c3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeHandleSizeRepository.kt @@ -0,0 +1,133 @@ +/* + * 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.content.res.Resources +import com.android.window.flags.Flags.enableWindowingEdgeDragResize +import com.android.wm.shell.R +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.util.KtProtoLog +import java.util.function.Consumer + +/** Repository for desktop mode drag resize handle sizes. */ +class ResizeHandleSizeRepository { + private val TAG = "ResizeHandleSizeRepository" + private var edgeResizeHandleSizePixels: Int? = null + private var sizeChangeFunctions: MutableList<Consumer<ResizeHandleSizeRepository>> = + mutableListOf() + + /** + * Resets the window edge resize handle size if necessary. + */ + fun resetResizeEdgeHandlePixels() { + if (enableWindowingEdgeDragResize() && edgeResizeHandleSizePixels != null) { + edgeResizeHandleSizePixels = null + applyToAll() + } + } + + /** + * Sets the window edge resize handle to the given size in pixels. + */ + fun setResizeEdgeHandlePixels(sizePixels: Int) { + if (enableWindowingEdgeDragResize()) { + KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "$TAG: Set edge handle size to $sizePixels") + if (edgeResizeHandleSizePixels != null && edgeResizeHandleSizePixels == sizePixels) { + // Skip updating since override is the same size + return + } + edgeResizeHandleSizePixels = sizePixels + applyToAll() + } else { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "$TAG: Can't set edge handle size to $sizePixels since " + + "enable_windowing_edge_drag_resize disabled" + ) + } + } + + /** + * Returns the resource value, in pixels, to use for the resize handle on the edge of the + * window. + */ + fun getResizeEdgeHandlePixels(res: Resources): Int { + try { + return if (enableWindowingEdgeDragResize()) { + val resPixelSize = res.getDimensionPixelSize(R.dimen.desktop_mode_edge_handle) + val size = edgeResizeHandleSizePixels ?: resPixelSize + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "$TAG: Get edge handle size of $size from (vs base value $resPixelSize)" + ) + size + } else { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "$TAG: Get edge handle size from freeform since flag is disabled" + ) + res.getDimensionPixelSize(R.dimen.freeform_resize_handle) + } + } catch (e: Resources.NotFoundException) { + KtProtoLog.e(WM_SHELL_DESKTOP_MODE, "$TAG: Unable to get edge handle size", e) + return 0 + } + } + + /** Register function to run when the resize handle size changes. */ + fun registerSizeChangeFunction(function: Consumer<ResizeHandleSizeRepository>) { + sizeChangeFunctions.add(function) + } + + private fun applyToAll() { + for (f in sizeChangeFunctions) { + f.accept(this) + } + } + + companion object { + private val TAG = "ResizeHandleSizeRepositoryCompanion" + + /** + * Returns the resource value in pixels to use for course input, such as touch, that + * benefits from a large square on each of the window's corners. + */ + @JvmStatic + fun getLargeResizeCornerPixels(res: Resources): Int { + return try { + res.getDimensionPixelSize(R.dimen.desktop_mode_corner_resize_large) + } catch (e: Resources.NotFoundException) { + KtProtoLog.e(WM_SHELL_DESKTOP_MODE, "$TAG: Unable to get large corner size", e) + 0 + } + } + + /** + * Returns the resource value, in pixels, to use for fine input, such as stylus, that can + * use a smaller square on each of the window's corners. + */ + @JvmStatic + fun getFineResizeCornerPixels(res: Resources): Int { + return try { + res.getDimensionPixelSize(R.dimen.freeform_resize_corner) + } catch (e: Resources.NotFoundException) { + KtProtoLog.e(WM_SHELL_DESKTOP_MODE, "$TAG: Unable to get fine corner size", e) + 0 + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 8de60b7acc91..cfe8e07aa6e5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -26,6 +26,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_STA import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; @@ -115,9 +116,9 @@ public class SplitLayoutTests extends ShellTestCase { @Test public void testUpdateDivideBounds() { - mSplitLayout.updateDividerBounds(anyInt()); + mSplitLayout.updateDividerBounds(anyInt(), anyBoolean()); verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class), anyInt(), - anyInt()); + anyInt(), anyBoolean()); } @Test 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 f67da5573b7d..d8d534bec6ea 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 @@ -25,6 +25,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.Intent import android.content.pm.ActivityInfo +import android.content.pm.ActivityInfo.CONFIG_DENSITY import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED @@ -115,6 +116,8 @@ import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture import org.mockito.quality.Strictness import java.util.Optional +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue import org.mockito.Mockito.`when` as whenever /** @@ -1045,17 +1048,6 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun handleRequest_freeformTask_freeformVisible_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - val freeformTask1 = setUpFreeformTask() - markTaskVisible(freeformTask1) - - val freeformTask2 = createFreeformTask() - assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull() - } - - @Test fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -1072,7 +1064,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun handleRequest_freeformTask_freeformNotVisible_returnSwitchToFullscreenWCT() { + fun handleRequest_freeformTask_freeformNotVisible_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) val freeformTask1 = setUpFreeformTask() @@ -1084,30 +1076,60 @@ class DesktopTasksControllerTest : ShellTestCase() { Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT) ) - assertThat(result?.changes?.get(freeformTask2.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + + assertThat(result?.hierarchyOps?.size).isEqualTo(2) + result!!.assertReorderAt(1, freeformTask2, toTop = true) } @Test - fun handleRequest_freeformTask_noOtherTasks_returnSwitchToFullscreenWCT() { + fun handleRequest_freeformTask_noOtherTasks_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) val task = createFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + + assertThat(result?.hierarchyOps?.size).isEqualTo(1) + result!!.assertReorderAt(0, task, toTop = true) } @Test - fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_returnSwitchToFullscreenWCT() { + fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) - createFreeformTask(displayId = SECOND_DISPLAY) + val taskSecondDisplay = createFreeformTask(displayId = SECOND_DISPLAY) val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) - assertThat(result?.changes?.get(taskDefaultDisplay.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + 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 @@ -1811,3 +1833,11 @@ private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component) } + +private fun WindowContainerTransaction?.anyDensityConfigChange( + token: WindowContainerToken +): Boolean { + return this?.changes?.any { change -> + change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0) + } ?: false +} 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 aa2cee79fcfc..282495de0fc1 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 @@ -121,6 +121,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockShellController: ShellController @Mock private lateinit var mockShellExecutor: ShellExecutor @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock private lateinit var mockResizeHandleSizeRepository: ResizeHandleSizeRepository @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler @Mock private lateinit var mockWindowManager: IWindowManager @@ -156,7 +157,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockInputMonitorFactory, transactionFactory, mockRootTaskDisplayAreaOrganizer, - windowDecorByTaskIdSpy + windowDecorByTaskIdSpy, + mockResizeHandleSizeRepository ) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -197,7 +199,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockMainHandler, mockMainChoreographer, mockSyncQueue, - mockRootTaskDisplayAreaOrganizer + mockRootTaskDisplayAreaOrganizer, + mockResizeHandleSizeRepository ) verify(decoration).close() } @@ -221,7 +224,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockMainHandler, mockMainChoreographer, mockSyncQueue, - mockRootTaskDisplayAreaOrganizer + mockRootTaskDisplayAreaOrganizer, + mockResizeHandleSizeRepository ) task.setWindowingMode(WINDOWING_MODE_FREEFORM) @@ -236,7 +240,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockMainHandler, mockMainChoreographer, mockSyncQueue, - mockRootTaskDisplayAreaOrganizer + mockRootTaskDisplayAreaOrganizer, + mockResizeHandleSizeRepository ) } @@ -296,7 +301,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskChanging(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any()) } @Test @@ -309,7 +314,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any()) } @Test @@ -406,7 +411,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any()) } finally { mockitoSession.finishMocking() } @@ -430,7 +435,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any()) } finally { mockitoSession.finishMocking() } @@ -453,7 +458,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any()) } finally { mockitoSession.finishMocking() } @@ -497,7 +502,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { val decoration = mock(DesktopModeWindowDecoration::class.java) whenever( mockDesktopModeWindowDecorFactory.create( - any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.isFocused).thenReturn(task.isFocused) 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 608f74b95280..cff93137ba45 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND; import static com.google.common.truth.Truth.assertThat; @@ -39,6 +40,9 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Handler; import android.os.SystemProperties; +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.testing.TestableContext; import android.view.Choreographer; @@ -51,6 +55,7 @@ import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; @@ -61,6 +66,7 @@ import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -83,6 +89,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final String USE_ROUNDED_CORNERS_SYSPROP_KEY = "persist.wm.debug.desktop_use_rounded_corners"; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + @Mock private DisplayController mMockDisplayController; @Mock @@ -107,6 +115,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory; @Mock private TypedArray mMockRoundedCornersRadiusArray; + @Mock + private ResizeHandleSizeRepository mMockResizeHandleSizeRepository; private final Configuration mConfiguration = new Configuration(); @@ -175,6 +185,48 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY) + public void updateRelayoutParams_appHeader_usesTaskDensity() { + final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources() + .getConfiguration().densityDpi; + final int customTaskDensity = systemDensity + 300; + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.configuration.densityDpi = customTaskDensity; + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY) + public void updateRelayoutParams_appHeader_usesSystemDensity() { + final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources() + .getConfiguration().densityDpi; + final int customTaskDensity = systemDensity + 300; + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskInfo.configuration.densityDpi = customTaskDensity; + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity); + } + + @Test public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); @@ -296,8 +348,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { return new DesktopModeWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mConfiguration, mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, - SurfaceControl.Builder::new, mMockTransactionSupplier, - WindowContainerTransaction::new, SurfaceControl::new, + mMockResizeHandleSizeRepository, SurfaceControl.Builder::new, + mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, mMockSurfaceControlViewHostFactory); } 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..62fb1c441118 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,7 @@ 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.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.Size; @@ -74,7 +71,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 @@ -147,8 +144,8 @@ public class DragResizeWindowGeometryTests { * capture all eligible input regardless of source (touch or cursor). */ @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testRegionUnion_edgeDragResizeEnabled_containsLargeCorners() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE); Region region = new Region(); GEOMETRY.union(region); // Make sure we're choosing a point outside of any debug region buffer. @@ -164,8 +161,8 @@ public class DragResizeWindowGeometryTests { * size. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testRegionUnion_edgeDragResizeDisabled_containsFineCorners() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE); Region region = new Region(); GEOMETRY.union(region); final int cornerRadius = DragResizeWindowGeometry.DEBUG @@ -176,16 +173,16 @@ public class DragResizeWindowGeometryTests { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeEnabled_edges() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE); // The input source (touch or cursor) shouldn't impact the edge resize size. validateCtrlTypeForEdges(/* isTouch= */ false); validateCtrlTypeForEdges(/* isTouch= */ true); } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeDisabled_edges() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE); // Edge resizing is not supported when the flag is disabled. validateCtrlTypeForEdges(/* isTouch= */ false); validateCtrlTypeForEdges(/* isTouch= */ false); @@ -203,8 +200,8 @@ public class DragResizeWindowGeometryTests { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeEnabled_corners() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE); final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); @@ -226,8 +223,8 @@ public class DragResizeWindowGeometryTests { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeDisabled_corners() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE); final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeHandleSizeRepositoryParameterizedTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeHandleSizeRepositoryParameterizedTests.kt new file mode 100644 index 000000000000..a9fddc623b96 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeHandleSizeRepositoryParameterizedTests.kt @@ -0,0 +1,150 @@ +/* + * 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.content.Context +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import java.util.function.Consumer +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameter +import org.junit.runners.Parameterized.Parameters +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +/** + * Tests for {@link ResizeHandleSizeRepository}. + * + * Build/Install/Run: atest WMShellUnitTests:ResizeHandleSizeRepositoryParameterizedTests + */ +@SmallTest +@RunWith(Parameterized::class) +class ResizeHandleSizeRepositoryParameterizedTests { + private val resources = ApplicationProvider.getApplicationContext<Context>().resources + private val resizeHandleSizeRepository = ResizeHandleSizeRepository() + @Mock private lateinit var mockSizeChangeFunctionOne: Consumer<ResizeHandleSizeRepository> + @Mock private lateinit var mockSizeChangeFunctionTwo: Consumer<ResizeHandleSizeRepository> + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + @Parameter(0) lateinit var name: String + // The current ResizeHandleSizeRepository API under test. + + @Parameter(1) lateinit var operation: (ResizeHandleSizeRepository) -> Unit + + @Before + fun setOverrideBeforeResetResizeHandle() { + MockitoAnnotations.initMocks(this) + if (name != "reset") return + val originalEdgeHandle = + resizeHandleSizeRepository.getResizeEdgeHandlePixels(resources) + resizeHandleSizeRepository.setResizeEdgeHandlePixels(originalEdgeHandle + 2) + } + + companion object { + @Parameters(name = "{index}: {0}") + @JvmStatic + fun data(): Iterable<Array<Any>> { + return listOf( + arrayOf( + "reset", + { sizeRepository: ResizeHandleSizeRepository -> + sizeRepository.resetResizeEdgeHandlePixels() + } + ), + arrayOf( + "set", + { sizeRepository: ResizeHandleSizeRepository -> + sizeRepository.setResizeEdgeHandlePixels(99) + } + ) + ) + } + } + + // ================= + // Validate that listeners are notified correctly for reset resize handle API. + // ================= + + @Test + fun testUpdateResizeHandleSize_flagDisabled() { + setFlagsRule.disableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + registerSizeChangeFunctions() + operation.invoke(resizeHandleSizeRepository) + // Nothing is notified since flag is disabled. + verify(mockSizeChangeFunctionOne, never()).accept(any()) + verify(mockSizeChangeFunctionTwo, never()).accept(any()) + } + + @Test + fun testUpdateResizeHandleSize_flagEnabled_noListeners() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + operation.invoke(resizeHandleSizeRepository) + // Nothing is notified since nothing was registered. + verify(mockSizeChangeFunctionOne, never()).accept(any()) + verify(mockSizeChangeFunctionTwo, never()).accept(any()) + } + + @Test + fun testUpdateResizeHandleSize_flagEnabled_listenersNotified() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + registerSizeChangeFunctions() + operation.invoke(resizeHandleSizeRepository) + // Functions notified when reset. + verify(mockSizeChangeFunctionOne).accept(any()) + verify(mockSizeChangeFunctionTwo).accept(any()) + } + + @Test + fun testUpdateResizeHandleSize_flagEnabled_listenerFails() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + registerSizeChangeFunctions() + operation.invoke(resizeHandleSizeRepository) + // Functions notified when reset. + verify(mockSizeChangeFunctionOne).accept(any()) + verify(mockSizeChangeFunctionTwo).accept(any()) + } + + @Test + fun testUpdateResizeHandleSize_flagEnabled_ignoreSecondListener() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + registerSizeChangeFunctions() + val extraConsumerMock = mock(Consumer::class.java) as Consumer<ResizeHandleSizeRepository> + resizeHandleSizeRepository.registerSizeChangeFunction(extraConsumerMock) + // First listener succeeds, second one that fails is ignored. + operation.invoke(resizeHandleSizeRepository) + // Functions notified when reset. + verify(mockSizeChangeFunctionOne).accept(any()) + verify(mockSizeChangeFunctionTwo).accept(any()) + verify(extraConsumerMock).accept(any()) + } + + private fun registerSizeChangeFunctions() { + resizeHandleSizeRepository.registerSizeChangeFunction(mockSizeChangeFunctionOne) + resizeHandleSizeRepository.registerSizeChangeFunction(mockSizeChangeFunctionTwo) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeHandleSizeRepositoryTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeHandleSizeRepositoryTests.kt new file mode 100644 index 000000000000..59bbc72a733b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeHandleSizeRepositoryTests.kt @@ -0,0 +1,77 @@ +/* + * 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.content.Context +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for {@link ResizeHandleSizeRepository}. Validate that get/reset/set work correctly. + * + * Build/Install/Run: atest WMShellUnitTests:ResizeHandleSizeRepositoryTests + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ResizeHandleSizeRepositoryTests { + private val resources = ApplicationProvider.getApplicationContext<Context>().resources + private val resizeHandleSizeRepository = ResizeHandleSizeRepository() + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + @Test + fun testOverrideResizeEdgeHandlePixels_flagEnabled_resetSucceeds() { + setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + // Reset does nothing when no override is set. + val originalEdgeHandle = + resizeHandleSizeRepository.getResizeEdgeHandlePixels(resources) + resizeHandleSizeRepository.resetResizeEdgeHandlePixels() + assertThat(resizeHandleSizeRepository.getResizeEdgeHandlePixels(resources)) + .isEqualTo(originalEdgeHandle) + + // Now try to set the value; reset should succeed. + resizeHandleSizeRepository.setResizeEdgeHandlePixels(originalEdgeHandle + 2) + resizeHandleSizeRepository.resetResizeEdgeHandlePixels() + assertThat(resizeHandleSizeRepository.getResizeEdgeHandlePixels(resources)) + .isEqualTo(originalEdgeHandle) + } + + @Test + fun testOverrideResizeEdgeHandlePixels_flagDisabled_resetFails() { + setFlagsRule.disableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + // Reset does nothing when no override is set. + val originalEdgeHandle = + resizeHandleSizeRepository.getResizeEdgeHandlePixels(resources) + resizeHandleSizeRepository.resetResizeEdgeHandlePixels() + assertThat(resizeHandleSizeRepository.getResizeEdgeHandlePixels(resources)) + .isEqualTo(originalEdgeHandle) + + // Now try to set the value; reset should do nothing. + val newEdgeHandle = originalEdgeHandle + 2 + resizeHandleSizeRepository.setResizeEdgeHandlePixels(newEdgeHandle) + resizeHandleSizeRepository.resetResizeEdgeHandlePixels() + assertThat(resizeHandleSizeRepository.getResizeEdgeHandlePixels(resources)) + .isEqualTo(originalEdgeHandle) + } +} |