Merge "Apply animation seen to onStateSetImmediately." into ub-launcher3-master
diff --git a/Android.mk b/Android.mk
index cddd53d..b8e6c85 100644
--- a/Android.mk
+++ b/Android.mk
@@ -28,6 +28,35 @@
 LOCAL_SDK_VERSION := current
 include $(BUILD_PREBUILT)
 
+include $(CLEAR_VARS)
+LOCAL_MODULE := libPluginCore
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+LOCAL_SRC_FILES := libs/plugin_core.jar
+LOCAL_UNINSTALLABLE_MODULE := true
+LOCAL_SDK_VERSION := current
+include $(BUILD_PREBUILT)
+
+#
+# Build rule for plugin lib (needed to write a plugin).
+#
+include $(CLEAR_VARS)
+LOCAL_USE_AAPT2 := true
+LOCAL_AAPT2_ONLY := true
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_STATIC_JAVA_LIBRARIES := libPluginCore
+
+LOCAL_SRC_FILES := \
+    $(call all-java-files-under, src_plugins)
+
+LOCAL_SDK_VERSION := current
+LOCAL_MIN_SDK_VERSION := 28
+LOCAL_MODULE := LauncherPluginLib
+LOCAL_PRIVILEGED_MODULE := true
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
 #
 # Build rule for Launcher3 dependencies lib.
 #
@@ -40,6 +69,8 @@
     androidx.recyclerview_recyclerview \
     androidx.dynamicanimation_dynamicanimation
 
+LOCAL_STATIC_JAVA_LIBRARIES := libPluginCore
+
 LOCAL_SRC_FILES := \
     $(call all-proto-files-under, protos) \
     $(call all-proto-files-under, proto_overrides)
@@ -74,6 +105,8 @@
     $(call all-java-files-under, src_flags)
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+# Proguard is disable for testing. Derivarive prjects to keep proguard enabled
+LOCAL_PROGUARD_ENABLED := disabled
 
 LOCAL_SDK_VERSION := current
 LOCAL_MIN_SDK_VERSION := 21
diff --git a/go/src_flags/com/android/launcher3/config/FeatureFlags.java b/go/src_flags/com/android/launcher3/config/FeatureFlags.java
index 6be9de8..a90808c 100644
--- a/go/src_flags/com/android/launcher3/config/FeatureFlags.java
+++ b/go/src_flags/com/android/launcher3/config/FeatureFlags.java
@@ -22,14 +22,10 @@
  * Defines a set of flags used to control various launcher behaviors
  */
 public final class FeatureFlags extends BaseFlags {
-    private static FeatureFlags instance = new FeatureFlags();
-
-    public static FeatureFlags getInstance(Context context) {
-        return instance;
+    private FeatureFlags() {
+        // Prevent instantiation
     }
 
-    private FeatureFlags() {}
-
     // Features to control Launcher3Go behavior
     public static final boolean GO_DISABLE_WIDGETS = true;
     public static final boolean LAUNCHER3_SPRING_ICONS = false;
diff --git a/libs/plugin_core.jar b/libs/plugin_core.jar
new file mode 100644
index 0000000..dd27f86
--- /dev/null
+++ b/libs/plugin_core.jar
Binary files differ
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index 7ff664d..c5a7c05 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/BackgroundAppState.java
index 53dcc74..fdb13b1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BackgroundAppState.java
@@ -15,10 +15,14 @@
  */
 package com.android.launcher3.uioverrides;
 
+import android.os.RemoteException;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.quickstep.QuickScrubController;
+import com.android.quickstep.RecentsModel;
 import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.recents.ISystemUiProxy;
 
 /**
  * State indicating that the Launcher is behind an app
@@ -43,4 +47,27 @@
         float progressDelta = (transitionLength / scrollRange);
         return super.getVerticalProgress(launcher) + progressDelta;
     }
+
+    @Override
+    public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) {
+        // Initialize the recents view scale to what it would be when starting swipe up/quickscrub
+        RecentsView recentsView = launcher.getOverviewPanel();
+        recentsView.getTaskSize(sTempRect);
+        int appWidth = launcher.getDragLayer().getWidth();
+        if (recentsView.shouldUseMultiWindowTaskSizeStrategy()) {
+            ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(launcher).getSystemUiProxy();
+            if (sysUiProxy != null) {
+                try {
+                    // Try to use the actual non-minimized app width (launcher will be resized to
+                    // the non-minimized bounds, which differs from the app width in landscape
+                    // multi-window mode
+                    appWidth = sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds().width();
+                } catch (RemoteException e) {
+                    // Ignore, fall back to just using the drag layer width
+                }
+            }
+        }
+        float scale = (float) appWidth / sTempRect.width();
+        return new float[] { scale, 0f };
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java
new file mode 100644
index 0000000..e9fac26
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 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.plugins;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.android.launcher3.Utilities;
+import com.android.systemui.shared.plugins.PluginEnabler;
+
+public class PluginEnablerImpl implements PluginEnabler {
+
+    final private SharedPreferences mSharedPrefs;
+
+    public PluginEnablerImpl(Context context) {
+        mSharedPrefs = Utilities.getDevicePrefs(context);
+    }
+
+    @Override
+    public void setEnabled(ComponentName component, boolean enabled) {
+        mSharedPrefs.edit().putBoolean(toPrefString(component), enabled).apply();
+    }
+
+    @Override
+    public boolean isEnabled(ComponentName component) {
+        return mSharedPrefs.getBoolean(toPrefString(component), true);
+    }
+
+    private String toPrefString(ComponentName component) {
+        return "PLUGIN_ENABLED_" + component.flattenToString();
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java
new file mode 100644
index 0000000..8a6aa05
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 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.plugins;
+
+import android.content.Context;
+import android.os.Looper;
+
+import com.android.launcher3.LauncherModel;
+import com.android.systemui.shared.plugins.PluginEnabler;
+import com.android.systemui.shared.plugins.PluginInitializer;
+
+public class PluginInitializerImpl implements PluginInitializer {
+    @Override
+    public Looper getBgLooper() {
+        return LauncherModel.getWorkerLooper();
+    }
+
+    @Override
+    public void onPluginManagerInit() {
+    }
+
+    @Override
+    public String[] getWhitelistedPlugins(Context context) {
+        return new String[0];
+    }
+
+    @Override
+    public PluginEnabler getPluginEnabler(Context context) {
+        return new PluginEnablerImpl(context);
+    }
+
+    @Override
+    public void handleWtfs() {
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java
new file mode 100644
index 0000000..ca12951
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 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.plugins;
+
+import android.content.Context;
+
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.shared.plugins.PluginEnabler;
+import com.android.systemui.shared.plugins.PluginInitializer;
+import com.android.systemui.shared.plugins.PluginManager;
+import com.android.systemui.shared.plugins.PluginManagerImpl;
+
+public class PluginManagerWrapper {
+
+    public static final MainThreadInitializedObject<PluginManagerWrapper> INSTANCE =
+            new MainThreadInitializedObject<>(PluginManagerWrapper::new);
+
+    private final PluginManager mPluginManager;
+    private final PluginEnabler mPluginEnabler;
+
+    private PluginManagerWrapper(Context c) {
+        PluginInitializer pluginInitializer  = new PluginInitializerImpl();
+        mPluginManager = new PluginManagerImpl(c, pluginInitializer);
+        mPluginEnabler = pluginInitializer.getPluginEnabler(c);
+    }
+
+    PluginEnabler getPluginEnabler() {
+        return mPluginEnabler;
+    }
+
+    public void addPluginListener(PluginListener<? extends Plugin> listener, Class<?> pluginClass) {
+        mPluginManager.addPluginListener(listener, pluginClass);
+    }
+
+    public void removePluginListener(PluginListener<? extends Plugin> listener) {
+        mPluginManager.removePluginListener(listener);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginPreferencesFragment.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginPreferencesFragment.java
new file mode 100644
index 0000000..3da4f84
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginPreferencesFragment.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2018 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.plugins;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.systemui.shared.plugins.PluginEnabler;
+import com.android.systemui.shared.plugins.PluginManager;
+import com.android.systemui.shared.plugins.PluginPrefs;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This class is copied from System UI Tuner, except using our PluginEnablerImpl. The reason we
+ * can't share a common base class in the shared lib is because the androidx preference dependency
+ * interferes with our recyclerview and fragment dependencies.
+ */
+public class PluginPreferencesFragment extends PreferenceFragment {
+    public static final String ACTION_PLUGIN_SETTINGS
+            = "com.android.systemui.action.PLUGIN_SETTINGS";
+
+    private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
+
+    private PluginPrefs mPluginPrefs;
+    private PluginEnabler mPluginEnabler;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addDataScheme("package");
+        getContext().registerReceiver(mReceiver, filter);
+        filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
+        getContext().registerReceiver(mReceiver, filter);
+
+        mPluginEnabler = PluginManagerWrapper.INSTANCE.get(getContext()).getPluginEnabler();
+        loadPrefs();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        getContext().unregisterReceiver(mReceiver);
+    }
+
+    private void loadPrefs() {
+        PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(getContext());
+        screen.setOrderingAsAdded(false);
+        Context prefContext = getContext();
+        mPluginPrefs = new PluginPrefs(getContext());
+        PackageManager pm = getContext().getPackageManager();
+
+        Set<String> pluginActions = mPluginPrefs.getPluginList();
+        ArrayMap<String, ArraySet<String>> plugins = new ArrayMap<>();
+        for (String action : pluginActions) {
+            String name = toName(action);
+            List<ResolveInfo> result = pm.queryIntentServices(
+                    new Intent(action), PackageManager.MATCH_DISABLED_COMPONENTS);
+            for (ResolveInfo info : result) {
+                String packageName = info.serviceInfo.packageName;
+                if (!plugins.containsKey(packageName)) {
+                    plugins.put(packageName, new ArraySet<>());
+                }
+                plugins.get(packageName).add(name);
+            }
+        }
+
+        List<PackageInfo> apps = pm.getPackagesHoldingPermissions(new String[]{PLUGIN_PERMISSION},
+                PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_SERVICES);
+        apps.forEach(app -> {
+            if (!plugins.containsKey(app.packageName)) return;
+            SwitchPreference pref = new PluginPreference(prefContext, app, mPluginEnabler);
+            pref.setSummary("Plugins: " + toString(plugins.get(app.packageName)));
+            screen.addPreference(pref);
+        });
+        setPreferenceScreen(screen);
+    }
+
+    private String toString(ArraySet<String> plugins) {
+        StringBuilder b = new StringBuilder();
+        for (String string : plugins) {
+            if (b.length() != 0) {
+                b.append(", ");
+            }
+            b.append(string);
+        }
+        return b.toString();
+    }
+
+    private String toName(String action) {
+        String str = action.replace("com.android.systemui.action.PLUGIN_", "");
+        StringBuilder b = new StringBuilder();
+        for (String s : str.split("_")) {
+            if (b.length() != 0) {
+                b.append(' ');
+            }
+            b.append(s.substring(0, 1));
+            b.append(s.substring(1).toLowerCase());
+        }
+        return b.toString();
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            loadPrefs();
+        }
+    };
+
+    private static class PluginPreference extends SwitchPreference {
+        private final boolean mHasSettings;
+        private final PackageInfo mInfo;
+        private final PluginEnabler mPluginEnabler;
+
+        public PluginPreference(Context prefContext, PackageInfo info, PluginEnabler pluginEnabler) {
+            super(prefContext);
+            PackageManager pm = prefContext.getPackageManager();
+            mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS)
+                    .setPackage(info.packageName), 0) != null;
+            mInfo = info;
+            mPluginEnabler = pluginEnabler;
+            setTitle(info.applicationInfo.loadLabel(pm));
+            setChecked(isPluginEnabled());
+            setWidgetLayoutResource(R.layout.switch_preference_with_settings);
+        }
+
+        private boolean isPluginEnabled() {
+            for (int i = 0; i < mInfo.services.length; i++) {
+                ComponentName componentName = new ComponentName(mInfo.packageName,
+                        mInfo.services[i].name);
+                if (!mPluginEnabler.isEnabled(componentName)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        protected boolean persistBoolean(boolean isEnabled) {
+            boolean shouldSendBroadcast = false;
+            for (int i = 0; i < mInfo.services.length; i++) {
+                ComponentName componentName = new ComponentName(mInfo.packageName,
+                        mInfo.services[i].name);
+
+                if (mPluginEnabler.isEnabled(componentName) != isEnabled) {
+                    mPluginEnabler.setEnabled(componentName, isEnabled);
+                    shouldSendBroadcast = true;
+                }
+            }
+            if (shouldSendBroadcast) {
+                final String pkg = mInfo.packageName;
+                final Intent intent = new Intent(PluginManager.PLUGIN_CHANGED,
+                        pkg != null ? Uri.fromParts("package", pkg, null) : null);
+                getContext().sendBroadcast(intent);
+            }
+            setChecked(isEnabled);
+            return true;
+        }
+
+        @Override
+        protected void onBindView(View view) {
+            super.onBindView(view);
+            view.findViewById(R.id.settings).setVisibility(mHasSettings ? View.VISIBLE
+                    : View.GONE);
+            view.findViewById(R.id.divider).setVisibility(mHasSettings ? View.VISIBLE
+                    : View.GONE);
+            view.findViewById(R.id.settings).setOnClickListener(v -> {
+                ResolveInfo result = v.getContext().getPackageManager().resolveActivity(
+                        new Intent(ACTION_PLUGIN_SETTINGS).setPackage(
+                                mInfo.packageName), 0);
+                if (result != null) {
+                    v.getContext().startActivity(new Intent().setComponent(
+                            new ComponentName(result.activityInfo.packageName,
+                                    result.activityInfo.name)));
+                }
+            });
+            view.setOnLongClickListener(v -> {
+                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+                intent.setData(Uri.fromParts("package", mInfo.packageName, null));
+                getContext().startActivity(intent);
+                return true;
+            });
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 206c8be..2331a4e 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -286,7 +286,7 @@
             }
 
             if (interactionType == INTERACTION_NORMAL) {
-                playScaleDownAnim(anim, activity);
+                playScaleDownAnim(anim, activity, endState);
             }
 
             anim.setDuration(transitionLength * 2);
@@ -304,14 +304,24 @@
         /**
          * Scale down recents from the center task being full screen to being in overview.
          */
-        private void playScaleDownAnim(AnimatorSet anim, Launcher launcher) {
+        private void playScaleDownAnim(AnimatorSet anim, Launcher launcher,
+                LauncherState endState) {
             RecentsView recentsView = launcher.getOverviewPanel();
             TaskView v = recentsView.getTaskViewAt(recentsView.getCurrentPage());
             if (v == null) {
                 return;
             }
+
+            // Setup the clip animation helper source/target rects in the final transformed state
+            // of the recents view (a scale may be applied prior to this animation starting to
+            // line up the side pages during swipe up)
+            float prevRvScale = recentsView.getScaleX();
+            float targetRvScale = endState.getOverviewScaleAndTranslationYFactor(launcher)[0];
+            SCALE_PROPERTY.set(recentsView, targetRvScale);
             ClipAnimationHelper clipHelper = new ClipAnimationHelper();
             clipHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), null);
+            SCALE_PROPERTY.set(recentsView, prevRvScale);
+
             if (!clipHelper.getSourceRect().isEmpty() && !clipHelper.getTargetRect().isEmpty()) {
                 float fromScale = clipHelper.getSourceRect().width()
                         / clipHelper.getTargetRect().width();
diff --git a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java
index 6557761..bd6204a 100644
--- a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java
+++ b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java
@@ -43,7 +43,6 @@
     private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
     private final DrawableFactory mDrawableFactory;
     private final boolean mDisableColorExtraction;
-    private LauncherIcons mLauncherIcons;
 
     public NormalizedIconLoader(Context context, TaskKeyLruCache<Drawable> iconCache,
             LruCache<ComponentName, ActivityInfo> activityInfoCache,
@@ -73,19 +72,18 @@
                 false));
     }
 
-    private synchronized BitmapInfo getBitmapInfo(Drawable drawable, int userId,
+    private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
             int primaryColor, boolean isInstantApp) {
-        if (mLauncherIcons == null) {
-            mLauncherIcons = LauncherIcons.obtain(mContext);
+        try (LauncherIcons la = LauncherIcons.obtain(mContext)) {
             if (mDisableColorExtraction) {
-                mLauncherIcons.disableColorExtraction();
+                la.disableColorExtraction();
             }
-        }
+            la.setWrapperBackgroundColor(primaryColor);
 
-        mLauncherIcons.setWrapperBackgroundColor(primaryColor);
-        // User version code O, so that the icon is always wrapped in an adaptive icon container.
-        return mLauncherIcons.createBadgedIconBitmap(drawable, UserHandle.of(userId),
-                Build.VERSION_CODES.O, isInstantApp);
+            // User version code O, so that the icon is always wrapped in an adaptive icon container
+            return la.createBadgedIconBitmap(drawable, UserHandle.of(userId),
+                    Build.VERSION_CODES.O, isInstantApp);
+        }
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
index 711ef58..8c84f29 100644
--- a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -90,8 +90,6 @@
 
     // Whether to boost the opening animation target layers, or the closing
     private int mBoostModeTargetLayers = -1;
-    // Wether or not applyTransform has been called yet since prepareAnimation()
-    private boolean mIsFirstFrame = true;
 
     private BiFunction<RemoteAnimationTargetCompat, Float, Float> mTaskAlphaCallback =
             (t, a1) -> a1;
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index bff3025..1205bdc 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -229,7 +229,9 @@
         }
     };
 
-    private int mLoadPlanId = -1;
+    // Used to keep track of the last requested load plan id, so that we do not request to load the
+    // tasks again if we have already requested it and the task list has not changed
+    private int mRequestedLoadPlanId = -1;
 
     // Only valid until the launcher state changes to NORMAL
     private int mRunningTaskId = -1;
@@ -447,6 +449,7 @@
             mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan));
             return;
         }
