From e5dbb75acdc47cd394ccf7e215e5dfc25c5ba6dd Mon Sep 17 00:00:00 2001 From: Pinyao Ting Date: Wed, 8 Jun 2022 15:00:20 -0700 Subject: [PATCH 1/4] Cache and reuses LauncherAppWidgetHostView when launcher resumes Currently by design when launcher enters the background, it stops listening to updates in widgets. This eventually causes the dilemma for launcher when it resumes, before the update can be returned from the system process via IPC, launcher could do one of the following to fill the gap: 1. show a deferred widget view -- a placeholder that renders the shape of the widget -- to let the user know widget is being reloaded. 2. show whichever widget view that was previously displayed to the user that may now contain stale content. There is a descrepancy here since in some edge cases we are showing the former while in most other cases we are showing the later. This CL added a short-term fix to address the descrepancy and favors the later where possible. Bug: 218067434 Test: manual Change-Id: I6cd2cd704186267227e2ec47f2581843fd526fa0 --- .../launcher3/config/FeatureFlags.java | 4 ++ .../widget/LauncherAppWidgetHost.java | 61 +++++++++++++++++-- .../widget/LauncherAppWidgetHostView.java | 21 ++++--- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 7e953af8be..99ada34dce 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -274,6 +274,10 @@ public final class FeatureFlags { "ENABLE_DISMISS_PREDICTION_UNDO", false, "Show an 'Undo' snackbar when users dismiss a predicted hotseat item"); + public static final BooleanFlag ENABLE_CACHED_WIDGET = getDebugFlag( + "ENABLE_CACHED_WIDGET", true, + "Show previously cached widgets as opposed to deferred widget where available"); + public static void initialize(Context context) { synchronized (sDebugFlags) { for (DebugFlag flag : sDebugFlags) { diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java index fe83f3f606..98a960c535 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java @@ -28,6 +28,7 @@ import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.util.SparseArray; +import android.widget.RemoteViews; import android.widget.Toast; import androidx.annotation.Nullable; @@ -37,6 +38,7 @@ import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.testing.TestLogging; @@ -70,13 +72,14 @@ public class LauncherAppWidgetHost extends AppWidgetHost { private final ArrayList 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; From c214335ebaf835ec2bbc23f9f696e14f223eadae Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Wed, 22 Jun 2022 11:29:20 -0700 Subject: [PATCH 2/4] Log when MotionPauseDetector detects onMotionPauseChanged Test: verified logging locally Bug: 232548865 Change-Id: I587481a380148127f715c5b1b59436b2cbc1533e --- .../quickstep/util/MotionPauseDetector.java | 23 +++++++++++++++---- src/com/android/launcher3/Alarm.java | 7 ++++++ 2 files changed, 26 insertions(+), 4 deletions(-) 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; + } } From a3070ea61d824e523cb211441ec64c557922494c Mon Sep 17 00:00:00 2001 From: Alex Chau Date: Wed, 22 Jun 2022 10:53:58 +0100 Subject: [PATCH 3/4] In AllApps search mode, only consume touch over visible container Bug: 236360930 Test: Tap on deadzone to dismiss AllApps in search mode Test: touch outside recycler view inside container, doesn't swipe left/right Change-Id: I18498ea50f217231c3bb4489d6d9e95fc0e7da53 --- .../android/launcher3/allapps/BaseAllAppsContainerView.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 Date: Mon, 20 Jun 2022 22:05:46 +0100 Subject: [PATCH 4/4] Add test to dismiss AllApps bottom sheet - http://docs/document/d/1ITZDMchoMndfa0nMuOTXbhKhK2aMDz0AT9dgusgxEro Bug: 236360930 Test: TaplTestsLauncher3.testAllAppsDeadzoneForTablet Test: TaplTestsNexus.testSearchDeadzoneForTablet Change-Id: I62adb1d6ef237f9bca3812b8cdce9e5a8383158f --- .../launcher3/ui/TaplTestsLauncher3.java | 12 ++++++++++ .../com/android/launcher3/tapl/Folder.java | 19 ++-------------- .../android/launcher3/tapl/HomeAllApps.java | 20 +++++++++++++++++ .../tapl/LauncherInstrumentation.java | 22 +++++++++++++++++++ .../launcher3/tapl/SearchResultFromQsb.java | 20 +++++++++++++++++ 5 files changed, 76 insertions(+), 17 deletions(-) diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 0f29abc3d2..8fa456083e 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; @@ -190,6 +191,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(); + } + } + } }