From b64fc6c26f6a59d2bc7c19d08860097a60d0b845 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 4 Jun 2020 18:29:40 -0700 Subject: [PATCH] Fixing jump when swiping up in landscape and in waterfall cutout > Adding tests for TaskViewSimulator to ensure proper calculations > Disabling orientation listener while user is interacting with quickstep Bug: 158781568 Bug: 156891776 Change-Id: I299c3b1243ac0dbf28faee1b8566c77ea3954e33 --- .../android/quickstep/BaseSwipeUpHandler.java | 35 +-- .../quickstep/util/TaskViewSimulator.java | 2 +- .../android/quickstep/views/RecentsView.java | 4 + .../quickstep/util/TaskViewSimulatorTest.java | 203 ++++++++++++++++++ .../quickstep/util/RecentsOrientedState.java | 14 +- .../config/robolectric.properties | 1 + .../launcher3/shadows/LShadowDisplay.java | 54 +++++ 7 files changed, 297 insertions(+), 16 deletions(-) create mode 100644 quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java create mode 100644 robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java index f5c587405a..9db6576765 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java @@ -121,6 +121,10 @@ public abstract class BaseSwipeUpHandler, Q extend }); mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + // Wait until the first scroll event before applying scroll to taskViewSimulator. + // Since, by default the current/running task already centered, this ensures that we + // do not move the running task, in case RecentsView has not yet laid out completely. + mRecentsViewScrollLinked = true; if (moveWindowWithRecentsScroll()) { updateFinalShift(); } @@ -128,7 +132,6 @@ public abstract class BaseSwipeUpHandler, Q extend runOnRecentsAnimationStart(() -> mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController, mRecentsAnimationTargets)); - mRecentsViewScrollLinked = true; } protected void startNewTask(Consumer resultCallback) { @@ -205,26 +208,30 @@ public abstract class BaseSwipeUpHandler, Q extend mRecentsAnimationController = recentsAnimationController; mRecentsAnimationTargets = targets; mTransformParams.setTargetSet(mRecentsAnimationTargets); - DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile(); RemoteAnimationTargetCompat runningTaskTarget = targets.findTask( mGestureState.getRunningTaskId()); - if (targets.minimizedHomeBounds != null && runningTaskTarget != null) { - Rect overviewStackBounds = mActivityInterface - .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget); - dp = dp.getMultiWindowProfile(mContext, - new WindowBounds(overviewStackBounds, targets.homeContentInsets)); - } else { - // If we are not in multi-window mode, home insets should be same as system insets. - dp = dp.copy(mContext); - } - dp.updateInsets(targets.homeContentInsets); - dp.updateIsSeascape(mContext); if (runningTaskTarget != null) { mTaskViewSimulator.setPreview(runningTaskTarget); } - initTransitionEndpoints(dp); + // Only initialize the device profile, if it has not been initialized before, as in some + // configurations targets.homeContentInsets may not be correct. + if (mActivity == null) { + DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile(); + if (targets.minimizedHomeBounds != null && runningTaskTarget != null) { + Rect overviewStackBounds = mActivityInterface + .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget); + dp = dp.getMultiWindowProfile(mContext, + new WindowBounds(overviewStackBounds, targets.homeContentInsets)); + } else { + // If we are not in multi-window mode, home insets should be same as system insets. + dp = dp.copy(mContext); + } + dp.updateInsets(targets.homeContentInsets); + dp.updateIsSeascape(mContext); + initTransitionEndpoints(dp); + } // Notify when the animation starts if (!mRecentsAnimationStartCallbacks.isEmpty()) { diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java index 196a7c487c..f62218f773 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java @@ -255,8 +255,8 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { float taskHeight = mTaskRect.height(); mMatrix.set(mPositionHelper.getMatrix()); - mMatrix.postScale(scale, scale); mMatrix.postTranslate(insets.left, insets.top); + mMatrix.postScale(scale, scale); // Apply TaskView matrix: translate, scale, scroll mMatrix.postTranslate(mTaskRect.left, mTaskRect.top); diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java index 99afe39acd..39775987d1 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java @@ -998,6 +998,7 @@ public abstract class RecentsView extends PagedView impl mDwbToastShown = false; mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0); LayoutUtils.setViewEnabled(mActionsView, true); + mOrientationState.setGestureActive(false); } public @Nullable TaskView getRunningTaskView() { @@ -1035,6 +1036,7 @@ public abstract class RecentsView extends PagedView impl */ public void onGestureAnimationStart(int runningTaskId) { // This needs to be called before the other states are set since it can create the task view + mOrientationState.setGestureActive(true); showCurrentTask(runningTaskId); setEnableFreeScroll(false); setEnableDrawingLiveTile(false); @@ -1097,6 +1099,8 @@ public abstract class RecentsView extends PagedView impl * Called when a gesture from an app has finished. */ public void onGestureAnimationEnd() { + mOrientationState.setGestureActive(false); + setOnScrollChangeListener(null); setEnableFreeScroll(true); setEnableDrawingLiveTile(true); diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java new file mode 100644 index 0000000000..a31ba2177b --- /dev/null +++ b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2020 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.quickstep.util; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.display.DisplayManager; +import android.view.Surface; +import android.view.SurfaceControl; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.shadows.LShadowDisplay; +import com.android.launcher3.util.DefaultDisplay; +import com.android.quickstep.LauncherActivityInterface; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowDisplayManager; + +@RunWith(RobolectricTestRunner.class) +@LooperMode(Mode.PAUSED) +public class TaskViewSimulatorTest { + + @Test + public void taskProperlyScaled_portrait_noRotation_sameInsets1() { + new TaskMatrixVerifier() + .withLauncherSize(1200, 2450) + .withInsets(new Rect(0, 80, 0, 120)) + .verifyNoTransforms(); + } + + @Test + public void taskProperlyScaled_portrait_noRotation_sameInsets2() { + new TaskMatrixVerifier() + .withLauncherSize(1200, 2450) + .withInsets(new Rect(55, 80, 55, 120)) + .verifyNoTransforms(); + } + + @Test + public void taskProperlyScaled_landscape_noRotation_sameInsets1() { + new TaskMatrixVerifier() + .withLauncherSize(2450, 1250) + .withInsets(new Rect(0, 80, 0, 40)) + .verifyNoTransforms(); + } + + @Test + public void taskProperlyScaled_landscape_noRotation_sameInsets2() { + new TaskMatrixVerifier() + .withLauncherSize(2450, 1250) + .withInsets(new Rect(0, 80, 120, 0)) + .verifyNoTransforms(); + } + + @Test + public void taskProperlyScaled_landscape_noRotation_sameInsets3() { + new TaskMatrixVerifier() + .withLauncherSize(2450, 1250) + .withInsets(new Rect(55, 80, 55, 120)) + .verifyNoTransforms(); + } + + @Test + public void taskProperlyScaled_landscape_rotated() { + new TaskMatrixVerifier() + .withLauncherSize(1200, 2450) + .withInsets(new Rect(0, 80, 0, 120)) + .withAppBounds( + new Rect(0, 0, 2450, 1200), + new Rect(0, 80, 0, 120), + Surface.ROTATION_90) + .verifyNoTransforms(); + } + + private static class TaskMatrixVerifier extends TransformParams { + + private final Context mContext = RuntimeEnvironment.application; + + private Rect mAppBounds = new Rect(); + private Rect mLauncherInsets = new Rect(); + + private Rect mAppInsets; + + private int mAppRotation = -1; + private DeviceProfile mDeviceProfile; + + TaskMatrixVerifier withLauncherSize(int width, int height) { + ShadowDisplayManager.changeDisplay(DEFAULT_DISPLAY, + String.format("w%sdp-h%sdp-mdpi", width, height)); + if (mAppBounds.isEmpty()) { + mAppBounds.set(0, 0, width, height); + } + return this; + } + + TaskMatrixVerifier withInsets(Rect insets) { + LShadowDisplay shadowDisplay = Shadow.extract( + mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY)); + shadowDisplay.setInsets(insets); + mLauncherInsets.set(insets); + return this; + } + + TaskMatrixVerifier withAppBounds(Rect bounds, Rect insets, int appRotation) { + mAppBounds.set(bounds); + mAppInsets = insets; + mAppRotation = appRotation; + return this; + } + + void verifyNoTransforms() { + mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(mContext) + .getDeviceProfile(mContext); + mDeviceProfile.updateInsets(mLauncherInsets); + + TaskViewSimulator tvs = new TaskViewSimulator(mContext, + LauncherActivityInterface.INSTANCE); + tvs.setDp(mDeviceProfile); + + int launcherRotation = DefaultDisplay.INSTANCE.get(mContext).getInfo().rotation; + if (mAppRotation < 0) { + mAppRotation = launcherRotation; + } + tvs.setLayoutRotation(launcherRotation, mAppRotation); + if (mAppInsets == null) { + mAppInsets = new Rect(mLauncherInsets); + } + tvs.setPreviewBounds(mAppBounds, mAppInsets); + + tvs.fullScreenProgress.value = 1; + tvs.recentsViewScale.value = tvs.getFullScreenScale(); + tvs.apply(this); + } + + @Override + public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) { + SurfaceParams.Builder builder = new SurfaceParams.Builder((SurfaceControl) null); + proxy.onBuildTargetParams(builder, null, this); + return new SurfaceParams[] {builder.build()}; + } + + @Override + public void applySurfaceParams(SurfaceParams[] params) { + // Verify that the task position remains the same + RectF newAppBounds = new RectF(mAppBounds); + params[0].matrix.mapRect(newAppBounds); + Assert.assertThat(newAppBounds, new AlmostSame(mAppBounds)); + + System.err.println("Bounds mapped: " + mAppBounds + " => " + newAppBounds); + } + } + + private static class AlmostSame extends TypeSafeMatcher { + + // Allow 1px error margin to account for float to int conversions + private final float mError = 1f; + private final Rect mExpected; + + AlmostSame(Rect expected) { + mExpected = expected; + } + + @Override + protected boolean matchesSafely(RectF item) { + return Math.abs(item.left - mExpected.left) < mError + && Math.abs(item.top - mExpected.top) < mError + && Math.abs(item.right - mExpected.right) < mError + && Math.abs(item.bottom - mExpected.bottom) < mError; + } + + @Override + public void describeTo(Description description) { + description.appendValue(mExpected); + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java index 68636cb1ab..90ee18f19e 100644 --- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java +++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java @@ -105,6 +105,9 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre private static final int FLAG_ROTATION_WATCHER_ENABLED = 1 << 6; // Enable home rotation for UI tests, ignoring home rotation value from prefs private static final int FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING = 1 << 7; + // Whether the swipe gesture is running, so the recents would stay locked in the + // current orientation + private static final int FLAG_SWIPE_UP_NOT_RUNNING = 1 << 8; private static final int MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE = FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY @@ -114,7 +117,8 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre // multi-window is enabled as in that case, activity itself rotates. private static final int VALUE_ROTATION_WATCHER_ENABLED = MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE | FLAG_SYSTEM_ROTATION_ALLOWED - | FLAG_ROTATION_WATCHER_SUPPORTED | FLAG_ROTATION_WATCHER_ENABLED; + | FLAG_ROTATION_WATCHER_SUPPORTED | FLAG_ROTATION_WATCHER_ENABLED + | FLAG_SWIPE_UP_NOT_RUNNING; private final Context mContext; private final ContentResolver mContentResolver; @@ -156,6 +160,7 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre if (originalSmallestWidth < 600) { mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY; } + mFlags |= FLAG_SWIPE_UP_NOT_RUNNING; initFlags(); } @@ -166,6 +171,13 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre setFlag(FLAG_MULTIWINDOW_ROTATION_ALLOWED, isMultiWindow); } + /** + * Sets if the swipe up gesture is currently running or not + */ + public void setGestureActive(boolean isGestureActive) { + setFlag(FLAG_SWIPE_UP_NOT_RUNNING, !isGestureActive); + } + /** * Sets the appropriate {@link PagedOrientationHandler} for {@link #mOrientationHandler} * @param touchRotation The rotation the nav bar region that is touched is in diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties index b1717120af..a8e0cb3acd 100644 --- a/robolectric_tests/config/robolectric.properties +++ b/robolectric_tests/config/robolectric.properties @@ -5,6 +5,7 @@ shadows= \ com.android.launcher3.shadows.LShadowAppWidgetManager \ com.android.launcher3.shadows.LShadowBackupManager \ com.android.launcher3.shadows.LShadowBitmap \ + com.android.launcher3.shadows.LShadowDisplay \ com.android.launcher3.shadows.LShadowLauncherApps \ com.android.launcher3.shadows.LShadowTypeface \ com.android.launcher3.shadows.LShadowUserManager \ diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java new file mode 100644 index 0000000000..3813fa182f --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 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.launcher3.shadows; + +import static org.robolectric.shadow.api.Shadow.directlyOn; + +import android.graphics.Point; +import android.graphics.Rect; +import android.view.Display; + +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.shadows.ShadowDisplay; + +/** + * Extension of {@link ShadowDisplay} with missing shadow methods + */ +@Implements(value = Display.class) +public class LShadowDisplay extends ShadowDisplay { + + private final Rect mInsets = new Rect(); + + @RealObject Display realObject; + + /** + * Sets the insets for the display + */ + public void setInsets(Rect insets) { + mInsets.set(insets); + } + + @Override + protected void getCurrentSizeRange(Point outSmallestSize, Point outLargestSize) { + directlyOn(realObject, Display.class).getCurrentSizeRange(outSmallestSize, outLargestSize); + outSmallestSize.x -= mInsets.left + mInsets.right; + outLargestSize.x -= mInsets.left + mInsets.right; + + outSmallestSize.y -= mInsets.top + mInsets.bottom; + outLargestSize.y -= mInsets.top + mInsets.bottom; + } +}