+
         TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
         if (stack == null) {
             removeAllViews();
@@ -615,8 +618,9 @@
      * and unloads the associated task data for tasks that are no longer visible.
      */
     public void loadVisibleTaskData() {
-        if (!mOverviewStateEnabled) {
-            // Skip loading visible task data if we've already left the overview state
+        if (!mOverviewStateEnabled || mRequestedLoadPlanId == -1) {
+            // Skip loading visible task data if we've already left the overview state, or if the
+            // task list hasn't been loaded yet (the task views will not reflect the task list)
             return;
         }
 
@@ -675,6 +679,7 @@
         mRunningTaskId = -1;
         mRunningTaskTileHidden = false;
         mIgnoreResetTaskId = -1;
+        mRequestedLoadPlanId = -1;
 
         unloadVisibleTaskData();
         setCurrentPage(0);
@@ -686,8 +691,8 @@
      * Reloads the view if anything in recents changed.
      */
     public void reloadIfNeeded() {
-        if (!mModel.isLoadPlanValid(mLoadPlanId)) {
-            mLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan);
+        if (!mModel.isLoadPlanValid(mRequestedLoadPlanId)) {
+            mRequestedLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan);
         }
     }
 
@@ -748,8 +753,8 @@
 
         setCurrentPage(0);
 
-        // Load the tasks (if the loading is already
-        mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan);
+        // Load the tasks
+        reloadIfNeeded();
     }
 
     public void showNextTask() {
diff --git a/res/drawable/qsb_host_view_focus_bg.xml b/res/drawable/qsb_host_view_focus_bg.xml
new file mode 100644
index 0000000..7300b6a
--- /dev/null
+++ b/res/drawable/qsb_host_view_focus_bg.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2018, 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.
+*/
+-->
+
+<!-- Used as the widget host view background when giving focus to a child via keyboard. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true">
+        <shape android:shape="rectangle">
+            <stroke android:color="#fff" android:width="2dp" />
+        </shape>
+    </item>
+    <item android:state_focused="true">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/focused_background" />
+        </shape>
+    </item>
+</selector>
\ No newline at end of file
diff --git a/res/layout/switch_preference_with_settings.xml b/res/layout/switch_preference_with_settings.xml
new file mode 100644
index 0000000..d319561
--- /dev/null
+++ b/res/layout/switch_preference_with_settings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:gravity="center_vertical">
+
+    <ImageView
+        android:id="@+id/settings"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/ic_setting"
+        android:tint="@android:color/black"
+        android:padding="12dp"
+        android:background="?android:attr/selectableItemBackgroundBorderless" />
+
+    <View
+        android:id="@+id/divider"
+        android:layout_width="1dp"
+        android:layout_height="30dp"
+        android:layout_marginEnd="8dp"
+        android:background="?android:attr/listDivider" />
+
+    <!-- Note: seems we need focusable="false" and clickable="false" when moving to androidx -->
+    <Switch
+        android:id="@android:id/switch_widget"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@null" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/xml/flag_preferences.xml b/res/xml/flag_preferences.xml
new file mode 100644
index 0000000..aea1a6a
--- /dev/null
+++ b/res/xml/flag_preferences.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2018 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.
+ */
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="feature_flags"
+    android:persistent="false">
+
+</PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml
index 3bba73a..1df7c2f 100644
--- a/res/xml/launcher_preferences.xml
+++ b/res/xml/launcher_preferences.xml
@@ -52,4 +52,10 @@
         android:defaultValue=""
         android:persistent="false" />
 
+    <PreferenceScreen
+        android:fragment="com.android.launcher3.config.FlagTogglerPreferenceFragment"
+        android:key="flag_toggler"
+        android:persistent="false"
+        android:title="Feature flags"/>
+
 </PreferenceScreen>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 4b181d8..55d6984 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1753,12 +1753,12 @@
     @Override
     public void bindScreens(IntArray orderedScreenIds) {
         // Make sure the first screen is always at the start.
-        if (FeatureFlags.getInstance(this).isQsbOnFirstScreenEnabled() &&
+        if (FeatureFlags.QSB_ON_FIRST_SCREEN.get() &&
                 orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != 0) {
             orderedScreenIds.removeValue(Workspace.FIRST_SCREEN_ID);
             orderedScreenIds.add(0, Workspace.FIRST_SCREEN_ID);
             LauncherModel.updateWorkspaceScreenOrder(this, orderedScreenIds);
-        } else if (!FeatureFlags.getInstance(this).isQsbOnFirstScreenEnabled()
+        } else if (!FeatureFlags.QSB_ON_FIRST_SCREEN.get()
                 && orderedScreenIds.isEmpty()) {
             // If there are no screens, we need to have an empty screen
             mWorkspace.addExtraEmptyScreen();
@@ -1775,8 +1775,7 @@
         int count = orderedScreenIds.size();
         for (int i = 0; i < count; i++) {
             int screenId = orderedScreenIds.get(i);
-            if (!FeatureFlags.getInstance(this).isQsbOnFirstScreenEnabled()
-                    || screenId != Workspace.FIRST_SCREEN_ID) {
+            if (!FeatureFlags.QSB_ON_FIRST_SCREEN.get() || screenId != Workspace.FIRST_SCREEN_ID) {
                 // No need to bind the first screen, as its always bound.
                 mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(screenId);
             }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index d571cf4..cbfde25 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -451,6 +451,11 @@
      * @return true if the page could be bound synchronously.
      */
     public boolean startLoader(int synchronousBindPage) {
+        if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS
+                && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) {
+            android.util.Log.d("b/117332845",
+                    android.util.Log.getStackTraceString(new Throwable()));
+        }
         // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
         InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
         synchronized (mLock) {
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index a423a90..7d62ada 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -70,9 +70,6 @@
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
 
 public class LauncherProvider extends ContentProvider {
     private static final String TAG = "LauncherProvider";
@@ -793,7 +790,7 @@
                     convertShortcutsToLauncherActivities(db);
                 case 26:
                     // QSB was moved to the grid. Clear the first row on screen 0.
-                    if (FeatureFlags.getInstance(mContext).isQsbOnFirstScreenEnabled() &&
+                    if (FeatureFlags.QSB_ON_FIRST_SCREEN.get() &&
                             !LauncherDbUtils.prepareScreenZeroToHostQsb(mContext, db)) {
                         break;
                     }
diff --git a/src/com/android/launcher3/MainProcessInitializer.java b/src/com/android/launcher3/MainProcessInitializer.java
index 0028f97..a18dfde 100644
--- a/src/com/android/launcher3/MainProcessInitializer.java
+++ b/src/com/android/launcher3/MainProcessInitializer.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.IconShapeOverride;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.ResourceBasedOverride;
@@ -35,6 +36,7 @@
 
     protected void init(Context context) {
         FileLog.setDir(context.getApplicationContext().getFilesDir());
+        FeatureFlags.initialize(context);
         IconShapeOverride.apply(context);
         SessionCommitReceiver.applyDefaultUserPrefs(context);
     }
diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java
index 1f80226..60edcda 100644
--- a/src/com/android/launcher3/SettingsActivity.java
+++ b/src/com/android/launcher3/SettingsActivity.java
@@ -43,6 +43,7 @@
 import android.widget.Adapter;
 import android.widget.ListView;
 
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.IconShapeOverride;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.util.ListViewHighlighter;
@@ -57,6 +58,8 @@
 public class SettingsActivity extends Activity
         implements PreferenceFragment.OnPreferenceStartFragmentCallback {
 
+    private static final String FLAGS_PREFERENCE_KEY = "flag_toggler";
+
     private static final String ICON_BADGING_PREFERENCE_KEY = "pref_icon_badging";
     /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
     public static final String NOTIFICATION_BADGING = "notification_badging";
@@ -126,6 +129,12 @@
             getPreferenceManager().setSharedPreferencesName(LauncherFiles.SHARED_PREFERENCES_KEY);
             addPreferencesFromResource(R.xml.launcher_preferences);
 
+            // Only show flag toggler UI if this build variant implements that.
+            Preference flagToggler = findPreference(FLAGS_PREFERENCE_KEY);
+            if (flagToggler != null && !FeatureFlags.showFlagTogglerUi()) {
+                getPreferenceScreen().removePreference(flagToggler);
+            }
+
             ContentResolver resolver = getActivity().getContentResolver();
 
             ButtonPreference iconBadgingPref =
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 5e1c54c..11e601c 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -480,7 +480,7 @@
      * @param qsb an existing qsb to recycle or null.
      */
     public void bindAndInitFirstWorkspaceScreen(View qsb) {
-        if (!FeatureFlags.getInstance(getContext()).isQsbOnFirstScreenEnabled()) {
+        if (!FeatureFlags.QSB_ON_FIRST_SCREEN.get()) {
             return;
         }
         // Add the first page
@@ -779,9 +779,7 @@
             int id = mWorkspaceScreens.keyAt(i);
             CellLayout cl = mWorkspaceScreens.valueAt(i);
             // FIRST_SCREEN_ID can never be removed.
-            boolean qsbFirstScreenEnabled = 
-                    FeatureFlags.getInstance(getContext()).isQsbOnFirstScreenEnabled();
-            if ((!qsbFirstScreenEnabled || id > FIRST_SCREEN_ID)
+            if ((!FeatureFlags.QSB_ON_FIRST_SCREEN.get() || id > FIRST_SCREEN_ID)
                     && cl.getShortcutsAndWidgets().getChildCount() == 0) {
                 removeScreens.add(id);
             }
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 7332e36..dc60c8f 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -16,6 +16,21 @@
 
 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 com.android.launcher3.Utilities;
+
+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.
  *
@@ -23,11 +38,21 @@
  *
  * <p>This class is kept package-private to prevent direct access.
  */
+@Keep
 abstract class BaseFlags {
 
-    private static final String TAG = "FeatureFlags";
+    private static final Object sLock = new Object();
+    @GuardedBy("sLock")
+    private static final List<TogglableFlag> sFlags = new ArrayList<>();
+
+    static final String FLAGS_PREF_NAME = "featureFlags";
 
     BaseFlags() {
+        throw new UnsupportedOperationException("Don't instantiate BaseFlags");
+    }
+
+    public static boolean showFlagTogglerUi() {
+        return Utilities.IS_DEBUG_DEVICE;
     }
 
     public static final boolean IS_DOGFOOD_BUILD = false;
@@ -36,17 +61,16 @@
     // When enabled the promise icon is visible in all apps while installation an app.
     public static final boolean LAUNCHER3_PROMISE_APPS_IN_ALL_APPS = false;
 
-    /** Feature flag to enable moving the QSB on the 0th screen of the workspace. */
-    public boolean isQsbOnFirstScreenEnabled() {
-        return true;
-    }
+    public static final TogglableFlag QSB_ON_FIRST_SCREEN = new TogglableFlag("QSB_ON_FIRST_SCREEN",
+            true,
+            "Enable moving the QSB on the 0th screen of the workspace");
+
+    public static final TogglableFlag EXAMPLE_FLAG = new TogglableFlag("EXAMPLE_FLAG", true,
+            "An example flag that doesn't do anything. Useful for testing");
 
     //Feature flag to enable pulling down navigation shade from workspace.
     public static final boolean PULL_DOWN_STATUS_BAR = true;
 
-    // When enabled the all-apps icon is not added to the hotseat.
-    public static final boolean NO_ALL_APPS_ICON = true;
-
     // When true, custom widgets are loaded using CustomWidgetParser.
     public static final boolean ENABLE_CUSTOM_WIDGETS = false;
 
@@ -59,4 +83,110 @@
     // When true, overview shows screenshots in the orientation they were taken rather than
     // trying to make them fit the orientation the device is in.
     public static final boolean OVERVIEW_USE_SCREENSHOT_ORIENTATION = true;
+
+    public static void initialize(Context context) {
+        // Avoid the disk read for builds without the flags UI.
+        if (showFlagTogglerUi()) {
+            SharedPreferences sharedPreferences =
+                    context.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE);
+            synchronized (sLock) {
+                for (TogglableFlag flag : sFlags) {
+                    flag.currentValue = sharedPreferences.getBoolean(flag.key, flag.defaultValue);
+                }
+            }
+        } else {
+            synchronized (sLock) {
+                for (TogglableFlag flag : sFlags) {
+                    flag.currentValue = flag.defaultValue;
+                }
+            }
+        }
+    }
+
+    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 BaseFlags 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.key, flag);
+            }
+        }
+        return new ArrayList<>(flagsByKey.values());
+    }
+
+    public static final class TogglableFlag {
+        private final String key;
+        private final boolean defaultValue;
+        private final String description;
+        private boolean currentValue;
+
+        TogglableFlag(
+                String key,
+                boolean defaultValue,
+                String description) {
+            this.key = checkNotNull(key);
+            this.defaultValue = defaultValue;
+            this.description = checkNotNull(description);
+            synchronized (sLock) {
+                sFlags.add(this);
+            }
+        }
+
+        String getKey() {
+            return key;
+        }
+
+        boolean getDefaultValue() {
+            return defaultValue;
+        }
+
+        /** Returns the value of the flag at process start, including any overrides present. */
+        public boolean get() {
+            return currentValue;
+        }
+
+        String getDescription() {
+            return description;
+        }
+
+        @Override
+        public String toString() {
+            return "TogglableFlag{"
+                    + "key=" + key + ", "
+                    + "defaultValue=" + defaultValue + ", "
+                    + "description=" + description
+                    + "}";
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (o instanceof TogglableFlag) {
+                TogglableFlag that = (TogglableFlag) o;
+                return (this.key.equals(that.getKey()))
+                        && (this.defaultValue == that.getDefaultValue())
+                        && (this.description.equals(that.getDescription()));
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            int h$ = 1;
+            h$ *= 1000003;
+            h$ ^= key.hashCode();
+            h$ *= 1000003;
+            h$ ^= defaultValue ? 1231 : 1237;
+            h$ *= 1000003;
+            h$ ^= description.hashCode();
+            return h$;
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/config/FlagTogglerPreferenceFragment.java b/src/com/android/launcher3/config/FlagTogglerPreferenceFragment.java
new file mode 100644
index 0000000..0a1fd2f
--- /dev/null
+++ b/src/com/android/launcher3/config/FlagTogglerPreferenceFragment.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 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.config;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Process;
+import android.preference.PreferenceDataStore;
+import android.preference.PreferenceFragment;
+import android.preference.SwitchPreference;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.android.launcher3.R;
+import com.android.launcher3.config.BaseFlags.TogglableFlag;
+
+/**
+ * Dev-build only UI allowing developers to toggle flag settings. See {@link FeatureFlags}.
+ */
+public final class FlagTogglerPreferenceFragment extends PreferenceFragment {
+    private static final String TAG = "FlagTogglerPrefFrag";
+
+    private SharedPreferences mSharedPreferences;
+    private MenuItem saveButton;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        addPreferencesFromResource(R.xml.flag_preferences);
+        mSharedPreferences = getContext().getSharedPreferences(
+                FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE);
+
+        // For flag overrides we only want to store when the engineer chose to override the
+        // 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.
+        getPreferenceManager().setPreferenceDataStore(new PreferenceDataStore() {
+            @Override
+            public void putBoolean(String key, boolean value) {
+                for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
+                    if (flag.getKey().equals(key)) {
+                        if (value == flag.getDefaultValue()) {
+                            mSharedPreferences.edit().remove(key).apply();
+                        } else {
+                            mSharedPreferences.edit().putBoolean(key, value).apply();
+                        }
+                    }
+                }
+            }
+        });
+
+        for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
+            SwitchPreference switchPreference = new SwitchPreference(getContext());
+            switchPreference.setKey(flag.getKey());
+            switchPreference.setDefaultValue(flag.getDefaultValue());
+            switchPreference.setChecked(getFlagStateFromSharedPrefs(flag));
+            switchPreference.setTitle(flag.getKey());
+            switchPreference.setSummaryOn(flag.getDefaultValue() ? "" : "overridden");
+            switchPreference.setSummaryOff(flag.getDefaultValue() ? "overridden" : "");
+            getPreferenceScreen().addPreference(switchPreference);
+        }
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        saveButton = menu.add("Apply");
+        saveButton.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item == saveButton) {
+            mSharedPreferences.edit().commit();
+            Log.e(TAG,
+                    "Killing launcher process " + Process.myPid() + " to apply new flag values");
+            System.exit(0);
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public void onStop() {
+        boolean anyChanged = false;
+        for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
+            anyChanged = anyChanged ||
+                    getFlagStateFromSharedPrefs(flag) != flag.get();
+        }
+
+        if (anyChanged) {
+            Toast.makeText(
+                    getContext(),
+                    "Flag won't be applied until you restart launcher",
+                    Toast.LENGTH_LONG).show();
+        }
+        super.onStop();
+    }
+
+    private boolean getFlagStateFromSharedPrefs(TogglableFlag flag) {
+        return mSharedPreferences.getBoolean(flag.getKey(), flag.getDefaultValue());
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index 0115fd9..2c1aa74 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -12,6 +12,7 @@
 import android.graphics.Point;
 import android.net.Uri;
 import android.util.Log;
+
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
@@ -109,6 +110,7 @@
 
     /**
      * Applied all the pending DB operations
+     *
      * @return true if any DB operation was commited.
      */
     private boolean applyOperations() throws Exception {
@@ -135,6 +137,7 @@
      * entries is more than what can fit in the new hotseat, we drop the entries with least weight.
      * For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION}
      * & {@see #WT_FOLDER_FACTOR}.
+     *
      * @return true if any DB change was made
      */
     protected boolean migrateHotseat() throws Exception {
@@ -235,7 +238,8 @@
                 int screenId = allScreens.get(i);
                 v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
                 v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
-                mUpdateOperations.add(ContentProviderOperation.newInsert(uri).withValues(v).build());
+                mUpdateOperations.add(ContentProviderOperation.newInsert(uri).withValues(
+                        v).build());
             }
         }
         return applyOperations();
@@ -254,8 +258,7 @@
     protected void migrateScreen(int screenId) {
         // If we are migrating the first screen, do not touch the first row.
         int startY =
-                (FeatureFlags.getInstance(mContext).isQsbOnFirstScreenEnabled()
-                        && screenId == Workspace.FIRST_SCREEN_ID)
+                (FeatureFlags.QSB_ON_FIRST_SCREEN.get() && screenId == Workspace.FIRST_SCREEN_ID)
                 ? 1 : 0;
 
         ArrayList<DbEntry> items = loadWorkspaceEntries(screenId);
@@ -280,9 +283,11 @@
             for (int y = mSrcY - 1; y >= startY; y--) {
                 // Use a deep copy when trying out a particular combination as it can change
                 // the underlying object.
-                ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items), outLoss);
+                ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items),
+                        outLoss);
 
-                if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1] < moveWt))) {
+                if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1]
+                        < moveWt))) {
                     removeWt = outLoss[0];
                     moveWt = outLoss[1];
                     removedCol = mShouldRemoveX ? x : removedCol;
@@ -363,6 +368,7 @@
 
     /**
      * Tries the remove the provided row and column.
+     *
      * @param items all the items on the screen under operation
      * @param outLoss array of size 2. The first entry is filled with weight loss, and the second
      * with the overall item movement.
@@ -438,6 +444,7 @@
 
         /**
          * Recursively finds a placement for the provided items.
+         *
          * @param index the position in {@link #itemsToPlace} to start looking at.
          * @param weightLoss total weight loss upto this point
          * @param moveCost total move cost upto this point
@@ -550,7 +557,8 @@
                     for (int x = 0; x < mTrgX; x++) {
                         if (!occupied.cells[x][y]) {
                             int dist = ignoreMove ? 0 :
-                                ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY - y));
+                                    ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY
+                                            - y));
                             if (dist < newDistance) {
                                 newX = x;
                                 newY = y;
@@ -815,7 +823,8 @@
 
         public float weight;
 
-        public DbEntry() { }
+        public DbEntry() {
+        }
 
         public DbEntry copy() {
             DbEntry entry = new DbEntry();
@@ -887,6 +896,7 @@
 
     /**
      * Migrates the workspace and hotseat in case their sizes changed.
+     *
      * @return false if the migration failed.
      */
     public static boolean migrateGridIfNeeded(Context context) {
@@ -896,7 +906,8 @@
         String gridSizeString = getPointString(idp.numColumns, idp.numRows);
 
         if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) &&
-                idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)) {
+                idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
+                        idp.numHotseatIcons)) {
             // Skip if workspace and hotseat sizes have not changed.
             return true;
         }
