Merge "Adding some extra logging around draglayer alpha" into ub-launcher3-master
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/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/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index 5980d85..b406b30 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -117,6 +117,7 @@
             launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() {
                 @Override
                 public void onStateSetImmediately(LauncherState state) {
+                    onStateTransitionComplete(state);
                 }
 
                 @Override
@@ -142,6 +143,7 @@
             launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() {
                 @Override
                 public void onStateSetImmediately(LauncherState state) {
+                    onStateTransitionComplete(state);
                 }
 
                 @Override
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/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/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 c5f593a..0395fbb 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/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/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 6db784a..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,19 +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;
@@ -89,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;
@@ -97,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);
@@ -114,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();
@@ -184,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.
      */
@@ -229,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 {
@@ -381,37 +275,6 @@
         Wait.atMost(message, () -> 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 LauncherActivityInfo getSettingsApp() {
         return LauncherAppsCompat.getInstance(mTargetContext)
                 .getActivityList("com.android.settings",
diff --git a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
index a6830fc..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();
+        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 f773000..d7a7f6b 100644
--- a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
+++ b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
@@ -46,24 +46,25 @@
         LauncherActivityInfo testApp = getSettingsApp();
 
         // Open all apps and wait for load complete
-        final UiObject2 appsContainer = openAllApps();
+        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 bc1b519..436c699 100644
--- a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
+++ b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
@@ -48,24 +48,25 @@
         LauncherActivityInfo testApp  = getSettingsApp();
 
         // Open all apps and wait for load complete.
-        final UiObject2 appsContainer = openAllApps();
+        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 14141f0..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();
+        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();
 
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index 8a05e2c..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();
+        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 b7342c4..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,29 +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);
 
+        // 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);
@@ -157,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",
@@ -172,20 +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);
+        setupContents(item);
+        verifyPendingWidgetPresent();
+
         // Item deleted from db
         mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
                 null, null, null, null, null);
@@ -205,12 +214,16 @@
     }
 
     @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());
@@ -221,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();
@@ -229,7 +242,9 @@
                 | LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
                 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
 
-        setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
+        setupContents(item);
+        verifyPendingWidgetPresent();
+
         // Verify item still exists in db
         mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
                 null, null, null, null, null);
@@ -255,7 +270,9 @@
         PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
         mSessionId = installer.createSession(params);
 
-        setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
+        setupContents(item);
+        verifyPendingWidgetPresent();
+
         // Verify item still exists in db
         mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
                 null, null, null, null, null);
@@ -271,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);
@@ -302,13 +316,17 @@
         // Launch the home activity
         mActivityMonitor.startLauncher();
         waitForModelLoaded();
+    }
 
-        // Verify UI
+    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));
     }
 
@@ -357,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(
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 839cfb2..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,7 +152,7 @@
         mActivityMonitor.startLauncher();
 
         // Open all apps and wait for load complete
-        final UiObject2 appsContainer = openAllApps();
+        final UiObject2 appsContainer = TestViewHelpers.openAllApps();
         Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT);
 
         // Open Pin item activity
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);
     }