Merge "Adding auto-investigation of one more flake" into ub-launcher3-master
diff --git a/proguard.flags b/proguard.flags
index 3e12283..01302cf 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -2,12 +2,6 @@
   *;
 }
 
-# Proguard will strip new callbacks in LauncherApps.Callback from
-# WrappedCallback if compiled against an older SDK. Don't let this happen.
--keep class com.android.launcher3.compat.** {
-  *;
-}
-
 -keep class com.android.launcher3.graphics.ShadowDrawable {
   public <init>(...);
 }
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index 1e01709..2df490e 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -151,7 +151,9 @@
     private DeviceProfile mDeviceProfile;
 
     private RemoteAnimationProvider mRemoteAnimationProvider;
+    // Strong refs to runners which are cleared when the launcher activity is destroyed
     private WrappedAnimationRunnerImpl mWallpaperOpenRunner;
+    private WrappedAnimationRunnerImpl mAppLaunchRunner;
 
     private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() {
         @Override
@@ -200,32 +202,9 @@
     public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
         if (hasControlRemoteAppTransitionPermission()) {
             boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
-            RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler,
-                    true /* startAtFrontOfQueue */) {
-
-                @Override
-                public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
-                        RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
-                    AnimatorSet anim = new AnimatorSet();
-
-                    boolean launcherClosing =
-                            launcherIsATargetWithMode(appTargets, MODE_CLOSING);
-
-                    if (isLaunchingFromRecents(v, appTargets)) {
-                        composeRecentsLaunchAnimator(anim, v, appTargets, wallpaperTargets,
-                                launcherClosing);
-                    } else {
-                        composeIconLaunchAnimator(anim, v, appTargets, wallpaperTargets,
-                                launcherClosing);
-                    }
-
-                    if (launcherClosing) {
-                        anim.addListener(mForceInvisibleListener);
-                    }
-
-                    result.setAnimation(anim, mLauncher);
-                }
-            };
+            mAppLaunchRunner = new AppLaunchAnimationRunner(mHandler, v);
+            RemoteAnimationRunnerCompat runner = new WrappedLauncherAnimationRunner<>(
+                    mAppLaunchRunner, true /* startAtFrontOfQueue */);
 
             // Note that this duration is a guess as we do not know if the animation will be a
             // recents launch or not for sure until we know the opening app targets.
@@ -627,6 +606,7 @@
             // Also clear strong references to the runners registered with the remote animation
             // definition so we don't have to wait for the system gc
             mWallpaperOpenRunner = null;
+            mAppLaunchRunner = null;
         }
     }
 
@@ -914,4 +894,47 @@
             result.setAnimation(anim, mLauncher);
         }
     }