@@ -907,7 +918,8 @@
 
             HashSet<String> validPackages = getValidPackages(context);
             // Hotseat
-            int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons);
+            int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
+                    idp.numHotseatIcons);
             if (srcHotseatCount != idp.numHotseatIcons) {
                 // Migrate hotseat.
 
@@ -920,7 +932,8 @@
             Point sourceSize = parsePoint(prefs.getString(
                     KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
 
-            if (new MultiStepMigrationTask(validPackages, context).migrate(sourceSize, targetSize)) {
+            if (new MultiStepMigrationTask(validPackages, context).migrate(sourceSize,
+                    targetSize)) {
                 dbChanged = true;
             }
 
@@ -970,9 +983,11 @@
 
     /**
      * Removes any broken item from the hotseat.
+     *
      * @return a map with occupied hotseat position set to non-null value.
      */
-    public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context) throws Exception {
+    public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context)
+            throws Exception {
         GridSizeMigrationTask task = new GridSizeMigrationTask(
                 context, LauncherAppState.getIDP(context), getValidPackages(context),
                 Integer.MAX_VALUE, Integer.MAX_VALUE);
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index bb3a760..94cf5c2 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -441,7 +441,7 @@
                 // Mark the first row as occupied (if the feature is enabled)
                 // in order to account for the QSB.
                 screen.markCells(0, 0, countX + 1, 1,
-                    FeatureFlags.getInstance(mContext).isQsbOnFirstScreenEnabled());
+                    FeatureFlags.QSB_ON_FIRST_SCREEN.get());
             }
             occupied.put(item.screenId, screen);
         }
diff --git a/src/com/android/launcher3/provider/ImportDataTask.java b/src/com/android/launcher3/provider/ImportDataTask.java
index 4edd30f..e1b2698 100644
--- a/src/com/android/launcher3/provider/ImportDataTask.java
+++ b/src/com/android/launcher3/provider/ImportDataTask.java
@@ -136,7 +136,7 @@
                 .getSerialNumberForUser(Process.myUserHandle()));
 
         boolean createEmptyRowOnFirstScreen;
