Merge "Subclassing Launcher instead of using UiFactory" into ub-launcher3-master
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index 3d1ecef..f889bc1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -75,6 +75,9 @@
     @Override
     public void onSwipeUpToRecentsComplete() {
         RecentsActivity activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
         RecentsView recentsView = activity.getOverviewPanel();
         recentsView.getClearAllButton().setVisibilityAlpha(1);
         recentsView.setDisallowScrollToClearAll(false);
@@ -236,12 +239,18 @@
     public void onLaunchTaskFailed() {
         // TODO: probably go back to overview instead.
         RecentsActivity activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
         activity.<RecentsView>getOverviewPanel().startHome();
     }
 
     @Override
     public void onLaunchTaskSuccess() {
         RecentsActivity activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
         activity.onTaskLaunched();
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 4406314..48b8fc6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -94,6 +94,9 @@
     @Override
     public void onTransitionCancelled(boolean activityVisible) {
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         LauncherState startState = launcher.getStateManager().getRestState();
         launcher.getStateManager().goToState(startState, activityVisible);
     }
@@ -102,32 +105,40 @@
     public void onSwipeUpToRecentsComplete() {
         // Re apply state in case we did something funky during the transition.
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         launcher.getStateManager().reapplyState();
         DiscoveryBounce.showForOverviewIfNeeded(launcher);
     }
 
     @Override
     public void onSwipeUpToHomeComplete() {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         // Ensure recents is at the correct position for NORMAL state. For example, when we detach
         // recents, we assume the first task is invisible, making translation off by one task.
-        Launcher launcher = getCreatedActivity();
         launcher.getStateManager().reapplyState();
         setLauncherHideBackArrow(false);
     }
 
     private void setLauncherHideBackArrow(boolean hideBackArrow) {
         Launcher launcher = getCreatedActivity();
-        if (launcher != null) {
-            launcher.getRootView().setForceHideBackArrow(hideBackArrow);
+        if (launcher == null) {
+            return;
         }
+        launcher.getRootView().setForceHideBackArrow(hideBackArrow);
     }
 
     @Override
     public void onAssistantVisibilityChanged(float visibility) {
         Launcher launcher = getCreatedActivity();
-        if (launcher != null) {
-            launcher.onAssistantVisibilityChanged(visibility);
+        if (launcher == null) {
+            return;
         }
+        launcher.onAssistantVisibilityChanged(visibility);
     }
 
     @NonNull
@@ -476,12 +487,18 @@
     @Override
     public void onLaunchTaskFailed() {
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         launcher.getStateManager().goToState(OVERVIEW);
     }
 
     @Override
     public void onLaunchTaskSuccess() {
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         launcher.getStateManager().moveToRestState();
     }
 
@@ -503,6 +520,9 @@
     public void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData,
             Runnable onFinishRunnable) {
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         RecentsView recentsView = launcher.getOverviewPanel();
         if (recentsView == null) {
             if (onFinishRunnable != null) {
@@ -516,8 +536,9 @@
     @Override
     public void setOnDeferredActivityLaunchCallback(Runnable r) {
         Launcher launcher = getCreatedActivity();
-        if (launcher != null) {
-            launcher.setOnDeferredActivityLaunchCallback(r);
+        if (launcher == null) {
+            return;
         }
+        launcher.setOnDeferredActivityLaunchCallback(r);
     }
 }
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index e3fcd2f..8d7a534 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -35,6 +35,7 @@
 import android.app.Service;
 import android.app.TaskInfo;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
@@ -49,19 +50,18 @@
 import android.view.MotionEvent;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
@@ -70,12 +70,14 @@
 import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
 import com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer;
 import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
+import com.android.quickstep.inputconsumers.OverscrollInputConsumer;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
-import com.android.quickstep.inputconsumers.QuickCaptureInputConsumer;
 import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.systemui.plugins.OverscrollPlugin;
+import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -113,7 +115,7 @@
  */
 @TargetApi(Build.VERSION_CODES.Q)
 public class TouchInteractionService extends Service implements
-        NavigationModeChangeListener {
+        NavigationModeChangeListener, PluginListener<OverscrollPlugin> {
 
     private static final String TAG = "TouchInteractionService";
 
@@ -122,6 +124,8 @@
     private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once";
     private static final int MAX_BACK_NOTIFICATION_COUNT = 3;
     private int mBackGestureNotificationCounter = -1;
+    @Nullable
+    private OverscrollPlugin mOverscrollPlugin;
 
     private final IBinder mMyBinder = new IOverviewProxy.Stub() {
 
@@ -280,6 +284,9 @@
 
         onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this));
         sConnected = true;
+
+        PluginManagerWrapper.INSTANCE.get(getBaseContext()).addPluginListener(this,
+                OverscrollPlugin.class, false /* allowMultiple */);
     }
 
     private void disposeEventHandlers() {
@@ -400,6 +407,9 @@
         if (TestProtocol.sDebugTracing) {
             Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE, "TIS destroyed");
         }
+
+        PluginManagerWrapper.INSTANCE.get(getBaseContext()).removePluginListener(this);
+
         sIsInitialized = false;
         if (mDeviceState.isUserUnlocked()) {
             mInputConsumer.unregisterInputConsumer();
@@ -489,10 +499,10 @@
                 base = new AssistantInputConsumer(this, newGestureState, base, mInputMonitorCompat);
             }
 
-            if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
-                // Put the Compose gesture as higher priority than the Assistant or base gestures
-                base = new QuickCaptureInputConsumer(this, newGestureState, base,
-                        mInputMonitorCompat);
+            if (mOverscrollPlugin != null) {
+                // Put the overscroll gesture as higher priority than the Assistant or base gestures
+                base = new OverscrollInputConsumer(this, newGestureState, base, mInputMonitorCompat,
+                        mOverscrollPlugin);
             }
 
             if (mDeviceState.isScreenPinningActive()) {
@@ -531,9 +541,9 @@
                     () -> mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT));
             if (!ActivityManagerWrapper.isHomeTask(runningTaskInfo)) {
                 final ComponentName homeComponent =
-                    mOverviewComponentObserver.getHomeIntent().getComponent();
+                        mOverviewComponentObserver.getHomeIntent().getComponent();
                 forceOverviewInputConsumer =
-                    runningTaskInfo.baseIntent.getComponent(). equals(homeComponent);
+                        runningTaskInfo.baseIntent.getComponent().equals(homeComponent);
             }
         }
 
@@ -763,4 +773,14 @@
         UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
                 .startRecentsActivity(intent, null, listener, null, null));
     }
+
+    @Override
+    public void onPluginConnected(OverscrollPlugin overscrollPlugin, Context context) {
+        mOverscrollPlugin = overscrollPlugin;
+    }
+
+    @Override
+    public void onPluginDisconnected(OverscrollPlugin overscrollPlugin) {
+        mOverscrollPlugin = null;
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 22ad180..f1b3598 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -1009,7 +1009,10 @@
 
     @Override
     public void onConsumerAboutToBeSwitched() {
-        if (!mGestureState.isRunningAnimationToLauncher()) {
+        if (mActivity != null) {
+            mActivity.setOnStartCallback(null);
+        }
+        if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) {
             cancelCurrentAnimation();
         } else {
             reset();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/QuickCaptureInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
similarity index 74%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/QuickCaptureInputConsumer.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
index 9826b3a..e3da98b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/QuickCaptureInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -24,39 +24,28 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 
-import android.app.ActivityOptions;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.graphics.PointF;
-import android.os.Bundle;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.R;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
- * Input consumer for handling events to launch quick capture from launcher
+ * Input consumer for handling events to pass to an {@code OverscrollPlugin}.
+ *
  * @param <T> Draggable activity subclass used by RecentsView
  */
-public class QuickCaptureInputConsumer<T extends BaseDraggingActivity>
-        extends DelegateInputConsumer {
+public class OverscrollInputConsumer<T extends BaseDraggingActivity> extends DelegateInputConsumer {
 
-    private static final String TAG = "QuickCaptureInputConsumer";
-
-    private static final String QUICK_CAPTURE_PACKAGE = "com.google.auxe.compose";
-    private static final String QUICK_CAPTURE_PACKAGE_DEV = "com.google.auxe.compose.debug";
-
-    private static final String EXTRA_DEVICE_STATE = "deviceState";
-    private static final String DEVICE_STATE_LOCKED = "Locked";
-    private static final String DEVICE_STATE_LAUNCHER = "Launcher";
-    private static final String DEVICE_STATE_APP = "App";
-    private static final String DEVICE_STATE_UNKNOWN = "Unknown";
+    private static final String TAG = "OverscrollInputConsumer";
 
     private static final int ANGLE_THRESHOLD = 35; // Degrees
 
@@ -71,14 +60,16 @@
 
     private final Context mContext;
     private final GestureState mGestureState;
+    @Nullable private final OverscrollPlugin mPlugin;
 
     private RecentsView mRecentsView;
 
-    public QuickCaptureInputConsumer(Context context, GestureState gestureState,
-            InputConsumer delegate, InputMonitorCompat inputMonitor) {
+    public OverscrollInputConsumer(Context context, GestureState gestureState,
+            InputConsumer delegate, InputMonitorCompat inputMonitor, OverscrollPlugin plugin) {
         super(delegate, inputMonitor);
         mContext = context;
         mGestureState = gestureState;
+        mPlugin = plugin;
 
         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
         mSquaredSlop = slop * slop;
@@ -89,7 +80,7 @@
 
     @Override
     public int getType() {
-        return TYPE_QUICK_CAPTURE | mDelegate.getType();
+        return TYPE_OVERSCROLL | mDelegate.getType();
     }
 
     private boolean onActivityInit(Boolean alreadyOnHome) {
@@ -149,7 +140,7 @@
                         mPassedSlop = true;
                         mStartDragPos.set(mLastPos.x, mLastPos.y);
 
-                        if (isValidQuickCaptureGesture()) {
+                        if (isOverscrolled()) {
                             setActive(ev);
                         } else {
                             mState = STATE_DELEGATE_ACTIVE;
@@ -161,8 +152,8 @@
             }
             case ACTION_CANCEL:
             case ACTION_UP:
-                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop) {
-                    startQuickCapture();
+                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
+                    mPlugin.onOverscroll(getDeviceState());
                 }
 
                 mPassedSlop = false;
@@ -175,7 +166,7 @@
         }
     }
 
-    private boolean isValidQuickCaptureGesture() {
+    private boolean isOverscrolled() {
         // Make sure there isn't an app to quick switch to on our right
         boolean atRightMostApp = (mRecentsView == null || mRecentsView.getRunningTaskIndex() <= 0);
 
@@ -187,37 +178,19 @@
         return atRightMostApp && angleInBounds;
     }
 
-    private void startQuickCapture() {
-        // Inspect our delegate's type to figure out where the user invoked Compose
-        String deviceState = DEVICE_STATE_UNKNOWN;
+    private String getDeviceState() {
+        String deviceState = OverscrollPlugin.DEVICE_STATE_UNKNOWN;
         int consumerType = mDelegate.getType();
         if (((consumerType & InputConsumer.TYPE_OVERVIEW) > 0)
                 || ((consumerType & InputConsumer.TYPE_OVERVIEW_WITHOUT_FOCUS)) > 0) {
-            deviceState = DEVICE_STATE_LAUNCHER;
+            deviceState = OverscrollPlugin.DEVICE_STATE_LAUNCHER;
         } else if ((consumerType & InputConsumer.TYPE_OTHER_ACTIVITY) > 0) {
-            deviceState = DEVICE_STATE_APP;
+            deviceState = OverscrollPlugin.DEVICE_STATE_APP;
         } else if (((consumerType & InputConsumer.TYPE_RESET_GESTURE) > 0)
                 || ((consumerType & InputConsumer.TYPE_DEVICE_LOCKED) > 0)) {
-            deviceState = DEVICE_STATE_LOCKED;
+            deviceState = OverscrollPlugin.DEVICE_STATE_LOCKED;
         }
 
-        // Then launch the app
-        PackageManager pm = mContext.getPackageManager();
-
-        Intent qcIntent = pm.getLaunchIntentForPackage(QUICK_CAPTURE_PACKAGE);
-
-        if (qcIntent == null) {
-            // If we couldn't find the regular app, try the dev version
-            qcIntent = pm.getLaunchIntentForPackage(QUICK_CAPTURE_PACKAGE_DEV);
-        }
-
-        if (qcIntent != null) {
-            qcIntent.putExtra(EXTRA_DEVICE_STATE, deviceState);
-
-            Bundle options = ActivityOptions.makeCustomAnimation(mContext, R.anim.slide_in_right,
-                    0).toBundle();
-
-            mContext.startActivity(qcIntent, options);
-        }
+        return deviceState;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 5a65c15..ca33605 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -378,10 +378,10 @@
     }
 
     @Override
-    public void resetTaskVisuals() {
-        super.resetTaskVisuals();
+    public void setContentAlpha(float alpha) {
+        super.setContentAlpha(alpha);
         if (mRecentsExtraViewContainer != null) {
-            mRecentsExtraViewContainer.setAlpha(mContentAlpha);
+            mRecentsExtraViewContainer.setAlpha(alpha);
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 918645d..3e84e7d 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -33,7 +33,7 @@
     int TYPE_SCREEN_PINNED = 1 << 6;
     int TYPE_OVERVIEW_WITHOUT_FOCUS = 1 << 7;
     int TYPE_RESET_GESTURE = 1 << 8;
-    int TYPE_QUICK_CAPTURE = 1 << 9;
+    int TYPE_OVERSCROLL = 1 << 9;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -45,7 +45,7 @@
             "TYPE_SCREEN_PINNED",           // 6
             "TYPE_OVERVIEW_WITHOUT_FOCUS",  // 7
             "TYPE_RESET_GESTURE",           // 8
-            "TYPE_QUICK_CAPTURE",           // 9
+            "TYPE_OVERSCROLL",              // 9
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/res/anim/slide_in_right.xml b/res/anim/slide_in_right.xml
deleted file mode 100644
index 55d3e54..0000000
--- a/res/anim/slide_in_right.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shareInterpolator="false" >
-  <translate
-      android:duration="@android:integer/config_shortAnimTime"
-      android:fromXDelta="100%"
-      android:toXDelta="0%"
-      />
-</set>
\ No newline at end of file
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 0a4f005..a00a6bd 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -300,8 +300,8 @@
 
     public LooperIdleLock newIdleLock(Object lock) {
         LooperIdleLock idleLock = new LooperIdleLock(lock, Looper.getMainLooper());
-        // If we are not binding, there is no reason to wait for idle.
-        if (mCallbacks.get() == null) {
+        // If we are not binding or if the main looper is already idle, there is no reason to wait
+        if (mCallbacks.get() == null || Looper.getMainLooper().getQueue().isIdle()) {
             idleLock.queueIdle();
         }
         return idleLock;
diff --git a/src/com/android/launcher3/util/LooperIdleLock.java b/src/com/android/launcher3/util/LooperIdleLock.java
index 2896535..f4ccf42 100644
--- a/src/com/android/launcher3/util/LooperIdleLock.java
+++ b/src/com/android/launcher3/util/LooperIdleLock.java
@@ -22,29 +22,30 @@
 /**
  * Utility class to block execution until the UI looper is idle.
  */
-public class LooperIdleLock implements MessageQueue.IdleHandler, Runnable {
+public class LooperIdleLock implements MessageQueue.IdleHandler {
 
     private final Object mLock;
 
     private boolean mIsLocked;
+    private Looper mLooper;
 
     public LooperIdleLock(Object lock, Looper looper) {
         mLock = lock;
+        mLooper = looper;
         mIsLocked = true;
         looper.getQueue().addIdleHandler(this);
     }
 
     @Override
-    public void run() {
-        Looper.myQueue().addIdleHandler(this);
-    }
-
-    @Override
     public boolean queueIdle() {
         synchronized (mLock) {
             mIsLocked = false;
             mLock.notify();
         }
+        // Manually remove from the list in case we're calling this outside of the idle callbacks
+        // (this is Ok in the normal flow as well because MessageQueue makes a copy of all handlers
+        // before calling back)
+        mLooper.getQueue().removeIdleHandler(this);
         return false;
     }
 
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
index 61ba4e5..5a131c8 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -55,7 +55,9 @@
             mLoadAnimationCompleted = true;
         }
 
-        attachObserver();
+        if (mAttachedView.isAttachedToWindow()) {
+            attachObserver();
+        }
     }
 
     private void attachObserver() {
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
new file mode 100644
index 0000000..60eb304
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 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.systemui.plugins;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Implement this interface to receive a callback when the user swipes right
+ * to left on the gesture area. It won't fire if the user has quick switched to a previous app
+ * (swiped right) and the current app isn't yet the active one (i.e., if swiping left would take
+ * the user to a more recent app).
+ */
+@ProvidesInterface(action = com.android.systemui.plugins.OverscrollPlugin.ACTION,
+        version = com.android.systemui.plugins.OverlayPlugin.VERSION)
+public interface OverscrollPlugin extends Plugin {
+
+    String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
+    int VERSION = 1;
+
+    String DEVICE_STATE_LOCKED = "Locked";
+    String DEVICE_STATE_LAUNCHER = "Launcher";
+    String DEVICE_STATE_APP = "App";
+    String DEVICE_STATE_UNKNOWN = "Unknown";
+
+    /**
+     * Called when the user completed a right to left swipe in the gesture area.
+     *
+     * @param deviceState One of the DEVICE_STATE_* constants.
+     */
+    void onOverscroll(String deviceState);
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 41a4bdb..484cbb6 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -497,9 +497,7 @@
     }
 
     public void waitForLauncherInitialized() {
-        // b/136278866
-        final int attempts = android.os.Build.MODEL.contains("Cuttlefish") ? 600 : 100;
-        for (int i = 0; i < attempts; ++i) {
+        for (int i = 0; i < 100; ++i) {
             if (getTestInfo(
                     TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
                     getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD)) {