Merge "Better diags and a small correction for scrolling in AllApps" into ub-launcher3-master
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index f3db20e..9123959 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,2 +1,2 @@
 [Hook Scripts]
-checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT}
diff --git a/SharedLibWrapper/build.gradle b/SharedLibWrapper/build.gradle
new file mode 100644
index 0000000..674e38a
--- /dev/null
+++ b/SharedLibWrapper/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'java'
+
+final String ANDROID_TOP = "${rootDir}/../../.."
+final String FRAMEWORK_PREBUILTS_DIR = "${ANDROID_TOP}/prebuilts/framework_intermediates/"
+
+sourceSets {
+    main {
+        java.srcDirs = ["${ANDROID_TOP}/frameworks/lib/systemui/SharedLibWrapper/src"]
+    }
+}
+
+sourceCompatibility = 1.8
+
+dependencies {
+    implementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
+    compileOnly fileTree(dir: "$ANDROID_TOP/prebuilts/fullsdk-${org.gradle.internal.os.OperatingSystem.current().isMacOsX() ? "darwin" : "linux"}/platforms/${COMPILE_SDK}", include: 'android.jar')
+}
diff --git a/build.gradle b/build.gradle
index e296455..534ca65 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,7 @@
     repositories {
         mavenCentral()
         google()
+        jcenter()
     }
     dependencies {
         classpath GRADLE_CLASS_PATH
@@ -62,12 +63,6 @@
             minSdkVersion 28
         }
 
-        withQuickstepIconRecents {
-            dimension "recents"
-
-            minSdkVersion 28
-        }
-
         withoutQuickstep {
             dimension "recents"
         }
@@ -78,11 +73,6 @@
         if (variant.buildType.name.endsWith('release')) {
             variant.setIgnore(true)
         }
-
-        // Icon recents is Go only
-        if (name.contains("WithQuickstepIconRecents") && !name.contains("l3go")) {
-            variant.setIgnore(true)
-        }
     }
 
     sourceSets {
@@ -96,10 +86,6 @@
             }
         }
 
-        debug {
-            manifest.srcFile "AndroidManifest.xml"
-        }
-
         androidTest {
             res.srcDirs = ['tests/res']
             java.srcDirs = ['tests/src', 'tests/tapl']
@@ -112,15 +98,30 @@
 
         aosp {
             java.srcDirs = ['src_flags', 'src_shortcuts_overrides']
+        }
+
+        aospWithoutQuickstep {
             manifest.srcFile "AndroidManifest.xml"
         }
 
+        aospWithQuickstep {
+            manifest.srcFile "quickstep/AndroidManifest-launcher.xml"
+        }
+
         l3go {
             res.srcDirs = ['go/res']
             java.srcDirs = ['go/src']
             manifest.srcFile "go/AndroidManifest.xml"
         }
 
+        l3goWithoutQuickstepDebug {
+            manifest.srcFile "AndroidManifest.xml"
+        }
+
+        l3goWithQuickstepDebug {
+            manifest.srcFile "quickstep/AndroidManifest-launcher.xml"
+        }
+
         withoutQuickstep {
             java.srcDirs = ['src_ui_overrides']
         }
@@ -130,20 +131,17 @@
             java.srcDirs = ['quickstep/src', 'quickstep/recents_ui_overrides/src']
             manifest.srcFile "quickstep/AndroidManifest.xml"
         }
-
-        withQuickstepIconRecents {
-            res.srcDirs = ['quickstep/res', 'go/quickstep/res']
-            java.srcDirs = ['quickstep/src', 'go/quickstep/src']
-            manifest.srcFile "quickstep/AndroidManifest.xml"
-        }
     }
 }
 