-        if (FeatureFlags.getInstance(mContext).isQsbOnFirstScreenEnabled()) {
+        if (FeatureFlags.QSB_ON_FIRST_SCREEN.get()) {
             try (Cursor c = mContext.getContentResolver().query(mOtherFavoritesUri, null,
                     // get items on the first row of the first screen
                     "profileId = ? AND container = -100 AND screen = ? AND cellY = 0",
diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java
index 82ab15c..ac1fafb 100644
--- a/src/com/android/launcher3/qsb/QsbContainerView.java
+++ b/src/com/android/launcher3/qsb/QsbContainerView.java
@@ -213,7 +213,7 @@
         }
 
         public boolean isQsbEnabled() {
-            return FeatureFlags.getInstance(getContext()).isQsbOnFirstScreenEnabled();
+            return FeatureFlags.QSB_ON_FIRST_SCREEN.get();
         }
 
         protected Bundle createBindOptions() {
diff --git a/src/com/android/launcher3/qsb/QsbWidgetHostView.java b/src/com/android/launcher3/qsb/QsbWidgetHostView.java
index cff5126..f5ecda3 100644
--- a/src/com/android/launcher3/qsb/QsbWidgetHostView.java
+++ b/src/com/android/launcher3/qsb/QsbWidgetHostView.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.qsb;
 
-import android.appwidget.AppWidgetHostView;
 import android.content.Context;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -26,17 +25,20 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.widget.NavigableAppWidgetHostView;
 
 /**
  * Appwidget host view with QSB specific logic.
  */
-public class QsbWidgetHostView extends AppWidgetHostView {
+public class QsbWidgetHostView extends NavigableAppWidgetHostView {
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private int mPreviousOrientation;
 
     public QsbWidgetHostView(Context context) {
         super(context);
+        setFocusable(true);
+        setBackgroundResource(R.drawable.qsb_host_view_focus_bg);
     }
 
     @Override
@@ -89,4 +91,9 @@
                 Launcher.getLauncher(v2.getContext()).startSearch("", false, null, true));
         return v;
     }
+
+    @Override
+    protected boolean shouldAllowDirectClick() {
+        return true;
+    }
 }
diff --git a/src/com/android/launcher3/util/ListViewHighlighter.java b/src/com/android/launcher3/util/ListViewHighlighter.java
index 360546e..c9fe228 100644
--- a/src/com/android/launcher3/util/ListViewHighlighter.java
+++ b/src/com/android/launcher3/util/ListViewHighlighter.java
@@ -75,7 +75,7 @@
     @Override
     public void onScroll(AbsListView view, int firstVisibleItem,
             int visibleItemCount, int totalItemCount) {
-        highlightIfVisible(firstVisibleItem, firstVisibleItem + visibleItemCount);
+        highlightIfVisible(firstVisibleItem, firstVisibleItem + visibleItemCount - 1);
     }
 
     private boolean highlightIfVisible(int start, int end) {
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 12859c7..95f8daa 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -16,16 +16,13 @@
 
 package com.android.launcher3.widget;
 
-import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.PointF;
-import android.graphics.Rect;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.SparseBooleanArray;
-import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -49,12 +46,10 @@
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
 
-import java.util.ArrayList;
-
 /**
  * {@inheritDoc}
  */
-public class LauncherAppWidgetHostView extends AppWidgetHostView
+public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
         implements TouchCompleteListener, View.OnLongClickListener {
 
     // Related to the auto-advancing of widgets
@@ -75,9 +70,6 @@
 
     private float mSlop;
 
-    @ViewDebug.ExportedProperty(category = "launcher")
-    private boolean mChildrenFocused;
-
     private boolean mIsScrollable;
     private boolean mIsAttachedToWindow;
     private boolean mIsAutoAdvanceRegistered;
@@ -267,98 +259,6 @@
         }
     }
 
-    @Override
-    public int getDescendantFocusability() {
-        return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS
-                : ViewGroup.FOCUS_BLOCK_DESCENDANTS;
-    }
-
-    @Override
-    public boolean dispatchKeyEvent(KeyEvent event) {
-        if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE
-                && event.getAction() == KeyEvent.ACTION_UP) {
-            mChildrenFocused = false;
-            requestFocus();
-            return true;
-        }
-        return super.dispatchKeyEvent(event);
-    }
-
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
-            event.startTracking();
-            return true;
-        }
-        return super.onKeyDown(keyCode, event);
-    }
-
-    @Override
-    public boolean onKeyUp(int keyCode, KeyEvent event) {
-        if (event.isTracking()) {
-            if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
-                mChildrenFocused = true;
-                ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD);
-                focusableChildren.remove(this);
-                int childrenCount = focusableChildren.size();
-                switch (childrenCount) {
-                    case 0:
-                        mChildrenFocused = false;
-                        break;
-                    case 1: {
-                        if (getTag() instanceof ItemInfo) {
-                            ItemInfo item = (ItemInfo) getTag();
-                            if (item.spanX == 1 && item.spanY == 1) {
-                                focusableChildren.get(0).performClick();
-                                mChildrenFocused = false;
-                                return true;
-                            }
-                        }
-                        // continue;
-                    }
-                    default:
-                        focusableChildren.get(0).requestFocus();
-                        return true;
-                }
-            }
-        }
-        return super.onKeyUp(keyCode, event);
-    }
-
-    @Override
-    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
-        if (gainFocus) {
-            mChildrenFocused = false;
-            dispatchChildFocus(false);
-        }
-        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-    }
-
-    @Override
-    public void requestChildFocus(View child, View focused) {
-        super.requestChildFocus(child, focused);
-        dispatchChildFocus(mChildrenFocused && focused != null);
-        if (focused != null) {
-            focused.setFocusableInTouchMode(false);
-        }
-    }
-
-    @Override
-    public void clearChildFocus(View child) {
-        super.clearChildFocus(child);
-        dispatchChildFocus(false);
-    }
-
-    @Override
-    public boolean dispatchUnhandledMove(View focused, int direction) {
-        return mChildrenFocused;
-    }
-
-    private void dispatchChildFocus(boolean childIsFocused) {
-        // The host view's background changes when selected, to indicate the focus is inside.
-        setSelected(childIsFocused);
-    }
-
     public void switchToErrorView() {
         // Update the widget with 0 Layout id, to reset the view to error view.
         updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
@@ -502,4 +402,13 @@
         mLauncher.removeItem(this, info, false  /* deleteFromDb */);
         mLauncher.bindAppWidget(info);
     }