+
+    /**
+     * Remote animation runner for animation to launch an app.
+     */
+    private class AppLaunchAnimationRunner implements WrappedAnimationRunnerImpl {
+
+        private final Handler mHandler;
+        private final View mV;
+
+        AppLaunchAnimationRunner(Handler handler, View v) {
+            mHandler = handler;
+            mV = v;
+        }
+
+        @Override
+        public Handler getHandler() {
+            return mHandler;
+        }
+
+        @Override
+        public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+                RemoteAnimationTargetCompat[] wallpaperTargets,
+                LauncherAnimationRunner.AnimationResult result) {
+            AnimatorSet anim = new AnimatorSet();
+
+            boolean launcherClosing =
+                    launcherIsATargetWithMode(appTargets, MODE_CLOSING);
+
+            if (isLaunchingFromRecents(mV, appTargets)) {
+                composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
+                        launcherClosing);
+            } else {
+                composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
+                        launcherClosing);
+            }
+
+            if (launcherClosing) {
+                anim.addListener(mForceInvisibleListener);
+            }
+
+            result.setAnimation(anim, mLauncher);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
new file mode 100644
index 0000000..3c3f397
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -0,0 +1,61 @@
+/*
+ * 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.launcher3.uioverrides;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.provider.DeviceConfig;
+
+import com.android.launcher3.config.FeatureFlags.DebugFlag;
+
+@TargetApi(Build.VERSION_CODES.P)
+public class DeviceFlag extends DebugFlag {
+
+    public static final String NAMESPACE_LAUNCHER = "launcher";
+
+    private final boolean mDefaultValueInCode;
+
+    public DeviceFlag(String key, boolean defaultValue, String description) {
+        super(key, getDeviceValue(key, defaultValue), description);
+        mDefaultValueInCode = defaultValue;
+    }
+
+    @Override
+    protected StringBuilder appendProps(StringBuilder src) {
+        return super.appendProps(src).append(", mDefaultValueInCode=").append(mDefaultValueInCode);
+    }
+
+    @Override
+    public void addChangeListener(Context context, Runnable r) {
+        DeviceConfig.addOnPropertiesChangedListener(
+                NAMESPACE_LAUNCHER,
+                context.getMainExecutor(),
+                properties -> {
+                    if (!NAMESPACE_LAUNCHER.equals(properties.getNamespace())) {
+                        return;
+                    }
+                    defaultValue = getDeviceValue(key, mDefaultValueInCode);
+                    initialize(context);
+                    r.run();
+                });
+    }
+
+    protected static boolean getDeviceValue(String key, boolean defaultValue) {
+        return DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, defaultValue);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TogglableFlag.java b/quickstep/src/com/android/launcher3/uioverrides/TogglableFlag.java
deleted file mode 100644
index 27d81ef..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/TogglableFlag.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.launcher3.uioverrides;
-
-import android.content.Context;
-import android.provider.DeviceConfig;
-
-import com.android.launcher3.config.FeatureFlags.BaseTogglableFlag;
-
-public class TogglableFlag extends BaseTogglableFlag {
-    public static final String NAMESPACE_LAUNCHER = "launcher";
-    public static final String TAG = "TogglableFlag";
-
-    public TogglableFlag(String key, boolean defaultValue, String description) {
-        super(key, defaultValue, description);
-    }
-
-    @Override
-    public boolean getOverridenDefaultValue(boolean value) {
-        return DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, getKey(), value);
-    }
-
-    @Override
-    public void addChangeListener(Context context, Runnable r) {
-        DeviceConfig.addOnPropertiesChangedListener(
-                NAMESPACE_LAUNCHER,
-                context.getMainExecutor(),
-                (properties) -> {
-                    if (!NAMESPACE_LAUNCHER.equals(properties.getNamespace())) {
-                        return;
-                    }
-                    initialize(context);
-                    r.run();
-                });
-    }
-}
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 04e32b0..bd2c539 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -95,6 +95,7 @@
         mDevice = UiDevice.getInstance(instrumentation);
         mDevice.setOrientationNatural();
         mLauncher = new LauncherInstrumentation();
+        mLauncher.enableCheckEventsForSuccessfulGestures();
 
         if (TestHelpers.isInLauncherProcess()) {
             Utilities.enableRunningInTestHarnessForTests();
diff --git a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
deleted file mode 100644
index d33fecd..0000000
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.android.launcher3.config;
-
-
-import com.android.launcher3.config.FeatureFlags.BaseTogglableFlag;
-import com.android.launcher3.uioverrides.TogglableFlag;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.lang.annotation.Annotation;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Repeatable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-/**
- * Test rule that makes overriding flags in Robolectric tests easier. This rule clears all flags
- * before and after your test, avoiding one test method affecting subsequent methods.
- *
- * <p>Usage:
- * <pre>
- * {@literal @}Rule public final FlagOverrideRule flags = new FlagOverrideRule();
- *
- * {@literal @}FlagOverride(flag = "FOO", value=true)
- * {@literal @}Test public void myTest() {
- *     ...
- * }
- * </pre>
- */
-public final class FlagOverrideRule implements TestRule {
-
-    private final HashMap<String, Boolean> mDefaultOverrides = new HashMap<>();
-
-    /**
-     * Container annotation for handling multiple {@link FlagOverride} annotations.
-     * <p>
-     * <p>Don't use this directly, use repeated {@link FlagOverride} annotations instead.
-     */
-    @Retention(RetentionPolicy.RUNTIME)
-    @Target({ElementType.METHOD})
-    public @interface FlagOverrides {
-        FlagOverride[] value();
-    }
-
-    @Retention(RetentionPolicy.RUNTIME)
-    @Target({ElementType.METHOD})
-    @Repeatable(FlagOverrides.class)
-    public @interface FlagOverride {
-        String key();
-
-        boolean value();
-    }
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        return new MyStatement(base, description);
-    }
-
-    /**
-     * Sets a default override to apply on all tests
-     */
-    public FlagOverrideRule setOverride(BaseTogglableFlag flag, boolean value) {
-        mDefaultOverrides.put(flag.getKey(), value);
-        return this;
-    }
-
-    private class MyStatement extends Statement {
-
-        private final Statement mBase;
-        private final Description mDescription;
-
-
-        MyStatement(Statement base, Description description) {
-            mBase = base;
-            mDescription = description;
-        }
-
-        @Override
-        public void evaluate() throws Throwable {
-            Map<String, BaseTogglableFlag> allFlags = FeatureFlags.getTogglableFlags().stream()
-                    .collect(Collectors.toMap(TogglableFlag::getKey, Function.identity()));
-
-            HashMap<BaseTogglableFlag, Boolean> changedValues = new HashMap<>();
-            FlagOverride[] overrides = new FlagOverride[0];
-            try {
-                for (Annotation annotation : mDescription.getAnnotations()) {
-                    if (annotation.annotationType() == FlagOverride.class) {
-                        overrides = new FlagOverride[] { (FlagOverride) annotation };
-                    } else if (annotation.annotationType() == FlagOverrides.class) {
-                        // Note: this branch is hit if the annotation is repeated
-                        overrides = ((FlagOverrides) annotation).value();
-                    }
-                }
-
-                HashMap<String, Boolean> allOverrides = new HashMap<>(mDefaultOverrides);
-                Arrays.stream(overrides).forEach(o -> allOverrides.put(o.key(), o.value()));
-
-                allOverrides.forEach((key, val) -> {
-                    BaseTogglableFlag flag = allFlags.get(key);
-                    changedValues.put(flag, flag.get());
-                    flag.setForTests(val);
-                });
-                mBase.evaluate();
-            } finally {
-                // Clear the values
-                changedValues.forEach(BaseTogglableFlag::setForTests);
-            }
-        }
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
deleted file mode 100644
index 2a359df..0000000
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.android.launcher3.config;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import com.android.launcher3.config.FlagOverrideRule.FlagOverride;
-import com.android.launcher3.util.LauncherRoboTestRunner;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Sample Robolectric test that demonstrates flag-overriding.
- */
-@RunWith(LauncherRoboTestRunner.class)
-public class FlagOverrideSampleTest {
-
-    // Check out https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html for more information
-    // on @Rules.
-    @Rule
-    public final FlagOverrideRule flags = new FlagOverrideRule();
-
-    /**
-     * Test if flag can be overriden to true via annoation.
-     */
-    @FlagOverride(key = "FAKE_LANDSCAPE_UI", value = true)
-    @Test
-    public void withFlagOn() {
-        assertTrue(FeatureFlags.FAKE_LANDSCAPE_UI.get());
-    }
-
-    /**
-     * Test if flag can be overriden to false via annoation.
-     */
-    @FlagOverride(key = "FAKE_LANDSCAPE_UI", value = false)
-    @Test
-    public void withFlagOff() {
-        assertFalse(FeatureFlags.FAKE_LANDSCAPE_UI.get());
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowTogglableFlag.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
similarity index 80%
rename from robolectric_tests/src/com/android/launcher3/shadows/ShadowTogglableFlag.java
rename to robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
index 3603dd8..344f532 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/ShadowTogglableFlag.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
@@ -18,7 +18,7 @@
 
 import android.content.Context;
 
-import com.android.launcher3.uioverrides.TogglableFlag;
+import com.android.launcher3.uioverrides.DeviceFlag;
 import com.android.launcher3.util.LooperExecutor;
 
 import org.robolectric.annotation.Implementation;
@@ -27,12 +27,17 @@
 /**
  * Shadow for {@link LooperExecutor} to provide reset functionality for static executors.
  */
-@Implements(value = TogglableFlag.class, isInAndroidSdk = false)
-public class ShadowTogglableFlag {
+@Implements(value = DeviceFlag.class, isInAndroidSdk = false)
+public class ShadowDeviceFlag {
 
     /**
      * Mock change listener as it uses internal system classes not available to robolectric
      */
     @Implementation
     protected void addChangeListener(Context context, Runnable r) { }
+
+    @Implementation
+    protected static boolean getDeviceValue(String key, boolean defaultValue) {
+        return defaultValue;
+    }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java b/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java
index 5c6b486..b8fff9c 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java
@@ -23,7 +23,7 @@
 import com.android.launcher3.shadows.LShadowUserManager;
 import com.android.launcher3.shadows.ShadowLooperExecutor;
 import com.android.launcher3.shadows.ShadowMainThreadInitializedObject;
-import com.android.launcher3.shadows.ShadowTogglableFlag;
+import com.android.launcher3.shadows.ShadowDeviceFlag;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 
 import org.junit.runners.model.InitializationError;
@@ -50,7 +50,7 @@
 
             ShadowLooperExecutor.class,
             ShadowMainThreadInitializedObject.class,
-            ShadowTogglableFlag.class,
+            ShadowDeviceFlag.class,
     };
 
     public LauncherRoboTestRunner(Class<?> testClass) throws InitializationError {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8066d38..06f3453 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -76,6 +76,7 @@
 import android.view.KeyboardShortcutInfo;
 import android.view.LayoutInflater;
 import android.view.Menu;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
@@ -121,6 +122,7 @@
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.states.RotationHelper;
+import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.touch.ItemClickHandler;
@@ -1778,10 +1780,21 @@
 
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            TestLogging.recordEvent("Key event: " + event);
+        }
         return (event.getKeyCode() == KeyEvent.KEYCODE_HOME) || super.dispatchKeyEvent(event);
     }
 
     @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS && ev.getAction() != MotionEvent.ACTION_MOVE) {
+            TestLogging.recordEvent("Touch event: " + ev);
+        }
+        return super.dispatchTouchEvent(ev);
+    }
+
+    @Override
     public void onBackPressed() {
         if (finishAutoCancelActionMode()) {
             return;
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 9a3a379..3278960 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -496,7 +496,7 @@
             // In transposed layout, we add the QSB in the Grid. As workspace does not touch the
             // edges, we do not need a full width QSB.
             qsb = LayoutInflater.from(getContext())
-                    .inflate(R.layout.search_container_workspace,firstPage, false);
+                    .inflate(R.layout.search_container_workspace, firstPage, false);
         }
 
         CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, firstPage.getCountX(), 1);
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 75609fe..8ccb369 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -16,37 +16,25 @@
 
 package com.android.launcher3.config;
 
-import static androidx.core.util.Preconditions.checkNotNull;
-
 import android.content.Context;
-import android.content.SharedPreferences;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.Keep;
-import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.uioverrides.TogglableFlag;
+import com.android.launcher3.uioverrides.DeviceFlag;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.SortedMap;
-import java.util.TreeMap;
 
 /**
  * Defines a set of flags used to control various launcher behaviors.
  *
  * <p>All the flags should be defined here with appropriate default values.
  */
-@Keep
 public final class FeatureFlags {
 
-    private static final Object sLock = new Object();
-    @GuardedBy("sLock")
-    private static final List<TogglableFlag> sFlags = new ArrayList<>();
+    private static final List<DebugFlag> sDebugFlags = new ArrayList<>();
 
-    static final String FLAGS_PREF_NAME = "featureFlags";
+    public static final String FLAGS_PREF_NAME = "featureFlags";
 
     private FeatureFlags() { }
 
@@ -62,7 +50,6 @@
      */
     public static final boolean QSB_ON_FIRST_SCREEN = true;
 
-
     /**
      * Feature flag to handle define config changes dynamically instead of killing the process.
      *
@@ -73,194 +60,149 @@
      *    and set a default value for the flag. This will be the default value on Debug builds.
      */
     // When enabled the promise icon is visible in all apps while installation an app.
-    public static final TogglableFlag PROMISE_APPS_IN_ALL_APPS = new TogglableFlag(
+    public static final BooleanFlag PROMISE_APPS_IN_ALL_APPS = getDebugFlag(
             "PROMISE_APPS_IN_ALL_APPS", false, "Add promise icon in all-apps");
 
     // When enabled a promise icon is added to the home screen when install session is active.
-    public static final TogglableFlag PROMISE_APPS_NEW_INSTALLS =
-            new TogglableFlag("PROMISE_APPS_NEW_INSTALLS", true,
-                    "Adds a promise icon to the home screen for new install sessions.");
+    public static final BooleanFlag PROMISE_APPS_NEW_INSTALLS = getDebugFlag(
+            "PROMISE_APPS_NEW_INSTALLS", true,
+            "Adds a promise icon to the home screen for new install sessions.");
 
-    public static final TogglableFlag APPLY_CONFIG_AT_RUNTIME = new TogglableFlag(
+    public static final BooleanFlag APPLY_CONFIG_AT_RUNTIME = getDebugFlag(
             "APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
 
-    public static final TogglableFlag QUICKSTEP_SPRINGS = new TogglableFlag("QUICKSTEP_SPRINGS",
-            true, "Enable springs for quickstep animations");
+    public static final BooleanFlag QUICKSTEP_SPRINGS = getDebugFlag(
+            "QUICKSTEP_SPRINGS", true, "Enable springs for quickstep animations");
 
-    public static final TogglableFlag UNSTABLE_SPRINGS = new TogglableFlag("UNSTABLE_SPRINGS",
-            false, "Enable unstable springs for quickstep animations");
+    public static final BooleanFlag UNSTABLE_SPRINGS = getDebugFlag(
+            "UNSTABLE_SPRINGS", false, "Enable unstable springs for quickstep animations");
 
-    public static final TogglableFlag ADAPTIVE_ICON_WINDOW_ANIM = new TogglableFlag(
-            "ADAPTIVE_ICON_WINDOW_ANIM", true,
-            "Use adaptive icons for window animations.");
+    public static final BooleanFlag ADAPTIVE_ICON_WINDOW_ANIM = getDebugFlag(
+            "ADAPTIVE_ICON_WINDOW_ANIM", true, "Use adaptive icons for window animations.");
 
-    public static final TogglableFlag ENABLE_QUICKSTEP_LIVE_TILE = new TogglableFlag(
+    public static final BooleanFlag ENABLE_QUICKSTEP_LIVE_TILE = getDebugFlag(
             "ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
 
-    public static final TogglableFlag ENABLE_HINTS_IN_OVERVIEW = new TogglableFlag(
-            "ENABLE_HINTS_IN_OVERVIEW", false,
-            "Show chip hints and gleams on the overview screen");
+    public static final BooleanFlag ENABLE_HINTS_IN_OVERVIEW = getDebugFlag(
+            "ENABLE_HINTS_IN_OVERVIEW", false, "Show chip hints and gleams on the overview screen");
 
-    public static final TogglableFlag FAKE_LANDSCAPE_UI = new TogglableFlag(
-            "FAKE_LANDSCAPE_UI", false,
-            "Rotate launcher UI instead of using transposed layout");
+    public static final BooleanFlag FAKE_LANDSCAPE_UI = getDebugFlag(
+            "FAKE_LANDSCAPE_UI", false, "Rotate launcher UI instead of using transposed layout");
 
-    public static final TogglableFlag FOLDER_NAME_SUGGEST = new TogglableFlag(
-            "FOLDER_NAME_SUGGEST", true,
-            "Suggests folder names instead of blank text.");
+    public static final BooleanFlag FOLDER_NAME_SUGGEST = getDebugFlag(
+            "FOLDER_NAME_SUGGEST", false, "Suggests folder names instead of blank text.");
 
-    public static final TogglableFlag APP_SEARCH_IMPROVEMENTS = new TogglableFlag(
+    public static final BooleanFlag APP_SEARCH_IMPROVEMENTS = new DeviceFlag(
             "APP_SEARCH_IMPROVEMENTS", false,
             "Adds localized title and keyword search and ranking");
 
-    public static final TogglableFlag ENABLE_PREDICTION_DISMISS = new TogglableFlag(
+    public static final BooleanFlag ENABLE_PREDICTION_DISMISS = getDebugFlag(
             "ENABLE_PREDICTION_DISMISS", false, "Allow option to dimiss apps from predicted list");
 
-    public static final TogglableFlag ENABLE_QUICK_CAPTURE_GESTURE = new TogglableFlag(
+    public static final BooleanFlag ENABLE_QUICK_CAPTURE_GESTURE = getDebugFlag(
             "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture");
 
-    public static final TogglableFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = new TogglableFlag(
+    public static final BooleanFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = getDebugFlag(
             "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
             "Allow Launcher to handle nav bar gestures while Assistant is running over it");
 
-    public static final TogglableFlag ENABLE_HYBRID_HOTSEAT = new TogglableFlag(
+    public static final BooleanFlag ENABLE_HYBRID_HOTSEAT = getDebugFlag(
             "ENABLE_HYBRID_HOTSEAT", false, "Fill gaps in hotseat with predicted apps");
 
-    public static final TogglableFlag ENABLE_DEEP_SHORTCUT_ICON_CACHE = new TogglableFlag(
+    public static final BooleanFlag ENABLE_DEEP_SHORTCUT_ICON_CACHE = getDebugFlag(
             "ENABLE_DEEP_SHORTCUT_ICON_CACHE", true, "R/W deep shortcut in IconCache");
 
-    public static final TogglableFlag ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER = new TogglableFlag(
+    public static final BooleanFlag ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER = getDebugFlag(
             "ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER", false,
             "Show launcher preview in grid picker");
 
-    public static final TogglableFlag ENABLE_OVERVIEW_ACTIONS = new TogglableFlag(
+    public static final BooleanFlag ENABLE_OVERVIEW_ACTIONS = getDebugFlag(
             "ENABLE_OVERVIEW_ACTIONS", false, "Show app actions in Overview");
 
-    public static final TogglableFlag ENABLE_DATABASE_RESTORE = new TogglableFlag(
+    public static final BooleanFlag ENABLE_DATABASE_RESTORE = getDebugFlag(
             "ENABLE_DATABASE_RESTORE", true,
             "Enable database restore when new restore session is created");
 
+    public static final BooleanFlag ENABLE_UNIVERSAL_SMARTSPACE = getDebugFlag(
+            "ENABLE_UNIVERSAL_SMARTSPACE", false,
+            "Replace Smartspace with a version rendered by System UI.");
+
     public static void initialize(Context context) {
-        // Avoid the disk read for user builds
-        if (Utilities.IS_DEBUG_DEVICE) {
-            synchronized (sLock) {
-                for (BaseTogglableFlag flag : sFlags) {
-                    flag.initialize(context);
-                }
+        synchronized (sDebugFlags) {
+            for (DebugFlag flag : sDebugFlags) {
+                flag.initialize(context);
             }
         }
     }
 
-    static List<TogglableFlag> getTogglableFlags() {
-        // By Java Language Spec 12.4.2
-        // https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.2, the
-        // TogglableFlag instances on FeatureFlags will be created before those on the FeatureFlags
-        // subclass. This code handles flags that are redeclared in FeatureFlags, ensuring the
-        // FeatureFlags one takes priority.
-        SortedMap<String, TogglableFlag> flagsByKey = new TreeMap<>();
-        synchronized (sLock) {
-            for (TogglableFlag flag : sFlags) {
-                flagsByKey.put(flag.getKey(), flag);
-            }
+    static List<DebugFlag> getDebugFlags() {
+        synchronized (sDebugFlags) {
+            return new ArrayList<>(sDebugFlags);
         }
-        return new ArrayList<>(flagsByKey.values());
     }
 
-    public static abstract class BaseTogglableFlag {
-        private final String key;
-        // should be value that is hardcoded in client side.
-        // Comparatively, getDefaultValue() can be overridden.
-        private final boolean defaultValue;
-        private final String description;
-        private boolean currentValue;
+    public static class BooleanFlag {
 
-        public BaseTogglableFlag(
-                String key,
-                boolean defaultValue,
-                String description) {
-            this.key = checkNotNull(key);
-            this.currentValue = this.defaultValue = defaultValue;
-            this.description = checkNotNull(description);
+        public final String key;
+        public boolean defaultValue;
 
-            synchronized (sLock) {
-                sFlags.add((TogglableFlag)this);
-            }
+        public BooleanFlag(String key, boolean defaultValue) {
+            this.key = key;
+            this.defaultValue = defaultValue;
         }
 
-        /** Set the value of this flag. This should only be used in tests. */
-        @VisibleForTesting
-        void setForTests(boolean value) {
-            currentValue = value;
-        }
-
-        public String getKey() {
-            return key;
-        }
-
-        protected void initialize(Context context) {
-            currentValue = getFromStorage(context, getDefaultValue());
-        }
-
-        protected abstract boolean getOverridenDefaultValue(boolean value);
-
-        protected abstract void addChangeListener(Context context, Runnable r);
-
-        public void updateStorage(Context context, boolean value) {
-            SharedPreferences.Editor editor = context.getSharedPreferences(FLAGS_PREF_NAME,
-                    Context.MODE_PRIVATE).edit();
-            if (value == getDefaultValue()) {
-                editor.remove(key).apply();
-            } else {
-                editor.putBoolean(key, value).apply();
-            }
-        }
-
-        boolean getFromStorage(Context context, boolean defaultValue) {
-            return context.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
-                    .getBoolean(key, getDefaultValue());
-        }
-
-        boolean getDefaultValue() {
-            return getOverridenDefaultValue(defaultValue);
-        }
-
-        /** Returns the value of the flag at process start, including any overrides present. */
         public boolean get() {
-            return currentValue;
-        }
-
-        String getDescription() {
-            return description;
+            return defaultValue;
         }
 
         @Override
         public String toString() {
-            return "TogglableFlag{"
-                    + "key=" + key + ", "
-                    + "defaultValue=" + defaultValue + ", "
-                    + "overriddenDefaultValue=" + getOverridenDefaultValue(defaultValue) + ", "
-                    + "currentValue=" + currentValue + ", "
-                    + "description=" + description
-                    + "}";
+            return appendProps(new StringBuilder()
+                    .append(getClass().getSimpleName()).append('{'))
+                    .append('}').toString();
+        }
+
+        protected StringBuilder appendProps(StringBuilder src) {
+            return src.append("key=").append(key).append(", defaultValue=").append(defaultValue);
+        }
+
+        public void addChangeListener(Context context, Runnable r) { }
+    }
+
+    public static class DebugFlag extends BooleanFlag {
+
+        public final String description;
+        private boolean mCurrentValue;
+
+        public DebugFlag(String key, boolean defaultValue, String description) {
+            super(key, defaultValue);
+            this.description = description;
+            mCurrentValue = this.defaultValue;
+            synchronized (sDebugFlags) {
+                sDebugFlags.add(this);
+            }
         }
 
         @Override
-        public boolean equals(Object o) {
-            if (o == this) {
-                return true;
-            }
-            if (o instanceof TogglableFlag) {
-                BaseTogglableFlag that = (BaseTogglableFlag) o;
-                return (this.key.equals(that.getKey()))
-                        && (this.getDefaultValue() == that.getDefaultValue())
-                        && (this.description.equals(that.getDescription()));
-            }
-            return false;
+        public boolean get() {
+            return mCurrentValue;
+        }
+
+        public void initialize(Context context) {
+            mCurrentValue = context.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
+                    .getBoolean(key, defaultValue);
         }
 
         @Override
-        public int hashCode() {
-            return key.hashCode();
+        protected StringBuilder appendProps(StringBuilder src) {
+            return super.appendProps(src).append(", mCurrentValue=").append(mCurrentValue)
+                    .append(", description=").append(description);
         }
     }
+
+    private static BooleanFlag getDebugFlag(String key, boolean defaultValue, String description) {
+        return Utilities.IS_DEBUG_DEVICE
+                ? new DebugFlag(key, defaultValue, description)
+                : new BooleanFlag(key, defaultValue);
+    }
 }
diff --git a/src/com/android/launcher3/config/FlagTogglerPrefUi.java b/src/com/android/launcher3/config/FlagTogglerPrefUi.java
index a7e3732..6729f74 100644
--- a/src/com/android/launcher3/config/FlagTogglerPrefUi.java
+++ b/src/com/android/launcher3/config/FlagTogglerPrefUi.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.config;
 
+import static com.android.launcher3.config.FeatureFlags.FLAGS_PREF_NAME;
+
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.os.Process;
@@ -31,8 +33,7 @@
 import androidx.preference.SwitchPreference;
 
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags.BaseTogglableFlag;
-import com.android.launcher3.uioverrides.TogglableFlag;
+import com.android.launcher3.config.FeatureFlags.DebugFlag;
 
 /**
  * Dev-build only UI allowing developers to toggle flag settings. See {@link FeatureFlags}.
@@ -49,23 +50,26 @@
 
         @Override
         public void putBoolean(String key, boolean value) {
-            for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
-                if (flag.getKey().equals(key)) {
-                    boolean prevValue = flag.get();
-                    flag.updateStorage(mContext, value);
-                    updateMenu();
-                    if (flag.get() != prevValue) {
-                        Toast.makeText(mContext, "Flag applied", Toast.LENGTH_SHORT).show();
+            for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
+                if (flag.key.equals(key)) {
+                    SharedPreferences.Editor editor = mContext.getSharedPreferences(
+                            FLAGS_PREF_NAME, Context.MODE_PRIVATE).edit();
+                    if (value == flag.defaultValue) {
+                        editor.remove(key).apply();
+                    } else {
+                        editor.putBoolean(key, value).apply();
                     }
+                    updateMenu();
                 }
             }
         }
 
         @Override
         public boolean getBoolean(String key, boolean defaultValue) {
-            for (BaseTogglableFlag flag : FeatureFlags.getTogglableFlags()) {
-                if (flag.getKey().equals(key)) {
-                    return flag.getFromStorage(mContext, defaultValue);
+            for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
+                if (flag.key.equals(key)) {
+                    return mContext.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
+                            .getBoolean(key, flag.defaultValue);
                 }
             }
             return defaultValue;
@@ -76,7 +80,7 @@
         mFragment = fragment;
         mContext = fragment.getActivity();
         mSharedPreferences = mContext.getSharedPreferences(
-                FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE);
+                FLAGS_PREF_NAME, Context.MODE_PRIVATE);
     }
 
     public void applyTo(PreferenceGroup parent) {
@@ -84,12 +88,12 @@
         // flag with a different value than the default. That way, when we flip flags in
         // future, engineers will pick up the new value immediately. To accomplish this, we use a
         // custom preference data store.
-        for (BaseTogglableFlag flag : FeatureFlags.getTogglableFlags()) {
+        for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
             SwitchPreference switchPreference = new SwitchPreference(mContext);
-            switchPreference.setKey(flag.getKey());
-            switchPreference.setDefaultValue(flag.getDefaultValue());
+            switchPreference.setKey(flag.key);
+            switchPreference.setDefaultValue(flag.defaultValue);
             switchPreference.setChecked(getFlagStateFromSharedPrefs(flag));
-            switchPreference.setTitle(flag.getKey());
+            switchPreference.setTitle(flag.key);
             updateSummary(switchPreference, flag);
             switchPreference.setPreferenceDataStore(mDataStore);
             parent.addPreference(switchPreference);
@@ -100,11 +104,11 @@
     /**
      * Updates the summary to show the description and whether the flag overrides the default value.
      */
-    private void updateSummary(SwitchPreference switchPreference, BaseTogglableFlag flag) {
-        String onWarning = flag.getDefaultValue() ? "" : "<b>OVERRIDDEN</b><br>";
-        String offWarning = flag.getDefaultValue() ? "<b>OVERRIDDEN</b><br>" : "";
-        switchPreference.setSummaryOn(Html.fromHtml(onWarning + flag.getDescription()));
-        switchPreference.setSummaryOff(Html.fromHtml(offWarning + flag.getDescription()));
+    private void updateSummary(SwitchPreference switchPreference, DebugFlag flag) {
+        String onWarning = flag.defaultValue ? "" : "<b>OVERRIDDEN</b><br>";
+        String offWarning = flag.defaultValue ? "<b>OVERRIDDEN</b><br>" : "";
+        switchPreference.setSummaryOn(Html.fromHtml(onWarning + flag.description));
+        switchPreference.setSummaryOff(Html.fromHtml(offWarning + flag.description));
     }
 
     private void updateMenu() {
@@ -135,12 +139,12 @@
         }
     }
 
-    private boolean getFlagStateFromSharedPrefs(BaseTogglableFlag flag) {
-        return mDataStore.getBoolean(flag.getKey(), flag.getDefaultValue());
+    private boolean getFlagStateFromSharedPrefs(DebugFlag flag) {
+        return mDataStore.getBoolean(flag.key, flag.defaultValue);
     }
 
     private boolean anyChanged() {
-        for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
+        for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
             if (getFlagStateFromSharedPrefs(flag) != flag.get()) {
                 return true;
             }
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index d76b73f..386de23 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -41,7 +41,7 @@
  */
 public class FolderNameProvider {
 
-    private static final String TAG = FeatureFlags.FOLDER_NAME_SUGGEST.getKey();
+    private static final String TAG = "FolderNameProvider";
     private static final boolean DEBUG = FeatureFlags.FOLDER_NAME_SUGGEST.get();
 
     /**
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/TogglableFlag.java b/src_ui_overrides/com/android/launcher3/uioverrides/DeviceFlag.java
similarity index 63%
rename from src_ui_overrides/com/android/launcher3/uioverrides/TogglableFlag.java
rename to src_ui_overrides/com/android/launcher3/uioverrides/DeviceFlag.java
index d7bb293..5c1ac28 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/TogglableFlag.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -16,21 +16,11 @@
 
 package com.android.launcher3.uioverrides;
 
-import android.content.Context;
+import com.android.launcher3.config.FeatureFlags.DebugFlag;
 
-import com.android.launcher3.config.FeatureFlags.BaseTogglableFlag;
+public class DeviceFlag extends DebugFlag {
 
-public class TogglableFlag extends BaseTogglableFlag {
-
-    public TogglableFlag(String key, boolean defaultValue, String description) {
+    public DeviceFlag(String key, boolean defaultValue, String description) {
         super(key, defaultValue, description);
     }
-
-    @Override
-    public boolean getOverridenDefaultValue(boolean value) {
-        return value;
-    }
-
-    @Override
-    public void addChangeListener(Context context, Runnable r) { }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index ace04f3..19997eb 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -103,6 +103,7 @@
     protected String mTargetPackage;
 
     protected AbstractLauncherUiTest() {
+        mLauncher.enableCheckEventsForSuccessfulGestures();
         try {
             mDevice.setOrientationNatural();
         } catch (RemoteException e) {
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
index 8f9fec9..835790d 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
@@ -56,7 +56,8 @@
 
             final int endY = start.y + swipeHeight;
             LauncherInstrumentation.log("AllAppsFromOverview.switchBackToOverview before swipe");
-            mLauncher.swipeToState(start.x, start.y, start.x, endY, 60, OVERVIEW_STATE_ORDINAL);
+            mLauncher.swipeToState(start.x, start.y, start.x, endY, 60, OVERVIEW_STATE_ORDINAL,
+                    LauncherInstrumentation.GestureScope.INSIDE);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("swiped down")) {
                 return new Overview(mLauncher);
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 50bdf5c..c37e451 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -62,6 +62,10 @@
         }
     }
 
+    protected boolean zeroButtonToOverviewGestureStartsInLauncher() {
+        return false;
+    }
+
     protected void goToOverviewUnchecked() {
         switch (mLauncher.getNavigationModel()) {
             case ZERO_BUTTON: {
@@ -74,19 +78,26 @@
                         new Point(centerX, startY - swipeHeight - mLauncher.getTouchSlop());
 
                 final long downTime = SystemClock.uptimeMillis();
-                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start);
+                final LauncherInstrumentation.GestureScope gestureScope =
+                        zeroButtonToOverviewGestureStartsInLauncher()
+                                ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+                                : LauncherInstrumentation.GestureScope.OUTSIDE;
+                mLauncher.sendPointer(
+                        downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
                 mLauncher.executeAndWaitForEvent(
                         () -> mLauncher.movePointer(
                                 downTime,
                                 downTime,
                                 ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION,
                                 start,
-                                end),
+                                end,
+                                gestureScope),
                         event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(event.getClassName()),
                         () -> "Pause wasn't detected");
                 mLauncher.runToState(
                         () -> mLauncher.sendPointer(
-                                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end),
+                                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end,
+                                gestureScope),
                         OVERVIEW_STATE_ORDINAL);
                 break;
             }
@@ -109,7 +120,8 @@
                     startY = endY = mLauncher.getDevice().getDisplayHeight() / 2;
                 }
 
-                mLauncher.swipeToState(startX, startY, endX, endY, 10, OVERVIEW_STATE_ORDINAL);
+                mLauncher.swipeToState(startX, startY, endX, endY, 10, OVERVIEW_STATE_ORDINAL,
+                        LauncherInstrumentation.GestureScope.OUTSIDE);
                 break;
             }
 
@@ -162,7 +174,12 @@
                     endX = startX;
                     endY = 0;
                 }
-                mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState);
+                mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState,
+                        mLauncher.getNavigationModel()
+                                == LauncherInstrumentation.NavigationModel.ZERO_BUTTON
+                                ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+                                : LauncherInstrumentation.GestureScope.OUTSIDE
+                );
                 break;
             }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index e13ea52..e5c83e2 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -82,7 +82,8 @@
                 flingForwardImpl();
             }
 
-            mLauncher.waitForObjectInContainer(verifyActiveContainer(), clearAllSelector).click();
+            mLauncher.clickLauncherObject(
+                    mLauncher.waitForObjectInContainer(verifyActiveContainer(), clearAllSelector));
         }
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index 1a0fe3d..c06e254 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -61,6 +61,11 @@
     }
 
     @Override
+    protected boolean zeroButtonToOverviewGestureStartsInLauncher() {
+        return true;
+    }
+
+    @Override
     protected int getExpectedStateForQuickSwitch() {
         return QUICK_SWITCH_STATE_ORDINAL;
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index f88a616..1722d5b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -56,7 +56,7 @@
                 mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
 
         mLauncher.executeAndWaitForEvent(
-                () -> mObject.click(),
+                () -> mLauncher.clickLauncherObject(mObject),
                 event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
                 () -> "Launching an app didn't open a new window: " + mObject.getText());
 
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 6df8790..a510df5 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -49,7 +49,6 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
@@ -69,9 +68,11 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Date;
 import java.util.Deque;
 import java.util.LinkedList;
 import java.util.List;
@@ -94,13 +95,14 @@
     private static final int GESTURE_STEP_MS = 16;
     private static long START_TIME = System.currentTimeMillis();
 
-    static final Pattern LOG_TIME = Pattern.compile(
-            "[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9]");
-
     static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
-            "(?<time>[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9])"
+            "[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9]"
                     + ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<event>.*)");
 
+    private static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
+    private static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
+    private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
+
     // Types for launcher containers that the user is interacting with. "Background" is a
     // pseudo-container corresponding to inactive launcher covered by another app.
     public enum ContainerType {
@@ -109,6 +111,13 @@
 
     public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
 
+    // Where the gesture happens: outside of Launcher, inside or from inside to outside.
+    enum GestureScope {
+        OUTSIDE, INSIDE, INSIDE_TO_OUTSIDE
+    }
+
+    ;
+
     // Base class for launcher containers.
     static abstract class VisibleContainer {
         protected final LauncherInstrumentation mLauncher;
@@ -159,6 +168,16 @@
     private List<Pattern> mExpectedEvents;
 
     private String mTimeBeforeFirstLogEvent;
+    private boolean mCheckEventsForSuccessfulGestures = false;
+
+    private static Pattern getTouchEventPattern(String action) {
+        // The pattern includes sanity checks that we don't get a multi-touch events or other
+        // surprises.
+        return Pattern.compile(
+                "Touch event: MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
+                        +
+                        ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount=1");
+    }
 
     /**
      * Constructs the root of TAPL hierarchy. You get all other objects from it.
@@ -221,6 +240,10 @@
         }
     }
 
+    public void enableCheckEventsForSuccessfulGestures() {
+        mCheckEventsForSuccessfulGestures = true;
+    }
+
     Context getContext() {
         return mInstrumentation.getContext();
     }
@@ -385,7 +408,7 @@
         log("Hierarchy dump for: " + message);
         dumpViewHierarchy();
 
-        final String eventMismatch = getEventMismatchMessage();
+        final String eventMismatch = getEventMismatchMessage(false);
 
         if (eventMismatch != null) {
             message = message + ",\nhaving produced wrong events:\n    " + eventMismatch;
@@ -577,7 +600,7 @@
                             displaySize.x / 2, displaySize.y - 1,
                             displaySize.x / 2, 0,
                             ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
-                            false);
+                            false, GestureScope.INSIDE_TO_OUTSIDE);
                     try (LauncherInstrumentation.Closable c = addContextLayer(
                             "Swiped up from context menu to home")) {
                         waitUntilGone(CONTEXT_MENU_RES_ID);
@@ -594,7 +617,10 @@
                         swipeToState(
                                 displaySize.x / 2, displaySize.y - 1,
                                 displaySize.x / 2, 0,
-                                ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL);
+                                ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
+                                hasLauncherObject(By.textStartsWith(""))
+                                        ? GestureScope.INSIDE_TO_OUTSIDE
+                                        : GestureScope.OUTSIDE);
                     }
                 }
             } else {
@@ -763,11 +789,18 @@
         return object;
     }
 
-    @Nullable
     private boolean hasLauncherObject(String resId) {
         return mDevice.hasObject(getLauncherObjectSelector(resId));
     }
 
+    private boolean hasLauncherObject(BySelector selector) {
+        return mDevice.hasObject(makeLauncherSelector(selector));
+    }
+
+    private BySelector makeLauncherSelector(BySelector selector) {
+        return By.copy(selector).pkg(getLauncherPackageName());
+    }
+
     @NonNull
     UiObject2 waitForLauncherObject(String resName) {
         return waitForObjectBySelector(getLauncherObjectSelector(resName));
@@ -775,12 +808,12 @@
 
     @NonNull
     UiObject2 waitForLauncherObject(BySelector selector) {
-        return waitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()));
+        return waitForObjectBySelector(makeLauncherSelector(selector));
     }
 
     @NonNull
     UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
-        return tryWaitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()), timeout);
+        return tryWaitForObjectBySelector(makeLauncherSelector(selector), timeout);
     }
 
     @NonNull
@@ -857,8 +890,11 @@
         return actualState == expectedState;
     }
 
-    void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState) {
-        runToState(() -> linearGesture(startX, startY, endX, endY, steps, false), expectedState);
+    void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState,
+            GestureScope gestureScope) {
+        runToState(
+                () -> linearGesture(startX, startY, endX, endY, steps, false, gestureScope),
+                expectedState);
     }
 
     int getBottomGestureSize() {
@@ -871,6 +907,12 @@
         return container.getVisibleBounds().bottom - bottomGestureStartOnScreen;
     }
 
+    void clickLauncherObject(UiObject2 object) {
+        expectEvent(LauncherInstrumentation.EVENT_TOUCH_DOWN);
+        expectEvent(LauncherInstrumentation.EVENT_TOUCH_UP);
+        object.click();
+    }
+
     void scrollToLastVisibleRow(
             UiObject2 container,
             Collection<UiObject2> items,
@@ -942,7 +984,8 @@
         }
 
         executeAndWaitForEvent(
-                () -> linearGesture(startX, startY, endX, endY, steps, slowDown),
+                () -> linearGesture(
+                        startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
                 event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
                 () -> "Didn't receive a scroll end message: " + startX + ", " + startY
                         + ", " + endX + ", " + endY);
@@ -950,21 +993,24 @@
 
     // Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
     // fixed interval each time.
-    void linearGesture(int startX, int startY, int endX, int endY, int steps, boolean slowDown) {
+    void linearGesture(int startX, int startY, int endX, int endY, int steps, boolean slowDown,
+            GestureScope gestureScope) {
         log("linearGesture: " + startX + ", " + startY + " -> " + endX + ", " + endY);
         final long downTime = SystemClock.uptimeMillis();
         final Point start = new Point(startX, startY);
         final Point end = new Point(endX, endY);
-        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start);
-        final long endTime = movePointer(start, end, steps, downTime, slowDown);
-        sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end);
+        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
+        final long endTime = movePointer(start, end, steps, downTime, slowDown, gestureScope);
+        sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
     }
 
-    long movePointer(Point start, Point end, int steps, long downTime, boolean slowDown) {
-        long endTime = movePointer(downTime, downTime, steps * GESTURE_STEP_MS, start, end);
+    long movePointer(Point start, Point end, int steps, long downTime, boolean slowDown,
+            GestureScope gestureScope) {
+        long endTime = movePointer(
+                downTime, downTime, steps * GESTURE_STEP_MS, start, end, gestureScope);
         if (slowDown) {
             endTime = movePointer(downTime, endTime + GESTURE_STEP_MS, 5 * GESTURE_STEP_MS, end,
-                    end);
+                    end, gestureScope);
         }
         return endTime;
     }
@@ -999,13 +1045,27 @@
                 0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
     }
 
-    void sendPointer(long downTime, long currentTime, int action, Point point) {
+    void sendPointer(long downTime, long currentTime, int action, Point point,
+            GestureScope gestureScope) {
+        if (gestureScope != GestureScope.OUTSIDE) {
+            switch (action) {
+                case MotionEvent.ACTION_DOWN:
+                    expectEvent(EVENT_TOUCH_DOWN);
+                    break;
+                case MotionEvent.ACTION_UP:
+                    expectEvent(gestureScope == GestureScope.INSIDE
+                            ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
+                    break;
+            }
+        }
+
         final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
         mInstrumentation.getUiAutomation().injectInputEvent(event, true);
         event.recycle();
     }
 
-    long movePointer(long downTime, long startTime, long duration, Point from, Point to) {
+    long movePointer(long downTime, long startTime, long duration, Point from, Point to,
+            GestureScope gestureScope) {
         log("movePointer: " + from + " to " + to);
         final Point point = new Point();
         long steps = duration / GESTURE_STEP_MS;
@@ -1019,7 +1079,7 @@
             point.x = from.x + (int) (progress * (to.x - from.x));
             point.y = from.y + (int) (progress * (to.y - from.y));
 
-            sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point);
+            sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point, gestureScope);
         }
         return currentTime;
     }
@@ -1032,9 +1092,10 @@
     UiObject2 clickAndGet(@NonNull final UiObject2 target, @NonNull String resName) {
         final Point targetCenter = target.getVisibleCenter();
         final long downTime = SystemClock.uptimeMillis();
-        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter);
+        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter, GestureScope.INSIDE);
         final UiObject2 result = waitForLauncherObject(resName);
-        sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter);
+        sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
+                GestureScope.INSIDE);
         return result;
     }
 
@@ -1133,9 +1194,6 @@
                             + " -s " + TestProtocol.TAPL_EVENTS_TAG);
             final Matcher matcher = EVENT_LOG_ENTRY.matcher(logcatEvents);
             while (matcher.find()) {
-                final String eventTime = matcher.group("time");
-                if (eventTime.equals(mTimeBeforeFirstLogEvent)) continue;
-
                 events.add(matcher.group("event"));
             }
             return events;
@@ -1147,15 +1205,9 @@
     private void startRecordingEvents() {
         Assert.assertTrue("Already recording events", mExpectedEvents == null);
         mExpectedEvents = new ArrayList<>();
-
-        try {
-            final String lastLogLine =
-                    mDevice.executeShellCommand("logcat -d --pid=" + getPid() + " -t 1");
-            final Matcher matcher = LOG_TIME.matcher(lastLogLine);
-            mTimeBeforeFirstLogEvent = matcher.find() ? matcher.group().replaceAll(" ", "") : null;
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
+        mTimeBeforeFirstLogEvent = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
+                .format(new Date())
+                .replaceAll(" ", "");
     }
 
     private void stopRecordingEvents() {
@@ -1172,14 +1224,12 @@
                 return; // There was a failure. Noo need to report another one.
             }
 
-            // Wait until Launcher generates expected number of events.
-            final long endTime = SystemClock.uptimeMillis() + WAIT_TIME_MS;
-            while (SystemClock.uptimeMillis() < endTime
-                    && getEvents().size() < mExpectedEvents.size()) {
-                SystemClock.sleep(100);
+            if (!mCheckEventsForSuccessfulGestures) {
+                stopRecordingEvents();
+                return;
             }
 
-            final String message = getEventMismatchMessage();
+            final String message = getEventMismatchMessage(true);
             if (message != null) {
                 Assert.fail(formatSystemHealthMessage(
                         "http://go/tapl : unexpected event sequence: " + message));
@@ -1191,11 +1241,21 @@
         if (mExpectedEvents != null) mExpectedEvents.add(expected);
     }
 
-    private String getEventMismatchMessage() {
+    private String getEventMismatchMessage(boolean waitForExpectedCount) {
         if (mExpectedEvents == null) return null;
 
         try {
-            final List<String> actual = getEvents();
+            List<String> actual = getEvents();
+
+            if (waitForExpectedCount) {
+                // Wait until Launcher generates the expected number of events.
+                final long endTime = SystemClock.uptimeMillis() + WAIT_TIME_MS;
+                while (SystemClock.uptimeMillis() < endTime
+                        && actual.size() < mExpectedEvents.size()) {
+                    SystemClock.sleep(100);
+                    actual = getEvents();
+                }
+            }
 
             for (int i = 0; i < mExpectedEvents.size(); ++i) {
                 if (i >= actual.size()) {
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
index 461610d..c2f701b 100644
--- a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
@@ -38,7 +38,7 @@
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
             LauncherInstrumentation.log("OptionsPopupMenuItem before click "
                     + mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
-            mObject.click();
+            mLauncher.clickLauncherObject(mObject);
             mLauncher.assertTrue(
                     "App didn't start: " + By.pkg(expectedPackageName),
                     mLauncher.getDevice().wait(Until.hasObject(By.pkg(expectedPackageName)),
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index 6622c6e..4d673a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -60,7 +60,8 @@
                     mLauncher.getDevice().getDisplayWidth() / 2,
                     0,
                     12,
-                    ALL_APPS_STATE_ORDINAL);
+                    ALL_APPS_STATE_ORDINAL,
+                    LauncherInstrumentation.GestureScope.INSIDE);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "swiped all way up from overview")) {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 0d93cbc..b21b242 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -53,7 +53,8 @@
             final Rect taskBounds = mTask.getVisibleBounds();
             final int centerX = taskBounds.centerX();
             final int centerY = taskBounds.centerY();
-            mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false);
+            mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false,
+                    LauncherInstrumentation.GestureScope.INSIDE);
             mLauncher.waitForIdle();
         }
     }
@@ -67,7 +68,7 @@
             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                     "clicking an overview task")) {
                 mLauncher.executeAndWaitForEvent(
-                        () -> mTask.click(),
+                        () -> mLauncher.clickLauncherObject(mTask),
                         event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
                         () -> "Launching task didn't open a new window: "
                                 + mTask.getParent().getContentDescription());
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 1d2c821..a0d5443 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -38,11 +38,21 @@
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.testing.TestProtocol;
 
+import java.util.regex.Pattern;
+
 /**
  * Operations on the workspace screen.
  */
 public final class Workspace extends Home {
     private static final int FLING_STEPS = 10;
+
+    static final Pattern EVENT_CTRL_W_DOWN = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_W"
+                    + ".*?metaState=META_CTRL_ON");
+    static final Pattern EVENT_CTRL_W_UP = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W"
+                    + ".*?metaState=META_CTRL_ON");
+
     private final UiObject2 mHotseat;
 
     Workspace(LauncherInstrumentation launcher) {
@@ -108,7 +118,7 @@
                     0,
                     startY - swipeHeight - mLauncher.getTouchSlop(),
                     12,
-                    ALL_APPS_STATE_ORDINAL);
+                    ALL_APPS_STATE_ORDINAL, LauncherInstrumentation.GestureScope.INSIDE);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "swiped to all apps")) {
@@ -195,17 +205,19 @@
         launcher.runToState(
                 () -> {
                     launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN,
-                            launchableCenter);
+                            launchableCenter, LauncherInstrumentation.GestureScope.INSIDE);
                     LauncherInstrumentation.log("dragIconToWorkspace: sent down");
                     launcher.waitForLauncherObject(longPressIndicator);
                     LauncherInstrumentation.log("dragIconToWorkspace: indicator");
-                    launcher.movePointer(launchableCenter, dest, 10, downTime, true);
+                    launcher.movePointer(launchableCenter, dest, 10, downTime, true,
+                            LauncherInstrumentation.GestureScope.INSIDE);
                 },
                 SPRING_LOADED_STATE_ORDINAL);
         LauncherInstrumentation.log("dragIconToWorkspace: moved pointer");
         launcher.runToState(
                 () -> launcher.sendPointer(
-                        downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest),
+                        downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
+                        LauncherInstrumentation.GestureScope.INSIDE),
                 NORMAL_STATE_ORDINAL);
         LauncherInstrumentation.log("dragIconToWorkspace: end");
         launcher.waitUntilGone("drop_target_bar");
@@ -248,6 +260,8 @@
     public Widgets openAllWidgets() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
             verifyActiveContainer();
+            mLauncher.expectEvent(EVENT_CTRL_W_DOWN);
+            mLauncher.expectEvent(EVENT_CTRL_W_UP);
             mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
                 return new Widgets(mLauncher);