diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java index b83e26e5de..a3f7ae03b5 100644 --- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java +++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java @@ -17,6 +17,7 @@ package com.android.quickstep.util; import android.content.Context; import android.content.res.Resources; +import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -31,6 +32,8 @@ import com.android.launcher3.testing.TestProtocol; */ public class MotionPauseDetector { + private static final String TAG = "MotionPauseDetector"; + // The percentage of the previous speed that determines whether this is a rapid deceleration. // The bigger this number, the easier it is to trigger the first pause. private static final float RAPID_DECELERATION_FACTOR = 0.6f; @@ -85,7 +88,8 @@ public class MotionPauseDetector { mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast); mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast); mForcePauseTimeout = new Alarm(); - mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */)); + mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */, + "Force pause timeout after " + alarm.getLastSetTimeout() + "ms" /* reason */)); mMakePauseHarderToTrigger = makePauseHarderToTrigger; mVelocityProvider = new SystemVelocityProvider(axis); } @@ -102,7 +106,7 @@ public class MotionPauseDetector { */ public void setDisallowPause(boolean disallowPause) { mDisallowPause = disallowPause; - updatePaused(mIsPaused); + updatePaused(mIsPaused, "Set disallowPause=" + disallowPause); } /** @@ -134,21 +138,27 @@ public class MotionPauseDetector { float speed = Math.abs(velocity); float previousSpeed = Math.abs(prevVelocity); boolean isPaused; + String isPausedReason = ""; if (mIsPaused) { // Continue to be paused until moving at a fast speed. isPaused = speed < mSpeedFast || previousSpeed < mSpeedFast; + isPausedReason = "Was paused, but started moving at a fast speed"; } else { if (velocity < 0 != prevVelocity < 0) { // We're just changing directions, not necessarily stopping. isPaused = false; + isPausedReason = "Velocity changed directions"; } else { isPaused = speed < mSpeedVerySlow && previousSpeed < mSpeedVerySlow; + isPausedReason = "Pause requires back to back slow speeds"; if (!isPaused && !mHasEverBeenPaused) { // We want to be more aggressive about detecting the first pause to ensure it // feels as responsive as possible; getting two very slow speeds back to back // takes too long, so also check for a rapid deceleration. boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR; isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast; + isPausedReason = "Didn't have back to back slow speeds, checking for rapid" + + " deceleration on first pause only"; } if (mMakePauseHarderToTrigger) { if (speed < mSpeedSlow) { @@ -156,22 +166,27 @@ public class MotionPauseDetector { mSlowStartTime = time; } isPaused = time - mSlowStartTime >= HARDER_TRIGGER_TIMEOUT; + isPausedReason = "Maintained slow speed for sufficient duration when making" + + " pause harder to trigger"; } else { mSlowStartTime = 0; isPaused = false; + isPausedReason = "Intentionally making pause harder to trigger"; } } } } - updatePaused(isPaused); + updatePaused(isPaused, isPausedReason); } - private void updatePaused(boolean isPaused) { + private void updatePaused(boolean isPaused, String reason) { if (mDisallowPause) { + reason = "Disallow pause; otherwise, would have been " + isPaused + " due to " + reason; isPaused = false; } if (mIsPaused != isPaused) { mIsPaused = isPaused; + Log.d(TAG, "onMotionPauseChanged, paused=" + mIsPaused + " reason=" + reason); boolean isFirstDetectedPause = !mHasEverBeenPaused && mIsPaused; if (mIsPaused) { AccessibilityManagerCompat.sendPauseDetectedEventToTest(mContext); diff --git a/src/com/android/launcher3/Alarm.java b/src/com/android/launcher3/Alarm.java index d5b434c647..e4aebf606d 100644 --- a/src/com/android/launcher3/Alarm.java +++ b/src/com/android/launcher3/Alarm.java @@ -30,6 +30,7 @@ public class Alarm implements Runnable{ private Handler mHandler; private OnAlarmListener mAlarmListener; private boolean mAlarmPending = false; + private long mLastSetTimeout; public Alarm() { mHandler = new Handler(); @@ -46,6 +47,7 @@ public class Alarm implements Runnable{ mAlarmPending = true; long oldTriggerTime = mAlarmTriggerTime; mAlarmTriggerTime = currentTime + millisecondsInFuture; + mLastSetTimeout = millisecondsInFuture; // If the previous alarm was set for a longer duration, cancel it. if (mWaitingForCallback && oldTriggerTime > mAlarmTriggerTime) { @@ -84,4 +86,9 @@ public class Alarm implements Runnable{ public boolean alarmPending() { return mAlarmPending; } + + /** Returns the last value passed to {@link #setAlarm(long)} */ + public long getLastSetTimeout() { + return mLastSetTimeout; + } } diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java index 499e749006..72a9b14734 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java @@ -304,7 +304,8 @@ public abstract class BaseAllAppsContainerView mProviderChangeListeners = new ArrayList<>(); private final SparseArray mViews = new SparseArray<>(); private final SparseArray mPendingViews = new SparseArray<>(); + private final SparseArray mDeferredViews = new SparseArray<>(); + private final SparseArray mCachedRemoteViews = new SparseArray<>(); private final Context mContext; private int mFlags = FLAG_STATE_IS_NORMAL; private IntConsumer mAppWidgetRemovedCallback = null; - public LauncherAppWidgetHost(Context context) { this(context, null); } @@ -95,6 +98,11 @@ public class LauncherAppWidgetHost extends AppWidgetHost { if (mPendingViews.get(appWidgetId) != null) { view = mPendingViews.get(appWidgetId); mPendingViews.remove(appWidgetId); + } else if (mDeferredViews.get(appWidgetId) != null) { + // In case the widget view is deferred, we will simply return the deferred view as + // opposed to instantiate a new instance of LauncherAppWidgetHostView since launcher + // already added the former to the workspace. + view = mDeferredViews.get(appWidgetId); } else { view = new LauncherAppWidgetHostView(context); } @@ -120,12 +128,25 @@ public class LauncherAppWidgetHost extends AppWidgetHost { // widgets upon bind anyway. See issue 14255011 for more context. } - // We go in reverse order and inflate any deferred widget + // We go in reverse order and inflate any deferred or cached widget for (int i = mViews.size() - 1; i >= 0; i--) { LauncherAppWidgetHostView view = mViews.valueAt(i); if (view instanceof DeferredAppWidgetHostView) { view.reInflate(); } + if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { + final int appWidgetId = mViews.keyAt(i); + if (view == mDeferredViews.get(appWidgetId)) { + // If the widget view was deferred, we'll need to call super.createView here + // to make the binder call to system process to fetch cumulative updates to this + // widget, as well as setting up this view for future updates. + super.createView(view.mLauncher, appWidgetId, view.getAppWidgetInfo()); + // At this point #onCreateView should have been called, which in turn returned + // the deferred view. There's no reason to keep the reference anymore, so we + // removed it here. + mDeferredViews.remove(appWidgetId); + } + } } } @@ -221,10 +242,28 @@ public class LauncherAppWidgetHost extends AppWidgetHost { CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv); return lahv; } else if ((mFlags & FLAG_LISTENING) == 0) { - DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context); - view.setAppWidget(appWidgetId, appWidget); - mViews.put(appWidgetId, view); - return view; + // Since the launcher hasn't started listening to widget updates, we can't simply call + // super.createView here because the later will make a binder call to retrieve + // RemoteViews from system process. + // TODO: have launcher always listens to widget updates in background so that this + // check can be removed altogether. + if (FeatureFlags.ENABLE_CACHED_WIDGET.get() + && mCachedRemoteViews.get(appWidgetId) != null) { + // We've found RemoteViews from cache for this widget, so we will instantiate a + // widget host view and populate it with the cached RemoteViews. + final LauncherAppWidgetHostView view = new LauncherAppWidgetHostView(context); + view.setAppWidget(appWidgetId, appWidget); + view.updateAppWidget(mCachedRemoteViews.get(appWidgetId)); + mDeferredViews.put(appWidgetId, view); + mViews.put(appWidgetId, view); + return view; + } else { + // When cache misses, a placeholder for the widget will be returned instead. + DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context); + view.setAppWidget(appWidgetId, appWidget); + mViews.put(appWidgetId, view); + return view; + } } else { try { return super.createView(context, appWidgetId, appWidget); @@ -281,6 +320,16 @@ public class LauncherAppWidgetHost extends AppWidgetHost { @Override public void clearViews() { super.clearViews(); + if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { + // First, we clear any previously cached content from existing widgets + mCachedRemoteViews.clear(); + // Then we proceed to cache the content from the widgets + for (int i = 0; i < mViews.size(); i++) { + final int appWidgetId = mViews.keyAt(i); + final LauncherAppWidgetHostView view = mViews.get(appWidgetId); + mCachedRemoteViews.put(appWidgetId, view.mLastRemoteViews); + } + } mViews.clear(); } diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java index 08651523a6..fc1e880373 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java @@ -43,6 +43,7 @@ import com.android.launcher3.CheckLongPressHelper; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -85,7 +86,7 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView private Runnable mAutoAdvanceRunnable; private long mDeferUpdatesUntilMillis = 0; - private RemoteViews mDeferredRemoteViews; + RemoteViews mLastRemoteViews; private boolean mHasDeferredColorChange = false; private @Nullable SparseIntArray mDeferredColorChange = null; @@ -150,11 +151,18 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView TRACE_METHOD_NAME + getAppWidgetInfo().provider, getAppWidgetId()); mTrackingWidgetUpdate = false; } - if (isDeferringUpdates()) { - mDeferredRemoteViews = remoteViews; - return; + if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { + mLastRemoteViews = remoteViews; + if (isDeferringUpdates()) { + return; + } + } else { + if (isDeferringUpdates()) { + mLastRemoteViews = remoteViews; + return; + } + mLastRemoteViews = null; } - mDeferredRemoteViews = null; super.updateAppWidget(remoteViews); @@ -218,8 +226,7 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView SparseIntArray deferredColors; boolean hasDeferredColors; mDeferUpdatesUntilMillis = 0; - remoteViews = mDeferredRemoteViews; - mDeferredRemoteViews = null; + remoteViews = mLastRemoteViews; deferredColors = mDeferredColorChange; hasDeferredColors = mHasDeferredColorChange; mDeferredColorChange = null; diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 0d74f52e86..ca39d2b441 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -25,6 +25,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import android.content.Intent; import android.graphics.Point; @@ -191,6 +192,17 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { isInState(() -> LauncherState.ALL_APPS)); } + @Test + @PortraitLandscape + public void testAllAppsDeadzoneForTablet() throws Exception { + assumeTrue(mLauncher.isTablet()); + + mLauncher.getWorkspace().switchToAllApps().dismissByTappingOutsideForTablet( + true /* tapRight */); + mLauncher.getWorkspace().switchToAllApps().dismissByTappingOutsideForTablet( + false /* tapRight */); + } + @Test @ScreenRecord // b/202433017 public void testWorkspace() throws Exception { diff --git a/tests/tapl/com/android/launcher3/tapl/Folder.java b/tests/tapl/com/android/launcher3/tapl/Folder.java index 26f0a8b26d..1352cc07c1 100644 --- a/tests/tapl/com/android/launcher3/tapl/Folder.java +++ b/tests/tapl/com/android/launcher3/tapl/Folder.java @@ -16,11 +16,6 @@ package com.android.launcher3.tapl; -import android.graphics.Point; -import android.graphics.Rect; -import android.os.SystemClock; -import android.view.MotionEvent; - import androidx.annotation.NonNull; import androidx.test.uiautomator.UiObject2; @@ -50,25 +45,15 @@ public class Folder { } } - private void touchOutsideFolder() { - Rect containerBounds = mLauncher.getVisibleBounds(this.mContainer); - final long downTime = SystemClock.uptimeMillis(); - Point containerLeftTopCorner = new Point(containerBounds.left - 1, containerBounds.top - 1); - mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, - containerLeftTopCorner, LauncherInstrumentation.GestureScope.INSIDE); - mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, - containerLeftTopCorner, LauncherInstrumentation.GestureScope.INSIDE); - } - /** - * CLose opened folder if possible. It throws assertion error if the folder is already closed. + * Close opened folder if possible. It throws assertion error if the folder is already closed. */ public Workspace close() { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "Want to close opened folder")) { mLauncher.waitForLauncherObject(FOLDER_CONTENT_RES_ID); - touchOutsideFolder(); + mLauncher.touchOutsideContainer(this.mContainer, false /* tapRight */); mLauncher.waitUntilLauncherObjectGone(FOLDER_CONTENT_RES_ID); return mLauncher.getWorkspace(); } diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java index c275f3b320..7123de44a9 100644 --- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java +++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java @@ -19,6 +19,7 @@ import androidx.annotation.NonNull; import androidx.test.uiautomator.UiObject2; public class HomeAllApps extends AllApps { + private static final String BOTTOM_SHEET_RES_ID = "bottom_sheet_background"; HomeAllApps(LauncherInstrumentation launcher) { super(launcher); @@ -45,4 +46,23 @@ public class HomeAllApps extends AllApps { protected boolean hasSearchBox() { return true; } + + /** + * Taps outside bottom sheet to dismiss and return to workspace. Available on tablets only. + * @param tapRight Tap on the right of bottom sheet if true, or left otherwise. + */ + public Workspace dismissByTappingOutsideForTablet(boolean tapRight) { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to tap outside AllApps bottom sheet on the " + + (tapRight ? "right" : "left"))) { + final UiObject2 allAppsBottomSheet = + mLauncher.waitForLauncherObject(BOTTOM_SHEET_RES_ID); + mLauncher.touchOutsideContainer(allAppsBottomSheet, tapRight); + try (LauncherInstrumentation.Closable tapped = mLauncher.addContextLayer( + "tapped outside AllApps bottom sheet")) { + return mLauncher.getWorkspace(); + } + } + } } diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index 99cab848a6..ae7c46a647 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -1831,4 +1831,26 @@ public final class LauncherInstrumentation { return ResourceUtils.getBoolByName( "config_supportsRoundedCornersOnWindows", resources, false); } + + /** + * Taps outside container to dismiss. + * @param container container to be dismissed + * @param tapRight tap on the right of the container if true, or left otherwise + */ + void touchOutsideContainer(UiObject2 container, boolean tapRight) { + try (LauncherInstrumentation.Closable c = addContextLayer( + "want to tap outside container on the " + (tapRight ? "right" : "left"))) { + Rect containerBounds = getVisibleBounds(container); + final long downTime = SystemClock.uptimeMillis(); + final Point tapTarget = new Point( + tapRight + ? (containerBounds.right + getRealDisplaySize().x) / 2 + : containerBounds.left / 2, + containerBounds.top + 1); + sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, tapTarget, + LauncherInstrumentation.GestureScope.INSIDE); + sendPointer(downTime, downTime, MotionEvent.ACTION_UP, tapTarget, + LauncherInstrumentation.GestureScope.INSIDE); + } + } } diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java index 82652c7f27..ddeeac225a 100644 --- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java +++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java @@ -26,6 +26,7 @@ import androidx.test.uiautomator.UiObject2; public class SearchResultFromQsb { // The input resource id in the search box. private static final String INPUT_RES = "input"; + private static final String BOTTOM_SHEET_RES_ID = "bottom_sheet_background"; private final LauncherInstrumentation mLauncher; SearchResultFromQsb(LauncherInstrumentation launcher) { @@ -47,4 +48,23 @@ public class SearchResultFromQsb { UiObject2 icon = mLauncher.waitForLauncherObject(By.clazz(TextView.class).text(appName)); return new AllAppsAppIcon(mLauncher, icon); } + + /** + * Taps outside bottom sheet to dismiss and return to workspace. Available on tablets only. + * @param tapRight Tap on the right of bottom sheet if true, or left otherwise. + */ + public Workspace dismissByTappingOutsideForTablet(boolean tapRight) { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to tap outside AllApps bottom sheet on the " + + (tapRight ? "right" : "left"))) { + final UiObject2 allAppsBottomSheet = + mLauncher.waitForLauncherObject(BOTTOM_SHEET_RES_ID); + mLauncher.touchOutsideContainer(allAppsBottomSheet, tapRight); + try (LauncherInstrumentation.Closable tapped = mLauncher.addContextLayer( + "tapped outside AllApps bottom sheet")) { + return mLauncher.getWorkspace(); + } + } + } }