+
+    @Override
+    protected boolean shouldAllowDirectClick() {
+        if (getTag() instanceof ItemInfo) {
+            ItemInfo item = (ItemInfo) getTag();
+            return item.spanX == 1 && item.spanY == 1;
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
new file mode 100644
index 0000000..68b1595
--- /dev/null
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2018 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.widget;
+
+import android.appwidget.AppWidgetHostView;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * Extension of AppWidgetHostView with support for controlled keyboard navigation.
+ */
+public abstract class NavigableAppWidgetHostView extends AppWidgetHostView {
+
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private boolean mChildrenFocused;
+
+    public NavigableAppWidgetHostView(Context context) {
+        super(context);
+    }
+
+    @Override
+    public int getDescendantFocusability() {
+        return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS
+                : ViewGroup.FOCUS_BLOCK_DESCENDANTS;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE
+                && event.getAction() == KeyEvent.ACTION_UP) {
+            mChildrenFocused = false;
+            requestFocus();
+            return true;
+        }
+        return super.dispatchKeyEvent(event);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
+            event.startTracking();
+            return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (event.isTracking()) {
+            if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
+                mChildrenFocused = true;
+                ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD);
+                focusableChildren.remove(this);
+                int childrenCount = focusableChildren.size();
+                switch (childrenCount) {
+                    case 0:
+                        mChildrenFocused = false;
+                        break;
+                    case 1: {
+                        if (shouldAllowDirectClick()) {
+                            focusableChildren.get(0).performClick();
+                            mChildrenFocused = false;
+                            return true;
+                        }
+                        // continue;
+                    }
+                    default:
+                        focusableChildren.get(0).requestFocus();
+                        return true;
+                }
+            }
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    /**
+     * For a widget with only a single interactive element, return true if whole widget should act
+     * as a single interactive element, and clicking 'enter' should activate the child element
+     * directly. Otherwise clicking 'enter' will only move the focus inside the widget.
+     */
+    protected abstract boolean shouldAllowDirectClick();
+
+    @Override
+    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+        if (gainFocus) {
+            mChildrenFocused = false;
+            dispatchChildFocus(false);
+        }
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        super.requestChildFocus(child, focused);
+        dispatchChildFocus(mChildrenFocused && focused != null);
+        if (focused != null) {
+            focused.setFocusableInTouchMode(false);
+        }
+    }
+
+    @Override
+    public void clearChildFocus(View child) {
+        super.clearChildFocus(child);
+        dispatchChildFocus(false);
+    }
+
+    @Override
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        return mChildrenFocused;
+    }
+
+    private void dispatchChildFocus(boolean childIsFocused) {
+        // The host view's background changes when selected, to indicate the focus is inside.
+        setSelected(childIsFocused);
+    }
+}
diff --git a/src_flags/com/android/launcher3/config/FeatureFlags.java b/src_flags/com/android/launcher3/config/FeatureFlags.java
index f02f227..73c6996 100644
--- a/src_flags/com/android/launcher3/config/FeatureFlags.java
+++ b/src_flags/com/android/launcher3/config/FeatureFlags.java
@@ -22,11 +22,7 @@
  * Defines a set of flags used to control various launcher behaviors
  */
 public final class FeatureFlags extends BaseFlags {
-    private static FeatureFlags instance = new FeatureFlags();
-
-    public static FeatureFlags getInstance(Context context) {
-        return instance;
+    private FeatureFlags() {
+        // Prevent instantiation
     }
-
-    private FeatureFlags() {}
 }
diff --git a/src_plugins/README.md b/src_plugins/README.md
new file mode 100644
index 0000000..c7ce1da
--- /dev/null
+++ b/src_plugins/README.md
@@ -0,0 +1,3 @@
+This directory contains plugin interfaces that launcher listens for and plugins implement. In other words, these are the hooks that specify what plugins launcher currently supports. 
+
+Details about how to create a new plugin interface, or to use existing interfaces to write a plugin can be found at go/gnl/plugins.
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java
new file mode 100644
index 0000000..fcb2abe
--- /dev/null
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 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.plugins;
+
+import android.content.Context;
+
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginListener;
+
+public class PluginManagerWrapper {
+
+    public static final MainThreadInitializedObject<PluginManagerWrapper> INSTANCE =
+            new MainThreadInitializedObject<>(PluginManagerWrapper::new);
+
+    private PluginManagerWrapper(Context c) {
+    }
+
+    public void addPluginListener(PluginListener<? extends Plugin> listener, Class<?> pluginClass) {
+    }
+
+    public void removePluginListener(PluginListener<? extends Plugin> listener) {
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 773ec9d..c878699 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -15,29 +15,21 @@
  */
 package com.android.launcher3.ui;
 
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
-
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static org.junit.Assert.fail;
+
 import android.app.Instrumentation;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.LauncherActivityInfo;
-import android.graphics.Point;
 import android.os.Process;
 import android.os.RemoteException;
-import android.os.SystemClock;
-import android.util.Log;
-import android.view.MotionEvent;
 import android.view.Surface;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiDevice;
@@ -46,18 +38,14 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.MainThreadExecutor;
-import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.TestHelpers;
-import com.android.launcher3.testcomponent.AppWidgetNoConfig;
-import com.android.launcher3.testcomponent.AppWidgetWithConfig;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.LauncherActivityRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
@@ -88,7 +76,6 @@
 
     public static final long SHORT_UI_TIMEOUT= 300;
     public static final long DEFAULT_UI_TIMEOUT = 10000;
-    public static final long DEFAULT_WORKER_TIMEOUT_SECS = 5;
 
     protected MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
     protected final UiDevice mDevice;
@@ -96,8 +83,6 @@
     protected Context mTargetContext;
     protected String mTargetPackage;
 
-    private static final String TAG = "AbstractLauncherUiTest";
-
     protected AbstractLauncherUiTest() {
         final Instrumentation instrumentation = getInstrumentation();
         mDevice = UiDevice.getInstance(instrumentation);
@@ -113,7 +98,8 @@
     @Rule
     public LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
 
-    @Rule public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher();
+    @Rule public ShellCommandRule mDefaultLauncherRule =
+            TestHelpers.isInLauncherProcess() ? ShellCommandRule.setDefaultLauncher() : null;
 
     @Rule public ShellCommandRule mDisableHeadsUpNotification =
             ShellCommandRule.disableHeadsUpNotification();
@@ -133,7 +119,8 @@
                     try {
                         // Create launcher activity if necessary and bring it to the front.
                         mDevice.pressHome();
-                        waitForLauncherCondition(launcher -> launcher != null);
+                        waitForLauncherCondition("Launcher activity wasn't created",
+                                launcher -> launcher != null);
 
                         executeOnLauncher(launcher ->
                                 launcher.getRotationHelper().forceAllowRotationForTesting(true));
@@ -170,7 +157,7 @@
     @After
     public void tearDown() throws Exception {
         // Limits UI tests affecting tests running after them.
-        resetLoaderState();
+        waitForModelLoaded();
     }
 
     protected void lockRotation(boolean naturalOrientation) throws RemoteException {
@@ -182,30 +169,6 @@
     }
 
     /**
-     * Opens all apps and returns the recycler view
-     */
-    protected UiObject2 openAllApps() {
-        mDevice.waitForIdle();
-        UiObject2 hotseat = mDevice.wait(
-                Until.findObject(getSelectorForId(R.id.hotseat)), 2500);
-        Point start = hotseat.getVisibleCenter();
-        int endY = (int) (mDevice.getDisplayHeight() * 0.1f);
-        // 100 px/step
-        mDevice.swipe(start.x, start.y, start.x, endY, (start.y - endY) / 100);
-        return findViewById(R.id.apps_list_view);
-    }
-
-    /**
-     * Opens widget tray and returns the recycler view.
-     */
-    protected UiObject2 openWidgetsTray() {
-        mDevice.pressMenu(); // Enter overview mode.
-        mDevice.wait(Until.findObject(
-                By.text(mTargetContext.getString(R.string.widget_button_text))), DEFAULT_UI_TIMEOUT).click();
-        return findViewById(R.id.widgets_list_view);
-    }
-
-    /**
      * Scrolls the {@param container} until it finds an object matching {@param condition}.
      * @return the matching object.
      */
@@ -227,73 +190,6 @@
     }
 
     /**
-     * Drags an icon to the center of homescreen.
-     * @param icon  object that is either app icon or shortcut icon
-     */
-    protected void dragToWorkspace(UiObject2 icon, boolean expectedToShowShortcuts) {
-        Point center = icon.getVisibleCenter();
-
-        // Action Down
-        sendPointer(MotionEvent.ACTION_DOWN, center);
-
-        UiObject2 dragLayer = findViewById(R.id.drag_layer);
-
-        if (expectedToShowShortcuts) {
-            // Make sure shortcuts show up, and then move a bit to hide them.
-            assertNotNull(findViewById(R.id.deep_shortcuts_container));
-
-            Point moveLocation = new Point(center);
-            int distanceToMove = mTargetContext.getResources().getDimensionPixelSize(
-                    R.dimen.deep_shortcuts_start_drag_threshold) + 50;
-            if (moveLocation.y - distanceToMove >= dragLayer.getVisibleBounds().top) {
-                moveLocation.y -= distanceToMove;
-            } else {
-                moveLocation.y += distanceToMove;
-            }
-            movePointer(center, moveLocation);
-
-            assertNull(findViewById(R.id.deep_shortcuts_container));
-        }
-
-        // Wait until Remove/Delete target is visible
-        assertNotNull(findViewById(R.id.delete_target_text));
-
-        Point moveLocation = dragLayer.getVisibleCenter();
-
-        // Move to center
-        movePointer(center, moveLocation);
-        sendPointer(MotionEvent.ACTION_UP, center);
-
-        // Wait until remove target is gone.
-        mDevice.wait(Until.gone(getSelectorForId(R.id.delete_target_text)), DEFAULT_UI_TIMEOUT);
-    }
-
-    private void movePointer(Point from, Point to) {
-        while(!from.equals(to)) {
-            from.x = getNextMoveValue(to.x, from.x);
-            from.y = getNextMoveValue(to.y, from.y);
-            sendPointer(MotionEvent.ACTION_MOVE, from);
-        }
-    }
-
-    private int getNextMoveValue(int targetValue, int oldValue) {
-        if (targetValue - oldValue > 10) {
-            return oldValue + 10;
-        } else if (targetValue - oldValue < -10) {
-            return oldValue - 10;
-        } else {
-            return targetValue;
-        }
-    }
-
-    protected void sendPointer(int action, Point point) {
-        MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
-                SystemClock.uptimeMillis(), action, point.x, point.y, 0);
-        getInstrumentation().sendPointerSync(event);
-        event.recycle();
-    }
-
-    /**
      * Removes all icons from homescreen and hotseat.
      */
     public void clearHomescreen() throws Throwable {
@@ -320,8 +216,7 @@
         } catch (Throwable t) {
             throw new IllegalArgumentException(t);
         }
-        waitForLauncherCondition(launcher ->
-                LauncherAppState.getInstance(mTargetContext).getModel().isModelLoaded());
+        waitForModelLoaded();
         if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS
                 && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) {
             android.util.Log.d("b/117332845",
@@ -329,6 +224,13 @@
         }
     }
 
+    protected void waitForModelLoaded() {
+        waitForLauncherCondition("Launcher model didn't load", launcher -> {
+            final LauncherModel model = LauncherAppState.getInstance(mTargetContext).getModel();
+            return model.getCallback() == null || model.isModelLoaded();
+        });
+    }
+
     /**
      * Runs the callback on the UI thread and returns the result.
      */
@@ -354,53 +256,23 @@
 
     // Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call expecting
     // the results of that gesture because the wait can hide flakeness.
-    protected boolean waitForState(LauncherState state) {
-        return waitForLauncherCondition(launcher -> launcher.getStateManager().getState() == state);
+    protected void waitForState(String message, LauncherState state) {
+        waitForLauncherCondition(message,
+                launcher -> launcher.getStateManager().getState() == state);
     }
 
     // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
     // flakiness.
-    protected boolean waitForLauncherCondition(Function<Launcher, Boolean> condition) {
-        return waitForLauncherCondition(condition, DEFAULT_ACTIVITY_TIMEOUT);
+    protected void waitForLauncherCondition(String message, Function<Launcher, Boolean> condition) {
+        waitForLauncherCondition(message, condition, DEFAULT_ACTIVITY_TIMEOUT);
     }
 
     // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
     // flakiness.
-    protected boolean waitForLauncherCondition(
-            Function<Launcher, Boolean> condition, long timeout) {
-        if (!TestHelpers.isInLauncherProcess()) return true;
-        return Wait.atMost(() -> getFromLauncher(condition), timeout);
-    }
-
-    /**
-     * Finds a widget provider which can fit on the home screen.
-     * @param hasConfigureScreen if true, a provider with a config screen is returned.
-     */
-    protected LauncherAppWidgetProviderInfo findWidgetProvider(final boolean hasConfigureScreen) {
-        LauncherAppWidgetProviderInfo info =
-                getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
-            @Override
-            public LauncherAppWidgetProviderInfo call() throws Exception {
-                ComponentName cn = new ComponentName(getInstrumentation().getContext(),
-                        hasConfigureScreen ? AppWidgetWithConfig.class : AppWidgetNoConfig.class);
-                Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString());
-                return AppWidgetManagerCompat.getInstance(mTargetContext)
-                        .findProvider(cn, Process.myUserHandle());
-            }
-        });
-        if (info == null) {
-            throw new IllegalArgumentException("No valid widget provider");
-        }
-        return info;
-    }
-
-    protected UiObject2 findViewById(int id) {
-        return mDevice.wait(Until.findObject(getSelectorForId(id)), DEFAULT_UI_TIMEOUT);
-    }
-
-    protected BySelector getSelectorForId(int id) {
-        String name = mTargetContext.getResources().getResourceEntryName(id);
-        return By.res(mTargetPackage, name);
+    protected void waitForLauncherCondition(
+            String message, Function<Launcher, Boolean> condition, long timeout) {
+        if (!TestHelpers.isInLauncherProcess()) return;
+        Wait.atMost(message, () -> getFromLauncher(condition), timeout);
     }
 
     protected LauncherActivityInfo getSettingsApp() {
diff --git a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
index 600d390..9160076 100644
--- a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
+++ b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
@@ -45,12 +45,12 @@
         mDevice.waitForIdle();
 
         // Open all apps and wait for load complete.
-        final UiObject2 appsContainer = openAllApps();
-        assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT));
+        final UiObject2 appsContainer = TestViewHelpers.openAllApps();
+        Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT);
 
         // Drag icon to homescreen.
         UiObject2 icon = scrollAndFind(appsContainer, By.text(settingsApp.getLabel().toString()));