-repositories {
-    maven { url "../../../prebuilts/fullsdk-darwin/extras/android/m2repository" }
-    maven { url "../../../prebuilts/fullsdk-linux/extras/android/m2repository" }
-    mavenCentral()
-    google()
+allprojects {
+    repositories {
+        maven { url "../../../prebuilts/sdk/current/androidx/m2repository" }
+        maven { url "../../../prebuilts/fullsdk-darwin/extras/android/m2repository" }
+        maven { url "../../../prebuilts/fullsdk-linux/extras/android/m2repository" }
+        mavenCentral()
+        google()
+    }
 }
 
 dependencies {
@@ -151,14 +149,12 @@
     implementation "androidx.recyclerview:recyclerview:${ANDROID_X_VERSION}"
     implementation "androidx.preference:preference:${ANDROID_X_VERSION}"
     implementation project(':IconLoader')
+    withQuickstepImplementation project(':SharedLibWrapper')
     implementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/libs", include: 'launcher_protos.jar')
 
     // Recents lib dependency
     withQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
 
-    // Recents lib dependency for Go
-    withQuickstepIconRecentsImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
-
     // Required for AOSP to compile. This is already included in the sysui_shared.jar
     withoutQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/libs", include: 'plugin_core.jar')
 
@@ -175,7 +171,7 @@
 protobuf {
     // Configure the protoc executable
     protoc {
-        artifact = 'com.google.protobuf:protoc:3.0.0-alpha-3'
+        artifact = 'com.google.protobuf:protoc:3.0.0'
 
         generateProtoTasks {
             all().each { task ->
diff --git a/go/src/com/android/launcher3/model/LoaderResults.java b/go/src/com/android/launcher3/model/LoaderResults.java
index 26c3313..7130531 100644
--- a/go/src/com/android/launcher3/model/LoaderResults.java
+++ b/go/src/com/android/launcher3/model/LoaderResults.java
@@ -16,10 +16,11 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.Callbacks;
-
-import java.lang.ref.WeakReference;
+import com.android.launcher3.util.LooperExecutor;
 
 /**
  * Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}.
@@ -27,8 +28,13 @@
 public class LoaderResults extends BaseLoaderResults {
 
     public LoaderResults(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+            AllAppsList allAppsList, Callbacks[] callbacks) {
+        this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+    }
+
+    public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+            AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+        super(app, dataModel, allAppsList, callbacks, executor);
     }
 
     @Override
diff --git a/gradle.properties b/gradle.properties
index a77f52a..7a51375 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,11 +2,11 @@
 android.useAndroidX = true
 android.enableJetifier = true
 
-ANDROID_X_VERSION=1.0.0-beta01
+ANDROID_X_VERSION=1+
 
-GRADLE_CLASS_PATH=com.android.tools.build:gradle:3.3.0
+GRADLE_CLASS_PATH=com.android.tools.build:gradle:3.5.1
 
-PROTOBUF_CLASS_PATH=com.google.protobuf:protobuf-gradle-plugin:0.8.6
+PROTOBUF_CLASS_PATH=com.google.protobuf:protobuf-gradle-plugin:0.8.8
 PROTOBUF_DEPENDENCY=com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7
 
 BUILD_TOOLS_VERSION=28.0.3
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
index 8a4d2b7..d7a62e1 100644
--- a/iconloaderlib/build.gradle
+++ b/iconloaderlib/build.gradle
@@ -3,7 +3,6 @@
 android {
     compileSdkVersion COMPILE_SDK
     buildToolsVersion BUILD_TOOLS_VERSION
-    publishNonDefault true
 
     defaultConfig {
         minSdkVersion 25
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
index 0fd4aac..923e050 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -77,7 +77,7 @@
     }
 
     void finishOnboarding() {
-        mLauncher.rebindModel();
+        mLauncher.getModel().rebindCallbacks();
         mLauncher.getSharedPrefs().edit().putBoolean(KEY_HOTSEAT_EDU_SEEN, true).apply();
         removeNotification();
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index bd37e56..73c0c97 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -173,7 +173,7 @@
 
     @Override
     public String getDescription(Launcher launcher) {
-        return launcher.getString(R.string.accessibility_desc_recent_apps);
+        return launcher.getString(R.string.accessibility_recent_apps);
     }
 
     public static float getDefaultSwipeHeight(Launcher launcher) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 613386e..4e08df9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -98,6 +98,7 @@
     private final float mYRange;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
+    private final LauncherRecentsView mRecentsView;
 
     private boolean mNoIntercept;
     private LauncherState mStartState;
@@ -119,6 +120,7 @@
         mMotionPauseDetector = new MotionPauseDetector(mLauncher);
         mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
                 R.dimen.motion_pause_detector_min_displacement_from_app);
+        mRecentsView = mLauncher.getOverviewPanel();
     }
 
     @Override
@@ -208,6 +210,15 @@
         updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder, ANIM_ALL);
         mNonOverviewAnim.dispatchOnStart();
 
+        if (mRecentsView.getTaskViewCount() == 0) {
+            mRecentsView.setOnEmptyMessageUpdatedListener(isEmpty -> {
+                if (!isEmpty && mSwipeDetector.isDraggingState()) {
+                    // We have loaded tasks, update the animators to start at the correct scale etc.
+                    setupOverviewAnimators();
+                }
+            });
+        }
+
         setupOverviewAnimators();
     }
 
@@ -228,25 +239,25 @@
         LauncherState.ScaleAndTranslation toScaleAndTranslation = toState
                 .getOverviewScaleAndTranslation(mLauncher);
         // Update RecentView's translationX to have it start offscreen.
-        LauncherRecentsView recentsView = mLauncher.getOverviewPanel();
         float startScale = Utilities.mapRange(
                 SCALE_DOWN_INTERPOLATOR.getInterpolation(Y_ANIM_MIN_PROGRESS),
                 fromScaleAndTranslation.scale,
                 toScaleAndTranslation.scale);
-        fromScaleAndTranslation.translationX = recentsView.getOffscreenTranslationX(startScale);
+        fromScaleAndTranslation.translationX = mRecentsView.getOffscreenTranslationX(startScale);
 
         // Set RecentView's initial properties.
-        recentsView.setScaleX(fromScaleAndTranslation.scale);
-        recentsView.setScaleY(fromScaleAndTranslation.scale);
-        recentsView.setTranslationX(fromScaleAndTranslation.translationX);
-        recentsView.setTranslationY(fromScaleAndTranslation.translationY);
-        recentsView.setContentAlpha(1);
+        mRecentsView.setScaleX(fromScaleAndTranslation.scale);
+        mRecentsView.setScaleY(fromScaleAndTranslation.scale);
+        mRecentsView.setTranslationX(fromScaleAndTranslation.translationX);
+        mRecentsView.setTranslationY(fromScaleAndTranslation.translationY);
+        mRecentsView.setContentAlpha(1);
+        mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
 
         // As we drag right, animate the following properties:
         //   - RecentsView translationX
         //   - OverviewScrim
         AnimatorSet xOverviewAnim = new AnimatorSet();
-        xOverviewAnim.play(ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_X,
+        xOverviewAnim.play(ObjectAnimator.ofFloat(mRecentsView, View.TRANSLATION_X,
                 toScaleAndTranslation.translationX));
         xOverviewAnim.play(ObjectAnimator.ofFloat(
                 mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
@@ -261,11 +272,11 @@
         //   - RecentsView scale
         //   - RecentsView fullscreenProgress
         AnimatorSet yAnimation = new AnimatorSet();
-        Animator translateYAnim = ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_Y,
+        Animator translateYAnim = ObjectAnimator.ofFloat(mRecentsView, View.TRANSLATION_Y,
                 toScaleAndTranslation.translationY);
-        Animator scaleAnim = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
+        Animator scaleAnim = ObjectAnimator.ofFloat(mRecentsView, SCALE_PROPERTY,
                 toScaleAndTranslation.scale);
-        Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS,
+        Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(mRecentsView, FULLSCREEN_PROGRESS,
                 fromState.getOverviewFullscreenProgress(), toState.getOverviewFullscreenProgress());
         scaleAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
         fullscreenProgressAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
@@ -466,5 +477,6 @@
         mYOverviewAnim = null;
         mIsHomeScreenVisible = true;
         mSwipeDetector.finishedScrolling();
+        mRecentsView.setOnEmptyMessageUpdatedListener(null);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index 630dd70..7ff8969 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
@@ -75,9 +76,11 @@
     protected static final Rect TEMP_RECT = new Rect();
 
     // Start resisting when swiping past this factor of mTransitionDragLength.
-    private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
+    private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = ENABLE_OVERVIEW_ACTIONS.get()
+            ? 2.8f : 1.4f;
     // This is how far down we can scale down, where 0f is full screen and 1f is recents.
-    private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
+    private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = ENABLE_OVERVIEW_ACTIONS.get()
+            ? 3.6f : 1.8f;
     private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
 
     // The distance needed to drag to reach the task size in recents.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
new file mode 100644
index 0000000..6f919c1
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.TouchController;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
+
+/**
+ * In 0-button mode, intercepts swipe up from the nav bar on FallbackRecentsView to go home.
+ */
+public class FallbackNavBarTouchController implements TouchController {
+
+    private final RecentsActivity mActivity;
+    @Nullable
+    private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
+
+    public FallbackNavBarTouchController(RecentsActivity activity) {
+        mActivity = activity;
+        SysUINavigationMode.Mode sysUINavigationMode = SysUINavigationMode.getMode(mActivity);
+        if (sysUINavigationMode == SysUINavigationMode.Mode.NO_BUTTON) {
+            NavBarPosition navBarPosition = new NavBarPosition(sysUINavigationMode,
+                    DefaultDisplay.INSTANCE.get(mActivity).getInfo());
+            mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mActivity,
+                    true /* disableHorizontalSwipe */, navBarPosition,
+                    null /* onInterceptTouch */, this::onSwipeUp);
+        } else {
+            mTriggerSwipeUpTracker = null;
+        }
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
+        if (cameFromNavBar && mTriggerSwipeUpTracker != null) {
+            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+                mTriggerSwipeUpTracker.init();
+            }
+            onControllerTouchEvent(ev);
+            return mTriggerSwipeUpTracker.interceptedTouch();
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        if (mTriggerSwipeUpTracker != null) {
+            mTriggerSwipeUpTracker.onMotionEvent(ev);
+            return true;
+        }
+        return false;
+    }
+
+    private void onSwipeUp(boolean wasFling) {
+        mActivity.<FallbackRecentsView>getOverviewPanel().startHome();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
index 1820729..de5fd7c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
@@ -48,7 +48,10 @@
     }
 
     public void setup() {
-        mControllers = new TouchController[] { new RecentsTaskController(mActivity) };
+        mControllers = new TouchController[] {
+                new RecentsTaskController(mActivity),
+                new FallbackNavBarTouchController(mActivity),
+        };
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
index 875ec29..ca15ca1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -15,23 +15,12 @@
  */
 package com.android.quickstep.inputconsumers;
 
-import static android.view.MotionEvent.ACTION_CANCEL;
-import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_MOVE;
-import static android.view.MotionEvent.ACTION_UP;
-
-import static com.android.launcher3.Utilities.squaredHypot;
-
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.PointF;
 import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogUtils;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -39,31 +28,22 @@
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 public class OverviewWithoutFocusInputConsumer implements InputConsumer {
 
     private final Context mContext;
-    private final RecentsAnimationDeviceState mDeviceState;
-    private final GestureState mGestureState;
     private final InputMonitorCompat mInputMonitor;
-    private final boolean mDisableHorizontalSwipe;
-    private final PointF mDownPos = new PointF();
-    private final float mSquaredTouchSlop;
-
-    private boolean mInterceptedTouch;
-    private VelocityTracker mVelocityTracker;
+    private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
 
     public OverviewWithoutFocusInputConsumer(Context context,
             RecentsAnimationDeviceState deviceState, GestureState gestureState,
             InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) {
         mContext = context;
-        mDeviceState = deviceState;
-        mGestureState = gestureState;
         mInputMonitor = inputMonitor;
-        mDisableHorizontalSwipe = disableHorizontalSwipe;
-        mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
-        mVelocityTracker = VelocityTracker.obtain();
+        mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, disableHorizontalSwipe,
+                deviceState.getNavBarPosition(), this::onInterceptTouch, this::onSwipeUp);
     }
 
     @Override
@@ -73,97 +53,31 @@
 
     @Override
     public boolean allowInterceptByParent() {
-        return !mInterceptedTouch;
-    }
-
-    private void endTouchTracking() {
-        if (mVelocityTracker != null) {
-            mVelocityTracker.recycle();
-            mVelocityTracker = null;
-        }
+        return !mTriggerSwipeUpTracker.interceptedTouch();
     }
 
     @Override
     public void onMotionEvent(MotionEvent ev) {
-        if (mVelocityTracker == null) {
-            return;
-        }
+        mTriggerSwipeUpTracker.onMotionEvent(ev);
+    }
 
-        mVelocityTracker.addMovement(ev);
-        switch (ev.getActionMasked()) {
-            case ACTION_DOWN: {
-                mDownPos.set(ev.getX(), ev.getY());
-                break;
-            }
-            case ACTION_MOVE: {
-                if (!mInterceptedTouch) {
-                    float displacementX = ev.getX() - mDownPos.x;
-                    float displacementY = ev.getY() - mDownPos.y;
-                    if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
-                        if (mDisableHorizontalSwipe
-                                && Math.abs(displacementX) > Math.abs(displacementY)) {
-                            // Horizontal gesture is not allowed in this region
-                            endTouchTracking();
-                            break;
-                        }
-
-                        mInterceptedTouch = true;
-
-                        if (mInputMonitor != null) {
-                            mInputMonitor.pilferPointers();
-                        }
-                    }
-                }
-                break;
-            }
-
-            case ACTION_CANCEL:
-                endTouchTracking();
-                break;
-
-            case ACTION_UP: {
-                finishTouchTracking(ev);
-                endTouchTracking();
-                break;
-            }
+    private void onInterceptTouch() {
+        if (mInputMonitor != null) {
+            mInputMonitor.pilferPointers();
         }
     }
 
-    private void finishTouchTracking(MotionEvent ev) {
-        mVelocityTracker.computeCurrentVelocity(100);
-        float velocityX = mVelocityTracker.getXVelocity();
-        float velocityY = mVelocityTracker.getYVelocity();
-        float velocity = mDeviceState.getNavBarPosition().isRightEdge()
-                ? -velocityX
-                : mDeviceState.getNavBarPosition().isLeftEdge()
-                        ? velocityX
-                        : -velocityY;
-
-        final boolean triggerQuickstep;
-        int touch = Touch.FLING;
-        if (Math.abs(velocity) >= ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity()) {
-            triggerQuickstep = velocity > 0;
-        } else {
-            float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
-            float displacementY = ev.getY() - mDownPos.y;
-            triggerQuickstep = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
-            touch = Touch.SWIPE;
-        }
-
-        if (triggerQuickstep) {
-            mContext.startActivity(new Intent(Intent.ACTION_MAIN)
-                    .addCategory(Intent.CATEGORY_HOME)
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-            ActiveGestureLog.INSTANCE.addLog("startQuickstep");
-            BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
-            int pageIndex = -1; // This number doesn't reflect workspace page index.
-                                // It only indicates that launcher client screen was shown.
-            int containerType = StatsLogUtils.getContainerTypeFromState(activity.getCurrentState());
-            activity.getUserEventDispatcher().logActionOnContainer(
-                    touch, Direction.UP, containerType, pageIndex);
-            activity.getUserEventDispatcher().setPreviousHomeGesture(true);
-        } else {
-            // ignore
-        }
+    private void onSwipeUp(boolean wasFling) {
+        mContext.startActivity(new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+        ActiveGestureLog.INSTANCE.addLog("startQuickstep");
+        BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
+        int pageIndex = -1; // This number doesn't reflect workspace page index.
+                            // It only indicates that launcher client screen was shown.
+        int containerType = StatsLogUtils.getContainerTypeFromState(activity.getCurrentState());
+        activity.getUserEventDispatcher().logActionOnContainer(
+                wasFling ? Touch.FLING : Touch.SWIPE, Direction.UP, containerType, pageIndex);
+        activity.getUserEventDispatcher().setPreviousHomeGesture(true);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
new file mode 100644
index 0000000..c71258b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Tracks motion events to determine whether a gesture on the nav bar is a swipe up.
+ */
+public class TriggerSwipeUpTouchTracker {
+
+    private final PointF mDownPos = new PointF();
+    private final float mSquaredTouchSlop;
+    private final float mMinFlingVelocity;
+    private final boolean mDisableHorizontalSwipe;
+    private final NavBarPosition mNavBarPosition;
+    private final Runnable mOnInterceptTouch;
+    private final OnSwipeUpListener mOnSwipeUp;
+
+    private boolean mInterceptedTouch;
+    private VelocityTracker mVelocityTracker;
+
+    public TriggerSwipeUpTouchTracker(Context context, boolean disableHorizontalSwipe,
+            NavBarPosition navBarPosition, Runnable onInterceptTouch,
+            OnSwipeUpListener onSwipeUp) {
+        mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
+        mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+        mNavBarPosition = navBarPosition;
+        mDisableHorizontalSwipe = disableHorizontalSwipe;
+        mOnInterceptTouch = onInterceptTouch;
+        mOnSwipeUp = onSwipeUp;
+
+        init();
+    }
+
+    /**
+     * Reset some initial values to prepare for the next gesture.
+     */
+    public void init() {
+        mInterceptedTouch = false;
+        mVelocityTracker = VelocityTracker.obtain();
+    }
+
+    /**
+     * @return Whether we have passed the touch slop and are still tracking the gesture.
+     */
+    public boolean interceptedTouch() {
+        return mInterceptedTouch;
+    }
+
+    /**
+     * Track motion events to determine whether an atomic swipe up has occurred.
+     */
+    public void onMotionEvent(MotionEvent ev) {
+        if (mVelocityTracker == null) {
+            return;
+        }
+
+        mVelocityTracker.addMovement(ev);
+        switch (ev.getActionMasked()) {
+            case ACTION_DOWN: {
+                mDownPos.set(ev.getX(), ev.getY());
+                break;
+            }
+            case ACTION_MOVE: {
+                if (!mInterceptedTouch) {
+                    float displacementX = ev.getX() - mDownPos.x;
+                    float displacementY = ev.getY() - mDownPos.y;
+                    if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
+                        if (mDisableHorizontalSwipe
+                                && Math.abs(displacementX) > Math.abs(displacementY)) {
+                            // Horizontal gesture is not allowed in this region
+                            endTouchTracking();
+                            break;
+                        }
+
+                        mInterceptedTouch = true;
+
+                        if (mOnInterceptTouch != null) {
+                            mOnInterceptTouch.run();
+                        }
+                    }
+                }
+                break;
+            }
+
+            case ACTION_CANCEL:
+                endTouchTracking();
+                break;
+
+            case ACTION_UP: {
+                onGestureEnd(ev);
+                endTouchTracking();
+                break;
+            }
+        }
+    }
+
+    private void endTouchTracking() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private void onGestureEnd(MotionEvent ev) {
+        mVelocityTracker.computeCurrentVelocity(1000);
+        float velocityX = mVelocityTracker.getXVelocity();
+        float velocityY = mVelocityTracker.getYVelocity();
+        float velocity = mNavBarPosition.isRightEdge()
+                ? -velocityX
+                : mNavBarPosition.isLeftEdge()
+                        ? velocityX
+                        : -velocityY;
+
+        final boolean wasFling = Math.abs(velocity) >= mMinFlingVelocity;
+        final boolean isSwipeUp;
+        if (wasFling) {
+            isSwipeUp = velocity > 0;
+        } else {
+            float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
+            float displacementY = ev.getY() - mDownPos.y;
+            isSwipeUp = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
+        }
+
+        if (isSwipeUp && mOnSwipeUp != null) {
+            mOnSwipeUp.onSwipeUp(wasFling);
+        }
+    }
+
+    /**
+     * Callback when the gesture ends and was determined to be a swipe from the nav bar.
+     */
+    public interface OnSwipeUpListener {
+        /**
+         * Called on touch up if a swipe up was detected.
+         * @param wasFling Whether the swipe was a fling, or just passed touch slop at low velocity.
+         */
+        void onSwipeUp(boolean wasFling);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index c836791..cb20ed0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -309,6 +309,7 @@
     private final Point mLastMeasureSize = new Point();
     private final int mEmptyMessagePadding;
     private boolean mShowEmptyMessage;
+    private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
     private Layout mEmptyTextLayout;
     private boolean mLiveTileOverlayAttached;
 
@@ -1456,6 +1457,10 @@
         return null;
     }
 
+    public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
+        mOnEmptyMessageUpdatedListener = listener;
+    }
+
     public void updateEmptyMessage() {
         boolean isEmpty = getTaskViewCount() == 0;
         boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
@@ -1467,6 +1472,10 @@
         mShowEmptyMessage = isEmpty;
         updateEmptyStateUi(hasSizeChanged);
         invalidate();
+
+        if (mOnEmptyMessageUpdatedListener != null) {
+            mOnEmptyMessageUpdatedListener.onEmptyMessageUpdated(mShowEmptyMessage);
+        }
     }
 
     @Override
@@ -1927,4 +1936,15 @@
         return !(view instanceof TaskView) && !(view instanceof ClearAllButton)
                 && index <= mTaskViewStartIndex;
     }
+
+    /**
+     * Used to register callbacks for when our empty message state changes.
+     *
+     * @see #setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener)
+     * @see #updateEmptyMessage()
+     */
+    public interface OnEmptyMessageUpdatedListener {
+        /** @param isEmpty Whether RecentsView is empty (i.e. has no children) */
+        void onEmptyMessageUpdated(boolean isEmpty);
+    }
 }
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index ce87527..2b21df8 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -29,9 +29,6 @@
     <!-- Title for an option to enter freeform mode for a given app -->
     <string name="recent_task_option_freeform">Freeform</string>
 
-    <!-- Content description for the recent apps panel (not shown on the screen). [CHAR LIMIT=NONE] -->
-    <string name="accessibility_desc_recent_apps">Overview</string>
-
     <!-- Recents: The empty recents string. [CHAR LIMIT=NONE] -->
     <string name="recents_empty_message">No recent items</string>
 
@@ -70,19 +67,19 @@
     <string  name="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
 
     <!-- Hotseat migration notification title -->
-    <string translatable="false" name="hotseat_migrate_prompt_title">Your Hotseat just got smarter</string>
+    <string translatable="false" name="hotseat_migrate_prompt_title">Get suggested apps on the home screen</string>
     <!-- Hotseat migration notification content -->
-    <string translatable="false" name="hotseat_migrate_prompt_content">Tap here to setup and learn more</string>
+    <string translatable="false" name="hotseat_migrate_prompt_content">Tap to set up</string>
     <!-- Hotseat migration wizard title -->
-    <string translatable="false" name="hotseat_migrate_title">Pixel Suggests apps you\'ll need next</string>
+    <string translatable="false" name="hotseat_migrate_title">Suggested apps replace the bottom row of apps</string>
     <!-- Hotseat migration wizard message -->
-    <string translatable="false" name="hotseat_migrate_message">Suggested apps will replace the bottom row of apps. To pin an app, drag it over a suggested app. Touch &amp; hold an app to hide it.</string>
+    <string translatable="false" name="hotseat_migrate_message">To pin a favorite app, drag it over a suggested app. To hide a suggested app, touch &amp; hold it.</string>
     <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
     <string translatable="false" name="hotseat_items_migrated">Your hotseat items have been moved to the last page.</string>
     <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
     <string translatable="false" name="hotseat_no_migration">You can remove items from the hotseat manually to see suggested apps in their spot.</string>
     <!-- Button text to opt in for fully predicted hotseat -->
-    <string translatable="false" name="hotseat_migrate_accept">Migrate</string>
+    <string translatable="false" name="hotseat_migrate_accept">I\'m in</string>
     <!-- Button text to dismiss opt in for fully predicted hotseat -->
     <string translatable="false" name="hotseat_migrate_dismiss">No thanks</string>
     <!-- Hotseat onboard notification title -->
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index d49ff89..b249f48 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep.util;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.content.Context;
@@ -26,7 +28,6 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.quickstep.SysUINavigationMode;
 
 import java.lang.annotation.Retention;
@@ -58,7 +59,7 @@
         } else {
             Resources res = context.getResources();
 
-            if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+            if (ENABLE_OVERVIEW_ACTIONS.get()) {
                 //TODO: this needs to account for the swipe gesture height and accessibility
                 // UI when shown.
                 extraSpace = 0;
@@ -111,7 +112,7 @@
             final int paddingResId;
             if (dp.isVerticalBarLayout()) {
                 paddingResId = R.dimen.landscape_task_card_horz_space;
-            } else if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+            } else if (ENABLE_OVERVIEW_ACTIONS.get()) {
                 paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
             } else {
                 paddingResId = R.dimen.portrait_task_card_horz_space;
@@ -146,6 +147,11 @@
 
     public static int getShelfTrackingDistance(Context context, DeviceProfile dp) {
         // Track the bottom of the window.
+        if (ENABLE_OVERVIEW_ACTIONS.get()) {
+            Rect taskSize = new Rect();
+            calculateLauncherTaskSize(context, dp, taskSize);
+            return (dp.heightPx - taskSize.height()) / 2;
+        }
         int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
         int spaceBetweenShelfAndRecents = (int) context.getResources().getDimension(
                 R.dimen.task_card_vert_space);
@@ -157,7 +163,7 @@
      * @return the margin in pixels.
      */
     public static int thumbnailBottomMargin(Resources resources) {
-        if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+        if (ENABLE_OVERVIEW_ACTIONS.get()) {
             return resources.getDimensionPixelSize(R.dimen.overview_actions_height);
         } else {
             return 0;
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 428e647..71d77fc 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -43,6 +43,7 @@
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 import com.android.quickstep.views.RecentsView;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -51,10 +52,18 @@
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsQuickstep extends AbstractQuickStepTest {
+    private int mLauncherPid;
+
     @Before
     public void setUp() throws Exception {
         super.setUp();
         TaplTestsLauncher3.initialize(this);
+        mLauncherPid = mLauncher.getPid();
+    }
+
+    @After
+    public void teardown() {
+        assertEquals("Launcher crashed, pid mismatch:", mLauncherPid, mLauncher.getPid());
     }
 
     private void startTestApps() throws Exception {
@@ -100,6 +109,7 @@
     @PortraitLandscape
     public void testOverview() throws Exception {
         startTestApps();
+        // mLauncher.pressHome() also tests an important case of pressing home while in background.
         Overview overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
                 isInState(LauncherState.OVERVIEW));
diff --git a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
index ea7c137..b7f2243 100644
--- a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
@@ -127,7 +127,7 @@
     @Test
     public void testAddItem_some_items_added() throws Exception {
         Callbacks callbacks = mock(Callbacks.class);
-        mModelHelper.getModel().initialize(callbacks);
+        mModelHelper.getModel().addCallbacks(callbacks);
 
         WorkspaceItemInfo info = new WorkspaceItemInfo();
         info.intent = new Intent().setComponent(mComponent1);
diff --git a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
new file mode 100644
index 0000000..6223760
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 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.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME;
+import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+
+import static org.junit.Assert.assertTrue;
+
+import android.database.sqlite.SQLiteDatabase;
+
+import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+
+/**
+ * Tests to verify backup and restore flow.
+ */
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(LooperMode.Mode.PAUSED)
+public class BackupRestoreTest {
+
+    private LauncherModelHelper mModelHelper;
+    private SQLiteDatabase mDb;
+
+    @Before
+    public void setUp() {
+        mModelHelper = new LauncherModelHelper();
+        RestoreDbTask.setPending(RuntimeEnvironment.application, true);
+        mDb = mModelHelper.provider.getDb();
+    }
+
+    @Test
+    public void testOnCreateDbIfNotExists_CreatesBackup() {
+        assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index e0ddcb1..f8ac010 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -16,8 +16,9 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
 import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
 import static org.robolectric.Shadows.shadowOf;
 import static org.robolectric.util.ReflectionHelpers.setField;
 
@@ -26,14 +27,10 @@
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageInstaller.SessionParams;
-import android.net.Uri;
-import android.provider.Settings;
 
 import com.android.launcher3.FolderInfo;
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.model.BgDataModel.Callbacks;
@@ -48,12 +45,7 @@
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.annotation.LooperMode.Mode;
-import org.robolectric.shadows.ShadowPackageManager;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStreamWriter;
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 
 /**
@@ -63,40 +55,22 @@
 @LooperMode(Mode.PAUSED)
 public class DefaultLayoutProviderTest {
 
-    private static final String SETTINGS_APP = "com.android.settings";
-    private static final String TEST_PROVIDER_AUTHORITY =
-            DefaultLayoutProviderTest.class.getName().toLowerCase();
-
-    private static final int BITMAP_SIZE = 10;
-    private static final int GRID_SIZE = 4;
-
     private LauncherModelHelper mModelHelper;
     private Context mTargetContext;
-    private InvariantDeviceProfile mIdp;
 
     @Before
     public void setUp() {
         mModelHelper = new LauncherModelHelper();
         mTargetContext = RuntimeEnvironment.application;
 
-        mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
-        mIdp.numRows = mIdp.numColumns = mIdp.numHotseatIcons = GRID_SIZE;
-        mIdp.iconBitmapSize = BITMAP_SIZE;
-
-        mModelHelper.provider.setAllowLoadDefaultFavorites(true);
-        Settings.Secure.putString(mTargetContext.getContentResolver(),
-                "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
-
-        ShadowPackageManager spm = shadowOf(mTargetContext.getPackageManager());
-        spm.addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
-                TEST_PROVIDER_AUTHORITY;
-        spm.addActivityIfNotPresent(new ComponentName(SETTINGS_APP, SETTINGS_APP));
+        shadowOf(mTargetContext.getPackageManager())
+                .addActivityIfNotPresent(new ComponentName(TEST_PACKAGE, TEST_PACKAGE));
     }
 
     @Test
     public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
         writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0)
-                .putApp(SETTINGS_APP, SETTINGS_APP));
+                .putApp(TEST_PACKAGE, TEST_PACKAGE));
 
         // Verify one item in hotseat
         assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
@@ -108,9 +82,9 @@
     @Test
     public void testCustomProfileLoaded_with_folder() throws Exception {
         writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
-                .addApp(SETTINGS_APP, SETTINGS_APP)
-                .addApp(SETTINGS_APP, SETTINGS_APP)
-                .addApp(SETTINGS_APP, SETTINGS_APP)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
                 .build());
 
         // Verify folder
@@ -146,19 +120,13 @@
     }
 
     private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
-        builder.build(new OutputStreamWriter(bos));
-
-        Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, mTargetContext);
-        shadowOf(mTargetContext.getContentResolver()).registerInputStream(layoutUri,
-                new ByteArrayInputStream(bos.toByteArray()));
+        mModelHelper.setupDefaultLayoutProvider(builder);
 
         LoaderResults results = new LoaderResults(
                 LauncherAppState.getInstance(mTargetContext),
                 mModelHelper.getBgDataModel(),
                 mModelHelper.getAllAppsList(),
-                0,
-                new WeakReference<>(mock(Callbacks.class)));
+                new Callbacks[0]);
         LoaderTask task = new LoaderTask(
                 LauncherAppState.getInstance(mTargetContext),
                 mModelHelper.getAllAppsList(),
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index 8dd7588..1ed4bca 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -190,7 +190,7 @@
 
     @Test
     public void testWorkspace_items_not_merged_in_next_screen() throws Exception {
-        // First screen has 2 items that need to be moved, but second screen has only one
+        // First screen has 2 mItems that need to be moved, but second screen has only one
         // empty space after migration (top-left corner)
         int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0,  0,  1},
@@ -277,7 +277,7 @@
     }
 
     /**
-     * Verifies that the workspace items are arranged in the provided order.
+     * Verifies that the workspace mItems are arranged in the provided order.
      * @param ids A 3d array where the first dimension represents the screen, and the rest two
      *            represent the workspace grid.
      */
diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 4854314..7fa3ee9 100644
--- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -46,7 +46,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.LauncherApps;
 import android.database.MatrixCursor;
 import android.os.Process;
 
@@ -77,7 +76,6 @@
     private MatrixCursor mCursor;
     private InvariantDeviceProfile mIDP;
     private Context mContext;
-    private LauncherApps mLauncherApps;
 
     private LoaderCursor mLoaderCursor;
 
@@ -86,7 +84,6 @@
         mContext = RuntimeEnvironment.application;
         mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
         mApp = LauncherAppState.getInstance(mContext);
-        mLauncherApps = mContext.getSystemService(LauncherApps.class);
 
         mCursor = new MatrixCursor(new String[] {
                 ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE,
@@ -174,7 +171,7 @@
         mIDP.numColumns = 4;
         mIDP.numHotseatIcons = 3;
 
-        // Overlapping items are not placed
+        // Overlapping mItems are not placed
         assertTrue(mLoaderCursor.checkItemPlacement(
                 newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1)));
         assertFalse(mLoaderCursor.checkItemPlacement(
@@ -200,7 +197,7 @@
         mIDP.numColumns = 4;
         mIDP.numHotseatIcons = 3;
 
-        // Hotseat items are only placed based on screenId
+        // Hotseat mItems are only placed based on screenId
         assertTrue(mLoaderCursor.checkItemPlacement(
                 newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1)));
         assertTrue(mLoaderCursor.checkItemPlacement(
diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
new file mode 100644
index 0000000..c7979b2
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 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.model;
+
+import static com.android.launcher3.util.Executors.createAndStartNewForegroundLooper;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.spy;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Process;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+import com.android.launcher3.util.LooperExecutor;
+import com.android.launcher3.util.ViewOnDrawExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Tests to verify multiple callbacks in Loader
+ */
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class ModelMultiCallbacksTest {
+
+    private LauncherModelHelper mModelHelper;
+
+    private ShadowPackageManager mSpm;
+    private LooperExecutor mTempMainExecutor;
+
+    @Before
+    public void setUp() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mModelHelper.installApp(TEST_PACKAGE);
+
+        mSpm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+
+        // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
+        // so that we can wait appropriately for the loader to complete.
+        mTempMainExecutor = new LooperExecutor(createAndStartNewForegroundLooper("tempMain"));
+        ReflectionHelpers.setField(mModelHelper.getModel(), "mMainExecutor", mTempMainExecutor);
+    }
+
+    @Test
+    public void testTwoCallbacks_loadedTogether() throws Exception {
+        setupWorkspacePages(3);
+
+        MyCallbacks cb1 = spy(MyCallbacks.class);
+        mModelHelper.getModel().addCallbacksAndLoad(cb1);
+
+        waitForLoaderAndTempMainThread();
+        cb1.verifySynchronouslyBound(3);
+
+        // Add a new callback
+        cb1.reset();
+        MyCallbacks cb2 = spy(MyCallbacks.class);
+        cb2.mPageToBindSync = 2;
+        mModelHelper.getModel().addCallbacksAndLoad(cb2);
+
+        waitForLoaderAndTempMainThread();
+        cb1.verifySynchronouslyBound(3);
+        cb2.verifySynchronouslyBound(3);
+
+        // Remove callbacks
+        cb1.reset();
+        cb2.reset();
+
+        // No effect on callbacks when removing an callback
+        mModelHelper.getModel().removeCallbacks(cb2);
+        waitForLoaderAndTempMainThread();
+        assertNull(cb1.mDeferredExecutor);
+        assertNull(cb2.mDeferredExecutor);
+
+        // Reloading only loads registered callbacks
+        mModelHelper.getModel().startLoader();
+        waitForLoaderAndTempMainThread();
+        cb1.verifySynchronouslyBound(3);
+        assertNull(cb2.mDeferredExecutor);
+    }
+
+    @Test
+    public void testTwoCallbacks_receiveUpdates() throws Exception {
+        setupWorkspacePages(1);
+
+        MyCallbacks cb1 = spy(MyCallbacks.class);
+        MyCallbacks cb2 = spy(MyCallbacks.class);
+        mModelHelper.getModel().addCallbacksAndLoad(cb1);
+        mModelHelper.getModel().addCallbacksAndLoad(cb2);
+        waitForLoaderAndTempMainThread();
+
+        cb1.verifyApps(TEST_PACKAGE);
+        cb2.verifyApps(TEST_PACKAGE);
+
+        // Install package 1
+        String pkg1 = "com.test.pkg1";
+        mModelHelper.installApp(pkg1);
+        mModelHelper.getModel().onPackageAdded(pkg1, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE, pkg1);
+        cb2.verifyApps(TEST_PACKAGE, pkg1);
+
+        // Install package 2
+        String pkg2 = "com.test.pkg2";
+        mModelHelper.installApp(pkg2);
+        mModelHelper.getModel().onPackageAdded(pkg2, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+        cb2.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+
+        // Uninstall package 2
+        mSpm.removePackage(pkg1);
+        mModelHelper.getModel().onPackageRemoved(pkg1, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE, pkg2);
+        cb2.verifyApps(TEST_PACKAGE, pkg2);
+
+        // Unregister a callback and verify updates no longer received
+        mModelHelper.getModel().removeCallbacks(cb2);
+        mSpm.removePackage(pkg2);
+        mModelHelper.getModel().onPackageRemoved(pkg2, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE);
+        cb2.verifyApps(TEST_PACKAGE, pkg2);
+    }
+
+    private void waitForLoaderAndTempMainThread() throws Exception {
+        Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+        mTempMainExecutor.submit(() -> { }).get();
+    }
+
+    private void setupWorkspacePages(int pageCount) throws Exception {
+        // Create a layout with 3 pages
+        LauncherLayoutBuilder builder = new LauncherLayoutBuilder();
+        for (int i = 0; i < pageCount; i++) {
+            builder.atWorkspace(1, 1, i).putApp(TEST_PACKAGE, TEST_PACKAGE);
+        }
+        mModelHelper.setupDefaultLayoutProvider(builder);
+    }
+
+    private abstract static class MyCallbacks implements Callbacks {
+
+        final List<ItemInfo> mItems = new ArrayList<>();
+        int mPageToBindSync = 0;
+        int mPageBoundSync = PagedView.INVALID_PAGE;
+        ViewOnDrawExecutor mDeferredExecutor;
+        AppInfo[] mAppInfos;
+
+        MyCallbacks() { }
+
+        @Override
+        public void onPageBoundSynchronously(int page) {
+            mPageBoundSync = page;
+        }
+
+        @Override
+        public void executeOnNextDraw(ViewOnDrawExecutor executor) {
+            mDeferredExecutor = executor;
+        }
+
+        @Override
+        public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
+            mItems.addAll(shortcuts);
+        }
+
+        @Override
+        public void bindAllApplications(AppInfo[] apps) {
+            mAppInfos = apps;
+        }
+
+        @Override
+        public int getPageToBindSynchronously() {
+            return mPageToBindSync;
+        }
+
+        public void reset() {
+            mItems.clear();
+            mPageBoundSync = PagedView.INVALID_PAGE;
+            mDeferredExecutor = null;
+            mAppInfos = null;
+        }
+
+        public void verifySynchronouslyBound(int totalItems) {
+            // Verify that the requested page is bound synchronously
+            assertEquals(mPageBoundSync, mPageToBindSync);
+            assertEquals(mItems.size(), 1);
+            assertEquals(mItems.get(0).screenId, mPageBoundSync);
+            assertNotNull(mDeferredExecutor);
+
+            // Verify that all other pages are bound properly
+            mDeferredExecutor.runAllTasks();
+            assertEquals(mItems.size(), totalItems);
+        }
+
+        public void verifyApps(String... apps) {
+            assertEquals(apps.length, mAppInfos.length);
+            assertEquals(Arrays.stream(mAppInfos)
+                    .map(ai -> ai.getTargetComponent().getPackageName())
+                    .collect(Collectors.toSet()),
+                    new HashSet<>(Arrays.asList(apps)));
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
index ccbc18a..166e28b 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
@@ -43,6 +43,7 @@
 
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * Extension of {@link ShadowLauncherApps} with missing shadow methods
@@ -93,4 +94,26 @@
         return RuntimeEnvironment.application.getPackageManager()
                 .getApplicationInfo(packageName, flags);
     }
+
+    @Implementation
+    public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
+        Intent intent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_LAUNCHER)
+                .setPackage(packageName);
+        return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
+                .stream()
+                .map(ri -> getLauncherActivityInfo(ri.activityInfo))
+                .collect(Collectors.toList());
+    }
+
+    @Implementation
+    public boolean hasShortcutHostPermission() {
+        return true;
+    }
+
+    @Override
+    protected List<LauncherActivityInfo> getShortcutConfigActivityList(String packageName,
+            UserHandle user) {
+        return Collections.emptyList();
+    }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 1a03f9f..e8b7157 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -20,13 +20,20 @@
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
 
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.Settings;
 
 import com.android.launcher3.AppInfo;
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
@@ -40,10 +47,14 @@
 import org.robolectric.Robolectric;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowPackageManager;
 import org.robolectric.util.ReflectionHelpers;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.lang.reflect.Field;
 import java.util.HashMap;
 import java.util.List;
@@ -63,6 +74,13 @@
     public static final int NO__ICON = -1;
     public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
 
+    // Authority for providing a dummy default-workspace-layout data.
+    private static final String TEST_PROVIDER_AUTHORITY =
+            LauncherModelHelper.class.getName().toLowerCase();
+    private static final int DEFAULT_BITMAP_SIZE = 10;
+    private static final int DEFAULT_GRID_SIZE = 4;
+
+
     private final HashMap<Class, HashMap<String, Field>> mFieldCache = new HashMap<>();
     public final TestLauncherProvider provider;
 
@@ -259,6 +277,8 @@
         Context context = RuntimeEnvironment.application;
         LauncherSettings.Settings.call(context.getContentResolver(),
                 LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+        LauncherSettings.Settings.call(context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
         int[][][] ids = new int[typeArray.length][][];
 
         for (int i = 0; i < typeArray.length; i++) {
@@ -285,4 +305,57 @@
 
         return ids;
     }
+
+    /**
+     * Sets up a dummy provider to load the provided layout by default, next time the layout loads
+     */
+    public void setupDefaultLayoutProvider(LauncherLayoutBuilder builder) throws Exception {
+        Context context = RuntimeEnvironment.application;
+        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
+        idp.numRows = idp.numColumns = idp.numHotseatIcons = DEFAULT_GRID_SIZE;
+        idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
+
+        Settings.Secure.putString(context.getContentResolver(),
+                "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
+
+        shadowOf(context.getPackageManager())
+                .addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
+                TEST_PROVIDER_AUTHORITY;
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        builder.build(new OutputStreamWriter(bos));
+        Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, context);
+        shadowOf(context.getContentResolver()).registerInputStream(layoutUri,
+                new ByteArrayInputStream(bos.toByteArray()));
+    }
+
+    /**
+     * Simulates an apk install with a default main activity with same class and package name
+     */
+    public void installApp(String component) throws NameNotFoundException {
+        ShadowPackageManager spm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+        ComponentName cn = new ComponentName(component, component);
+        spm.addActivityIfNotPresent(cn);
+
+        IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
+        filter.addCategory(Intent.CATEGORY_LAUNCHER);
+        filter.addCategory(Intent.CATEGORY_DEFAULT);
+        spm.addIntentFilterForActivity(cn, filter);
+    }
+
+    /**
+     * An extension of LauncherProvider backed up by in-memory database.
+     */
+    public static class TestLauncherProvider extends LauncherProvider {
+
+        @Override
+        public boolean onCreate() {
+            return true;
+        }
+
+        public SQLiteDatabase getDb() {
+            createDbIfNotExists();
+            return mOpenHelper.getWritableDatabase();
+        }
+    }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java b/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java
deleted file mode 100644
index 7e873e8..0000000
--- a/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.android.launcher3.util;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-
-import com.android.launcher3.LauncherProvider;
-
-/**
- * An extension of LauncherProvider backed up by in-memory database.
- */
-public class TestLauncherProvider extends LauncherProvider {
-
-    private boolean mAllowLoadDefaultFavorites;
-
-    @Override
-    public boolean onCreate() {
-        return true;
-    }
-
-    @Override
-    protected synchronized void createDbIfNotExists() {
-        if (mOpenHelper == null) {
-            mOpenHelper = new MyDatabaseHelper(getContext(), mAllowLoadDefaultFavorites);
-        }
-    }
-
-    public void setAllowLoadDefaultFavorites(boolean allowLoadDefaultFavorites) {
-        mAllowLoadDefaultFavorites = allowLoadDefaultFavorites;
-    }
-
-    public SQLiteDatabase getDb() {
-        createDbIfNotExists();
-        return mOpenHelper.getWritableDatabase();
-    }
-
-    private static class MyDatabaseHelper extends DatabaseHelper {
-
-        private final boolean mAllowLoadDefaultFavorites;
-
-        MyDatabaseHelper(Context context, boolean allowLoadDefaultFavorites) {
-            super(context, null);
-            mAllowLoadDefaultFavorites = allowLoadDefaultFavorites;
-            initIds();
-        }
-
-        @Override
-        public long getDefaultUserSerial() {
-            return 0;
-        }
-
-        @Override
-        protected void onEmptyDbCreated() {
-            if (mAllowLoadDefaultFavorites) {
-                super.onEmptyDbCreated();
-            }
-        }
-
-        @Override
-        protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { }
-    }
-}
diff --git a/settings.gradle b/settings.gradle
index b52bd4f..ce13bfb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,5 @@
 include ':IconLoader'
 project(':IconLoader').projectDir = new File(rootDir, 'iconloaderlib')
+
+include ':SharedLibWrapper'
+project(':SharedLibWrapper').projectDir = new File(rootDir, 'SharedLibWrapper')
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index bd48aec..423f2bb 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -126,7 +126,8 @@
             onAccessibilityDrop(null, item);
             ModelWriter modelWriter = mLauncher.getModelWriter();
             Runnable onUndoClicked = () -> {
-                modelWriter.abortDelete(itemPage);
+                mLauncher.setPageToBindSynchronously(itemPage);
+                modelWriter.abortDelete();
                 mLauncher.getUserEventDispatcher().logActionOnControl(TAP, UNDO);
             };
             Snackbar.show(mLauncher, R.string.item_removed, R.string.undo,
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 8b6d209..5b453c3 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -44,7 +44,7 @@
      * Implemented by listeners of the back key.
      */
     public interface OnBackKeyListener {
-        public boolean onBackKey();
+        boolean onBackKey();
     }
 
     private OnBackKeyListener mBackKeyListener;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index a7bb9ee..f5fafbf 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -292,6 +292,7 @@
     private PopupDataProvider mPopupDataProvider;
 
     private int mSynchronouslyBoundPage = PagedView.INVALID_PAGE;
+    private int mPageToBindSynchronously = PagedView.INVALID_PAGE;
 
     // We only want to get the SharedPreferences once since it does an FS stat each time we get
     // it from the context.
@@ -348,7 +349,7 @@
 
         LauncherAppState app = LauncherAppState.getInstance(this);
         mOldConfig = new Configuration(getResources().getConfiguration());
-        mModel = app.setLauncher(this);
+        mModel = app.getModel();
         mRotationHelper = new RotationHelper(this);
         InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
         initDeviceProfile(idp);
@@ -386,22 +387,18 @@
 
         // We only load the page synchronously if the user rotates (or triggers a
         // configuration change) while launcher is in the foreground
-        int currentScreen = PagedView.INVALID_RESTORE_PAGE;
+        int currentScreen = PagedView.INVALID_PAGE;
         if (savedInstanceState != null) {
             currentScreen = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, currentScreen);
         }
+        mPageToBindSynchronously = currentScreen;
 
-        if (!mModel.startLoader(currentScreen)) {
+        if (!mModel.addCallbacksAndLoad(this)) {
             if (!internalStateHandled) {
                 // If we are not binding synchronously, show a fade in animation when
                 // the first page bind completes.
                 mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).setValue(0);
             }
-        } else {
-            // Pages bound synchronously.
-            mWorkspace.setCurrentPage(currentScreen);
-
-            setWorkspaceLoading(true);
         }
 
         // For handling default keys
@@ -522,15 +519,6 @@
     }
 
     @Override
-    public void rebindModel() {
-        int currentPage = mWorkspace.getNextPage();
-        if (mModel.startLoader(currentPage)) {
-            mWorkspace.setCurrentPage(currentPage);
-            setWorkspaceLoading(true);
-        }
-    }
-
-    @Override
     public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
         onIdpChanged(idp);
     }
@@ -548,7 +536,7 @@
         // initialized properly.
         onSaveInstanceState(new Bundle());
         if (oldWallpaperProfile != getWallpaperDeviceProfile()) {
-            rebindModel();
+            mModel.rebindCallbacks();
         }
     }
 
@@ -1543,13 +1531,7 @@
         mWorkspace.removeFolderListeners();
         PluginManagerWrapper.INSTANCE.get(this).removePluginListener(this);
 
-        // Stop callbacks from LauncherModel
-        // It's possible to receive onDestroy after a new Launcher activity has
-        // been created. In this case, don't interfere with the new Launcher.
-        if (mModel.isCurrentCallbacks(this)) {
-            mModel.stopLoader();
-            LauncherAppState.getInstance(this).setLauncher(null);
-        }
+        mModel.removeCallbacks(this);
         mRotationHelper.destroy();
 
         try {
@@ -1957,11 +1939,21 @@
     }
 
     /**
+     * Sets the next page to bind synchronously on next bind.
+     * @param page
+     */
+    public void setPageToBindSynchronously(int page) {
+        mPageToBindSynchronously = page;
+    }
+
+    /**
      * Implementation of the method from LauncherModel.Callbacks.
      */
     @Override
-    public int getCurrentWorkspaceScreen() {
-        if (mWorkspace != null) {
+    public int getPageToBindSynchronously() {
+        if (mPageToBindSynchronously != PagedView.INVALID_PAGE) {
+            return mPageToBindSynchronously;
+        } else  if (mWorkspace != null) {
             return mWorkspace.getCurrentPage();
         } else {
             return 0;
@@ -2339,6 +2331,8 @@
 
     public void onPageBoundSynchronously(int page) {
         mSynchronouslyBoundPage = page;
+        mWorkspace.setCurrentPage(page);
+        mPageToBindSynchronously = PagedView.INVALID_PAGE;
     }
 
     @Override
@@ -2403,6 +2397,7 @@
         // Since we are just resetting the current page without user interaction,
         // override the previous page so we don't log the page switch.
         mWorkspace.setCurrentPage(pageBoundFirst, pageBoundFirst /* overridePrevPage */);
+        mPageToBindSynchronously = PagedView.INVALID_PAGE;
 
         // Cache one page worth of icons
         getViewCache().setCacheSize(R.layout.folder_application,
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index c6946ca..4cd038d 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -160,11 +160,6 @@
         }
     }
 
-    LauncherModel setLauncher(Launcher launcher) {
-        mModel.initialize(launcher);
-        return mModel;
-    }
-
     public IconCache getIconCache() {
         return mIconCache;
     }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 63b0e1e..cf978b5 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -16,6 +16,12 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
+import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherApps;
@@ -52,13 +58,12 @@
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.Thunk;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -67,12 +72,6 @@
 import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
-import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
-import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
-
 /**
  * Maintains in-memory state of the Launcher. It is expected that there should be only one
  * LauncherModel object held in a static. Also provide APIs for updating the database state
@@ -83,11 +82,12 @@
 
     static final String TAG = "Launcher.Model";
 
-    @Thunk final LauncherAppState mApp;
-    @Thunk final Object mLock = new Object();
-    @Thunk
-    LoaderTask mLoaderTask;
-    @Thunk boolean mIsLoaderTaskRunning;
+    private final LauncherAppState mApp;
+    private final Object mLock = new Object();
+    private final LooperExecutor mMainExecutor = MAIN_EXECUTOR;
+
+    private LoaderTask mLoaderTask;
+    private boolean mIsLoaderTaskRunning;
 
     // Indicates whether the current model data is valid or not.
     // We start off with everything not loaded. After that, we assume that
@@ -100,7 +100,7 @@
         }
     }
 
-    @Thunk WeakReference<Callbacks> mCallbacks;
+    private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
 
     // < only access in worker thread >
     private final AllAppsList mBgAllAppsList;
@@ -141,9 +141,8 @@
      * Adds the provided items to the workspace.
      */
     public void addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList) {
-        Callbacks callbacks = getCallback();
-        if (callbacks != null) {
-            callbacks.preAddApps();
+        for (Callbacks cb : getCallbacks()) {
+            cb.preAddApps();
         }
         enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
     }
@@ -153,16 +152,6 @@
                 hasVerticalHotseat, verifyChanges);
     }
 
-    /**
-     * Set this as the current Launcher activity object for the loader.
-     */
-    public void initialize(Callbacks callbacks) {
-        synchronized (mLock) {
-            Preconditions.assertUIThread();
-            mCallbacks = new WeakReference<>(callbacks);
-        }
-    }
-
     @Override
     public void onPackageChanged(String packageName, UserHandle user) {
         int op = PackageUpdatedTask.OP_UPDATE;
@@ -262,21 +251,19 @@
                 }
             }
         } else if (IS_DOGFOOD_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
-            Launcher l = (Launcher) getCallback();
-            l.reload();
+            for (Callbacks cb : getCallbacks()) {
+                if (cb instanceof Launcher) {
+                    ((Launcher) cb).recreate();
+                }
+            }
         }
     }
 
-    public void forceReload() {
-        forceReload(-1);
-    }
-
     /**
      * Reloads the workspace items from the DB and re-binds the workspace. This should generally
      * not be called as DB updates are automatically followed by UI update
-     * @param synchronousBindPage The page to bind first. Can pass -1 to use the current page.
      */
-    public void forceReload(int synchronousBindPage) {
+    public void forceReload() {
         synchronized (mLock) {
             // Stop any existing loaders first, so they don't set mModelLoaded to true later
             stopLoader();
@@ -285,37 +272,77 @@
 
         // Start the loader if launcher is already running, otherwise the loader will run,
         // the next time launcher starts
-        Callbacks callbacks = getCallback();
-        if (callbacks != null) {
-            if (synchronousBindPage < 0) {
-                synchronousBindPage = callbacks.getCurrentWorkspaceScreen();
-            }
-            startLoader(synchronousBindPage);
+        if (hasCallbacks()) {
+            startLoader();
         }
     }
 
-    public boolean isCurrentCallbacks(Callbacks callbacks) {
-        return (mCallbacks != null && mCallbacks.get() == callbacks);
+    /**
+     * Rebinds all existing callbacks with already loaded model
+     */
+    public void rebindCallbacks() {
+        if (hasCallbacks()) {
+            startLoader();
+        }
+    }
+
+    /**
+     * Removes an existing callback
+     */
+    public void removeCallbacks(Callbacks callbacks) {
+        synchronized (mCallbacksList) {
+            Preconditions.assertUIThread();
+            if (mCallbacksList.remove(callbacks)) {
+                if (stopLoader()) {
+                    // Rebind existing callbacks
+                    startLoader();
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     * @return true if workspace load was performed synchronously
+     */
+    public boolean addCallbacksAndLoad(Callbacks callbacks) {
+        synchronized (mLock) {
+            addCallbacks(callbacks);
+            return startLoader();
+
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     */
+    public void addCallbacks(Callbacks callbacks) {
+        Preconditions.assertUIThread();
+        synchronized (mCallbacksList) {
+            mCallbacksList.add(callbacks);
+        }
     }
 
     /**
      * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
      * @return true if the page could be bound synchronously.
      */
-    public boolean startLoader(int synchronousBindPage) {
+    public boolean startLoader() {
         // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
         InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
         synchronized (mLock) {
             // Don't bother to start the thread if we know it's not going to do anything
-            if (mCallbacks != null && mCallbacks.get() != null) {
-                final Callbacks oldCallbacks = mCallbacks.get();
+            final Callbacks[] callbacksList = getCallbacks();
+            if (callbacksList.length > 0) {
                 // Clear any pending bind-runnables from the synchronized load process.
-                MAIN_EXECUTOR.execute(oldCallbacks::clearPendingBinds);
+                for (Callbacks cb : callbacksList) {
+                    mMainExecutor.execute(cb::clearPendingBinds);
+                }
 
                 // If there is already one running, tell it to stop.
                 stopLoader();
-                LoaderResults loaderResults = new LoaderResults(mApp, mBgDataModel,
-                        mBgAllAppsList, synchronousBindPage, mCallbacks);
+                LoaderResults loaderResults = new LoaderResults(
+                        mApp, mBgDataModel, mBgAllAppsList, callbacksList, mMainExecutor);
                 if (mModelLoaded && !mIsLoaderTaskRunning) {
                     // Divide the set of loaded items into those that we are binding synchronously,
                     // and everything else that is to be bound normally (asynchronously).
@@ -336,14 +363,17 @@
 
     /**
      * If there is already a loader task running, tell it to stop.
+     * @return true if an existing loader was stopped.
      */
-    public void stopLoader() {
+    public boolean stopLoader() {
         synchronized (mLock) {
             LoaderTask oldTask = mLoaderTask;
             mLoaderTask = null;
             if (oldTask != null) {
                 oldTask.stopLocked();
+                return true;
             }
+            return false;
         }
     }
 
@@ -498,7 +528,7 @@
     }
 
     public void enqueueModelUpdateTask(ModelUpdateTask task) {
-        task.init(mApp, this, mBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
+        task.init(mApp, this, mBgDataModel, mBgAllAppsList, mMainExecutor);
         MODEL_EXECUTOR.execute(task);
     }
 
@@ -572,7 +602,21 @@
         mBgDataModel.dump(prefix, fd, writer, args);
     }
 
-    public Callbacks getCallback() {
-        return mCallbacks != null ? mCallbacks.get() : null;
+    /**
+     * Returns true if there are any callbacks attached to the model
+     */
+    public boolean hasCallbacks() {
+        synchronized (mCallbacksList) {
+            return !mCallbacksList.isEmpty();
+        }
+    }
+
+    /**
+     * Returns an array of currently attached callbacks
+     */
+    public Callbacks[] getCallbacks() {
+        synchronized (mCallbacksList) {
+            return mCallbacksList.toArray(new Callbacks[mCallbacksList.size()]);
+        }
     }
 }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 900c966..b0ab35c 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.annotation.TargetApi;
 import android.app.backup.BackupManager;
@@ -45,6 +46,7 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -87,6 +89,8 @@
     private static final boolean LOGD = false;
 
     private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
+    private static final String TOKEN_RESTORE_BACKUP_TABLE = "restore_backup_table";
+    private static final long RESTORE_BACKUP_TABLE_DELAY = 60000;
 
     /**
      * Represents the schema of the database. Changes in scheme need not be backwards compatible.
@@ -387,6 +391,14 @@
                         tableExists(mOpenHelper.getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
                 return null;
             }
+            case LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE: {
+                final Handler handler = MODEL_EXECUTOR.getHandler();
+                handler.removeCallbacksAndMessages(TOKEN_RESTORE_BACKUP_TABLE);
+                handler.postDelayed(() -> RestoreDbTask.restoreIfPossible(
+                        getContext(), mOpenHelper, new BackupManager(getContext())),
+                        TOKEN_RESTORE_BACKUP_TABLE, RESTORE_BACKUP_TABLE_DELAY);
+                return null;
+            }
         }
         return null;
     }
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index ec307db..4c5c61c 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -300,6 +300,8 @@
 
         public static final String METHOD_REFRESH_BACKUP_TABLE = "refresh_backup_table";
 
+        public static final String METHOD_RESTORE_BACKUP_TABLE = "restore_backup_table";
+
         public static final String EXTRA_VALUE = "value";
 
         public static Bundle call(ContentResolver cr, String method) {
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index ff2b400..a1888bf 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -64,7 +64,7 @@
     private static final String TAG = "PagedView";
     private static final boolean DEBUG = false;
 
-    protected static final int INVALID_PAGE = -1;
+    public static final int INVALID_PAGE = -1;
     protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE;
 
     public static final int PAGE_SNAP_ANIMATION_DURATION = 750;
@@ -84,8 +84,6 @@
     private static final int MIN_SNAP_VELOCITY = 1500;
     private static final int MIN_FLING_VELOCITY = 250;
 
-    public static final int INVALID_RESTORE_PAGE = -1001;
-
     private boolean mFreeScroll = false;
 
     protected int mFlingThresholdVelocity;
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index f0bae02..89f0a3d 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -78,6 +78,7 @@
         }
 
         InstallSessionHelper packageInstallerCompat = InstallSessionHelper.INSTANCE.get(context);
+        packageInstallerCompat.restoreDbIfApplicable(info);
         if (TextUtils.isEmpty(info.getAppPackageName())
                 || info.getInstallReason() != PackageManager.INSTALL_REASON_USER
                 || packageInstallerCompat.promiseIconAddedForId(info.getSessionId())) {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 81dcba3..75609fe 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -136,6 +136,10 @@
     public static final TogglableFlag ENABLE_OVERVIEW_ACTIONS = new TogglableFlag(
             "ENABLE_OVERVIEW_ACTIONS", false, "Show app actions in Overview");
 
+    public static final TogglableFlag ENABLE_DATABASE_RESTORE = new TogglableFlag(
+            "ENABLE_DATABASE_RESTORE", true,
+            "Enable database restore when new restore session is created");
+
     public static void initialize(Context context) {
         // Avoid the disk read for user builds
         if (Utilities.IS_DEBUG_DEVICE) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 52d8f7f..844189f 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -297,16 +297,22 @@
     }
 
     public void startEditingFolderName() {
-        post(new Runnable() {
-            @Override
-            public void run() {
-                mFolderName.setHint("");
-                mIsEditingName = true;
+        post(() -> {
+            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+                if (TextUtils.isEmpty(mFolderName.getText())) {
+                    final String[] suggestedNames = new String[FolderNameProvider.SUGGEST_MAX];
+                    mLauncher.getFolderNameProvider().getSuggestedFolderName(getContext(),
+                            mInfo.contents, suggestedNames);
+                    mFolderName.setText(suggestedNames[0]);
+                    mFolderName.displayCompletions(Arrays.asList(suggestedNames).subList(1,
+                            suggestedNames.length));
+                }
             }
+            mFolderName.setHint("");
+            mIsEditingName = true;
         });
     }
 
-
     @Override
     public boolean onBackKey() {
         // Convert to a string here to ensure that no other state associated with the text field
@@ -316,10 +322,18 @@
         mFolderIcon.onTitleChanged(newTitle);
         mLauncher.getModelWriter().updateItemInDatabase(mInfo);
 
-        if (TextUtils.isEmpty(mInfo.title)) {
-            mFolderName.setHint(R.string.folder_hint_text);
+        if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+            mFolderName.setText(mInfo.title);
+            // TODO: depending on whether the title was manually edited or automatically
+            // suggested, apply different hint.
+            mFolderName.setHint("");
         } else {
-            mFolderName.setHint(null);
+            if (TextUtils.isEmpty(mInfo.title)) {
+                mFolderName.setHint(R.string.folder_hint_text);
+                mFolderName.setText("");
+            } else {
+                mFolderName.setHint(null);
+            }
         }
 
         sendCustomAccessibilityEvent(
@@ -403,7 +417,11 @@
             mFolderName.setHint(null);
         } else {
             mFolderName.setText("");
-            mFolderName.setHint(R.string.folder_hint_text);
+            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+                mFolderName.setHint("");
+            } else {
+                mFolderName.setHint(R.string.folder_hint_text);
+            }
         }
         // In case any children didn't come across during loading, clean up the folder accordingly
         mFolderIcon.post(() -> {
@@ -420,7 +438,7 @@
         if (FeatureFlags.FOLDER_NAME_SUGGEST.get()
                 && TextUtils.isEmpty(mFolderName.getText().toString())) {
             if (suggestName.length > 0 && !TextUtils.isEmpty(suggestName[0])) {
-                mFolderName.setHint(suggestName[0]);
+                mFolderName.setHint("");
                 mFolderName.setText(suggestName[0]);
                 mInfo.title = suggestName[0];
                 animateOpen(mInfo.contents, 0, true);
@@ -534,6 +552,9 @@
             openFolder.close(true);
         }
 
+        if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+            mLauncher.getFolderNameProvider().load(getContext());
+        }
         mContent.bindItems(items);
         centerAboutIcon();
         mItemsInvalidated = true;
@@ -1350,6 +1371,7 @@
         return itemsOnCurrentPage;
     }
 
+    @Override
     public void onFocusChange(View v, boolean hasFocus) {
         if (v == mFolderName) {
             if (hasFocus) {
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index e58d484..d76b73f 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -18,13 +18,16 @@
 import android.content.Context;
 import android.os.Process;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.config.FeatureFlags;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -38,16 +41,26 @@
  */
 public class FolderNameProvider {
 
+    private static final String TAG = FeatureFlags.FOLDER_NAME_SUGGEST.getKey();
+    private static final boolean DEBUG = FeatureFlags.FOLDER_NAME_SUGGEST.get();
+
     /**
      * IME usually has up to 3 suggest slots. In total, there are 4 suggest slots as the folder
      * name edit box can also be used to provide suggestion.
      */
     public static final int SUGGEST_MAX = 4;
 
+    /**
+     * When inheriting class requires precaching, override this method.
+     */
+    public void load(Context context) {}
+
     public CharSequence getSuggestedFolderName(Context context,
             ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] candidates) {
 
-        CharSequence suggest;
+        if (DEBUG) {
+            Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates));
+        }
         // If all the icons are from work profile,
         // Then, suggest "Work" as the folder name
         List<WorkspaceItemInfo> distinctItemInfos = workspaceItemInfos.stream()
@@ -75,19 +88,28 @@
             // Place it as first viable suggestion and shift everything else
             info.ifPresent(i -> setAsFirstSuggestion(candidates, i.title.toString()));
         }
+        if (DEBUG) {
+            Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates));
+        }
         return candidates[0];
     }
 
     private void setAsFirstSuggestion(CharSequence[] candidatesOut, CharSequence candidate) {
-        for (int i = candidatesOut.length - 1; i > 0; i--) {
-            if (TextUtils.isEmpty(candidatesOut[i])) {
-                candidatesOut[i - 1] = candidatesOut[i];
-            }
-            candidatesOut[0] = candidate;
+        if (contains(candidatesOut, candidate)) {
+            return;
         }
+        for (int i = candidatesOut.length - 1; i > 0; i--) {
+            if (!TextUtils.isEmpty(candidatesOut[i - 1])) {
+                candidatesOut[i] = candidatesOut[i - 1];
+            }
+        }
+        candidatesOut[0] = candidate;
     }
 
     private void setAsLastSuggestion(CharSequence[] candidatesOut, CharSequence candidate) {
+        if (contains(candidatesOut, candidate)) {
+            return;
+        }
         for (int i = 0; i < candidate.length(); i++) {
             if (TextUtils.isEmpty(candidatesOut[i])) {
                 candidatesOut[i] = candidate;
@@ -95,6 +117,12 @@
         }
     }
 
+    private boolean contains(CharSequence[] list, CharSequence key) {
+        return Arrays.asList(list).stream()
+                .filter(s -> s != null)
+                .anyMatch(s -> s.toString().equalsIgnoreCase(key.toString()));
+    }
+
     // This method can be moved to some Utility class location.
     private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
         Map<Object, Boolean> map = new ConcurrentHashMap<>();
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index bfe7351..def76e8 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -70,6 +70,7 @@
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.model.AllAppsList;
 import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.LoaderResults;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
@@ -377,7 +378,7 @@
             if (!mModel.isModelLoaded()) {
                 Log.d(TAG, "Workspace not loaded, loading now");
                 mModel.startLoaderForResults(
-                        new LoaderResults(mApp, mBgDataModel, mAllAppsList, 0, null));
+                        new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
                 return new ArrayList<>();
             }
             return mBgDataModel.workspaceItems;
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 76c2951..0d12183 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -18,9 +18,7 @@
 
 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
 import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-import android.os.Looper;
 import android.util.Log;
 
 import com.android.launcher3.AppInfo;
@@ -32,12 +30,13 @@
 import com.android.launcher3.PagedView;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.LooperIdleLock;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -49,40 +48,29 @@
     protected static final int INVALID_SCREEN_ID = -1;
     private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons
 
-    protected final Executor mUiExecutor;
+    protected final LooperExecutor mUiExecutor;
 
     protected final LauncherAppState mApp;
     protected final BgDataModel mBgDataModel;
     private final AllAppsList mBgAllAppsList;
-    protected final int mPageToBindFirst;
 
-    protected final WeakReference<Callbacks> mCallbacks;
+    private final Callbacks[] mCallbacksList;
 
     private int mMyBindingId;
 
     public BaseLoaderResults(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        mUiExecutor = MAIN_EXECUTOR;
+            AllAppsList allAppsList, Callbacks[] callbacksList, LooperExecutor uiExecutor) {
+        mUiExecutor = uiExecutor;
         mApp = app;
         mBgDataModel = dataModel;
         mBgAllAppsList = allAppsList;
-        mPageToBindFirst = pageToBindFirst;
-        mCallbacks = callbacks == null ? new WeakReference<>(null) : callbacks;
+        mCallbacksList = callbacksList;
     }
 
     /**
      * Binds all loaded data to actual views on the main thread.
      */
     public void bindWorkspace() {
-        Callbacks callbacks = mCallbacks.get();
-        // Don't use these two variables in any of the callback runnables.
-        // Otherwise we hold a reference to them.
-        if (callbacks == null) {
-            // This launcher has exited and nobody bothered to tell us.  Just bail.
-            Log.w(TAG, "LoaderTask running with no launcher");
-            return;
-        }
-
         // Save a copy of all the bg-thread collections
         ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
         ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
@@ -96,97 +84,9 @@
             mMyBindingId = mBgDataModel.lastBindId;
         }
 
-        final int currentScreen;
-        {
-            int currScreen = mPageToBindFirst != PagedView.INVALID_RESTORE_PAGE
-                    ? mPageToBindFirst : callbacks.getCurrentWorkspaceScreen();
-            if (currScreen >= orderedScreenIds.size()) {
-                // There may be no workspace screens (just hotseat items and an empty page).
-                currScreen = PagedView.INVALID_RESTORE_PAGE;
-            }
-            currentScreen = currScreen;
-        }
-        final boolean validFirstPage = currentScreen >= 0;
-        final int currentScreenId =
-                validFirstPage ? orderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
-
-        // Separate the items that are on the current screen, and all the other remaining items
-        ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
-        ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
-
-        filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
-                otherWorkspaceItems);
-        filterCurrentWorkspaceItems(currentScreenId, appWidgets, currentAppWidgets,
-                otherAppWidgets);
-        final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
-        sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
-        sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
-
-        // Tell the workspace that we're about to start binding items
-        executeCallbacksTask(c -> {
-            c.clearPendingBinds();
-            c.startBinding();
-        }, mUiExecutor);
-
-        // Bind workspace screens
-        executeCallbacksTask(c -> c.bindScreens(orderedScreenIds), mUiExecutor);
-
-        Executor mainExecutor = mUiExecutor;
-        // Load items on the current page.
-        bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
-        bindAppWidgets(currentAppWidgets, mainExecutor);
-        // In case of validFirstPage, only bind the first screen, and defer binding the
-        // remaining screens after first onDraw (and an optional the fade animation whichever
-        // happens later).
-        // This ensures that the first screen is immediately visible (eg. during rotation)
-        // In case of !validFirstPage, bind all pages one after other.
-        final Executor deferredExecutor =
-                validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
-
-        executeCallbacksTask(c -> c.finishFirstPageBind(
-                validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
-
-        bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
-        bindAppWidgets(otherAppWidgets, deferredExecutor);
-        // Tell the workspace that we're done binding items
-        executeCallbacksTask(c -> c.finishBindingItems(mPageToBindFirst), deferredExecutor);
-
-        if (validFirstPage) {
-            executeCallbacksTask(c -> {
-                // We are loading synchronously, which means, some of the pages will be
-                // bound after first draw. Inform the callbacks that page binding is
-                // not complete, and schedule the remaining pages.
-                if (currentScreen != PagedView.INVALID_RESTORE_PAGE) {
-                    c.onPageBoundSynchronously(currentScreen);
-                }
-                c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
-
-            }, mUiExecutor);
-        }
-    }
-
-    protected void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems,
-            final Executor executor) {
-        // Bind the workspace items
-        int N = workspaceItems.size();
-        for (int i = 0; i < N; i += ITEMS_CHUNK) {
-            final int start = i;
-            final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
-            executeCallbacksTask(
-                    c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
-                    executor);
-        }
-    }
-
-    private void bindAppWidgets(ArrayList<LauncherAppWidgetInfo> appWidgets, Executor executor) {
-        int N;// Bind the widgets, one at a time
-        N = appWidgets.size();
-        for (int i = 0; i < N; i++) {
-            final ItemInfo widget = appWidgets.get(i);
-            executeCallbacksTask(
-                    c -> c.bindItems(Collections.singletonList(widget), false), executor);
+        for (Callbacks cb : mCallbacksList) {
+            new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
+                    workspaceItems, appWidgets, orderedScreenIds).bind();
         }
     }
 
@@ -206,19 +106,155 @@
                 Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
                 return;
             }
-            Callbacks callbacks = mCallbacks.get();
-            if (callbacks != null) {
-                task.execute(callbacks);
+            for (Callbacks cb : mCallbacksList) {
+                task.execute(cb);
             }
         });
     }
 
     public LooperIdleLock newIdleLock(Object lock) {
-        LooperIdleLock idleLock = new LooperIdleLock(lock, Looper.getMainLooper());
+        LooperIdleLock idleLock = new LooperIdleLock(lock, mUiExecutor.getLooper());
         // If we are not binding or if the main looper is already idle, there is no reason to wait
-        if (mCallbacks.get() == null || Looper.getMainLooper().getQueue().isIdle()) {
+        if (mUiExecutor.getLooper().getQueue().isIdle()) {
             idleLock.queueIdle();
         }
         return idleLock;
     }
+
+    private static class WorkspaceBinder {
+
+        private final Executor mUiExecutor;
+        private final Callbacks mCallbacks;
+
+        private final LauncherAppState mApp;
+        private final BgDataModel mBgDataModel;
+
+        private final int mMyBindingId;
+        private final ArrayList<ItemInfo> mWorkspaceItems;
+        private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
+        private final IntArray mOrderedScreenIds;
+
+
+        WorkspaceBinder(Callbacks callbacks,
+                Executor uiExecutor,
+                LauncherAppState app,
+                BgDataModel bgDataModel,
+                int myBindingId,
+                ArrayList<ItemInfo> workspaceItems,
+                ArrayList<LauncherAppWidgetInfo> appWidgets,
+                IntArray orderedScreenIds) {
+            mCallbacks = callbacks;
+            mUiExecutor = uiExecutor;
+            mApp = app;
+            mBgDataModel = bgDataModel;
+            mMyBindingId = myBindingId;
+            mWorkspaceItems = workspaceItems;
+            mAppWidgets = appWidgets;
+            mOrderedScreenIds = orderedScreenIds;
+        }
+
+        private void bind() {
+            final int currentScreen;
+            {
+                // Create an anonymous scope to calculate currentScreen as it has to be a
+                // final variable.
+                int currScreen = mCallbacks.getPageToBindSynchronously();
+                if (currScreen >= mOrderedScreenIds.size()) {
+                    // There may be no workspace screens (just hotseat items and an empty page).
+                    currScreen = PagedView.INVALID_PAGE;
+                }
+                currentScreen = currScreen;
+            }
+            final boolean validFirstPage = currentScreen >= 0;
+            final int currentScreenId =
+                    validFirstPage ? mOrderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
+
+            // Separate the items that are on the current screen, and all the other remaining items
+            ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
+            ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
+            ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
+            ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
+
+            filterCurrentWorkspaceItems(currentScreenId, mWorkspaceItems, currentWorkspaceItems,
+                    otherWorkspaceItems);
+            filterCurrentWorkspaceItems(currentScreenId, mAppWidgets, currentAppWidgets,
+                    otherAppWidgets);
+            final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
+            sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
+            sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
+
+            // Tell the workspace that we're about to start binding items
+            executeCallbacksTask(c -> {
+                c.clearPendingBinds();
+                c.startBinding();
+            }, mUiExecutor);
+
+            // Bind workspace screens
+            executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
+
+            Executor mainExecutor = mUiExecutor;
+            // Load items on the current page.
+            bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
+            bindAppWidgets(currentAppWidgets, mainExecutor);
+            // In case of validFirstPage, only bind the first screen, and defer binding the
+            // remaining screens after first onDraw (and an optional the fade animation whichever
+            // happens later).
+            // This ensures that the first screen is immediately visible (eg. during rotation)
+            // In case of !validFirstPage, bind all pages one after other.
+            final Executor deferredExecutor =
+                    validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
+
+            executeCallbacksTask(c -> c.finishFirstPageBind(
+                    validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
+
+            bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
+            bindAppWidgets(otherAppWidgets, deferredExecutor);
+            // Tell the workspace that we're done binding items
+            executeCallbacksTask(c -> c.finishBindingItems(currentScreen), deferredExecutor);
+
+            if (validFirstPage) {
+                executeCallbacksTask(c -> {
+                    // We are loading synchronously, which means, some of the pages will be
+                    // bound after first draw. Inform the mCallbacks that page binding is
+                    // not complete, and schedule the remaining pages.
+                    c.onPageBoundSynchronously(currentScreen);
+                    c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
+
+                }, mUiExecutor);
+            }
+        }
+
+        private void bindWorkspaceItems(
+                final ArrayList<ItemInfo> workspaceItems, final Executor executor) {
+            // Bind the workspace items
+            int count = workspaceItems.size();
+            for (int i = 0; i < count; i += ITEMS_CHUNK) {
+                final int start = i;
+                final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
+                executeCallbacksTask(
+                        c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
+                        executor);
+            }
+        }
+
+        private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets, Executor executor) {
+            // Bind the widgets, one at a time
+            int count = appWidgets.size();
+            for (int i = 0; i < count; i++) {
+                final ItemInfo widget = appWidgets.get(i);
+                executeCallbacksTask(
+                        c -> c.bindItems(Collections.singletonList(widget), false), executor);
+            }
+        }
+
+        protected void executeCallbacksTask(CallbackTask task, Executor executor) {
+            executor.execute(() -> {
+                if (mMyBindingId != mBgDataModel.lastBindId) {
+                    Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
+                    return;
+                }
+                task.execute(mCallbacks);
+            });
+        }
+    }
 }
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index e12633b..5a7b4d3 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -20,17 +20,16 @@
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.widget.WidgetListRowEntry;
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -78,13 +77,9 @@
      * Schedules a {@param task} to be executed on the current callbacks.
      */
     public final void scheduleCallbackTask(final CallbackTask task) {
-        final Callbacks callbacks = mModel.getCallback();
-        mUiExecutor.execute(() -> {
-            Callbacks cb = mModel.getCallback();
-            if (callbacks == cb && cb != null) {
-                task.execute(callbacks);
-            }
-        });
+        for (final Callbacks cb : mModel.getCallbacks()) {
+            mUiExecutor.execute(() -> task.execute(cb));
+        }
     }
 
     public ModelWriter getModelWriter() {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 88f2a09..c24b939 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -436,9 +436,10 @@
     }
 
     public interface Callbacks {
-        void rebindModel();
-
-        int getCurrentWorkspaceScreen();
+        /**
+         * Returns the page number to bind first, synchronously if possible or -1
+         */
+        int getPageToBindSynchronously();
         void clearPendingBinds();
         void startBinding();
         void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
diff --git a/src/com/android/launcher3/model/GridBackupTable.java b/src/com/android/launcher3/model/GridBackupTable.java
index 11d4edd..fc9948e 100644
--- a/src/com/android/launcher3/model/GridBackupTable.java
+++ b/src/com/android/launcher3/model/GridBackupTable.java
@@ -27,6 +27,8 @@
 import android.os.Process;
 import android.util.Log;
 
+import androidx.annotation.IntDef;
+
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
 import com.android.launcher3.pm.UserCache;
@@ -45,6 +47,19 @@
     private static final String KEY_GRID_Y_SIZE = Favorites.SPANY;
     private static final String KEY_DB_VERSION = Favorites.RANK;
 
+    public static final int OPTION_REQUIRES_SANITIZATION = 1;
+
+    /** STATE_NOT_FOUND indicates backup doesn't exist in the db. */
+    private static final int STATE_NOT_FOUND = 0;
+    /**
+     *  STATE_RAW indicates the backup has not yet been sanitized. This implies it might still
+     *  posses app info that doesn't exist in the workspace and needed to be sanitized before
+     *  put into use.
+     */
+    private static final int STATE_RAW = 1;
+    /** STATE_SANITIZED indicates the backup has already been sanitized, thus can be used as-is. */
+    private static final int STATE_SANITIZED = 2;
+
     private final Context mContext;
     private final SQLiteDatabase mDb;
 
@@ -56,6 +71,9 @@
     private int mRestoredGridX;
     private int mRestoredGridY;
 
+    @IntDef({STATE_NOT_FOUND, STATE_RAW, STATE_SANITIZED})
+    private @interface BackupState { }
+
     public GridBackupTable(Context context, SQLiteDatabase db,
             int hotseatSize, int gridX, int gridY) {
         mContext = context;
@@ -66,6 +84,10 @@
         mOldGridY = gridY;
     }
 
+    /**
+     * Create a backup from current workspace layout if one isn't created already (Note backup
+     * created this way is always sanitized). Otherwise restore from the backup instead.
+     */
     public boolean backupOrRestoreAsNeeded() {
         // Check if backup table exists
         if (!tableExists(mDb, BACKUP_TABLE_NAME)) {
@@ -74,16 +96,16 @@
                 // No need to copy if empty DB was created.
                 return false;
             }
-
-            copyTable(Favorites.TABLE_NAME, BACKUP_TABLE_NAME);
-            encodeDBProperties();
+            doBackup(UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+                    Process.myUserHandle()), 0);
             return false;
         }
-
-        if (!loadDbProperties()) {
+        if (loadDBProperties() != STATE_SANITIZED) {
             return false;
         }
-        copyTable(BACKUP_TABLE_NAME, Favorites.TABLE_NAME);
+        long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+                Process.myUserHandle());
+        copyTable(BACKUP_TABLE_NAME, Favorites.TABLE_NAME, userSerial);
         Log.d(TAG, "Backup table found");
         return true;
     }
@@ -93,43 +115,84 @@
         return mRestoredHotseatSize;
     }
 
-    private void copyTable(String from, String to) {
-        long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
-                Process.myUserHandle());
+    /**
+     * Copy valid grid entries from one table to another.
+     */
+    private void copyTable(String from, String to, long userSerial) {
         dropTable(mDb, to);
         Favorites.addTableToDb(mDb, userSerial, false, to);
         mDb.execSQL("INSERT INTO " + to + " SELECT * FROM " + from + " where _id > " + ID_PROPERTY);
     }
 
-    private void encodeDBProperties() {
+    private void encodeDBProperties(int options) {
         ContentValues values = new ContentValues();
         values.put(Favorites._ID, ID_PROPERTY);
         values.put(KEY_DB_VERSION, mDb.getVersion());
         values.put(KEY_GRID_X_SIZE, mOldGridX);
         values.put(KEY_GRID_Y_SIZE, mOldGridY);
         values.put(KEY_HOTSEAT_SIZE, mOldHotseatSize);
+        values.put(Favorites.OPTIONS, options);
         mDb.insert(BACKUP_TABLE_NAME, null, values);
     }
 
-    private boolean loadDbProperties() {
+    /**
+     * Load DB properties from grid backup table.
+     */
+    public @BackupState int loadDBProperties() {
         try (Cursor c = mDb.query(BACKUP_TABLE_NAME, new String[] {
-                        KEY_DB_VERSION,     // 0
-                        KEY_GRID_X_SIZE,    // 1
-                        KEY_GRID_Y_SIZE,    // 2
-                        KEY_HOTSEAT_SIZE},  // 3
+                KEY_DB_VERSION,     // 0
+                KEY_GRID_X_SIZE,    // 1
+                KEY_GRID_Y_SIZE,    // 2
+                KEY_HOTSEAT_SIZE,   // 3
+                Favorites.OPTIONS}, // 4
                 "_id=" + ID_PROPERTY, null, null, null, null)) {
             if (!c.moveToNext()) {
                 Log.e(TAG, "Meta data not found in backup table");
-                return false;
+                return STATE_NOT_FOUND;
             }
-            if (mDb.getVersion() != c.getInt(0)) {
-                return false;
+            if (!validateDBVersion(mDb.getVersion(), c.getInt(0))) {
+                return STATE_NOT_FOUND;
             }
 
             mRestoredGridX = c.getInt(1);
             mRestoredGridY = c.getInt(2);
             mRestoredHotseatSize = c.getInt(3);
-            return true;
+            boolean isSanitized = (c.getInt(4) & OPTION_REQUIRES_SANITIZATION) == 0;
+            return isSanitized ? STATE_SANITIZED : STATE_RAW;
         }
     }
+
+    /**
+     * Restore workspace from raw backup if available.
+     */
+    public boolean restoreFromRawBackupIfAvailable(long oldProfileId) {
+        if (!tableExists(mDb, Favorites.BACKUP_TABLE_NAME)
+                || loadDBProperties() != STATE_RAW
+                || mOldHotseatSize != mRestoredHotseatSize
+                || mOldGridX != mRestoredGridX
+                || mOldGridY != mRestoredGridY) {
+            // skip restore if dimensions in backup table differs from current setup.
+            return false;
+        }
+        copyTable(Favorites.BACKUP_TABLE_NAME, Favorites.TABLE_NAME, oldProfileId);
+        Log.d(TAG, "Backup restored");
+        return true;
+    }
+
+    /**
+     * Performs a backup on the workspace layout.
+     */
+    public void doBackup(long profileId, int options) {
+        copyTable(Favorites.TABLE_NAME, Favorites.BACKUP_TABLE_NAME, profileId);
+        encodeDBProperties(options);
+    }
+
+    private static boolean validateDBVersion(int expected, int actual) {
+        if (expected != actual) {
+            Log.e(TAG, String.format("Launcher.db version mismatch, expecting %d but %d was found",
+                    expected, actual));
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/src/com/android/launcher3/model/ModelPreload.java b/src/com/android/launcher3/model/ModelPreload.java
index 2bd6cd4..713492b 100644
--- a/src/com/android/launcher3/model/ModelPreload.java
+++ b/src/com/android/launcher3/model/ModelPreload.java
@@ -18,14 +18,15 @@
 import android.content.Context;
 import android.util.Log;
 
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 
 import java.util.concurrent.Executor;
 
-import androidx.annotation.WorkerThread;
-
 /**
  * Utility class to preload LauncherModel
  */
@@ -50,7 +51,7 @@
     @Override
     public final void run() {
         mModel.startLoaderForResultsIfNotLoaded(
-                new LoaderResults(mApp, mBgDataModel, mAllAppsList, 0, null));
+                new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
         Log.d(TAG, "Preload completed : " + mModel.isModelLoaded());
         onComplete(mModel.isModelLoaded());
     }
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index bdf3a69..ccd1554 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -41,7 +41,6 @@
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.ItemInfoMatcher;
 
@@ -350,12 +349,15 @@
         mDeleteRunnables.clear();
     }
 
-    public void abortDelete(int pageToBindFirst) {
+    /**
+     * Aborts a previous delete operation pending commit
+     */
+    public void abortDelete() {
         mPreparingToUndo = false;
         mDeleteRunnables.clear();
         // We do a full reload here instead of just a rebind because Folders change their internal
         // state when dragging an item out, which clobbers the rebind unless we load from the DB.
-        mModel.forceReload(pageToBindFirst);
+        mModel.forceReload();
     }
 
     private class UpdateItemRunnable extends UpdateItemBaseRunnable {
@@ -472,7 +474,7 @@
         }
 
         void verifyModel() {
-            if (!mVerifyChanges || mModel.getCallback() == null) {
+            if (!mVerifyChanges || !mModel.hasCallbacks()) {
                 return;
             }
 
@@ -488,11 +490,9 @@
                     // Bound model has not changed during the job
                     return;
                 }
+
                 // Bound model was changed between submitting the job and executing the job
-                Callbacks callbacks = mModel.getCallback();
-                if (callbacks != null) {
-                    callbacks.rebindModel();
-                }
+                mModel.rebindCallbacks();
             });
         }
     }
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 186293f..976d7ba 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -29,6 +29,10 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.SessionCommitReceiver;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
@@ -52,6 +56,8 @@
     // Set<String> of session ids of promise icons that have been added to the home screen
     // as FLAG_PROMISE_NEW_INSTALLS.
     protected static final String PROMISE_ICON_IDS = "promise_icon_ids";
+    public static final String KEY_INSTALL_SESSION_CREATED_TIMESTAMP =
+            "key_install_session_created_timestamp";
 
     private static final boolean DEBUG = false;
 
@@ -159,6 +165,34 @@
         return list;
     }
 
+    /**
+     * Attempt to restore workspace layout if the session is triggered due to device restore and it
+     * has a newer timestamp.
+     */
+    public boolean restoreDbIfApplicable(@NonNull final SessionInfo info) {
+        if (!Utilities.ATLEAST_OREO || !FeatureFlags.ENABLE_DATABASE_RESTORE.get()) {
+            return false;
+        }
+        if (isRestore(info) && hasNewerTimestamp(mAppContext, info)) {
+            LauncherSettings.Settings.call(mAppContext.getContentResolver(),
+                    LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE);
+            return true;
+        }
+        return false;
+    }
+
+    @RequiresApi(26)
+    private static boolean isRestore(@NonNull final SessionInfo info) {
+        return info.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_RESTORE;
+    }
+
+    private static boolean hasNewerTimestamp(
+            @NonNull final Context context, @NonNull final SessionInfo info) {
+        return PackageManagerHelper.getSessionCreatedTimeInMillis(info)
+                > Utilities.getDevicePrefs(context).getLong(
+                        KEY_INSTALL_SESSION_CREATED_TIMESTAMP, 0);
+    }
+
     public boolean promiseIconAddedForId(int sessionId) {
         return mPromiseIconIds.contains(sessionId);
     }
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 72c95c4..b764a07 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -351,7 +351,9 @@
     }
 
     public void applyNotificationInfos(List<NotificationInfo> notificationInfos) {
-        mNotificationItemView.applyNotificationInfos(notificationInfos);
+        if (mNotificationItemView != null) {
+            mNotificationItemView.applyNotificationInfos(notificationInfos);
+        }
     }
 
     private void updateHiddenShortcuts() {
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 9987994..407ff31 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.provider;
 
+import static com.android.launcher3.pm.InstallSessionHelper.KEY_INSTALL_SESSION_CREATED_TIMESTAMP;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 
 import android.app.backup.BackupManager;
@@ -25,23 +26,28 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.LongSparseArray;
 import android.util.SparseLongArray;
 
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.AppWidgetsRestoredReceiver;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.GridBackupTable;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LogConfig;
 
 import java.io.InvalidObjectException;
+import java.util.Arrays;
 
 /**
  * Utility class to update DB schema after it has been restored.
@@ -65,6 +71,7 @@
         SQLiteDatabase db = helper.getWritableDatabase();
         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             RestoreDbTask task = new RestoreDbTask();
+            task.backupWorkspace(context, db);
             task.sanitizeDB(helper, db, backupManager);
             task.restoreAppWidgetIdsIfExists(context);
             t.commit();
@@ -76,6 +83,47 @@
     }
 
     /**
+     * Restore the workspace if backup is available.
+     */
+    public static boolean restoreIfPossible(@NonNull Context context,
+            @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager) {
+        Utilities.getDevicePrefs(context).edit().putLong(
+                KEY_INSTALL_SESSION_CREATED_TIMESTAMP, System.currentTimeMillis()).apply();
+        final SQLiteDatabase db = helper.getWritableDatabase();
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
+            RestoreDbTask task = new RestoreDbTask();
+            task.restoreWorkspace(context, db, helper, backupManager);
+            task.restoreAppWidgetIdsIfExists(context);
+            t.commit();
+            return true;
+        } catch (Exception e) {
+            FileLog.e(TAG, "Failed to restore db", e);
+            return false;
+        }
+    }
+
+    /**
+     * Backup the workspace so that if things go south in restore, we can recover these entries.
+     */
+    private void backupWorkspace(Context context, SQLiteDatabase db) throws Exception {
+        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+        new GridBackupTable(context, db, idp.numHotseatIcons, idp.numColumns, idp.numRows)
+                .doBackup(getDefaultProfileId(db), GridBackupTable.OPTION_REQUIRES_SANITIZATION);
+    }
+
+    private void restoreWorkspace(@NonNull Context context, @NonNull SQLiteDatabase db,
+            @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)
+            throws Exception {
+        final InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+        GridBackupTable backupTable = new GridBackupTable(context, db,
+                idp.numHotseatIcons, idp.numColumns, idp.numRows);
+        if (backupTable.restoreFromRawBackupIfAvailable(getDefaultProfileId(db))) {
+            sanitizeDB(helper, db, backupManager);
+            LauncherAppState.getInstance(context).getModel().forceReload();
+        }
+    }
+
+    /**
      * Makes the following changes in the provider DB.
      *   1. Removes all entries belonging to any profiles that were not restored.
      *   2. Marks all entries as restored. The flags are updated during first load or as
@@ -107,22 +155,14 @@
         int numProfiles = profileMapping.size();
         String[] profileIds = new String[numProfiles];
         profileIds[0] = Long.toString(oldProfileId);
-        StringBuilder whereClause = new StringBuilder("profileId != ?");
-        for (int i = profileMapping.size() - 1; i >= 1; --i) {
-            whereClause.append(" AND profileId != ?");
+        for (int i = numProfiles - 1; i >= 1; --i) {
             profileIds[i] = Long.toString(profileMapping.keyAt(i));
         }
-        try {
-            int itemsDeleted = db.delete(Favorites.TABLE_NAME, whereClause.toString(), profileIds);
-            FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
-        } catch (IllegalArgumentException exception) {
-            // b/147114476
-            FileLog.e(TAG, new StringBuilder("Failed to execute delete, where clause: '")
-                    .append(whereClause).append("', profile Id size:").append(profileIds.length)
-                    .append("profileIds: ").append(String.join(", ", profileIds)).toString()
-            );
-            throw exception;
-        }
+        final String[] args = new String[profileIds.length];
+        Arrays.fill(args, "?");
+        final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
+        int itemsDeleted = db.delete(Favorites.TABLE_NAME, where, profileIds);
+        FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
 
         // Mark all items as restored.
         boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS);
@@ -132,15 +172,16 @@
         db.update(Favorites.TABLE_NAME, values, null, null);
 
         // Mark widgets with appropriate restore flag.
-        values.put(Favorites.RESTORED,  LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
-                LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
-                LauncherAppWidgetInfo.FLAG_UI_NOT_READY |
-                (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0));
+        values.put(Favorites.RESTORED,  LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
+                | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
+                | LauncherAppWidgetInfo.FLAG_UI_NOT_READY
+                | (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0));
         db.update(Favorites.TABLE_NAME, values, "itemType = ?",
                 new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)});
 
-        // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp location.
-        // Using Long.MIN_VALUE since profile ids can not be negative, so there will be no overlap.
+        // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp
+        // location. Using Long.MIN_VALUE since profile ids can not be negative, so there will
+        // be no overlap.
         final long tempLocationOffset = Long.MIN_VALUE;
         SparseLongArray tempMigratedIds = new SparseLongArray(profileMapping.size());
         int numTempMigrations = 0;
@@ -198,10 +239,10 @@
     private LongSparseArray<Long> getManagedProfileIds(SQLiteDatabase db, long defaultProfileId) {
         LongSparseArray<Long> ids = new LongSparseArray<>();
         try (Cursor c = db.rawQuery("SELECT profileId from favorites WHERE profileId != ? "
-                + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})){
-                while (c.moveToNext()) {
-                    ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null);
-                }
+                + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})) {
+            while (c.moveToNext()) {
+                ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null);
+            }
         }
         return ids;
     }
@@ -222,7 +263,7 @@
      * Returns the profile id used in the favorites table of the provided db.
      */
     protected long getDefaultProfileId(SQLiteDatabase db) throws Exception {
-        try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)){
+        try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)) {
             int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME);
             while (c.moveToNext()) {
                 if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) {
diff --git a/src/com/android/launcher3/testing/TestLogging.java b/src/com/android/launcher3/testing/TestLogging.java
new file mode 100644
index 0000000..fd066c1
--- /dev/null
+++ b/src/com/android/launcher3/testing/TestLogging.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 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.testing;
+
+import android.util.Log;
+
+import com.android.launcher3.Utilities;
+
+public final class TestLogging {
+    public static void recordEvent(String event) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.TAPL_EVENTS_TAG, event);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 929315a..01c207f 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -31,6 +31,7 @@
     public static final int QUICK_SWITCH_STATE_ORDINAL = 4;
     public static final int ALL_APPS_STATE_ORDINAL = 5;
     public static final int BACKGROUND_APP_STATE_ORDINAL = 6;
+    public static final String TAPL_EVENTS_TAG = "TaplEvents";
 
     public static String stateOrdinalToString(int ordinal) {
         switch (ordinal) {
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index f470edb..d193bef 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -508,7 +508,7 @@
             mAtomicComponentsController.getAnimationPlayer().end();
             mAtomicComponentsController = null;
         }
-        cancelAnimationControllers();
+        clearState();
         boolean shouldGoToTargetState = true;
         if (mPendingAnimation != null) {
             boolean reachedTarget = mToState == targetState;
@@ -546,13 +546,13 @@
             mAtomicAnim = null;
         }
         mScheduleResumeAtomicComponent = false;
+        mDetector.finishedScrolling();
+        mDetector.setDetectableScrollConditions(0, false);
     }
 
     private void cancelAnimationControllers() {
         mCurrentAnimation = null;
         cancelAtomicComponentsController();
-        mDetector.finishedScrolling();
-        mDetector.setDetectableScrollConditions(0, false);
     }
 
     private void cancelAtomicComponentsController() {
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 12ca5ee..30283da 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -24,6 +24,10 @@
 import android.view.ViewConfiguration;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.LinkedList;
+import java.util.Queue;
 
 /**
  * Scroll/drag/swipe gesture detector.
@@ -49,13 +53,15 @@
     protected final boolean mIsRtl;
     protected final float mTouchSlop;
     protected final float mMaxVelocity;
+    private final Queue<Runnable> mSetStateQueue = new LinkedList<>();
 
     private int mActivePointerId = INVALID_POINTER_ID;
     private VelocityTracker mVelocityTracker;
     private PointF mLastDisplacement = new PointF();
     private PointF mDisplacement = new PointF();
     protected PointF mSubtractDisplacement = new PointF();
-    private ScrollState mState = ScrollState.IDLE;
+    @VisibleForTesting ScrollState mState = ScrollState.IDLE;
+    private boolean mIsSettingState;
 
     protected boolean mIgnoreSlopWhenSettling;
 
@@ -195,6 +201,12 @@
     // SETTLING -> (View settled) -> IDLE
 
     private void setState(ScrollState newState) {
+        if (mIsSettingState) {
+            mSetStateQueue.add(() -> setState(newState));
+            return;
+        }
+        mIsSettingState = true;
+
         if (DBG) {
             Log.d(TAG, "setState:" + mState + "->" + newState);
         }
@@ -212,6 +224,10 @@
         }
 
         mState = newState;
+        mIsSettingState = false;
+        if (!mSetStateQueue.isEmpty()) {
+            mSetStateQueue.remove().run();
+        }
     }
 
     private void initializeDragging() {
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 66fdc94..310d598 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -25,7 +25,6 @@
 
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.util.Log;
 import android.view.GestureDetector;
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
@@ -37,10 +36,8 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -178,9 +175,6 @@
                 mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
                         Action.Direction.NONE, ContainerType.WORKSPACE,
                         mWorkspace.getCurrentPage());
-                if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                    Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Opening options popup on long press");
-                }
                 OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
             } else {
                 cancelLongPress();
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 8b2ee36..6c18747 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.util;
 
+import static android.content.pm.PackageInstaller.SessionInfo;
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
 
 import android.app.AppOpsManager;
@@ -44,6 +45,8 @@
 import android.util.Pair;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppWidgetInfo;
@@ -345,4 +348,15 @@
         }
         return false;
     }
+
+    /**
+     * Returns the created time in millis of given session info. Returns 0 if not available.
+     */
+    public static long getSessionCreatedTimeInMillis(@NonNull final SessionInfo info) {
+        try {
+            return (long) SessionInfo.class.getDeclaredMethod("getCreatedMillis").invoke(info);
+        } catch (Exception e) {
+            return 0;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
index 5a131c8..451ae28 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -23,6 +23,8 @@
 import android.view.View.OnAttachStateChangeListener;
 import android.view.ViewTreeObserver.OnDrawListener;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.Launcher;
 
 import java.util.ArrayList;
@@ -118,7 +120,11 @@
         return mCompleted;
     }
 
-    protected void runAllTasks() {
+    /**
+     * Executes all tasks immediately
+     */
+    @VisibleForTesting
+    public void runAllTasks() {
         for (final Runnable r : mTasks) {
             r.run();
         }
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
index 789bfd8..dcb4636 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
@@ -16,12 +16,14 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.widget.WidgetListRowEntry;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashMap;
 
@@ -31,8 +33,13 @@
 public class LoaderResults extends BaseLoaderResults {
 
     public LoaderResults(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+            AllAppsList allAppsList, Callbacks[] callbacks) {
+        this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+    }
+
+    public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+            AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+        super(app, dataModel, allAppsList, callbacks, executor);
     }
 
     @Override
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index 5174e4d..6d463b5 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -21,9 +21,11 @@
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
 
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyFloat;
 import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -168,4 +170,21 @@
         // TODO: actually calculate the following parameters and do exact value checks.
         verify(mMockListener).onDragEnd(anyFloat());
     }
+
+    @Test
+    public void testInterleavedSetState() {
+        doAnswer(invocationOnMock -> {
+            // Sets state to IDLE. (Normally onDragEnd() will have state SETTLING.)
+            mDetector.finishedScrolling();
+            return null;
+        }).when(mMockListener).onDragEnd(anyFloat());
+
+        mGenerator.put(0, 100, 100);
+        mGenerator.move(0, 100, 100 + mTouchSlop);
+        mGenerator.move(0, 100, 100 + mTouchSlop * 2);
+        mGenerator.lift(0);
+        verify(mMockListener).onDragEnd(anyFloat());
+        assertTrue("SwipeDetector should be IDLE but was " + mDetector.mState,
+                mDetector.isIdleState());
+    }
 }
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index f98957e..b394bcb 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -102,14 +102,15 @@
 
         final String launcherVersion;
         try {
+            final String launcherPackageName = UiDevice.getInstance(getInstrumentation())
+                    .getLauncherPackageName();
+            Log.d(TAG, "Launcher package: " + launcherPackageName);
+
             launcherVersion = getInstrumentation().
                     getContext().
                     getPackageManager().
-                    getPackageInfo(
-                            UiDevice.getInstance(getInstrumentation()).
-                                    getLauncherPackageName(),
-                            0).
-                    versionName;
+                    getPackageInfo(launcherPackageName, 0)
+                    .versionName;
         } catch (PackageManager.NameNotFoundException e) {
             throw new RuntimeException(e);
         }
@@ -148,7 +149,8 @@
             Log.d(TAG, "PLATFORM PRESUBMIT");
             runFlavor = PLATFORM_PRESUBMIT;
         } else if (launcherBuildMatcher.group("platform") != null
-                && platformBuildMatcher.group("postsubmit") != null) {
+                && (platformBuildMatcher.group("postsubmit") != null
+                || platformBuildMatcher.group("commandLine") != null)) {
             Log.d(TAG, "PLATFORM POSTSUBMIT");
             runFlavor = PLATFORM_POSTSUBMIT;
         } else {
diff --git a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
index 03d1600..afb50e0 100644
--- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
+++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
@@ -37,8 +37,10 @@
     }
 
     public void addAutomatically() {
-        mLauncher.waitForObjectInContainer(
-                mWidgetCell.getParent().getParent().getParent().getParent(),
-                By.text(ADD_AUTOMATICALLY)).click();
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.waitForObjectInContainer(
+                    mWidgetCell.getParent().getParent().getParent().getParent(),
+                    By.text(ADD_AUTOMATICALLY)).click();
+        }
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index e344bbd..4a2d699 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -99,8 +99,9 @@
      */
     @NonNull
     public AppIcon getAppIcon(String appName) {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "getting app icon " + appName + " on all apps")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "getting app icon " + appName + " on all apps")) {
             final UiObject2 allAppsContainer = verifyActiveContainer();
             final UiObject2 appListRecycler = mLauncher.waitForObjectInContainer(allAppsContainer,
                     "apps_list_view");
@@ -200,7 +201,8 @@
      * Flings forward (down) and waits the fling's end.
      */
     public void flingForward() {
-        try (LauncherInstrumentation.Closable c =
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling forward in all apps")) {
             final UiObject2 allAppsContainer = verifyActiveContainer();
             // Start the gesture in the center to avoid starting at elements near the top.
@@ -214,7 +216,8 @@
      * Flings backward (up) and waits the fling's end.
      */
     public void flingBackward() {
-        try (LauncherInstrumentation.Closable c =
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling backward in all apps")) {
             final UiObject2 allAppsContainer = verifyActiveContainer();
             // Start the gesture in the center, for symmetry with forward.
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
index f48d4dd..8f9fec9 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
@@ -42,8 +42,9 @@
      */
     @NonNull
     public Overview switchBackToOverview() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to switch back from all apps to overview")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to switch back from all apps to overview")) {
             final UiObject2 allAppsContainer = verifyActiveContainer();
             // Swipe from the search box to the bottom.
             final UiObject2 qsb = mLauncher.waitForObjectInContainer(
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 2da6344..0a6ed7f 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -26,6 +26,7 @@
  * App icon, whether in all apps or in workspace/
  */
 public final class AppIcon extends Launchable {
+
     AppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
         super(launcher, icon);
     }
@@ -38,8 +39,10 @@
      * Long-clicks the icon to open its menu.
      */
     public AppIconMenu openMenu() {
-        return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
-                mObject, "deep_shortcuts_container"));
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
+                    mObject, "deep_shortcuts_container"));
+        }
     }
 
     @Override
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index d9ae778..50bdf5c 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -52,8 +52,9 @@
      */
     @NonNull
     public BaseOverview switchToOverview() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to switch from background to overview")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to switch from background to overview")) {
             verifyActiveContainer();
             goToOverviewUnchecked();
             return mLauncher.isFallbackOverview() ?
@@ -124,8 +125,9 @@
      * Swipes right or double presses the square button to switch to the previous app.
      */
     public Background quickSwitchToPreviousApp() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to quick switch to the previous app")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to quick switch to the previous app")) {
             verifyActiveContainer();
             quickSwitchToPreviousApp(getExpectedStateForQuickSwitch());
             return new Background(mLauncher);
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 339e14f..e13ea52 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -48,6 +48,12 @@
      * Flings forward (left) and waits the fling's end.
      */
     public void flingForward() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            flingForwardImpl();
+        }
+    }
+
+    private void flingForwardImpl() {
         try (LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling forward in overview")) {
             LauncherInstrumentation.log("Overview.flingForward before fling");
@@ -65,14 +71,15 @@
      * Dismissed all tasks by scrolling to Clear-all button and pressing it.
      */
     public void dismissAllTasks() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "dismissing all tasks")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "dismissing all tasks")) {
             final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
             for (int i = 0;
                     i < FLINGS_FOR_DISMISS_LIMIT
                             && !verifyActiveContainer().hasObject(clearAllSelector);
                     ++i) {
-                flingForward();
+                flingForwardImpl();
             }
 
             mLauncher.waitForObjectInContainer(verifyActiveContainer(), clearAllSelector).click();
@@ -83,7 +90,8 @@
      * Flings backward (right) and waits the fling's end.
      */
     public void flingBackward() {
-        try (LauncherInstrumentation.Closable c =
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling backward in overview")) {
             LauncherInstrumentation.log("Overview.flingBackward before fling");
             final UiObject2 overview = verifyActiveContainer();
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index 1e4d937..1a0fe3d 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -48,8 +48,9 @@
     @NonNull
     @Override
     public Overview switchToOverview() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to switch from home to overview")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to switch from home to overview")) {
             verifyActiveContainer();
             goToOverviewUnchecked();
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 6881197..f88a616 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -46,7 +46,9 @@
      * Clicks the object to launch its app.
      */
     public Background launch(String expectedPackageName) {
-        return launch(By.pkg(expectedPackageName));
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            return launch(By.pkg(expectedPackageName));
+        }
     }
 
     private Background launch(BySelector selector) {
@@ -69,17 +71,20 @@
      * Drags an object to the center of homescreen.
      */
     public void dragToWorkspace() {
-        final Point launchableCenter = getObject().getVisibleCenter();
-        final Point displaySize = mLauncher.getRealDisplaySize();
-        final int width = displaySize.x / 2;
-        Workspace.dragIconToWorkspace(
-                mLauncher,
-                this,
-                new Point(
-                        launchableCenter.x >= width ?
-                                launchableCenter.x - width / 2 : launchableCenter.x + width / 2,
-                        displaySize.y / 2),
-                getLongPressIndicator());
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final Point launchableCenter = getObject().getVisibleCenter();
+            final Point displaySize = mLauncher.getRealDisplaySize();
+            final int width = displaySize.x / 2;
+            Workspace.dragIconToWorkspace(
+                    mLauncher,
+                    this,
+                    new Point(
+                            launchableCenter.x >= width
+                                    ? launchableCenter.x - width / 2
+                                    : launchableCenter.x + width / 2,
+                            displaySize.y / 2),
+                    getLongPressIndicator());
+        }
     }
 
     protected abstract String getLongPressIndicator();
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index de6fdb1..6df8790 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -79,6 +79,8 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 /**
@@ -92,6 +94,13 @@
     private static final int GESTURE_STEP_MS = 16;
     private static long START_TIME = System.currentTimeMillis();
 
+    static final Pattern LOG_TIME = Pattern.compile(
+            "[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9]");
+
+    static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
+            "(?<time>[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9])"
+                    + ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<event>.*)");
+
     // Types for launcher containers that the user is interacting with. "Background" is a
     // pseudo-container corresponding to inactive launcher covered by another app.
     public enum ContainerType {
@@ -146,6 +155,11 @@
 
     private Consumer<ContainerType> mOnSettledStateAction;
 
+    // Not null when we are collecting expected events to compare with actual ones.
+    private List<Pattern> mExpectedEvents;
+
+    private String mTimeBeforeFirstLogEvent;
+
     /**
      * Constructs the root of TAPL hierarchy. You get all other objects from it.
      */
@@ -256,9 +270,9 @@
 
     Closable addContextLayer(String piece) {
         mDiagnosticContext.addLast(piece);
-        log("Added context: " + getContextDescription());
+        log("Entering context: " + piece);
         return () -> {
-            log("Removing context: " + getContextDescription());
+            log("Leaving context: " + piece);
             mDiagnosticContext.removeLast();
         };
     }
@@ -299,8 +313,11 @@
     public void checkForAnomaly() {
         final String anomalyMessage = getAnomalyMessage();
         if (anomalyMessage != null) {
-            failWithSystemHealth(
-                    "Tests are broken by a non-Launcher system error: " + anomalyMessage);
+            String message = "Tests are broken by a non-Launcher system error: " + anomalyMessage;
+            log("Hierarchy dump for: " + message);
+            dumpViewHierarchy();
+
+            Assert.fail(formatSystemHealthMessage(message));
         }
     }
 
@@ -339,7 +356,7 @@
         mOnSettledStateAction = onSettledStateAction;
     }
 
-    private String getSystemHealthMessage() {
+    private String formatSystemHealthMessage(String message) {
         final String testPackage = getContext().getPackageName();
 
         mInstrumentation.getUiAutomation().grantRuntimePermission(
@@ -347,30 +364,34 @@
         mInstrumentation.getUiAutomation().grantRuntimePermission(
                 testPackage, "android.permission.PACKAGE_USAGE_STATS");
 
-        return mSystemHealthSupplier != null
+        final String systemHealth = mSystemHealthSupplier != null
                 ? mSystemHealthSupplier.apply(START_TIME)
                 : TestHelpers.getSystemHealthMessage(getContext(), START_TIME);
+
+        if (systemHealth != null) {
+            return message
+                    + ",\nperhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
+                    + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
+        }
+
+        return message;
     }
 
     private void fail(String message) {
         checkForAnomaly();
 
-        failWithSystemHealth("http://go/tapl : " + getContextDescription() + message +
-                " (visible state: " + getVisibleStateMessage() + ")");
-    }
-
-    private void failWithSystemHealth(String message) {
-        final String systemHealth = getSystemHealthMessage();
-        if (systemHealth != null) {
-            message = message
-                    + ", perhaps because of system health problems:\n<<<<<<<<<<<<<<<<<<\n"
-                    + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
-        }
-
+        message = "http://go/tapl : " + getContextDescription() + message
+                + " (visible state: " + getVisibleStateMessage() + ")";
         log("Hierarchy dump for: " + message);
         dumpViewHierarchy();
 
-        Assert.fail(message);
+        final String eventMismatch = getEventMismatchMessage();
+
+        if (eventMismatch != null) {
+            message = message + ",\nhaving produced wrong events:\n    " + eventMismatch;
+        }
+
+        Assert.fail(formatSystemHealthMessage(message));
     }
 
     private String getContextDescription() {
@@ -539,61 +560,63 @@
      * @return the Workspace object.
      */
     public Workspace pressHome() {
-        // Click home, then wait for any accessibility event, then wait until accessibility events
-        // stop.
-        // We need waiting for any accessibility event generated after pressing Home because
-        // otherwise waitForIdle may return immediately in case when there was a big enough pause in
-        // accessibility events prior to pressing Home.
-        final String action;
-        if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
-            checkForAnomaly();
+        try (LauncherInstrumentation.Closable e = eventsCheck()) {
+            // Click home, then wait for any accessibility event, then wait until accessibility
+            // events stop.
+            // We need waiting for any accessibility event generated after pressing Home because
+            // otherwise waitForIdle may return immediately in case when there was a big enough
+            // pause in accessibility events prior to pressing Home.
+            final String action;
+            if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
+                checkForAnomaly();
 
-            final Point displaySize = getRealDisplaySize();
+                final Point displaySize = getRealDisplaySize();
 
-            if (hasLauncherObject(CONTEXT_MENU_RES_ID)) {
-                linearGesture(
-                        displaySize.x / 2, displaySize.y - 1,
-                        displaySize.x / 2, 0,
-                        ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
-                        false);
-                try (LauncherInstrumentation.Closable c = addContextLayer(
-                        "Swiped up from context menu to home")) {
-                    waitUntilGone(CONTEXT_MENU_RES_ID);
-                }
-            }
-            if (hasLauncherObject(WORKSPACE_RES_ID)) {
-                log(action = "already at home");
-            } else {
-                log("Hierarchy before swiping up to home:");
-                dumpViewHierarchy();
-                log(action = "swiping up to home from " + getVisibleStateMessage());
-
-                try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
-                    swipeToState(
+                if (hasLauncherObject(CONTEXT_MENU_RES_ID)) {
+                    linearGesture(
                             displaySize.x / 2, displaySize.y - 1,
                             displaySize.x / 2, 0,
-                            ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL);
+                            ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
+                            false);
+                    try (LauncherInstrumentation.Closable c = addContextLayer(
+                            "Swiped up from context menu to home")) {
+                        waitUntilGone(CONTEXT_MENU_RES_ID);
+                    }
+                }
+                if (hasLauncherObject(WORKSPACE_RES_ID)) {
+                    log(action = "already at home");
+                } else {
+                    log("Hierarchy before swiping up to home:");
+                    dumpViewHierarchy();
+                    log(action = "swiping up to home from " + getVisibleStateMessage());
+
+                    try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
+                        swipeToState(
+                                displaySize.x / 2, displaySize.y - 1,
+                                displaySize.x / 2, 0,
+                                ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL);
+                    }
+                }
+            } else {
+                log("Hierarchy before clicking home:");
+                dumpViewHierarchy();
+                log(action = "clicking home button from " + getVisibleStateMessage());
+                try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
+                    mDevice.waitForIdle();
+                    runToState(
+                            waitForSystemUiObject("home")::click,
+                            NORMAL_STATE_ORDINAL,
+                            !hasLauncherObject(WORKSPACE_RES_ID)
+                                    && (hasLauncherObject(APPS_RES_ID)
+                                    || hasLauncherObject(OVERVIEW_RES_ID)));
+                    mDevice.waitForIdle();
                 }
             }
-        } else {
-            log("Hierarchy before clicking home:");
-            dumpViewHierarchy();
-            log(action = "clicking home button from " + getVisibleStateMessage());
-            try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
-                mDevice.waitForIdle();
-                runToState(
-                        () -> waitForSystemUiObject("home").click(),
-                        NORMAL_STATE_ORDINAL,
-                        !hasLauncherObject(WORKSPACE_RES_ID)
-                                && (hasLauncherObject(APPS_RES_ID)
-                                || hasLauncherObject(OVERVIEW_RES_ID)));
-                mDevice.waitForIdle();
+            try (LauncherInstrumentation.Closable c = addContextLayer(
+                    "performed action to switch to Home - " + action)) {
+                return getWorkspace();
             }
         }
-        try (LauncherInstrumentation.Closable c = addContextLayer(
-                "performed action to switch to Home - " + action)) {
-            return getWorkspace();
-        }
     }
 
     /**
@@ -1099,4 +1122,104 @@
         }
         return tasks;
     }
+
+    private List<String> getEvents() {
+        final ArrayList<String> events = new ArrayList<>();
+        try {
+            final String logcatTimeParameter =
+                    mTimeBeforeFirstLogEvent != null ? " -t " + mTimeBeforeFirstLogEvent : "";
+            final String logcatEvents = mDevice.executeShellCommand(
+                    "logcat -d --pid=" + getPid() + logcatTimeParameter
+                            + " -s " + TestProtocol.TAPL_EVENTS_TAG);
+            final Matcher matcher = EVENT_LOG_ENTRY.matcher(logcatEvents);
+            while (matcher.find()) {
+                final String eventTime = matcher.group("time");
+                if (eventTime.equals(mTimeBeforeFirstLogEvent)) continue;
+
+                events.add(matcher.group("event"));
+            }
+            return events;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void startRecordingEvents() {
+        Assert.assertTrue("Already recording events", mExpectedEvents == null);
+        mExpectedEvents = new ArrayList<>();
+
+        try {
+            final String lastLogLine =
+                    mDevice.executeShellCommand("logcat -d --pid=" + getPid() + " -t 1");
+            final Matcher matcher = LOG_TIME.matcher(lastLogLine);
+            mTimeBeforeFirstLogEvent = matcher.find() ? matcher.group().replaceAll(" ", "") : null;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void stopRecordingEvents() {
+        mExpectedEvents = null;
+    }
+
+    Closable eventsCheck() {
+        // Entering events check block.
+        startRecordingEvents();
+
+        return () -> {
+            // Leaving events check block.
+            if (mExpectedEvents == null) {
+                return; // There was a failure. Noo need to report another one.
+            }
+
+            // Wait until Launcher generates expected number of events.
+            final long endTime = SystemClock.uptimeMillis() + WAIT_TIME_MS;
+            while (SystemClock.uptimeMillis() < endTime
+                    && getEvents().size() < mExpectedEvents.size()) {
+                SystemClock.sleep(100);
+            }
+
+            final String message = getEventMismatchMessage();
+            if (message != null) {
+                Assert.fail(formatSystemHealthMessage(
+                        "http://go/tapl : unexpected event sequence: " + message));
+            }
+        };
+    }
+
+    void expectEvent(Pattern expected) {
+        if (mExpectedEvents != null) mExpectedEvents.add(expected);
+    }
+
+    private String getEventMismatchMessage() {
+        if (mExpectedEvents == null) return null;
+
+        try {
+            final List<String> actual = getEvents();
+
+            for (int i = 0; i < mExpectedEvents.size(); ++i) {
+                if (i >= actual.size()) {
+                    return formatEventMismatchMessage("too few actual events", actual, i);
+                }
+                if (!mExpectedEvents.get(i).matcher(actual.get(i)).find()) {
+                    return formatEventMismatchMessage("mismatched event", actual, i);
+                }
+            }
+
+            if (actual.size() > mExpectedEvents.size()) {
+                return formatEventMismatchMessage(
+                        "too many actual events", actual, mExpectedEvents.size());
+            }
+        } finally {
+            stopRecordingEvents();
+        }
+
+        return null;
+    }
+
+    private String formatEventMismatchMessage(String message, List<String> actual, int position) {
+        return message + ", pos=" + position
+                + ", expected=" + mExpectedEvents
+                + ", actual=" + actual;
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
index 8527d05..461610d 100644
--- a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
@@ -35,12 +35,14 @@
      */
     @NonNull
     public void launch(@NonNull String expectedPackageName) {
-        LauncherInstrumentation.log("OptionsPopupMenuItem before click "
-                + mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
-        mObject.click();
-        mLauncher.assertTrue(
-                "App didn't start: " + By.pkg(expectedPackageName),
-                mLauncher.getDevice().wait(Until.hasObject(By.pkg(expectedPackageName)),
-                        LauncherInstrumentation.WAIT_TIME_MS));
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            LauncherInstrumentation.log("OptionsPopupMenuItem before click "
+                    + mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
+            mObject.click();
+            mLauncher.assertTrue(
+                    "App didn't start: " + By.pkg(expectedPackageName),
+                    mLauncher.getDevice().wait(Until.hasObject(By.pkg(expectedPackageName)),
+                            LauncherInstrumentation.WAIT_TIME_MS));
+        }
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index 16a64a7..6622c6e 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -45,8 +45,9 @@
      */
     @NonNull
     public AllAppsFromOverview switchToAllApps() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to switch from overview to all apps")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to switch from overview to all apps")) {
             verifyActiveContainer();
 
             // Swipe from an app icon to the top.
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 46f8ba5..0d93cbc 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -45,8 +45,9 @@
      * Swipes the task up.
      */
     public void dismiss() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to dismiss a task")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to dismiss a task")) {
             verifyActiveContainer();
             // Dismiss the task via flinging it up.
             final Rect taskBounds = mTask.getVisibleBounds();
@@ -61,15 +62,17 @@
      * Clicks at the task.
      */
     public Background open() {
-        verifyActiveContainer();
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "clicking an overview task")) {
-            mLauncher.executeAndWaitForEvent(
-                    () -> mTask.click(),
-                    event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                    () -> "Launching task didn't open a new window: "
-                            + mTask.getParent().getContentDescription());
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            verifyActiveContainer();
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "clicking an overview task")) {
+                mLauncher.executeAndWaitForEvent(
+                        () -> mTask.click(),
+                        event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+                        () -> "Launching task didn't open a new window: "
+                                + mTask.getParent().getContentDescription());
+            }
+            return new Background(mLauncher);
         }
-        return new Background(mLauncher);
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java
index 1b6d8c4..dfd74ed 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widget.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widget.java
@@ -22,6 +22,7 @@
  * Widget in workspace or a widget list.
  */
 public final class Widget extends Launchable {
+
     Widget(LauncherInstrumentation launcher, UiObject2 icon) {
         super(launcher, icon);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index d208c66..ede5bd9 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -41,8 +41,9 @@
      * Flings forward (down) and waits the fling's end.
      */
     public void flingForward() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to fling forward in widgets")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to fling forward in widgets")) {
             LauncherInstrumentation.log("Widgets.flingForward enter");
             final UiObject2 widgetsContainer = verifyActiveContainer();
             mLauncher.scroll(
@@ -62,8 +63,9 @@
      * Flings backward (up) and waits the fling's end.
      */
     public void flingBackward() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to fling backwards in widgets")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to fling backwards in widgets")) {
             LauncherInstrumentation.log("Widgets.flingBackward enter");
             final UiObject2 widgetsContainer = verifyActiveContainer();
             mLauncher.scroll(
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index af7e552..1d2c821 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -85,7 +85,8 @@
      */
     @NonNull
     public AllApps switchToAllApps() {
-        try (LauncherInstrumentation.Closable c =
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to switch from workspace to all apps")) {
             verifyActiveContainer();
             final int deviceHeight = mLauncher.getDevice().getDisplayHeight();
@@ -156,21 +157,23 @@
      * second screen.
      */
     public void ensureWorkspaceIsScrollable() {
-        final UiObject2 workspace = verifyActiveContainer();
-        if (!isWorkspaceScrollable(workspace)) {
-            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                    "dragging icon to a second page of workspace to make it scrollable")) {
-                dragIconToWorkspace(
-                        mLauncher,
-                        getHotseatAppIcon("Chrome"),
-                        new Point(mLauncher.getDevice().getDisplayWidth(),
-                                workspace.getVisibleBounds().centerY()),
-                        "deep_shortcuts_container");
-                verifyActiveContainer();
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final UiObject2 workspace = verifyActiveContainer();
+            if (!isWorkspaceScrollable(workspace)) {
+                try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                        "dragging icon to a second page of workspace to make it scrollable")) {
+                    dragIconToWorkspace(
+                            mLauncher,
+                            getHotseatAppIcon("Chrome"),
+                            new Point(mLauncher.getDevice().getDisplayWidth(),
+                                    workspace.getVisibleBounds().centerY()),
+                            "deep_shortcuts_container");
+                    verifyActiveContainer();
+                }
             }
+            assertTrue("Home screen workspace didn't become scrollable",
+                    isWorkspaceScrollable(workspace));
         }
-        assertTrue("Home screen workspace didn't become scrollable",
-                isWorkspaceScrollable(workspace));
     }
 
     private boolean isWorkspaceScrollable(UiObject2 workspace) {
@@ -213,11 +216,13 @@
      * recoil to complete.
      */
     public void flingForward() {
-        final UiObject2 workspace = verifyActiveContainer();
-        mLauncher.scroll(workspace, Direction.RIGHT,
-                new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0),
-                FLING_STEPS, false);
-        verifyActiveContainer();
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final UiObject2 workspace = verifyActiveContainer();
+            mLauncher.scroll(workspace, Direction.RIGHT,
+                    new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0),
+                    FLING_STEPS, false);
+            verifyActiveContainer();
+        }
     }
 
     /**
@@ -225,11 +230,13 @@
      * recoil to complete.
      */
     public void flingBackward() {
-        final UiObject2 workspace = verifyActiveContainer();
-        mLauncher.scroll(workspace, Direction.LEFT,
-                new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0),
-                FLING_STEPS, false);
-        verifyActiveContainer();
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final UiObject2 workspace = verifyActiveContainer();
+            mLauncher.scroll(workspace, Direction.LEFT,
+                    new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0),
+                    FLING_STEPS, false);
+            verifyActiveContainer();
+        }
     }
 
     /**
@@ -239,10 +246,12 @@
      */
     @NonNull
     public Widgets openAllWidgets() {
-        verifyActiveContainer();
-        mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
-            return new Widgets(mLauncher);
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            verifyActiveContainer();
+            mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
+                return new Widgets(mLauncher);
+            }
         }
     }
 
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
new file mode 100644
index 0000000..0f4163d
--- /dev/null
+++ b/tools/checkstyle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd" [
+  <!ENTITY defaultCopyrightCheck SYSTEM "../../../../prebuilts/checkstyle/default-copyright-check.xml">
+  <!ENTITY defaultJavadocChecks SYSTEM "../../../../prebuilts/checkstyle/default-javadoc-checks.xml">
+  <!ENTITY defaultTreewalkerChecks SYSTEM "../../../../prebuilts/checkstyle/default-treewalker-checks.xml">
+  <!ENTITY defaultModuleChecks SYSTEM "../../../../prebuilts/checkstyle/default-module-checks.xml">
+]>
+
+<module name="Checker">
+  &defaultModuleChecks;
+  &defaultCopyrightCheck;
+  <module name="TreeWalker">
+    &defaultJavadocChecks;
+    &defaultTreewalkerChecks;
+  </module>
+
+  <module name="SuppressionFilter">
+    <property name="file" value="tools/checkstyle_suppression.xml" />
+  </module>
+</module>
diff --git a/tools/checkstyle_suppression.xml b/tools/checkstyle_suppression.xml
new file mode 100644
index 0000000..799e750
--- /dev/null
+++ b/tools/checkstyle_suppression.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN" "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
+<suppressions>
+
+    <!-- Robolectric uses magic method names like `__constructor__` -->
+    <suppress files="/robolectric_tests" checks="MethodName|JavadocType|JavadocMethod" />
+
+</suppressions>
diff --git a/print_db.py b/tools/print_db.py
similarity index 100%
rename from print_db.py
rename to tools/print_db.py