-        dragToWorkspace(icon, true);
+        TestViewHelpers.dragToWorkspace(icon, true);
 
         // Verify that the icon works on homescreen.
         mDevice.findObject(By.text(settingsApp.getLabel().toString())).click();
diff --git a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
index 6a007ae..d7a7f6b 100644
--- a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
+++ b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
@@ -46,25 +46,25 @@
         LauncherActivityInfo testApp = getSettingsApp();
 
         // Open all apps and wait for load complete
-        final UiObject2 appsContainer = openAllApps();
-        assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2),
-                DEFAULT_UI_TIMEOUT));
+        final UiObject2 appsContainer = TestViewHelpers.openAllApps();
+        Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT);
 
         // Find settings app and verify shortcuts appear when long pressed
         UiObject2 icon = scrollAndFind(appsContainer, By.text(testApp.getLabel().toString()));
         // Press icon center until shortcuts appear
         Point iconCenter = icon.getVisibleCenter();
-        sendPointer(MotionEvent.ACTION_DOWN, iconCenter);
-        UiObject2 deepShortcutsContainer = findViewById(R.id.deep_shortcuts_container);
+        TestViewHelpers.sendPointer(MotionEvent.ACTION_DOWN, iconCenter);
+        UiObject2 deepShortcutsContainer = TestViewHelpers.findViewById(
+                R.id.deep_shortcuts_container);
         assertNotNull(deepShortcutsContainer);
-        sendPointer(MotionEvent.ACTION_UP, iconCenter);
+        TestViewHelpers.sendPointer(MotionEvent.ACTION_UP, iconCenter);
 
         // Verify that launching a shortcut opens a page with the same text
         assertTrue(deepShortcutsContainer.getChildCount() > 0);
 
         // Pick second children as it starts showing shortcuts.
         UiObject2 shortcut = deepShortcutsContainer.getChildren().get(1)
-                .findObject(getSelectorForId(R.id.bubble_text));
+                .findObject(TestViewHelpers.getSelectorForId(R.id.bubble_text));
         shortcut.click();
         assertTrue(mDevice.wait(Until.hasObject(By.pkg(
                 testApp.getComponentName().getPackageName())
diff --git a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
index c23f6ef..436c699 100644
--- a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
+++ b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
@@ -48,25 +48,25 @@
         LauncherActivityInfo testApp  = getSettingsApp();
 
         // Open all apps and wait for load complete.
-        final UiObject2 appsContainer = openAllApps();
-        assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2),
-                DEFAULT_UI_TIMEOUT));
+        final UiObject2 appsContainer = TestViewHelpers.openAllApps();
+        Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT);
 
         // Find the app and long press it to show shortcuts.
         UiObject2 icon = scrollAndFind(appsContainer, By.text(testApp.getLabel().toString()));
         // Press icon center until shortcuts appear
         Point iconCenter = icon.getVisibleCenter();
-        sendPointer(MotionEvent.ACTION_DOWN, iconCenter);
-        UiObject2 deepShortcutsContainer = findViewById(R.id.deep_shortcuts_container);
+        TestViewHelpers.sendPointer(MotionEvent.ACTION_DOWN, iconCenter);
+        UiObject2 deepShortcutsContainer = TestViewHelpers.findViewById(
+                R.id.deep_shortcuts_container);
         assertNotNull(deepShortcutsContainer);
-        sendPointer(MotionEvent.ACTION_UP, iconCenter);
+        TestViewHelpers.sendPointer(MotionEvent.ACTION_UP, iconCenter);
 
         // Drag the first shortcut to the home screen.
         assertTrue(deepShortcutsContainer.getChildCount() > 0);
         UiObject2 shortcut = deepShortcutsContainer.getChildren().get(1)
-                .findObject(getSelectorForId(R.id.bubble_text));
+                .findObject(TestViewHelpers.getSelectorForId(R.id.bubble_text));
         String shortcutName = shortcut.getText();
-        dragToWorkspace(shortcut, false);
+        TestViewHelpers.dragToWorkspace(shortcut, false);
 
         // Verify that the shortcut works on home screen
         // (the app opens and has the same text as the shortcut).
diff --git a/tests/src/com/android/launcher3/ui/TestViewHelpers.java b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
new file mode 100644
index 0000000..5244386
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2017 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.ui;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Point;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.R;
+import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.testcomponent.AppWidgetNoConfig;
+import com.android.launcher3.testcomponent.AppWidgetWithConfig;
+
+import java.util.concurrent.Callable;
+
+public class TestViewHelpers {
+    private static final String TAG = "TestViewHelpers";
+
+    private static UiDevice getDevice() {
+        return UiDevice.getInstance(getInstrumentation());
+    }
+
+    /**
+     * Opens all apps and returns the recycler view
+     */
+    public static UiObject2 openAllApps() {
+        final UiDevice device = getDevice();
+        device.waitForIdle();
+        UiObject2 hotseat = device.wait(
+                Until.findObject(getSelectorForId(R.id.hotseat)), 2500);
+        Point start = hotseat.getVisibleCenter();
+        int endY = (int) (device.getDisplayHeight() * 0.1f);
+        // 100 px/step
+        device.swipe(start.x, start.y, start.x, endY, (start.y - endY) / 100);
+        return findViewById(R.id.apps_list_view);
+    }
+
+    public static UiObject2 findViewById(int id) {
+        return getDevice().wait(Until.findObject(getSelectorForId(id)),
+                AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT);
+    }
+
+    public static BySelector getSelectorForId(int id) {
+        final Context targetContext = getTargetContext();
+        String name = targetContext.getResources().getResourceEntryName(id);
+        return By.res(targetContext.getPackageName(), name);
+    }
+
+    /**
+     * Finds a widget provider which can fit on the home screen.
+     *
+     * @param test               test suite.
+     * @param hasConfigureScreen if true, a provider with a config screen is returned.
+     */
+    public static LauncherAppWidgetProviderInfo findWidgetProvider(AbstractLauncherUiTest test,
+            final boolean hasConfigureScreen) {
+        LauncherAppWidgetProviderInfo info =
+                test.getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
+                    @Override
+                    public LauncherAppWidgetProviderInfo call() throws Exception {
+                        ComponentName cn = new ComponentName(getInstrumentation().getContext(),
+                                hasConfigureScreen ? AppWidgetWithConfig.class
+                                        : AppWidgetNoConfig.class);
+                        Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString());
+                        return AppWidgetManagerCompat.getInstance(getTargetContext())
+                                .findProvider(cn, Process.myUserHandle());
+                    }
+                });
+        if (info == null) {
+            throw new IllegalArgumentException("No valid widget provider");
+        }
+        return info;
+    }
+
+    /**
+     * Drags an icon to the center of homescreen.
+     *
+     * @param icon object that is either app icon or shortcut icon
+     */
+    public static void dragToWorkspace(UiObject2 icon, boolean expectedToShowShortcuts) {
+        Point center = icon.getVisibleCenter();
+
+        // Action Down
+        sendPointer(MotionEvent.ACTION_DOWN, center);
+
+        UiObject2 dragLayer = findViewById(R.id.drag_layer);
+
+        if (expectedToShowShortcuts) {
+            // Make sure shortcuts show up, and then move a bit to hide them.
+            assertNotNull(findViewById(R.id.deep_shortcuts_container));
+
+            Point moveLocation = new Point(center);
+            int distanceToMove =
+                    getTargetContext().getResources().getDimensionPixelSize(
+                            R.dimen.deep_shortcuts_start_drag_threshold) + 50;
+            if (moveLocation.y - distanceToMove >= dragLayer.getVisibleBounds().top) {
+                moveLocation.y -= distanceToMove;
+            } else {
+                moveLocation.y += distanceToMove;
+            }
+            movePointer(center, moveLocation);
+
+            assertNull(findViewById(R.id.deep_shortcuts_container));
+        }
+
+        // Wait until Remove/Delete target is visible
+        assertNotNull(findViewById(R.id.delete_target_text));
+
+        Point moveLocation = dragLayer.getVisibleCenter();
+
+        // Move to center
+        movePointer(center, moveLocation);
+        sendPointer(MotionEvent.ACTION_UP, center);
+
+        // Wait until remove target is gone.
+        getDevice().wait(Until.gone(getSelectorForId(R.id.delete_target_text)),
+                AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT);
+    }
+
+    private static void movePointer(Point from, Point to) {
+        while (!from.equals(to)) {
+            from.x = getNextMoveValue(to.x, from.x);
+            from.y = getNextMoveValue(to.y, from.y);
+            sendPointer(MotionEvent.ACTION_MOVE, from);
+        }
+    }
+
+    private static int getNextMoveValue(int targetValue, int oldValue) {
+        if (targetValue - oldValue > 10) {
+            return oldValue + 10;
+        } else if (targetValue - oldValue < -10) {
+            return oldValue - 10;
+        } else {
+            return targetValue;
+        }
+    }
+
+    public static void sendPointer(int action, Point point) {
+        MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), action, point.x, point.y, 0);
+        getInstrumentation().sendPointerSync(event);
+        event.recycle();
+    }
+
+    /**
+     * Opens widget tray and returns the recycler view.
+     */
+    public static UiObject2 openWidgetsTray() {
+        final UiDevice device = getDevice();
+        device.pressMenu(); // Enter overview mode.
+        device.wait(Until.findObject(
+                By.text(getTargetContext().getString(R.string.widget_button_text))),
+                AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT).click();
+        return findViewById(R.id.widgets_list_view);
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 83fcc60..80561fc 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -15,20 +15,20 @@
  */
 package com.android.launcher3.ui.widget;
 
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertTrue;
-
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
 import android.appwidget.AppWidgetManager;
 import android.content.Intent;
+import android.view.View;
+
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiObject2;
-import android.view.View;
 
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppWidgetInfo;
@@ -36,7 +36,7 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.testcomponent.WidgetConfigActivity;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.Condition;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ShellCommandRule;
@@ -55,7 +55,7 @@
 @RunWith(AndroidJUnit4.class)
 public class AddConfigWidgetTest extends AbstractLauncherUiTest {
 
-    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grandWidgetBind();
+    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
     private LauncherAppWidgetProviderInfo mWidgetInfo;
     private AppWidgetManager mAppWidgetManager;
@@ -66,7 +66,7 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mWidgetInfo = findWidgetProvider(true /* hasConfigureScreen */);
+        mWidgetInfo = TestViewHelpers.findWidgetProvider(this, true /* hasConfigureScreen */);
         mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext);
     }
 
@@ -105,14 +105,14 @@
         mActivityMonitor.startLauncher();
 
         // Open widget tray and wait for load complete.
-        final UiObject2 widgetContainer = openWidgetsTray();
-        assertTrue(Wait.atMost(Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT));
+        final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray();
+        Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT);
 
         // Drag widget to homescreen
         WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
         UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class)
                 .hasDescendant(By.text(mWidgetInfo.getLabel(mTargetContext.getPackageManager()))));
-        dragToWorkspace(widget, false);
+        TestViewHelpers.dragToWorkspace(widget, false);
         // Widget id for which the config activity was opened
         mWidgetId = monitor.getWidgetId();
 
@@ -128,12 +128,12 @@
 
         setResult(acceptConfig);
         if (acceptConfig) {
-            assertTrue(Wait.atMost(new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT));
+            Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT);
             assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
         } else {
             // Verify that the widget id is deleted.
-            assertTrue(Wait.atMost(() -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
-                    DEFAULT_ACTIVITY_TIMEOUT));
+            Wait.atMost(null, () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
+                    DEFAULT_ACTIVITY_TIMEOUT);
         }
     }
 
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index d9fef81..7d3cf2b 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Workspace.ItemOperator;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.Condition;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ShellCommandRule;
@@ -45,7 +46,7 @@
 @RunWith(AndroidJUnit4.class)
 public class AddWidgetTest extends AbstractLauncherUiTest {
 
-    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grandWidgetBind();
+    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
     @Test
     @Ignore
@@ -66,16 +67,16 @@
         mActivityMonitor.startLauncher();
 
         final LauncherAppWidgetProviderInfo widgetInfo =
-                findWidgetProvider(false /* hasConfigureScreen */);
+                TestViewHelpers.findWidgetProvider(this, false /* hasConfigureScreen */);
 
         // Open widget tray and wait for load complete.
-        final UiObject2 widgetContainer = openWidgetsTray();
-        assertTrue(Wait.atMost(Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT));
+        final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray();
+        Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT);
 
         // Drag widget to homescreen
         UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class)
                 .hasDescendant(By.text(widgetInfo.getLabel(mTargetContext.getPackageManager()))));
-        dragToWorkspace(widget, false);
+        TestViewHelpers.dragToWorkspace(widget, false);
 
         assertTrue(mActivityMonitor.itemExists(new ItemOperator() {
             @Override
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index 738ad84..22bc05c 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -30,21 +30,17 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.os.Bundle;
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-import androidx.test.uiautomator.UiSelector;
 
 import com.android.launcher3.LauncherAppWidgetHost;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.ContentWriter;
-import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
@@ -56,12 +52,14 @@
 import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
 import java.util.Set;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.UiSelector;
 import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Tests for bind widget flow.
@@ -72,6 +70,9 @@
 @RunWith(AndroidJUnit4.class)
 public class BindWidgetTest extends AbstractLauncherUiTest {
 
+    @Rule
+    public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
+
     private ContentResolver mResolver;
     private AppWidgetManagerCompat mWidgetManager;
 
@@ -118,30 +119,33 @@
 
     @Test
     public void testBindNormalWidget_withConfig() {
-        LauncherAppWidgetProviderInfo info = findWidgetProvider(true);
+        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
         LauncherAppWidgetInfo item = createWidgetInfo(info, true);
 
-        setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label);
+        setupContents(item);
+        verifyWidgetPresent(info);
     }
 
     @Test
     public void testBindNormalWidget_withoutConfig() {
-        LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
+        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
         LauncherAppWidgetInfo item = createWidgetInfo(info, true);
 
-        setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label);
+        setupContents(item);
+        verifyWidgetPresent(info);
     }
 
     @Test @Ignore
-    public void testUnboundWidget_removed() throws Exception {
-        LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
+    public void testUnboundWidget_removed() {
+        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
         LauncherAppWidgetInfo item = createWidgetInfo(info, false);
         item.appWidgetId = -33;
 
-        // Since there is no widget to verify, just wait until the workspace is ready.
-        setupAndVerifyContents(item, Workspace.class, null);
+        setupContents(item);
 
-        waitUntilLoaderIdle();
+        // Since there is no widget to verify, just wait until the workspace is ready.
+        // TODO: fix LauncherInstrumentation#LAUNCHER_PKG
+        mLauncher.getWorkspace();
         // Item deleted from db
         mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
                 null, null, null, null, null);
@@ -158,13 +162,15 @@
                     "Test Started @ " + android.util.Log.getStackTraceString(new Throwable()));
         }
         // A non-restored widget with no config screen gets restored automatically.
-        LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
+        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
 
         // Do not bind the widget
         LauncherAppWidgetInfo item = createWidgetInfo(info, false);
         item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
 
-        setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label);
+        setupContents(item);
+        verifyWidgetPresent(info);
+
         if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS
                 && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) {
             android.util.Log.d("b/117332845",
@@ -173,21 +179,22 @@
     }
 
     @Test
-    public void testPendingWidget_withConfigScreen() throws Exception {
+    public void testPendingWidget_withConfigScreen() {
         if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS
                 && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) {
             android.util.Log.d("b/117332845",
                     "Test Started @ " + android.util.Log.getStackTraceString(new Throwable()));
         }
         // A non-restored widget with config screen get bound and shows a 'Click to setup' UI.
-        LauncherAppWidgetProviderInfo info = findWidgetProvider(true);
+        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
 
         // Do not bind the widget
         LauncherAppWidgetInfo item = createWidgetInfo(info, false);
         item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
 
-        setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
-        waitUntilLoaderIdle();
+        setupContents(item);
+        verifyPendingWidgetPresent();
+
         // Item deleted from db
         mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
                 null, null, null, null, null);
@@ -207,16 +214,19 @@
     }
 
     @Test @Ignore
-    public void testPendingWidget_notRestored_removed() throws Exception {
+    public void testPendingWidget_notRestored_removed() {
         LauncherAppWidgetInfo item = getInvalidWidgetInfo();
         item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
                 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
 
-        setupAndVerifyContents(item, Workspace.class, null);
+        setupContents(item);
+
+        // Since there is no widget to verify, just wait until the workspace is ready.
+        // TODO: fix LauncherInstrumentation#LAUNCHER_PKG
+        mLauncher.getWorkspace();
         // The view does not exist
         assertFalse(mDevice.findObject(
                 new UiSelector().className(PendingAppWidgetHostView.class)).exists());
-        waitUntilLoaderIdle();
         // Item deleted from db
         mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
                 null, null, null, null, null);
@@ -224,7 +234,7 @@
     }
 
     @Test
-    public void testPendingWidget_notRestored_brokenInstall() throws Exception {
+    public void testPendingWidget_notRestored_brokenInstall() {
         // A widget which is was being installed once, even if its not being
         // installed at the moment is not removed.
         LauncherAppWidgetInfo item = getInvalidWidgetInfo();
@@ -232,9 +242,10 @@
                 | LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
                 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
 
-        setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
+        setupContents(item);
+        verifyPendingWidgetPresent();
+
         // Verify item still exists in db
-        waitUntilLoaderIdle();
         mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
                 null, null, null, null, null);
         assertEquals(1, mCursor.getCount());
@@ -259,9 +270,10 @@
         PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
         mSessionId = installer.createSession(params);
 
-        setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
+        setupContents(item);
+        verifyPendingWidgetPresent();
+
         // Verify item still exists in db
-        waitUntilLoaderIdle();
         mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
                 null, null, null, null, null);
         assertEquals(1, mCursor.getCount());
@@ -276,11 +288,8 @@
     /**
      * Adds {@param item} on the homescreen on the 0th screen at 0,0, and verifies that the
      * widget class is displayed on the homescreen.
-     * @param widgetClass the View class which is displayed on the homescreen
-     * @param desc the content description of the view or null.
      */
-    private void setupAndVerifyContents(
-            LauncherAppWidgetInfo item, Class<?> widgetClass, String desc) {
+    private void setupContents(LauncherAppWidgetInfo item) {
         int screenId = Workspace.FIRST_SCREEN_ID;
         // Update the screen id counter for the provider.
         LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
@@ -306,12 +315,18 @@
 
         // Launch the home activity
         mActivityMonitor.startLauncher();
-        // Verify UI
+        waitForModelLoaded();
+    }
+
+    private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
         UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName())
-                .className(widgetClass);
-        if (desc != null) {
-            selector = selector.description(desc);
-        }
+                .className(LauncherAppWidgetHostView.class).description(info.label);
+        assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT));
+    }
+
+    private void verifyPendingWidgetPresent() {
+        UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName())
+                .className(PendingAppWidgetHostView.class);
         assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT));
     }
 
@@ -360,13 +375,9 @@
         int count = 0;
         String pkg = invalidPackage;
 
-        Set<String> activePackage = getOnUiThread(new Callable<Set<String>>() {
-            @Override
-            public Set<String> call() throws Exception {
-                return PackageInstallerCompat.getInstance(mTargetContext)
-                        .updateAndGetActiveSessionCache().keySet();
-            }
-        });
+        Set<String> activePackage = getOnUiThread(() ->
+                PackageInstallerCompat.getInstance(mTargetContext)
+                        .updateAndGetActiveSessionCache().keySet());
         while(true) {
             try {
                 mTargetContext.getPackageManager().getPackageInfo(
@@ -390,15 +401,4 @@
         item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
         return item;
     }
-
-    /**
-     * Blocks the current thread until all the jobs in the main worker thread are complete.
-     */
-    private void waitUntilLoaderIdle() throws Exception {
-        new LooperExecutor(LauncherModel.getWorkerLooper())
-                .submit(new Runnable() {
-                    @Override
-                    public void run() { }
-                }).get(DEFAULT_WORKER_TIMEOUT_SECS, TimeUnit.SECONDS);
-    }
 }
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 67a7fde..95a1289 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -42,6 +42,7 @@
 import com.android.launcher3.testcomponent.AppWidgetWithConfig;
 import com.android.launcher3.testcomponent.RequestPinItemActivity;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.Condition;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ShellCommandRule;
@@ -62,7 +63,7 @@
 @RunWith(AndroidJUnit4.class)
 public class RequestPinItemTest extends AbstractLauncherUiTest {
 
-    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grandWidgetBind();
+    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
     private String mCallbackAction;
     private String mShortcutId;
@@ -151,8 +152,8 @@
         mActivityMonitor.startLauncher();
 
         // Open all apps and wait for load complete
-        final UiObject2 appsContainer = openAllApps();
-        assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT));
+        final UiObject2 appsContainer = TestViewHelpers.openAllApps();
+        Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT);
 
         // Open Pin item activity
         BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver(
@@ -191,7 +192,7 @@
 
         // Go back to home
         mActivityMonitor.returnToHome();
-        assertTrue(Wait.atMost(new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT));
+        Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT);
     }
 
     /**
diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java
index f9e53ba..0e41c02 100644
--- a/tests/src/com/android/launcher3/util/Wait.java
+++ b/tests/src/com/android/launcher3/util/Wait.java
@@ -2,6 +2,8 @@
 
 import android.os.SystemClock;
 
+import org.junit.Assert;
+
 /**
  * A utility class for waiting for a condition to be true.
  */
@@ -9,16 +11,16 @@
 
     private static final long DEFAULT_SLEEP_MS = 200;
 
-    public static boolean atMost(Condition condition, long timeout) {
-        return atMost(condition, timeout, DEFAULT_SLEEP_MS);
+    public static void atMost(String message, Condition condition, long timeout) {
+        atMost(message, condition, timeout, DEFAULT_SLEEP_MS);
     }
 
-    public static boolean atMost(Condition condition, long timeout, long sleepMillis) {
+    public static void atMost(String message, Condition condition, long timeout, long sleepMillis) {
         long endTime = SystemClock.uptimeMillis() + timeout;
         while (SystemClock.uptimeMillis() < endTime) {
             try {
                 if (condition.isTrue()) {
-                    return true;
+                    return;
                 }
             } catch (Throwable t) {
                 // Ignore
@@ -29,11 +31,11 @@
         // Check once more before returning false.
         try {
             if (condition.isTrue()) {
-                return true;
+                return;
             }
         } catch (Throwable t) {
             // Ignore
         }
-        return false;
+        Assert.fail(message);
     }
 }
diff --git a/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java b/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java
index 4531ab1..0ec0f02 100644
--- a/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java
+++ b/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java
@@ -63,7 +63,7 @@
     /**
      * Grants the launcher permission to bind widgets.
      */
-    public static ShellCommandRule grandWidgetBind() {
+    public static ShellCommandRule grantWidgetBind() {
         return new ShellCommandRule("appwidget grantbind --package "
                 + InstrumentationRegistry.getTargetContext().getPackageName(), null);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index a11f6df..b7ae9f1 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -42,20 +42,8 @@
     /**
      * Clicks the icon to launch its app.
      */
-    @Deprecated
-    public Background launch() {
-        LauncherInstrumentation.log("AppIcon.launch before click");
-        LauncherInstrumentation.assertTrue(
-                "Launching an app didn't open a new window: " + mIcon.getText(),
-                mIcon.clickAndWait(Until.newWindow(), LauncherInstrumentation.WAIT_TIME_MS));
-        return new Background(mLauncher);
-    }
-
-    /**
-     * Clicks the icon to launch its app.
-     */
     public Background launch(String packageName) {
-        LauncherInstrumentation.log("AppIcon.launch before click");
+        LauncherInstrumentation.log("AppIcon.launch before click " + mIcon.getVisibleCenter());
         LauncherInstrumentation.assertTrue(
                 "Launching an app didn't open a new window: " + mIcon.getText(),
                 mIcon.clickAndWait(Until.newWindow(), LauncherInstrumentation.WAIT_TIME_MS));
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 26295c8..27e0954 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -17,11 +17,10 @@
 package com.android.launcher3.tapl;
 
 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
+import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
 
 import static org.junit.Assert.assertTrue;
 
-import static androidx.test.InstrumentationRegistry.getTargetContext;
-
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiObject2;
@@ -52,9 +51,8 @@
     public BaseOverview switchToOverview() {
         verifyActiveContainer();
         goToOverviewUnchecked();
-        assertTrue("Overview not visible", mLauncher.getDevice().wait(Until.hasObject(By.pkg(
-                getTargetContext().getPackageName())),
-                WAIT_TIME_MS));
+        assertTrue("Overview not visible", mLauncher.getDevice().wait(
+                Until.hasObject(By.pkg(getOverviewPackageName())), WAIT_TIME_MS));
         return new BaseOverview(mLauncher);
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 87d0a79..4fce211 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.tapl;
 
-import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
-
 import java.util.Collections;
 import java.util.List;
 
diff --git a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
index c393cdf..93554d2 100644
--- a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
+++ b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
@@ -20,10 +20,12 @@
 import static androidx.test.InstrumentationRegistry.getTargetContext;
 
 import android.app.Instrumentation;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
 
 import java.util.List;
 
@@ -70,4 +72,13 @@
         }
         return launchers.get(0).activityInfo;
     }
+
+    public static String getOverviewPackageName() {
+        Resources res = Resources.getSystem();
+        int id = res.getIdentifier("config_recentsComponentName", "string", "android");
+        if (id != 0) {
+            return ComponentName.unflattenFromString(res.getString(id)).getPackageName();
+        }
+        return "com.android.systemui";
+    }
 }