Merge "Support having dup items in folder for migration" into ub-launcher3-rvc-dev
diff --git a/Android.mk b/Android.mk
index fcd4a94..7805b32 100644
--- a/Android.mk
+++ b/Android.mk
@@ -67,7 +67,10 @@
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src) \
     $(call all-java-files-under, src_shortcuts_overrides) \
-    $(call all-java-files-under, src_ui_overrides)
+    $(call all-java-files-under, src_ui_overrides) \
+    $(call all-java-files-under, ext_tests/src)
+    
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/ext_tests/res
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 # Proguard is disable for testing. Derivarive prjects to keep proguard enabled
diff --git a/ext_tests/res/values/overrides.xml b/ext_tests/res/values/overrides.xml
new file mode 100644
index 0000000..3f071d4
--- /dev/null
+++ b/ext_tests/res/values/overrides.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="test_information_handler_class" translatable="false">com.android.launcher3.testing.DebugTestInformationHandler</string>
+</resources>
+
diff --git a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
new file mode 100644
index 0000000..0b7d4bf
--- /dev/null
+++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
@@ -0,0 +1,185 @@
+/*
+ * 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 static android.graphics.Bitmap.Config.ARGB_8888;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Debug;
+import android.system.Os;
+import android.view.View;
+
+import androidx.annotation.Keep;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class to handle requests from tests, including debug ones.
+ */
+public class DebugTestInformationHandler extends TestInformationHandler {
+    private static LinkedList sLeaks;
+    private static Collection<String> sEvents;
+
+    public DebugTestInformationHandler(Context context) {
+        init(context);
+    }
+
+    private static void runGcAndFinalizersSync() {
+        Runtime.getRuntime().gc();
+        Runtime.getRuntime().runFinalization();
+
+        final CountDownLatch fence = new CountDownLatch(1);
+        createFinalizationObserver(fence);
+        try {
+            do {
+                Runtime.getRuntime().gc();
+                Runtime.getRuntime().runFinalization();
+            } while (!fence.await(100, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    // Create the observer in the scope of a method to minimize the chance that
+    // it remains live in a DEX/machine register at the point of the fence guard.
+    // This must be kept to avoid R8 inlining it.
+    @Keep
+    private static void createFinalizationObserver(CountDownLatch fence) {
+        new Object() {
+            @Override
+            protected void finalize() throws Throwable {
+                try {
+                    fence.countDown();
+                } finally {
+                    super.finalize();
+                }
+            }
+        };
+    }
+
+    @Override
+    public Bundle call(String method) {
+        final Bundle response = new Bundle();
+        switch (method) {
+            case TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS: {
+                return getLauncherUIProperty(Bundle::putInt,
+                        l -> l.getAppsView().getAppsStore().getDeferUpdatesFlags());
+            }
+
+            case TestProtocol.REQUEST_ENABLE_DEBUG_TRACING:
+                TestProtocol.sDebugTracing = true;
+                return response;
+
+            case TestProtocol.REQUEST_DISABLE_DEBUG_TRACING:
+                TestProtocol.sDebugTracing = false;
+                return response;
+
+            case TestProtocol.REQUEST_PID: {
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, Os.getpid());
+                return response;
+            }
+
+            case TestProtocol.REQUEST_TOTAL_PSS_KB: {
+                runGcAndFinalizersSync();
+                Debug.MemoryInfo mem = new Debug.MemoryInfo();
+                Debug.getMemoryInfo(mem);
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, mem.getTotalPss());
+                return response;
+            }
+
+            case TestProtocol.REQUEST_JAVA_LEAK: {
+                if (sLeaks == null) sLeaks = new LinkedList();
+
+                // Allocate and dirty the memory.
+                final int leakSize = 1024 * 1024;
+                final byte[] bytes = new byte[leakSize];
+                for (int i = 0; i < leakSize; i += 239) {
+                    bytes[i] = (byte) (i % 256);
+                }
+                sLeaks.add(bytes);
+                return response;
+            }
+
+            case TestProtocol.REQUEST_NATIVE_LEAK: {
+                if (sLeaks == null) sLeaks = new LinkedList();
+
+                // Allocate and dirty a bitmap.
+                final Bitmap bitmap = Bitmap.createBitmap(512, 512, ARGB_8888);
+                bitmap.eraseColor(Color.RED);
+                sLeaks.add(bitmap);
+                return response;
+            }
+
+            case TestProtocol.REQUEST_VIEW_LEAK: {
+                if (sLeaks == null) sLeaks = new LinkedList();
+                sLeaks.add(new View(mContext));
+                return response;
+            }
+
+            case TestProtocol.REQUEST_START_EVENT_LOGGING: {
+                sEvents = new ArrayList<>();
+                TestLogging.setEventConsumer(
+                        (sequence, event) -> {
+                            final Collection<String> events = sEvents;
+                            if (events != null) {
+                                synchronized (events) {
+                                    events.add(sequence + '/' + event);
+                                }
+                            }
+                        });
+                return response;
+            }
+
+            case TestProtocol.REQUEST_STOP_EVENT_LOGGING: {
+                TestLogging.setEventConsumer(null);
+                sEvents = null;
+                return response;
+            }
+
+            case TestProtocol.REQUEST_GET_TEST_EVENTS: {
+                synchronized (sEvents) {
+                    response.putStringArrayList(
+                            TestProtocol.TEST_INFO_RESPONSE_FIELD, new ArrayList<>(sEvents));
+                }
+                return response;
+            }
+
+            case TestProtocol.REQUEST_CLEAR_DATA: {
+                LauncherSettings.Settings.call(mContext.getContentResolver(),
+                        LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+                MAIN_EXECUTOR.submit(() ->
+                        LauncherAppState.getInstance(mContext).getModel().forceReload());
+                return response;
+            }
+
+            default:
+                return super.call(method);
+        }
+    }
+}
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
index d1185bd..5611969 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -71,6 +71,15 @@
 
 // Represents the apps container within search results.
 message SearchResultContainer {
+
+  // Length of search term.
+  optional int32 query_length = 1;
+
+  // Container from where search was invoked.
+  oneof ParentContainer {
+    WorkspaceContainer workspace = 2;
+    AllAppsContainer all_apps_container = 3;
+  }
 }
 
 // Container for package specific shortcuts to deep links and notifications.
@@ -96,8 +105,21 @@
   ADD_TO_HOMESCREEN = 6;    // play install + launcher home setting
   ALLAPPS_PREDICTION = 7;   // from prediction bar in all apps container
   HOTSEAT_PREDICTION = 8;   // from prediction bar in hotseat container
-  SUGGESTED_LABEL = 9;      // folder icon's label was suggested
-  MANUAL_LABEL = 10;        // folder icon's label was manually edited
+
+  // Folder's label is one of the non-empty suggested values.
+  SUGGESTED_LABEL = 9;
+
+  // Folder's label is non-empty, manually entered by the user
+  // and different from any of suggested values.
+  MANUAL_LABEL = 10;
+
+  // Folder's label is not yet assigned( i.e., title == null).
+  // Eligible for auto-labeling.
+  UNLABELED = 11;
+
+  // Folder's label is empty(i.e., title == "").
+  // Not eligible for auto-labeling.
+  EMPTY_LABEL = 12;
 }
 
 // Main app icons
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 527bfc3..60afddb 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -52,7 +52,7 @@
             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
-            android:taskAffinity="${packageName}.launcher"
+            android:taskAffinity=""
             android:enabled="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index b2286f1..e49f2ec 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -22,11 +22,16 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.launcher3" >
 
+    <permission
+        android:name="${packageName}.permission.HOTSEAT_EDU"
+        android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
+        android:protectionLevel="signatureOrSystem" />
+
     <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="${packageName}.permission.HOTSEAT_EDU" />
 
-    
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
         android:fullBackupOnly="true"
@@ -99,13 +104,24 @@
             android:name="com.android.quickstep.interaction.GestureSandboxActivity"
             android:autoRemoveFromRecents="true"
             android:excludeFromRecents="true"
-            android:taskAffinity="${packageName}.launcher"
             android:screenOrientation="portrait">
             <intent-filter>
                 <action android:name="com.android.quickstep.action.GESTURE_SANDBOX" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
+        <activity
+            android:name=".hybridhotseat.HotseatEduActivity"
+            android:theme="@android:style/Theme.NoDisplay"
+            android:noHistory="true"
+            android:launchMode="singleTask"
+            android:clearTaskOnLaunch="true"
+            android:permission="${packageName}.permission.HOTSEAT_EDU">
+            <intent-filter>
+                <action android:name="com.android.launcher3.action.SHOW_HYBRID_HOTSEAT_EDU"/>
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
 
     </application>
 
diff --git a/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml b/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
index 7b3e378..cd64a94 100644
--- a/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
+++ b/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
@@ -13,25 +13,30 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.quickstep.fallback.RecentsRootView
+<com.android.launcher3.LauncherRootView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/drag_layer"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:clipChildren="false"
     android:fitsSystemWindows="true">
 
-    <com.android.quickstep.fallback.FallbackRecentsView
-        android:id="@id/overview_panel"
+    <com.android.quickstep.fallback.RecentsDragLayer
+        android:id="@+id/drag_layer"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:clipChildren="false"
-        android:clipToPadding="false"
-        android:outlineProvider="none"
-        android:theme="@style/HomeScreenElementTheme" />
+        android:clipChildren="false">
 
-    <include
-        android:id="@+id/overview_actions_view"
-        layout="@layout/overview_actions_container" />
+        <com.android.quickstep.fallback.FallbackRecentsView
+            android:id="@id/overview_panel"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:outlineProvider="none"
+            android:theme="@style/HomeScreenElementTheme" />
 
-</com.android.quickstep.fallback.RecentsRootView>
+        <include
+            android:id="@+id/overview_actions_view"
+            layout="@layout/overview_actions_container" />
+
+    </com.android.quickstep.fallback.RecentsDragLayer>
+</com.android.launcher3.LauncherRootView>
diff --git a/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
index b9621e4..36c9b00 100644
--- a/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
+++ b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
@@ -72,33 +72,42 @@
                 android:layout_height="0dp"
                 launcher:containerType="hotseat" />
 
-            <FrameLayout
+            <LinearLayout
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:paddingLeft="@dimen/bottom_sheet_edu_padding"
                 android:paddingTop="8dp"
                 android:paddingRight="@dimen/bottom_sheet_edu_padding">
 
-                <Button
-                    android:id="@+id/turn_predictions_on"
-                    android:layout_width="wrap_content"
+                <FrameLayout
+                    android:layout_width="0dp"
                     android:layout_height="wrap_content"
-                    android:layout_gravity="end"
-                    android:background="?android:attr/selectableItemBackground"
-                    android:text="@string/hotseat_edu_accept"
-                    android:textAlignment="textEnd"
-                    android:textColor="@android:color/white" />
-
-                <Button
-                    android:layout_width="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_weight=".4">
+                    <Button
+                        android:id="@+id/no_thanks"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:background="?android:attr/selectableItemBackground"
+                        android:text="@string/hotseat_edu_dismiss"
+                        android:textColor="@android:color/white"/>
+                </FrameLayout>
+                <FrameLayout
+                    android:layout_width="0dp"
                     android:layout_height="wrap_content"
-                    android:id="@+id/no_thanks"
-                    android:text="@string/hotseat_edu_dismiss"
-                    android:layout_gravity="start"
-                    android:background="?android:attr/selectableItemBackground"
-                    android:textColor="@android:color/white" />
+                    android:layout_gravity="center_vertical"
+                    android:layout_weight=".6">
+                    <Button
+                        android:id="@+id/turn_predictions_on"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:background="?android:attr/selectableItemBackground"
+                        android:gravity="end"
+                        android:text="@string/hotseat_edu_accept"
+                        android:textColor="@android:color/white"/>
+                </FrameLayout>
 
-            </FrameLayout>
+            </LinearLayout>
         </LinearLayout>
     </LinearLayout>
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
index 0113570..5e54cd2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -16,9 +16,11 @@
 
 package com.android.launcher3.appprediction;
 
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_RANKED;
 
 import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
@@ -26,6 +28,7 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
@@ -38,8 +41,6 @@
 import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
-import com.android.launcher3.logger.LauncherAtom;
-import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.shortcuts.ShortcutKey;
@@ -51,6 +52,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.OptionalInt;
 import java.util.stream.IntStream;
 
@@ -306,40 +308,25 @@
     }
 
     /**
-     * Logs ranking info for launched app within all apps prediction.
+     * Returns ranking info for the app within all apps prediction.
      * Only applicable when {@link ItemInfo#itemType} is one of the followings:
      * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
      * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT},
      * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT}
      */
-    public void logLaunchedAppRankingInfo(@NonNull ItemInfo itemInfo, InstanceId instanceId) {
-        if (itemInfo.getTargetComponent() == null || itemInfo.user == null
-                || (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-                && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
-                && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
-            return;
-        }
+    public OptionalInt getAllAppsRank(@Nullable ItemInfo itemInfo) {
+        Optional<ComponentKey> componentKey = Optional.ofNullable(itemInfo)
+                .filter(item -> item.itemType == ITEM_TYPE_APPLICATION
+                        || item.itemType == ITEM_TYPE_SHORTCUT
+                        || item.itemType == ITEM_TYPE_DEEP_SHORTCUT)
+                .map(ItemInfo::getTargetComponent)
+                .map(componentName -> new ComponentKey(componentName, itemInfo.user));
 
-        Launcher launcher = Launcher.getLauncher(mAppsView.getContext());
-        final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
-        final List<ComponentKeyMapper> predictedApps = getCurrentState().apps;
-        OptionalInt rank = IntStream.range(0, predictedApps.size())
-                .filter((i) -> k.equals(predictedApps.get(i).getComponentKey()))
-                .findFirst();
-        if (!rank.isPresent()) {
-            return;
-        }
-
-        LauncherAtom.ItemInfo.Builder atomBuilder = LauncherAtom.ItemInfo.newBuilder();
-        atomBuilder.setRank(rank.getAsInt());
-        atomBuilder.setContainerInfo(
-                LauncherAtom.ContainerInfo.newBuilder().setPredictionContainer(
-                        LauncherAtom.PredictionContainer.newBuilder().build()).build());
-        launcher.getStatsLogManager().log(LAUNCHER_ALL_APPS_RANKED, instanceId,
-                atomBuilder.build());
+        return componentKey.map(key -> IntStream.range(0, getCurrentState().apps.size())
+                .filter(index -> key.equals(getCurrentState().apps.get(index).getComponentKey()))
+                .findFirst()).orElseGet(OptionalInt::empty);
     }
 
-
     /**
      * Fill in predicted_rank field based on app prediction.
      * Only applicable when {@link ItemInfo#itemType} is one of the followings:
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
new file mode 100644
index 0000000..c968de9
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
@@ -0,0 +1,59 @@
+/*
+ * 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.hybridhotseat;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.ActivityTracker;
+
+/**
+ * Proxy activity to return user to home screen and show halfsheet education
+ */
+public class HotseatEduActivity extends Activity {
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setPackage(getPackageName())
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        new HotseatActivityTracker<>().addToIntent(homeIntent);
+        startActivity(homeIntent);
+        finish();
+    }
+
+    static class HotseatActivityTracker<T extends QuickstepLauncher> implements
+            ActivityTracker.SchedulerCallback {
+
+        @Override
+        public boolean init(BaseActivity activity, boolean alreadyOnHome) {
+            QuickstepLauncher launcher = (QuickstepLauncher) activity;
+            if (launcher != null && launcher.getHotseatPredictionController() != null) {
+                launcher.getHotseatPredictionController().showEdu();
+            }
+            return false;
+        }
+
+    }
+}
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 522a2dc..a1218ae 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
@@ -84,7 +84,7 @@
         }
         Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled,
                 R.string.hotseat_prediction_settings, null,
-                () -> mLauncher.startActivity(new Intent(SETTINGS_ACTION)));
+                () -> mLauncher.startActivity(getSettingsIntent()));
     }
 
     /**
@@ -114,9 +114,9 @@
         if (!putIntoFolder.isEmpty()) {
             ItemInfo firstItem = putIntoFolder.get(0);
             FolderInfo folderInfo = new FolderInfo();
-            folderInfo.setTitle("");
             mLauncher.getModelWriter().addItemToDatabase(folderInfo, firstItem.container,
                     firstItem.screenId, firstItem.cellX, firstItem.cellY);
+            folderInfo.setTitle("", mLauncher.getModelWriter());
             folderInfo.contents.addAll(putIntoFolder);
             for (int i = 0; i < folderInfo.contents.size(); i++) {
                 ItemInfo item = folderInfo.contents.get(i);
@@ -237,7 +237,7 @@
                 < mLauncher.getDeviceProfile().inv.numHotseatIcons) {
             Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
                     R.string.hotseat_prediction_settings, null,
-                    () -> mLauncher.startActivity(new Intent(SETTINGS_ACTION)));
+                    () -> mLauncher.startActivity(getSettingsIntent()));
         } else {
             new ArrowTipView(mLauncher).show(
                     mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
@@ -281,5 +281,9 @@
         mActiveDialog.setHotseatEduController(this);
         mActiveDialog.show(mPredictedApps);
     }
+
+    static Intent getSettingsIntent() {
+        return new Intent(SETTINGS_ACTION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    }
 }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index 30a34e4..7334d80 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -17,7 +17,8 @@
 
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.hybridhotseat.HotseatEduController.SETTINGS_ACTION;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.hybridhotseat.HotseatEduController.getSettingsIntent;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_RANKED;
 
 import android.animation.Animator;
@@ -29,9 +30,9 @@
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetEvent;
 import android.content.ComponentName;
-import android.content.Intent;
 import android.os.Process;
 import android.util.Log;
+import android.view.HapticFeedbackConstants;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -54,7 +55,8 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.PredictedHotseatContainer;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
@@ -69,6 +71,7 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.views.ArrowTipView;
 import com.android.launcher3.views.Snackbar;
 
@@ -120,9 +123,18 @@
     private final View.OnLongClickListener mPredictionLongClickListener = v -> {
         if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
         if (mLauncher.getWorkspace().isSwitchingState()) return false;
+        if (!mLauncher.getOnboardingPrefs().getBoolean(
+                OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN)) {
+            Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
+                    R.string.hotseat_prediction_settings, null,
+                    () -> mLauncher.startActivity(getSettingsIntent()));
+            mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN);
+            mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+            return true;
+        }
         // Start the drag
         mLauncher.getWorkspace().beginDragShared(v, this, new DragOptions());
-        return false;
+        return true;
     };
 
     public HotseatPredictionController(Launcher launcher) {
@@ -154,36 +166,35 @@
      * Shows appropriate hotseat education based on prediction enabled and migration states.
      */
     public void showEdu() {
-        if (mComponentKeyMappers.isEmpty()) {
-            // launcher has empty predictions set
-            Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_disabled,
-                    R.string.hotseat_prediction_settings, null,
-                    () -> mLauncher.startActivity(
-                            new Intent(SETTINGS_ACTION)));
-        } else if (isEduSeen()) {
-            // user has already went through education
-            new ArrowTipView(mLauncher).show(
-                    mLauncher.getString(R.string.hotsaet_tip_prediction_enabled),
-                    mHotseat.getTop());
-        } else {
-            HotseatEduController eduController = new HotseatEduController(mLauncher, mRestoreHelper,
-                    this::createPredictor);
-            eduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
-            eduController.showEdu();
-        }
+        mLauncher.getStateManager().goToState(NORMAL, true, () -> {
+            if (mComponentKeyMappers.isEmpty()) {
+                // launcher has empty predictions set
+                Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_disabled,
+                        R.string.hotseat_prediction_settings, null,
+                        () -> mLauncher.startActivity(getSettingsIntent()));
+            } else if (isEduSeen() || getPredictedIcons().size() >= (mHotSeatItemsCount + 1) / 2) {
+                showDiscoveryTip();
+            } else {
+                HotseatEduController eduController = new HotseatEduController(mLauncher,
+                        mRestoreHelper,
+                        this::createPredictor);
+                eduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
+                eduController.showEdu();
+            }
+        });
     }
 
     /**
      * Shows educational tip for hotseat if user does not go through Tips app.
      */
-    public void showDiscoveryTip() {
-        if (getPredictedIcons().size() == mHotSeatItemsCount) {
+    private void showDiscoveryTip() {
+        if (getPredictedIcons().isEmpty()) {
             new ArrowTipView(mLauncher).show(
                     mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
         } else {
             Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
                     R.string.hotseat_prediction_settings, null,
-                    () -> mLauncher.startActivity(new Intent(SETTINGS_ACTION)));
+                    () -> mLauncher.startActivity(getSettingsIntent()));
         }
     }
 
@@ -661,24 +672,25 @@
         if (!rank.isPresent()) {
             return;
         }
-        LauncherAtom.PredictedHotseatContainer.Builder containerBuilder =
-                LauncherAtom.PredictedHotseatContainer.newBuilder();
-        LauncherAtom.ItemInfo.Builder atomBuilder = LauncherAtom.ItemInfo.newBuilder();
+
         int cardinality = 0;
         for (PredictedAppIcon icon : getPredictedIcons()) {
             ItemInfo info = (ItemInfo) icon.getTag();
             cardinality |= 1 << info.screenId;
         }
+
+        PredictedHotseatContainer.Builder containerBuilder = PredictedHotseatContainer.newBuilder();
         containerBuilder.setCardinality(cardinality);
-        atomBuilder.setRank(rank.getAsInt());
         if (itemInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             containerBuilder.setIndex(rank.getAsInt());
         }
-        atomBuilder.setContainerInfo(
-                LauncherAtom.ContainerInfo.newBuilder().setPredictedHotseatContainer(
-                        containerBuilder).build());
-        mLauncher.getStatsLogManager().log(LAUNCHER_HOTSEAT_RANKED, instanceId,
-                atomBuilder.build());
+        mLauncher.getStatsLogManager().logger()
+                .withInstanceId(instanceId)
+                .withRank(rank.getAsInt())
+                .withContainerInfo(ContainerInfo.newBuilder()
+                        .setPredictedHotseatContainer(containerBuilder)
+                        .build())
+                .log(LAUNCHER_HOTSEAT_RANKED);
     }
 
     private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 0e690eb..7d86cea 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
@@ -45,9 +46,9 @@
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.hybridhotseat.HotseatEduController;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -80,6 +81,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.OptionalInt;
 import java.util.stream.Stream;
 
 public class QuickstepLauncher extends BaseQuickstepLauncher {
@@ -108,26 +110,16 @@
     }
 
     @Override
-    protected void onNewIntent(Intent intent) {
-        super.onNewIntent(intent);
-        if (HotseatEduController.HOTSEAT_EDU_ACTION.equals(intent.getAction())
-                && mHotseatPredictionController != null) {
-            boolean alreadyOnHome = hasWindowFocus() && ((intent.getFlags()
-                    & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT)
-                    != Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
-            getStateManager().goToState(NORMAL, alreadyOnHome, () -> {
-                mHotseatPredictionController.showEdu();
-            });
-        }
-    }
-
-    @Override
     protected void logAppLaunch(ItemInfo info, InstanceId instanceId) {
-        super.logAppLaunch(info, instanceId);
+        StatsLogger logger = getStatsLogManager()
+                .logger().withItemInfo(info).withInstanceId(instanceId);
+        OptionalInt allAppsRank = PredictionUiStateManager.INSTANCE.get(this).getAllAppsRank(info);
+        allAppsRank.ifPresent(logger::withRank);
+        logger.log(LAUNCHER_APP_LAUNCH_TAP);
+
         if (mHotseatPredictionController != null) {
             mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
         }
-        PredictionUiStateManager.INSTANCE.get(this).logLaunchedAppRankingInfo(info, instanceId);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index ba8656d..a0af797 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
+import static com.android.launcher3.WorkspaceStateTransitionAnimation.getSpringScaleAnimator;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
@@ -90,6 +91,10 @@
     protected static final int NEXT_INDEX = RecentsAtomicAnimationFactory.NEXT_INDEX
             + MY_ANIM_COUNT;
 
+    // Due to use of physics, duration may differ between devices so we need to calculate and
+    // cache the value.
+    private int mHintToNormalDuration = -1;
+
     public static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
 
     public QuickstepAtomicAnimationFactory(QuickstepLauncher activity) {
@@ -221,6 +226,14 @@
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, translationInterpolator);
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, translationInterpolator);
             config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
+        } else if (fromState == HINT_STATE && toState == NORMAL) {
+            config.setInterpolator(ANIM_DEPTH, DEACCEL_3);
+            if (mHintToNormalDuration == -1) {
+                ValueAnimator va = getSpringScaleAnimator(mActivity, mActivity.getWorkspace(),
+                        toState.getWorkspaceScaleAndTranslation(mActivity).scale);
+                mHintToNormalDuration = (int) va.getDuration();
+            }
+            config.duration = Math.max(config.duration, mHintToNormalDuration);
         }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 39bbfb9..c1a585e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -15,11 +15,15 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_EDU;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
 import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
@@ -41,6 +45,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
@@ -112,7 +117,8 @@
             }
             return true;
         }
-        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+        int typeToClose = ENABLE_ALL_APPS_EDU.get() ? TYPE_ALL & ~TYPE_ALL_APPS_EDU : TYPE_ALL;
+        if (AbstractFloatingView.getTopOpenViewWithType(mLauncher, typeToClose) != null) {
             if (TestProtocol.sDebugTracing) {
                 Log.d(TestProtocol.PAUSE_NOT_DETECTED,
                         "NavBarToHomeTouchController.canInterceptTouch true 2 "
@@ -242,5 +248,9 @@
                 startContainerType,
                 mEndState.containerType,
                 mLauncher.getWorkspace().getCurrentPage());
+        mLauncher.getStatsLogManager().logger()
+                .withSrcState(StatsLogManager.containerTypeToAtomState(mStartState.containerType))
+                .withDstState(StatsLogManager.containerTypeToAtomState(mEndState.containerType))
+                .log(LAUNCHER_HOME_GESTURE);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index 4cc8256..9316938 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -26,6 +26,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.graphics.PointF;
 import android.util.Log;
@@ -35,6 +36,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.testing.TestProtocol;
@@ -65,6 +67,9 @@
     // The last recorded displacement before we reached overview.
     private PointF mStartDisplacement = new PointF();
 
+    // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
+    private ObjectAnimator mNormalToHintOverviewScrimAnimator;
+
     public NoButtonNavbarToOverviewTouchController(Launcher l) {
         super(l);
         mRecentsView = l.getOverviewPanel();
@@ -110,11 +115,31 @@
     @Override
     public void onDragStart(boolean start, float startDisplacement) {
         super.onDragStart(start, startDisplacement);
-
+        if (mFromState == NORMAL && mToState == HINT_STATE) {
+            mNormalToHintOverviewScrimAnimator = ObjectAnimator.ofFloat(
+                    mLauncher.getDragLayer().getOverviewScrim(),
+                    OverviewScrim.SCRIM_PROGRESS,
+                    mFromState.getOverviewScrimAlpha(mLauncher),
+                    mToState.getOverviewScrimAlpha(mLauncher));
+        }
         mReachedOverview = false;
     }
 
     @Override
+    protected void updateProgress(float fraction) {
+        super.updateProgress(fraction);
+        if (mNormalToHintOverviewScrimAnimator != null) {
+            mNormalToHintOverviewScrimAnimator.setCurrentFraction(fraction);
+        }
+    }
+
+    @Override
+    public void onDragEnd(float velocity) {
+        super.onDragEnd(velocity);
+        mNormalToHintOverviewScrimAnimator = null;
+    }
+
+    @Override
     protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
             LauncherState targetState, float velocity, boolean isFling) {
         super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity,
@@ -132,6 +157,7 @@
         if (mCurrentAnimation == null) {
             return;
         }
+        mNormalToHintOverviewScrimAnimator = null;
         mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(() -> {
             mLauncher.getStateManager().goToState(OVERVIEW, true, () -> {
                 mReachedOverview = true;
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 da304e5..1b439d1 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
@@ -28,6 +28,10 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
+import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
@@ -61,6 +65,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.OverviewScrim;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.BothAxesSwipeDetector;
@@ -429,6 +434,13 @@
                 mStartState.containerType,
                 targetState.containerType,
                 mLauncher.getWorkspace().getCurrentPage());
+        mLauncher.getStatsLogManager().logger()
+                .withSrcState(LAUNCHER_STATE_HOME)
+                .withDstState(StatsLogManager.containerTypeToAtomState(targetState.containerType))
+                .log(getLauncherAtomEvent(mStartState.containerType, targetState.containerType,
+                        targetState.ordinal > mStartState.ordinal
+                                ? LAUNCHER_UNKNOWN_SWIPEUP
+                                : LAUNCHER_UNKNOWN_SWIPEDOWN));
         mLauncher.getStateManager().goToState(targetState, false, this::clearState);
     }
 
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 f5c5874..02bae64 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -205,26 +205,30 @@
         mRecentsAnimationController = recentsAnimationController;
         mRecentsAnimationTargets = targets;
         mTransformParams.setTargetSet(mRecentsAnimationTargets);
-        DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
         RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
                 mGestureState.getRunningTaskId());
 
-        if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
-            Rect overviewStackBounds = mActivityInterface
-                    .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
-            dp = dp.getMultiWindowProfile(mContext,
-                    new WindowBounds(overviewStackBounds, targets.homeContentInsets));
-        } else {
-            // If we are not in multi-window mode, home insets should be same as system insets.
-            dp = dp.copy(mContext);
-        }
-        dp.updateInsets(targets.homeContentInsets);
-        dp.updateIsSeascape(mContext);
         if (runningTaskTarget != null) {
             mTaskViewSimulator.setPreview(runningTaskTarget);
         }
 
-        initTransitionEndpoints(dp);
+        // Only initialize the device profile, if it has not been initialized before, as in some
+        // configurations targets.homeContentInsets may not be correct.
+        if (mActivity == null) {
+            DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
+            if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
+                Rect overviewStackBounds = mActivityInterface
+                        .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
+                dp = dp.getMultiWindowProfile(mContext,
+                        new WindowBounds(overviewStackBounds, targets.homeContentInsets));
+            } else {
+                // If we are not in multi-window mode, home insets should be same as system insets.
+                dp = dp.copy(mContext);
+            }
+            dp.updateInsets(targets.homeContentInsets);
+            dp.updateIsSeascape(mContext);
+            initTransitionEndpoints(dp);
+        }
 
         // Notify when the animation starts
         if (!mRecentsAnimationStartCallbacks.isEmpty()) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
index e825c5f..413a813 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
@@ -22,6 +22,12 @@
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
@@ -62,6 +68,7 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
@@ -275,8 +282,7 @@
         if (mActivity == activity) {
             return true;
         }
-        mTaskViewSimulator.setLayoutRotation(mDeviceState.getCurrentActiveRotation(),
-                mDeviceState.getDisplayRotation());
+
         if (mActivity != null) {
             // The launcher may have been recreated as a result of device rotation.
             int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES;
@@ -329,6 +335,7 @@
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return;
         }
+        mTaskViewSimulator.setRecentsConfiguration(mActivity.getResources().getConfiguration());
 
         // If we've already ended the gesture and are going home, don't prepare recents UI,
         // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
@@ -891,6 +898,27 @@
                 ContainerType.NAVBAR, ContainerType.APP,
                 endTarget.containerType,
                 pageIndex);
+        StatsLogManager.EventEnum event;
+        switch (endTarget) {
+            case HOME:
+                event = LAUNCHER_HOME_GESTURE;
+                break;
+            case RECENTS:
+                event = LAUNCHER_OVERVIEW_GESTURE;
+                break;
+            case LAST_TASK:
+            case NEW_TASK:
+                event = (mLogDirection == Direction.LEFT)
+                        ? LAUNCHER_QUICKSWITCH_LEFT
+                        : LAUNCHER_QUICKSWITCH_RIGHT;
+                break;
+            default:
+                event = IGNORE;
+        }
+        StatsLogManager.newInstance(mContext).logger()
+                .withSrcState(LAUNCHER_STATE_BACKGROUND)
+                .withDstState(StatsLogManager.containerTypeToAtomState(endTarget.containerType))
+                .log(event);
     }
 
     /** Animates to the given progress, where 0 is the current app and 1 is overview. */
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
index e49c466..ba8ba33 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
@@ -17,6 +17,7 @@
 package com.android.quickstep;
 
 import static android.content.Intent.EXTRA_STREAM;
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
 
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.util.ImageActionUtils.persistBitmapAndStartActivity;
@@ -67,7 +68,9 @@
 
         UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(mContext,
                 mBitmapSupplier.get(), crop, intent, (uri, intentForUri) -> {
-                    intentForUri.putExtra(EXTRA_STREAM, uri);
+                    intentForUri
+                            .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
+                            .putExtra(EXTRA_STREAM, uri);
                     return new Intent[]{intentForUri};
                 }, TAG));
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 6751723..edefbe1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -120,9 +120,7 @@
     @Override
     public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
             boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
-        ((RecentsView) getCreatedActivity().getOverviewPanel())
-                .setLayoutRotation(deviceState.getCurrentActiveRotation(),
-                        deviceState.getDisplayRotation());
+        notifyRecentsOfOrientation(deviceState);
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback) {
             @Override
             public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
index ebc83c6..6f4d34c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -4,21 +4,18 @@
 import android.content.Context;
 import android.os.Bundle;
 
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.testing.TestInformationHandler;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
 import com.android.quickstep.util.LayoutUtils;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
 public class QuickstepTestInformationHandler extends TestInformationHandler {
 
-    private final Context mContext;
+    protected final Context mContext;
+
     public QuickstepTestInformationHandler(Context context) {
         mContext = context;
     }
@@ -27,6 +24,15 @@
     public Bundle call(String method) {
         final Bundle response = new Bundle();
         switch (method) {
+            case TestProtocol.REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT: {
+                return getLauncherUIProperty(Bundle::putInt, l -> {
+                    final float progress = LauncherState.OVERVIEW.getVerticalProgress(l)
+                            - LauncherState.ALL_APPS.getVerticalProgress(l);
+                    final float distance = l.getAllAppsController().getShiftRange() * progress;
+                    return (int) distance;
+                });
+            }
+
             case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
                 final float swipeHeight =
                         LayoutUtils.getDefaultSwipeHeight(mContext, mDeviceProfile);
@@ -47,26 +53,6 @@
                         Bundle::putInt, PortraitStatesTouchController::getHotseatTop);
             }
 
-            case TestProtocol.REQUEST_RECENT_TASKS_LIST: {
-                ArrayList<String> taskBaseIntentComponents = new ArrayList<>();
-                CountDownLatch latch = new CountDownLatch(1);
-                RecentsModel.INSTANCE.get(mContext).getTasks((tasks) -> {
-                    for (Task t : tasks) {
-                        taskBaseIntentComponents.add(
-                                t.key.baseIntent.getComponent().flattenToString());
-                    }
-                    latch.countDown();
-                });
-                try {
-                    latch.await(2, TimeUnit.SECONDS);
-                } catch (InterruptedException e) {
-                    throw new RuntimeException(e);
-                }
-                response.putStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD,
-                        taskBaseIntentComponents);
-                return response;
-            }
-
             case TestProtocol.REQUEST_OVERVIEW_ACTIONS_ENABLED: {
                 response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
                         FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get());
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index 1701020..6f2f9fb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -55,7 +55,7 @@
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.fallback.FallbackRecentsStateController;
 import com.android.quickstep.fallback.FallbackRecentsView;
-import com.android.quickstep.fallback.RecentsRootView;
+import com.android.quickstep.fallback.RecentsDragLayer;
 import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.util.RecentsAtomicAnimationFactory;
 import com.android.quickstep.views.OverviewActionsView;
@@ -78,7 +78,8 @@
             new ActivityTracker<>();
 
     private Handler mUiHandler = new Handler(Looper.getMainLooper());
-    private RecentsRootView mRecentsRootView;
+
+    private RecentsDragLayer mDragLayer;
     private FallbackRecentsView mFallbackRecentsView;
     private OverviewActionsView mActionsView;
 
@@ -89,13 +90,14 @@
     /**
      * Init drag layer and overview panel views.
      */
-    protected void initViews() {
-        setContentView(R.layout.fallback_recents_activity);
-        mRecentsRootView = findViewById(R.id.drag_layer);
+    protected void setupViews() {
+        inflateRootView(R.layout.fallback_recents_activity);
+        setContentView(getRootView());
+        mDragLayer = findViewById(R.id.drag_layer);
         mFallbackRecentsView = findViewById(R.id.overview_panel);
         mActionsView = findViewById(R.id.overview_actions_view);
 
-        mRecentsRootView.recreateControllers();
+        mDragLayer.recreateControllers();
         mFallbackRecentsView.init(mActionsView);
     }
 
@@ -105,12 +107,6 @@
         super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig);
     }
 
-    public void onRootViewSizeChanged() {
-        if (isInMultiWindowMode()) {
-            onHandleConfigChanged();
-        }
-    }
-
     @Override
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
@@ -130,7 +126,7 @@
         dispatchDeviceProfileChanged();
 
         reapplyUi();
-        mRecentsRootView.recreateControllers();
+        mDragLayer.recreateControllers();
     }
 
     /**
@@ -142,19 +138,14 @@
 
         // In case we are reusing IDP, create a copy so that we don't conflict with Launcher
         // activity.
-        return (mRecentsRootView != null) && isInMultiWindowMode()
+        return (mDragLayer != null) && isInMultiWindowMode()
                 ? dp.getMultiWindowProfile(this, getMultiWindowDisplaySize())
                 : dp.copy(this);
     }
 
     @Override
     public BaseDragLayer getDragLayer() {
-        return mRecentsRootView;
-    }
-
-    @Override
-    public View getRootView() {
-        return mRecentsRootView;
+        return mDragLayer;
     }
 
     @Override
@@ -252,7 +243,7 @@
 
         mOldConfig = new Configuration(getResources().getConfiguration());
         initDeviceProfile();
-        initViews();
+        setupViews();
 
         getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
                 Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index ed07062..01936e4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -66,7 +66,7 @@
             }
         }
         RecentsOrientedState orientedState = taskView.getRecentsView().getPagedViewOrientedState();
-        boolean canLauncherRotate = orientedState.canLauncherRotate();
+        boolean canLauncherRotate = orientedState.canRecentsActivityRotate();
         boolean isInLandscape = orientedState.getTouchRotation() != ROTATION_0;
 
         // Add overview actions to the menu when in in-place rotate landscape mode.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 37314ea..6e0b517 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -471,7 +471,6 @@
                 Log.d(TestProtocol.NO_SWIPE_TO_HOME, "TouchInteractionService.onInputEvent:DOWN");
             }
             mDeviceState.setOrientationTransformIfNeeded(event);
-            GestureState newGestureState;
 
             if (mDeviceState.isInSwipeUpTouchRegion(event)) {
                 if (TestProtocol.sDebugTracing) {
@@ -481,35 +480,31 @@
                 // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
                 // onConsumerInactive and wipe the previous gesture state
                 GestureState prevGestureState = new GestureState(mGestureState);
-                newGestureState = createGestureState();
+                GestureState newGestureState = createGestureState(mGestureState);
                 mConsumer.onConsumerAboutToBeSwitched();
-                mConsumer = newConsumer(prevGestureState, newGestureState, event);
+                mGestureState = newGestureState;
+                mConsumer = newConsumer(prevGestureState, mGestureState, event);
 
                 ActiveGestureLog.INSTANCE.addLog("setInputConsumer: " + mConsumer.getName());
                 mUncheckedConsumer = mConsumer;
             } else if (mDeviceState.isUserUnlocked() && mDeviceState.isFullyGesturalNavMode()) {
-                newGestureState = createGestureState();
-                ActivityManager.RunningTaskInfo runningTask = newGestureState.getRunningTask();
+                mGestureState = createGestureState(mGestureState);
+                ActivityManager.RunningTaskInfo runningTask = mGestureState.getRunningTask();
                 if (mDeviceState.canTriggerAssistantAction(event, runningTask)) {
                     // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
                     // should not interrupt it. QuickSwitch assumes that interruption can only
                     // happen if the next gesture is also quick switch.
                     mUncheckedConsumer = new AssistantInputConsumer(
                             this,
-                            newGestureState,
+                            mGestureState,
                             InputConsumer.NO_OP, mInputMonitorCompat,
                             mOverviewComponentObserver.assistantGestureIsConstrained());
                 } else {
-                    newGestureState = DEFAULT_STATE;
                     mUncheckedConsumer = InputConsumer.NO_OP;
                 }
             } else {
-                newGestureState = DEFAULT_STATE;
                 mUncheckedConsumer = InputConsumer.NO_OP;
             }
-
-            // Save the current gesture state
-            mGestureState = newGestureState;
         } else {
             // Other events
             if (mUncheckedConsumer != InputConsumer.NO_OP) {
@@ -519,7 +514,17 @@
         }
 
         if (mUncheckedConsumer != InputConsumer.NO_OP) {
-            ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
+            switch (event.getActionMasked()) {
+                case ACTION_DOWN:
+                case ACTION_UP:
+                    ActiveGestureLog.INSTANCE.addLog("onMotionEvent("
+                            + (int) event.getRawX() + ", " + (int) event.getRawY() + ")",
+                            event.getActionMasked());
+                    break;
+                default:
+                    ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
+                    break;
+            }
         }
 
         boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL)
@@ -533,14 +538,14 @@
         TraceHelper.INSTANCE.endFlagsOverride(traceToken);
     }
 
-    private GestureState createGestureState() {
+    private GestureState createGestureState(GestureState previousGestureState) {
         GestureState gestureState = new GestureState(mOverviewComponentObserver,
                 ActiveGestureLog.INSTANCE.generateAndSetLogId());
         if (mTaskAnimationManager.isRecentsAnimationRunning()) {
-            gestureState.updateRunningTask(mGestureState.getRunningTask());
-            gestureState.updateLastStartedTaskId(mGestureState.getLastStartedTaskId());
+            gestureState.updateRunningTask(previousGestureState.getRunningTask());
+            gestureState.updateLastStartedTaskId(previousGestureState.getLastStartedTaskId());
             gestureState.updatePreviouslyAppearedTaskIds(
-                    mGestureState.getPreviouslyAppearedTaskIds());
+                    previousGestureState.getPreviouslyAppearedTaskIds());
         } else {
             gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
                     () -> mAM.getRunningTask(false /* filterOnlyVisibleRecents */)));
@@ -743,7 +748,7 @@
 
     private void reset() {
         mConsumer = mUncheckedConsumer = mResetGestureInputConsumer;
-        mGestureState = new GestureState();
+        mGestureState = DEFAULT_STATE;
     }
 
     private void preloadOverview(boolean fromInit) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 9242771..d20bbe9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -107,11 +107,22 @@
     }
 
     @Override
+    protected boolean shouldAddDummyTaskView(int runningTaskId) {
+        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == runningTaskId
+                && getTaskViewCount() == 0) {
+            // Do not add a dummy task if we are running over home with empty recents, so that we
+            // show the empty recents message instead of showing a dummy task and later removing it.
+            return false;
+        }
+        return super.shouldAddDummyTaskView(runningTaskId);
+    }
+
+    @Override
     protected void applyLoadPlan(ArrayList<Task> tasks) {
         // When quick-switching on 3p-launcher, we add a "dummy" tile corresponding to Launcher
         // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
         // track the index of the next task appropriately, as if we are switching on any other app.
-        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == mRunningTaskId) {
+        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == mRunningTaskId && !tasks.isEmpty()) {
             // Check if the task list has running task
             boolean found = false;
             for (Task t : tasks) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java
new file mode 100644
index 0000000..a00015a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.RecentsActivity;
+
+/**
+ * Drag layer for fallback recents activity
+ */
+public class RecentsDragLayer extends BaseDragLayer<RecentsActivity> {
+
+    public RecentsDragLayer(Context context, AttributeSet attrs) {
+        super(context, attrs, 1 /* alphaChannelCount */);
+    }
+
+    @Override
+    public void recreateControllers() {
+        mControllers = new TouchController[] {
+                new RecentsTaskController(mActivity),
+                new FallbackNavBarTouchController(mActivity),
+        };
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        super.setInsets(insets);
+        setBackground(insets.top == 0  || !mAllowSysuiScrims
+                ? null
+                : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim));
+    }
+}
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
deleted file mode 100644
index 7f5ec9b..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.util.TouchController;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.RecentsActivity;
-
-public class RecentsRootView extends BaseDragLayer<RecentsActivity> {
-
-    private static final int MIN_SIZE = 10;
-
-    private final Point mLastKnownSize = new Point(MIN_SIZE, MIN_SIZE);
-
-    public RecentsRootView(Context context, AttributeSet attrs) {
-        super(context, attrs, 1 /* alphaChannelCount */);
-        setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                | SYSTEM_UI_FLAG_LAYOUT_STABLE);
-    }
-
-    public Point getLastKnownSize() {
-        return mLastKnownSize;
-    }
-
-    @Override
-    public void recreateControllers() {
-        mControllers = new TouchController[] {
-                new RecentsTaskController(mActivity),
-                new FallbackNavBarTouchController(mActivity),
-        };
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        // Check size changes before the actual measure, to avoid multiple measure calls.
-        int width = Math.max(MIN_SIZE, MeasureSpec.getSize(widthMeasureSpec));
-        int height = Math.max(MIN_SIZE, MeasureSpec.getSize(heightMeasureSpec));
-        if (mLastKnownSize.x != width || mLastKnownSize.y != height) {
-            mLastKnownSize.set(width, height);
-            mActivity.onRootViewSizeChanged();
-        }
-
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-    }
-
-    @TargetApi(23)
-    @Override
-    protected boolean fitSystemWindows(Rect insets) {
-        // Update device profile before notifying the children.
-        mActivity.getDeviceProfile().updateInsets(insets);
-        setInsets(insets);
-        return false; // Let children get the full insets
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        // If the insets haven't changed, this is a no-op. Avoid unnecessary layout caused by
-        // modifying child layout params.
-        if (!insets.equals(mInsets)) {
-            super.setInsets(insets);
-        }
-        setBackground(insets.top == 0  || !mAllowSysuiScrims
-                ? null
-                : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim));
-    }
-
-    public void dispatchInsets() {
-        mActivity.getDeviceProfile().updateInsets(mInsets);
-        super.setInsets(mInsets);
-    }
-}
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index a9f138e..4e967cf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -23,6 +23,8 @@
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
+import static com.android.launcher3.PagedView.ACTION_MOVE_ALLOW_EASY_FLING;
+import static com.android.launcher3.PagedView.DEBUG_FAILED_QUICKSWITCH;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
@@ -38,6 +40,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
@@ -76,7 +79,8 @@
     private static final String UP_EVT = "OtherActivityInputConsumer.UP";
 
     // TODO: Move to quickstep contract
-    public static final float QUICKSTEP_TOUCH_SLOP_RATIO = 3;
+    public static final float QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON = 9;
+    public static final float QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL = 2;
 
     private final RecentsAnimationDeviceState mDeviceState;
     private final NavBarPosition mNavBarPosition;
@@ -150,10 +154,12 @@
 
         boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
         mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
-        mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
 
-        float slop = QUICKSTEP_TOUCH_SLOP_RATIO * mTouchSlop;
-        mSquaredTouchSlop = slop * slop;
+        float slopMultiplier = mDeviceState.isFullyGesturalNavMode()
+                ? QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL
+                : QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON;
+        mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+        mSquaredTouchSlop = slopMultiplier * mTouchSlop * mTouchSlop;
 
         mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
@@ -187,6 +193,10 @@
                 && !mRecentsViewDispatcher.hasConsumer()) {
             mRecentsViewDispatcher.setConsumer(mInteractionHandler
                     .getRecentsViewDispatcher(mNavBarPosition.getRotation()));
+            int action = ev.getAction();
+            ev.setAction(ACTION_MOVE_ALLOW_EASY_FLING);
+            mRecentsViewDispatcher.dispatchEvent(ev);
+            ev.setAction(action);
         }
         int edgeFlags = ev.getEdgeFlags();
         ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
@@ -317,6 +327,13 @@
             }
             case ACTION_CANCEL:
             case ACTION_UP: {
+                if (DEBUG_FAILED_QUICKSWITCH && !mPassedWindowMoveSlop) {
+                    float displacementX = mLastPos.x - mDownPos.x;
+                    float displacementY = mLastPos.y - mDownPos.y;
+                    Log.d("Quickswitch", "mPassedWindowMoveSlop=false"
+                            + " disp=" + squaredHypot(displacementX, displacementY)
+                            + " slop=" + mSquaredTouchSlop);
+                }
                 finishTouchTracking(ev);
                 break;
             }
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 4440a04..d972c0f 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,6 +15,9 @@
  */
 package com.android.quickstep.inputconsumers;
 
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
@@ -22,9 +25,10 @@
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.logging.StatsLogUtils;
+import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.quickstep.GestureState;
@@ -40,11 +44,13 @@
     private final Context mContext;
     private final InputMonitorCompat mInputMonitor;
     private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
+    private final GestureState mGestureState;
 
     public OverviewWithoutFocusInputConsumer(Context context,
             RecentsAnimationDeviceState deviceState, GestureState gestureState,
             InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) {
         mContext = context;
+        mGestureState = gestureState;
         mInputMonitor = inputMonitor;
         mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, disableHorizontalSwipe,
                 deviceState.getNavBarPosition(), this::onInterceptTouch, this);
@@ -81,10 +87,21 @@
         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());
+        int containerType = (mGestureState != null && mGestureState.getEndTarget() != null)
+                ? mGestureState.getEndTarget().containerType
+                : LauncherLogProto.ContainerType.WORKSPACE;
         activity.getUserEventDispatcher().logActionOnContainer(
                 wasFling ? Touch.FLING : Touch.SWIPE, Direction.UP, containerType, pageIndex);
         activity.getUserEventDispatcher().setPreviousHomeGesture(true);
+        activity.getStatsLogManager().logger()
+                .withSrcState(LAUNCHER_STATE_HOME)
+                .withDstState(LAUNCHER_STATE_HOME)
+                .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                        .setWorkspace(
+                                LauncherAtom.WorkspaceContainer.newBuilder()
+                                        .setPageIndex(-1))
+                        .build())
+                .log(LAUNCHER_HOME_GESTURE);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
index 196a7c4..46013d3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -22,6 +22,7 @@
 
 import android.animation.TimeInterpolator;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -125,6 +126,14 @@
     }
 
     /**
+     * @see com.android.quickstep.views.RecentsView#onConfigurationChanged(Configuration)
+     */
+    public void setRecentsConfiguration(Configuration configuration) {
+        mOrientationState.setActivityConfiguration(configuration);
+        mLayoutValid = false;
+    }
+
+    /**
      * @see com.android.quickstep.views.RecentsView#FULLSCREEN_PROGRESS
      */
     public float getFullScreenScale() {
@@ -141,7 +150,8 @@
      */
     public void setPreview(RemoteAnimationTargetCompat runningTarget) {
         setPreviewBounds(runningTarget.screenSpaceBounds, runningTarget.contentInsets);
-        mRunningTargetWindowPosition.set(runningTarget.position.x, runningTarget.position.y);
+        mRunningTargetWindowPosition.set(runningTarget.screenSpaceBounds.left,
+                runningTarget.screenSpaceBounds.top);
     }
 
     /**
@@ -204,7 +214,8 @@
     public void applyWindowToHomeRotation(Matrix matrix) {
         mMatrix.postTranslate(mDp.windowX, mDp.windowY);
         postDisplayRotation(deltaRotation(
-                mOrientationState.getLauncherRotation(), mOrientationState.getDisplayRotation()),
+                mOrientationState.getRecentsActivityRotation(),
+                mOrientationState.getDisplayRotation()),
                 mDp.widthPx, mDp.heightPx, matrix);
         matrix.postTranslate(-mRunningTargetWindowPosition.x, -mRunningTargetWindowPosition.y);
     }
@@ -225,7 +236,7 @@
             mPositionHelper.updateThumbnailMatrix(
                     mThumbnailPosition, mThumbnailData,
                     mTaskRect.width(), mTaskRect.height(),
-                    mDp, mOrientationState.getLauncherRotation());
+                    mDp, mOrientationState.getRecentsActivityRotation());
             mPositionHelper.getMatrix().invert(mInversePositionMatrix);
 
             PagedOrientationHandler poh = mOrientationState.getOrientationHandler();
@@ -255,8 +266,8 @@
         float taskHeight = mTaskRect.height();
 
         mMatrix.set(mPositionHelper.getMatrix());
-        mMatrix.postScale(scale, scale);
         mMatrix.postTranslate(insets.left, insets.top);
+        mMatrix.postScale(scale, scale);
 
         // Apply TaskView matrix: translate, scale, scroll
         mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index a19026b..846b944 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -31,6 +31,7 @@
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.os.Build;
+import android.os.UserHandle;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.Surface;
@@ -42,6 +43,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
@@ -52,6 +54,7 @@
 import com.android.quickstep.util.TransformParams;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.RecentsExtraCard;
+import com.android.systemui.shared.recents.model.Task;
 
 /**
  * {@link RecentsView} used in Launcher activity
@@ -167,14 +170,21 @@
     }
 
     @Override
-    protected void onTaskLaunched(boolean success) {
+    protected void onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             mActivity.getStateManager().goToState(NORMAL, false /* animate */);
         } else {
             LauncherState state = mActivity.getStateManager().getState();
             mActivity.getAllAppsController().setState(state);
         }
-        super.onTaskLaunched(success);
+        super.onTaskLaunchAnimationEnd(success);
+    }
+
+    @Override
+    public void onTaskLaunched(Task task) {
+        UserHandle user =  UserHandle.of(task.key.userId);
+        AppLaunchTracker.INSTANCE.get(getContext()).onStartApp(task.getTopComponent(), user,
+                AppLaunchTracker.CONTAINER_OVERVIEW);
     }
 
     @Override
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 885ea5a..68b8975 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
@@ -59,6 +59,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -105,6 +106,7 @@
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.PagedOrientationHandler.CurveProperties;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -146,8 +148,8 @@
  * A list of recent tasks.
  */
 @TargetApi(Build.VERSION_CODES.P)
-public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable,
-        TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
+public abstract class RecentsView<T extends StatefulActivity> extends PagedView implements
+        Insettable, TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
         InvariantDeviceProfile.OnIDPChangeListener, TaskVisualsChangeListener,
         SplitScreenBounds.OnChangeListener {
 
@@ -208,7 +210,7 @@
                 }
             };
 
-    protected final RecentsOrientedState mOrientationState;
+    protected RecentsOrientedState mOrientationState;
     protected final BaseActivityInterface mSizeStrategy;
     protected RecentsAnimationController mRecentsAnimationController;
     protected RecentsAnimationTargets mRecentsAnimationTargets;
@@ -371,11 +373,17 @@
 
     private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener =
             (inMultiWindowMode) -> {
-        if (!inMultiWindowMode && mOverviewStateEnabled) {
-            // TODO: Re-enable layout transitions for addition of the unpinned task
-            reloadIfNeeded();
-        }
-    };
+                if (mOrientationState != null) {
+                    mOrientationState.setMultiWindowMode(inMultiWindowMode);
+                    setLayoutRotation(mOrientationState.getTouchRotation(),
+                            mOrientationState.getDisplayRotation());
+                    rotateAllChildTasks();
+                }
+                if (!inMultiWindowMode && mOverviewStateEnabled) {
+                    // TODO: Re-enable layout transitions for addition of the unpinned task
+                    reloadIfNeeded();
+                }
+            };
 
     public RecentsView(Context context, AttributeSet attrs, int defStyleAttr,
             BaseActivityInterface sizeStrategy) {
@@ -383,12 +391,13 @@
         setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
         setEnableFreeScroll(true);
         mSizeStrategy = sizeStrategy;
+        mActivity = BaseActivity.fromContext(context);
         mOrientationState = new RecentsOrientedState(
                 context, mSizeStrategy, this::animateRecentsRotationInPlace);
+        mOrientationState.setActivityConfiguration(context.getResources().getConfiguration());
 
         mFastFlingVelocity = getResources()
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
-        mActivity = BaseActivity.fromContext(context);
         mModel = RecentsModel.INSTANCE.get(context);
         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
 
@@ -792,8 +801,10 @@
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
             TaskView taskView = getTaskViewAt(i);
             if (mIgnoreResetTaskId != taskView.getTask().key.id) {
-                taskView.resetVisualProperties();
+                taskView.resetViewTransforms();
                 taskView.setStableAlpha(mContentAlpha);
+                taskView.setFullscreenProgress(mFullscreenProgress);
+                taskView.setModalness(mTaskModalness);
             }
         }
         if (mRunningTaskTileHidden) {
@@ -841,7 +852,6 @@
 
     private void resetPaddingFromTaskSize() {
         DeviceProfile dp = mActivity.getDeviceProfile();
-        mOrientationState.setMultiWindowMode(dp.isMultiWindowMode);
         getTaskSize(mTempRect);
         mTaskWidth = mTempRect.width();
         mTaskHeight = mTempRect.height();
@@ -993,6 +1003,7 @@
         mDwbToastShown = false;
         mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
         LayoutUtils.setViewEnabled(mActionsView, true);
+        mOrientationState.setGestureActive(false);
     }
 
     public @Nullable TaskView getRunningTaskView() {
@@ -1030,6 +1041,7 @@
      */
     public void onGestureAnimationStart(int runningTaskId) {
         // This needs to be called before the other states are set since it can create the task view
+        mOrientationState.setGestureActive(true);
         showCurrentTask(runningTaskId);
         setEnableFreeScroll(false);
         setEnableDrawingLiveTile(false);
@@ -1053,9 +1065,8 @@
     }
 
     private void animateRecentsRotationInPlace(int newRotation) {
-        if (mOrientationState.canLauncherRotate()) {
-            // Update the rotation but let system take care of the rotation animation
-            setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
+        if (mOrientationState.canRecentsActivityRotate()) {
+            // Let system take care of the rotation
             return;
         }
         AnimatorSet pa = setRecentsChangedOrientation(true);
@@ -1093,6 +1104,8 @@
      * Called when a gesture from an app has finished.
      */
     public void onGestureAnimationEnd() {
+        mOrientationState.setGestureActive(false);
+
         setOnScrollChangeListener(null);
         setEnableFreeScroll(true);
         setEnableDrawingLiveTile(true);
@@ -1105,13 +1118,20 @@
     }
 
     /**
+     * Returns true if we should add a dummy taskView for the running task id
+     */
+    protected boolean shouldAddDummyTaskView(int runningTaskId) {
+        return getTaskView(runningTaskId) == null;
+    }
+
+    /**
      * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
      *
      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
      * is called.  Also scrolls the view to this task.
      */
     public void showCurrentTask(int runningTaskId) {
-        if (getTaskView(runningTaskId) == null) {
+        if (shouldAddDummyTaskView(runningTaskId)) {
             boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView = mTaskViewPool.getView();
@@ -1628,27 +1648,41 @@
         }
     }
 
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (mOrientationState.setActivityConfiguration(newConfig)) {
+            updateOrientationHandler();
+        }
+    }
+
     public void setLayoutRotation(int touchRotation, int displayRotation) {
         if (mOrientationState.update(touchRotation, displayRotation)) {
-            mOrientationHandler = mOrientationState.getOrientationHandler();
-            mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
-            setLayoutDirection(mIsRtl
-                    ? View.LAYOUT_DIRECTION_RTL
-                    : View.LAYOUT_DIRECTION_LTR);
-            mClearAllButton.setLayoutDirection(mIsRtl
-                    ? View.LAYOUT_DIRECTION_LTR
-                    : View.LAYOUT_DIRECTION_RTL);
-            mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
-            mActivity.getDragLayer().recreateControllers();
-            boolean isInLandscape = touchRotation != 0
-                    || mOrientationState.getLauncherRotation() != ROTATION_0;
-            mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
-                    !mOrientationState.canLauncherRotate() && isInLandscape);
-            resetPaddingFromTaskSize();
-            requestLayout();
+            updateOrientationHandler();
         }
     }
 
+    private void updateOrientationHandler() {
+        mOrientationHandler = mOrientationState.getOrientationHandler();
+        mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
+        setLayoutDirection(mIsRtl
+                ? View.LAYOUT_DIRECTION_RTL
+                : View.LAYOUT_DIRECTION_LTR);
+        mClearAllButton.setLayoutDirection(mIsRtl
+                ? View.LAYOUT_DIRECTION_LTR
+                : View.LAYOUT_DIRECTION_RTL);
+        mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
+        mActivity.getDragLayer().recreateControllers();
+        boolean isInLandscape = mOrientationState.getTouchRotation() != 0
+                || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
+        mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
+                !mOrientationState.canRecentsActivityRotate() && isInLandscape);
+        resetPaddingFromTaskSize();
+        requestLayout();
+        // Reapply the current page to update page scrolls.
+        setCurrentPage(mCurrentPage);
+    }
+
     public RecentsOrientedState getPagedViewOrientedState() {
         return mOrientationState;
     }
@@ -1932,7 +1966,7 @@
         mPendingAnimation.addEndListener((endState) -> {
             if (endState.isSuccess) {
                 Consumer<Boolean> onLaunchResult = (result) -> {
-                    onTaskLaunched(result);
+                    onTaskLaunchAnimationEnd(result);
                     if (!result) {
                         tv.notifyTaskLaunchFailed(TAG);
                     }
@@ -1947,7 +1981,7 @@
                             .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
                 }
             } else {
-                onTaskLaunched(false);
+                onTaskLaunchAnimationEnd(false);
             }
             mPendingAnimation = null;
         });
@@ -1959,12 +1993,17 @@
 
     public abstract boolean shouldUseMultiWindowTaskSizeStrategy();
 
-    protected void onTaskLaunched(boolean success) {
+    protected void onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             resetTaskVisuals();
         }
     }
 
+    /**
+     * Called when task activity is launched
+     */
+    public void onTaskLaunched(Task task){ }
+
     @Override
     protected void notifyPageSwitchListener(int prevPage) {
         super.notifyPageSwitchListener(prevPage);
@@ -2214,7 +2253,7 @@
             getCurrentPageTaskView().setModalness(modalness);
         }
         // Only show actions view when it's modal for in-place landscape mode.
-        boolean inPlaceLandscape = !mOrientationState.canLauncherRotate()
+        boolean inPlaceLandscape = !mOrientationState.canRecentsActivityRotate()
                 && mOrientationState.getTouchRotation() != ROTATION_0;
         mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, modalness < 1 && inPlaceLandscape);
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index 411bab4..222f6e6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -30,7 +30,8 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent
+        .LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
 
 import android.animation.Animator;
@@ -384,6 +385,7 @@
                             }
                         }, resultCallbackHandler);
             }
+            getRecentsView().onTaskLaunched(mTask);
         }
     }
 
@@ -545,7 +547,7 @@
         setIconAndDimTransitionProgress(iconScale, invert);
     }
 
-    private void resetViewTransforms() {
+    protected void resetViewTransforms() {
         setCurveScale(1);
         setTranslationX(0f);
         setTranslationY(0f);
@@ -554,12 +556,6 @@
         setIconScaleAndDim(1);
     }
 
-    public void resetVisualProperties() {
-        resetViewTransforms();
-        setFullscreenProgress(0);
-        setModalness(0);
-    }
-
     public void setStableAlpha(float parentAlpha) {
         mStableAlpha = parentAlpha;
         setAlpha(mStableAlpha);
@@ -953,9 +949,6 @@
      */
     public void setFullscreenProgress(float progress) {
         progress = Utilities.boundToRange(progress, 0, 1);
-        if (progress == mFullscreenProgress) {
-            return;
-        }
         mFullscreenProgress = progress;
         boolean isFullscreen = mFullscreenProgress > 0;
         mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 2b11ca0..0f2955b 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -17,7 +17,9 @@
     <string name="task_overlay_factory_class" translatable="false"/>
 
     <!-- Activities which block home gesture -->
-    <string-array name="gesture_blocking_activities" translatable="false"/>
+    <string-array name="gesture_blocking_activities" translatable="false">
+        <item>com.android.launcher3/com.android.quickstep.interaction.GestureSandboxActivity</item>
+    </string-array>
 
     <string name="stats_log_manager_class" translatable="false">com.android.quickstep.logging.StatsLogCompatManager</string>
 
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 1b82826..769d298 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -87,7 +87,7 @@
     <string name="hotseat_tip_gaps_filled">App suggestions added to empty space</string>
     <!-- tip shown when user migrates and predictions are enabled in hotseat -->
     <string name="hotsaet_tip_prediction_enabled">App suggestions enabled</string>
-    <!-- tip shown when hotseat edu is requested while predicions are disabled -->
+    <!-- tip shown when hotseat edu is requested while predictions are disabled -->
     <string name="hotsaet_tip_prediction_disabled">App suggestions are disabled</string>
 
     <!-- content description for hotseat items -->
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
new file mode 100644
index 0000000..a31ba21
--- /dev/null
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.quickstep.util;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.display.DisplayManager;
+import android.view.Surface;
+import android.view.SurfaceControl;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.shadows.LShadowDisplay;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.quickstep.LauncherActivityInterface;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowDisplayManager;
+
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class TaskViewSimulatorTest {
+
+    @Test
+    public void taskProperlyScaled_portrait_noRotation_sameInsets1() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(1200, 2450)
+                .withInsets(new Rect(0, 80, 0, 120))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_portrait_noRotation_sameInsets2() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(1200, 2450)
+                .withInsets(new Rect(55, 80, 55, 120))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_landscape_noRotation_sameInsets1() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(2450, 1250)
+                .withInsets(new Rect(0, 80, 0, 40))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_landscape_noRotation_sameInsets2() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(2450, 1250)
+                .withInsets(new Rect(0, 80, 120, 0))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_landscape_noRotation_sameInsets3() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(2450, 1250)
+                .withInsets(new Rect(55, 80, 55, 120))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_landscape_rotated() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(1200, 2450)
+                .withInsets(new Rect(0, 80, 0, 120))
+                .withAppBounds(
+                        new Rect(0, 0, 2450, 1200),
+                        new Rect(0, 80, 0, 120),
+                        Surface.ROTATION_90)
+                .verifyNoTransforms();
+    }
+
+    private static class TaskMatrixVerifier extends TransformParams {
+
+        private final Context mContext = RuntimeEnvironment.application;
+
+        private Rect mAppBounds = new Rect();
+        private Rect mLauncherInsets = new Rect();
+
+        private Rect mAppInsets;
+
+        private int mAppRotation = -1;
+        private DeviceProfile mDeviceProfile;
+
+        TaskMatrixVerifier withLauncherSize(int width, int height) {
+            ShadowDisplayManager.changeDisplay(DEFAULT_DISPLAY,
+                    String.format("w%sdp-h%sdp-mdpi", width, height));
+            if (mAppBounds.isEmpty()) {
+                mAppBounds.set(0, 0, width, height);
+            }
+            return this;
+        }
+
+        TaskMatrixVerifier withInsets(Rect insets) {
+            LShadowDisplay shadowDisplay = Shadow.extract(
+                    mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
+            shadowDisplay.setInsets(insets);
+            mLauncherInsets.set(insets);
+            return this;
+        }
+
+        TaskMatrixVerifier withAppBounds(Rect bounds, Rect insets, int appRotation) {
+            mAppBounds.set(bounds);
+            mAppInsets = insets;
+            mAppRotation = appRotation;
+            return this;
+        }
+
+        void verifyNoTransforms() {
+            mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(mContext)
+                    .getDeviceProfile(mContext);
+            mDeviceProfile.updateInsets(mLauncherInsets);
+
+            TaskViewSimulator tvs = new TaskViewSimulator(mContext,
+                    LauncherActivityInterface.INSTANCE);
+            tvs.setDp(mDeviceProfile);
+
+            int launcherRotation = DefaultDisplay.INSTANCE.get(mContext).getInfo().rotation;
+            if (mAppRotation < 0) {
+                mAppRotation = launcherRotation;
+            }
+            tvs.setLayoutRotation(launcherRotation, mAppRotation);
+            if (mAppInsets == null) {
+                mAppInsets = new Rect(mLauncherInsets);
+            }
+            tvs.setPreviewBounds(mAppBounds, mAppInsets);
+
+            tvs.fullScreenProgress.value = 1;
+            tvs.recentsViewScale.value = tvs.getFullScreenScale();
+            tvs.apply(this);
+        }
+
+        @Override
+        public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) {
+            SurfaceParams.Builder builder = new SurfaceParams.Builder((SurfaceControl) null);
+            proxy.onBuildTargetParams(builder, null, this);
+            return new SurfaceParams[] {builder.build()};
+        }
+
+        @Override
+        public void applySurfaceParams(SurfaceParams[] params) {
+            // Verify that the task position remains the same
+            RectF newAppBounds = new RectF(mAppBounds);
+            params[0].matrix.mapRect(newAppBounds);
+            Assert.assertThat(newAppBounds, new AlmostSame(mAppBounds));
+
+            System.err.println("Bounds mapped: " + mAppBounds + " => " + newAppBounds);
+        }
+    }
+
+    private static class AlmostSame extends TypeSafeMatcher<RectF>  {
+
+        // Allow 1px error margin to account for float to int conversions
+        private final float mError = 1f;
+        private final Rect mExpected;
+
+        AlmostSame(Rect expected) {
+            mExpected = expected;
+        }
+
+        @Override
+        protected boolean matchesSafely(RectF item) {
+            return Math.abs(item.left - mExpected.left) < mError
+                    && Math.abs(item.top - mExpected.top) < mError
+                    && Math.abs(item.right - mExpected.right) < mError
+                    && Math.abs(item.bottom - mExpected.bottom) < mError;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendValue(mExpected);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index d2e0339..47ce320 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -83,6 +83,7 @@
         super.onCreate(savedInstanceState);
 
         SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this);
+        addMultiWindowModeChangedListener(mDepthController);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index 2b08dcd..fe8f0c6 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -20,11 +20,15 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
 import android.os.IBinder;
 import android.util.FloatProperty;
 import android.view.View;
 import android.view.ViewTreeObserver;
 
+import com.android.launcher3.BaseActivity;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
@@ -41,7 +45,8 @@
 /**
  * Controls blur and wallpaper zoom, for the Launcher surface only.
  */
-public class DepthController implements StateHandler<LauncherState> {
+public class DepthController implements StateHandler<LauncherState>,
+        BaseActivity.MultiWindowModeChangedListener {
 
     public static final FloatProperty<DepthController> DEPTH =
             new FloatProperty<DepthController>("depth") {
@@ -104,6 +109,9 @@
      */
     private float mDepth;
 
+    // Workaround for animating the depth when multiwindow mode changes.
+    private boolean mIgnoreStateChangesDuringMultiWindowAnimation = false;
+
     private View.OnAttachStateChangeListener mOnAttachListener;
 
     public DepthController(Launcher l) {
@@ -171,7 +179,7 @@
 
     @Override
     public void setState(LauncherState toState) {
-        if (mSurface == null) {
+        if (mSurface == null || mIgnoreStateChangesDuringMultiWindowAnimation) {
             return;
         }
 
@@ -186,7 +194,8 @@
             PendingAnimation animation) {
         if (mSurface == null
                 || config.onlyPlayAtomicComponent()
-                || config.hasAnimationFlag(SKIP_DEPTH_CONTROLLER)) {
+                || config.hasAnimationFlag(SKIP_DEPTH_CONTROLLER)
+                || mIgnoreStateChangesDuringMultiWindowAnimation) {
             return;
         }
 
@@ -231,4 +240,21 @@
                     .apply();
         }
     }
+
+    @Override
+    public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+        mIgnoreStateChangesDuringMultiWindowAnimation = true;
+
+        ObjectAnimator mwAnimation = ObjectAnimator.ofFloat(this, DEPTH,
+                mLauncher.getStateManager().getState().getDepth(mLauncher, isInMultiWindowMode))
+                .setDuration(300);
+        mwAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mIgnoreStateChangesDuringMultiWindowAnimation = false;
+            }
+        });
+        mwAnimation.setAutoCancel(true);
+        mwAnimation.start();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 8745814..20ee61d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -16,6 +16,8 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
+import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -36,7 +38,6 @@
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
-import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
@@ -124,7 +125,7 @@
                 return false;
             }
         }
-        if (AbstractFloatingView.getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE) != null) {
+        if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) {
             return false;
         }
         return true;
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 0863aa2..eac45e9 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -17,12 +17,12 @@
 package com.android.quickstep.logging;
 
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER;
-import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.PREDICTED_HOTSEAT_CONTAINER;
+import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__ALLAPPS;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__BACKGROUND;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__OVERVIEW;
-import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__SRC_STATE__HOME;
 
 import android.content.Context;
 import android.util.Log;
@@ -32,6 +32,10 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.FolderIcon;
+import com.android.launcher3.logger.LauncherAtom.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.logging.StatsLogManager;
@@ -41,11 +45,14 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.LogConfig;
 import com.android.systemui.shared.system.SysUiStatsLog;
 
 import java.util.ArrayList;
+import java.util.Optional;
 import java.util.OptionalInt;
 
 /**
@@ -68,6 +75,7 @@
     // from nano to lite, bake constant to prevent robo test failure.
     private static final int DEFAULT_PAGE_INDEX = -2;
     private static final int FOLDER_HIERARCHY_OFFSET = 100;
+    private static final int SEARCH_RESULT_HIERARCHY_OFFSET = 200;
 
     public StatsLogCompatManager(Context context) {
         sContext = context;
@@ -79,38 +87,6 @@
     }
 
     /**
-     * Logs an event.
-     *
-     * @param event an enum implementing EventEnum interface.
-     * @param atomInfo item typically containing app or task launch related information.
-     */
-    public void log(EventEnum event, InstanceId instanceId, LauncherAtom.ItemInfo atomInfo) {
-        LauncherAppState.getInstance(sContext).getModel().enqueueModelUpdateTask(
-                new BaseModelUpdateTask() {
-                    @Override
-                    public void execute(LauncherAppState app, BgDataModel dataModel,
-                            AllAppsList apps) {
-                        write(event, instanceId, atomInfo, LAUNCHER_UICHANGED__DST_STATE__HOME,
-                                LAUNCHER_UICHANGED__DST_STATE__BACKGROUND, OptionalInt.empty());
-                    }
-                });
-    }
-
-    /**
-     * Logs an event.
-     *
-     * @param event an enum implementing EventEnum interface.
-     * @param atomItemInfo item typically containing app or task launch related information.
-     */
-    @Override
-    public void log(EventEnum event, @Nullable LauncherAtom.ItemInfo atomItemInfo, int srcState,
-            int dstState) {
-        write(event, DEFAULT_INSTANCE_ID,
-                atomItemInfo == null ? LauncherAtom.ItemInfo.getDefaultInstance() : atomItemInfo,
-                srcState, dstState, OptionalInt.empty());
-    }
-
-    /**
      * Logs a ranking event and accompanying {@link InstanceId} and package name.
      */
     @Override
@@ -123,49 +99,6 @@
                 position /* position_picked = 4; */);
     }
 
-    private  void write(EventEnum event, InstanceId instanceId,
-            LauncherAtom.ItemInfo atomInfo,
-            int srcState, int dstState, OptionalInt mRank) {
-        if (IS_VERBOSE) {
-            String name = (event instanceof Enum) ? ((Enum) event).name() :
-                    event.getId() + "";
-
-            Log.d(TAG, instanceId == DEFAULT_INSTANCE_ID
-                    ? String.format("\n%s (State:%s->%s)\n%s", name, getStateString(srcState),
-                    getStateString(dstState),  atomInfo)
-                    : String.format("\n%s (State:%s->%s) (InstanceId:%s)\n%s", name,
-                            getStateString(srcState), getStateString(dstState), instanceId,
-                            atomInfo));
-        }
-
-        SysUiStatsLog.write(
-                SysUiStatsLog.LAUNCHER_EVENT,
-                SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */,
-                srcState,
-                dstState,
-                null /* launcher extensions, deprecated */,
-                false /* quickstep_enabled, deprecated */,
-                event.getId() /* event_id */,
-                atomInfo.getItemCase().getNumber() /* target_id */,
-                instanceId.getId() /* instance_id TODO */,
-                0 /* uid TODO */,
-                getPackageName(atomInfo) /* package_name */,
-                getComponentName(atomInfo) /* component_name */,
-                getGridX(atomInfo, false) /* grid_x */,
-                getGridY(atomInfo, false) /* grid_y */,
-                getPageId(atomInfo, false) /* page_id */,
-                getGridX(atomInfo, true) /* grid_x_parent */,
-                getGridY(atomInfo, true) /* grid_y_parent */,
-                getPageId(atomInfo, true) /* page_id_parent */,
-                getHierarchy(atomInfo) /* hierarchy */,
-                atomInfo.getIsWork() /* is_work_profile */,
-                mRank.orElse(atomInfo.getRank()) /* rank */,
-                atomInfo.getFolderIcon().getFromLabelState().getNumber() /* fromState */,
-                atomInfo.getFolderIcon().getToLabelState().getNumber() /* toState */,
-                atomInfo.getFolderIcon().getLabelInfo() /* edittext */,
-                getCardinality(atomInfo) /* cardinality */);
-    }
-
     /**
      * Logs the workspace layout information on the model thread.
      */
@@ -192,10 +125,14 @@
                 writeSnapshot(atomInfo, mInstanceId);
             }
             for (FolderInfo fInfo : folders) {
-                for (ItemInfo info : fInfo.contents) {
-                    LauncherAtom.ItemInfo atomInfo = info.buildProto(fInfo);
-                    writeSnapshot(atomInfo, mInstanceId);
-                }
+                try {
+                    ArrayList<WorkspaceItemInfo> folderContents =
+                            (ArrayList) Executors.MAIN_EXECUTOR.submit(fInfo.contents::clone).get();
+                    for (ItemInfo info : folderContents) {
+                        LauncherAtom.ItemInfo atomInfo = info.buildProto(fInfo);
+                        writeSnapshot(atomInfo, mInstanceId);
+                    }
+                } catch (Exception e) { }
             }
             for (ItemInfo info : appWidgets) {
                 LauncherAtom.ItemInfo atomInfo = info.buildProto(null);
@@ -212,7 +149,7 @@
             return;
         }
         SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_SNAPSHOT,
-                0 /* event_id */,
+                LAUNCHER_WORKSPACE_SNAPSHOT.getId() /* event_id */,
                 info.getItemCase().getNumber() /* target_id */,
                 instanceId.getId() /* instance_id */,
                 0 /* uid */,
@@ -220,10 +157,10 @@
                 getComponentName(info) /* component_name */,
                 getGridX(info, false) /* grid_x */,
                 getGridY(info, false) /* grid_y */,
-                getPageId(info, false) /* page_id */,
+                getPageId(info) /* page_id */,
                 getGridX(info, true) /* grid_x_parent */,
                 getGridY(info, true) /* grid_y_parent */,
-                getPageId(info, true) /* page_id_parent */,
+                getParentPageId(info) /* page_id_parent */,
                 getHierarchy(info) /* hierarchy */,
                 info.getIsWork() /* is_work_profile */,
                 info.getAttribute().getNumber() /* origin */,
@@ -235,15 +172,25 @@
     /**
      * Helps to construct and write statsd compatible log message.
      */
-    private class StatsCompatLogger implements StatsLogger {
-        private ItemInfo mItemInfo = new ItemInfo();
+    private static class StatsCompatLogger implements StatsLogger {
+
+        private static final ItemInfo DEFAULT_ITEM_INFO = new ItemInfo();
+        private ItemInfo mItemInfo = DEFAULT_ITEM_INFO;
         private InstanceId mInstanceId = DEFAULT_INSTANCE_ID;
         private OptionalInt mRank = OptionalInt.empty();
-        private int mSrcState = LAUNCHER_UICHANGED__SRC_STATE__HOME;
-        private int mDstState = LAUNCHER_UICHANGED__DST_STATE__BACKGROUND;
+        private Optional<ContainerInfo> mContainerInfo = Optional.empty();
+        private int mSrcState = LAUNCHER_STATE_UNSPECIFIED;
+        private int mDstState = LAUNCHER_STATE_UNSPECIFIED;
+        private Optional<FromState> mFromState = Optional.empty();
+        private Optional<ToState> mToState = Optional.empty();
+        private Optional<String> mEditText = Optional.empty();
 
         @Override
         public StatsLogger withItemInfo(ItemInfo itemInfo) {
+            if (mContainerInfo.isPresent()) {
+                throw new IllegalArgumentException(
+                        "ItemInfo and ContainerInfo are mutual exclusive; cannot log both.");
+            }
             this.mItemInfo = itemInfo;
             return this;
         }
@@ -273,6 +220,34 @@
         }
 
         @Override
+        public StatsLogger withContainerInfo(ContainerInfo containerInfo) {
+            if (mItemInfo != DEFAULT_ITEM_INFO) {
+                throw new IllegalArgumentException(
+                        "ItemInfo and ContainerInfo are mutual exclusive; cannot log both.");
+            }
+            this.mContainerInfo = Optional.of(containerInfo);
+            return this;
+        }
+
+        @Override
+        public StatsLogger withFromState(FromState fromState) {
+            this.mFromState = Optional.of(fromState);
+            return this;
+        }
+
+        @Override
+        public StatsLogger withToState(ToState toState) {
+            this.mToState = Optional.of(toState);
+            return this;
+        }
+
+        @Override
+        public StatsLogger withEditText(String editText) {
+            this.mEditText = Optional.of(editText);
+            return this;
+        }
+
+        @Override
         public void log(EventEnum event) {
             if (!Utilities.ATLEAST_R) {
                 return;
@@ -280,7 +255,8 @@
 
             if (mItemInfo.container < 0) {
                 // Item is not within a folder. Write to StatsLog in same thread.
-                write(event, mInstanceId, mItemInfo.buildProto(), mSrcState, mDstState, mRank);
+                write(event, mInstanceId, applyOverwrites(mItemInfo.buildProto()), mSrcState,
+                        mDstState);
             } else {
                 // Item is inside the folder, fetch folder info in a BG thread
                 // and then write to StatsLog.
@@ -290,18 +266,85 @@
                             public void execute(LauncherAppState app, BgDataModel dataModel,
                                     AllAppsList apps) {
                                 FolderInfo folderInfo = dataModel.folders.get(mItemInfo.container);
-                                write(event, mInstanceId, mItemInfo.buildProto(folderInfo),
-                                        mSrcState, mDstState, mRank);
+                                write(event, mInstanceId,
+                                        applyOverwrites(mItemInfo.buildProto(folderInfo)),
+                                        mSrcState, mDstState);
                             }
                         });
             }
         }
+
+        private LauncherAtom.ItemInfo applyOverwrites(LauncherAtom.ItemInfo atomInfo) {
+            LauncherAtom.ItemInfo.Builder itemInfoBuilder =
+                    (LauncherAtom.ItemInfo.Builder) atomInfo.toBuilder();
+
+            mRank.ifPresent(itemInfoBuilder::setRank);
+            mContainerInfo.ifPresent(itemInfoBuilder::setContainerInfo);
+
+            if (mFromState.isPresent() || mToState.isPresent() || mEditText.isPresent()) {
+                FolderIcon.Builder folderIconBuilder = (FolderIcon.Builder) itemInfoBuilder
+                        .getFolderIcon()
+                        .toBuilder();
+                mFromState.ifPresent(folderIconBuilder::setFromLabelState);
+                mToState.ifPresent(folderIconBuilder::setToLabelState);
+                mEditText.ifPresent(folderIconBuilder::setLabelInfo);
+                itemInfoBuilder.setFolderIcon(folderIconBuilder);
+            }
+            return itemInfoBuilder.build();
+        }
+
+        private void write(EventEnum event, InstanceId instanceId, LauncherAtom.ItemInfo atomInfo,
+                int srcState, int dstState) {
+            if (IS_VERBOSE) {
+                String name = (event instanceof Enum) ? ((Enum) event).name() :
+                        event.getId() + "";
+
+                Log.d(TAG, instanceId == DEFAULT_INSTANCE_ID
+                        ? String.format("\n%s (State:%s->%s)\n%s", name, getStateString(srcState),
+                        getStateString(dstState), atomInfo)
+                        : String.format("\n%s (State:%s->%s) (InstanceId:%s)\n%s", name,
+                                getStateString(srcState), getStateString(dstState), instanceId,
+                                atomInfo));
+            }
+
+            SysUiStatsLog.write(
+                    SysUiStatsLog.LAUNCHER_EVENT,
+                    SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */,
+                    srcState,
+                    dstState,
+                    null /* launcher extensions, deprecated */,
+                    false /* quickstep_enabled, deprecated */,
+                    event.getId() /* event_id */,
+                    atomInfo.getItemCase().getNumber() /* target_id */,
+                    instanceId.getId() /* instance_id TODO */,
+                    0 /* uid TODO */,
+                    getPackageName(atomInfo) /* package_name */,
+                    getComponentName(atomInfo) /* component_name */,
+                    getGridX(atomInfo, false) /* grid_x */,
+                    getGridY(atomInfo, false) /* grid_y */,
+                    getPageId(atomInfo) /* page_id */,
+                    getGridX(atomInfo, true) /* grid_x_parent */,
+                    getGridY(atomInfo, true) /* grid_y_parent */,
+                    getParentPageId(atomInfo) /* page_id_parent */,
+                    getHierarchy(atomInfo) /* hierarchy */,
+                    atomInfo.getIsWork() /* is_work_profile */,
+                    atomInfo.getRank() /* rank */,
+                    atomInfo.getFolderIcon().getFromLabelState().getNumber() /* fromState */,
+                    atomInfo.getFolderIcon().getToLabelState().getNumber() /* toState */,
+                    atomInfo.getFolderIcon().getLabelInfo() /* edittext */,
+                    getCardinality(atomInfo) /* cardinality */);
+        }
     }
 
     private static int getCardinality(LauncherAtom.ItemInfo info) {
-        return info.getContainerInfo().getContainerCase().equals(PREDICTED_HOTSEAT_CONTAINER)
-                ? info.getContainerInfo().getPredictedHotseatContainer().getCardinality()
-                : info.getFolderIcon().getCardinality();
+        switch (info.getContainerInfo().getContainerCase()){
+            case PREDICTED_HOTSEAT_CONTAINER:
+                return info.getContainerInfo().getPredictedHotseatContainer().getCardinality();
+            case SEARCH_RESULT_CONTAINER:
+                return info.getContainerInfo().getSearchResultContainer().getQueryLength();
+            default:
+                return info.getFolderIcon().getCardinality();
+        }
     }
 
     private static String getPackageName(LauncherAtom.ItemInfo info) {
@@ -358,15 +401,24 @@
         }
     }
 
-    private static int getPageId(LauncherAtom.ItemInfo info, boolean parent) {
-        if (info.getContainerInfo().getContainerCase() == FOLDER) {
-            if (parent) {
-                return info.getContainerInfo().getFolder().getWorkspace().getPageIndex();
-            } else {
+    private static int getPageId(LauncherAtom.ItemInfo info) {
+        switch (info.getContainerInfo().getContainerCase()) {
+            case FOLDER:
                 return info.getContainerInfo().getFolder().getPageIndex();
-            }
-        } else {
-            return info.getContainerInfo().getWorkspace().getPageIndex();
+            default:
+                return info.getContainerInfo().getWorkspace().getPageIndex();
+        }
+    }
+
+    private static int getParentPageId(LauncherAtom.ItemInfo info) {
+        switch (info.getContainerInfo().getContainerCase()) {
+            case FOLDER:
+                return info.getContainerInfo().getFolder().getWorkspace().getPageIndex();
+            case SEARCH_RESULT_CONTAINER:
+                return info.getContainerInfo().getSearchResultContainer().getWorkspace()
+                        .getPageIndex();
+            default:
+                return info.getContainerInfo().getWorkspace().getPageIndex();
         }
     }
 
@@ -374,6 +426,9 @@
         if (info.getContainerInfo().getContainerCase() == FOLDER) {
             return info.getContainerInfo().getFolder().getParentContainerCase().getNumber()
                     + FOLDER_HIERARCHY_OFFSET;
+        } else if (info.getContainerInfo().getContainerCase() == SEARCH_RESULT_CONTAINER) {
+            return info.getContainerInfo().getSearchResultContainer().getParentContainerCase()
+                    .getNumber() + SEARCH_RESULT_HIERARCHY_OFFSET;
         } else {
             return info.getContainerInfo().getContainerCase().getNumber();
         }
diff --git a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
index 0b48a8b..e998e9a 100644
--- a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
+++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
@@ -19,6 +19,7 @@
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
 
+import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import android.content.ClipData;
@@ -55,6 +56,9 @@
 public class ImageActionUtils {
 
     private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".overview.fileprovider";
+    private static final long FILE_LIFE = 1000L /*ms*/ * 60L /*s*/ * 60L /*m*/ * 24L /*h*/;
+    private static final String SUB_FOLDER = "Overview";
+    private static final String BASE_NAME = "overview_image_";
 
     /**
      * Saves screenshot to location determine by SystemUiProxy
@@ -88,8 +92,14 @@
     @WorkerThread
     public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop,
             Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag) {
-        context.startActivities(
-                uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent));
+        Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent);
+
+        // Work around b/159412574
+        if (intents.length == 1) {
+            context.startActivity(intents[0]);
+        } else {
+            context.startActivities(intents);
+        }
     }
 
     /**
@@ -104,10 +114,13 @@
      */
     @WorkerThread
     public static Uri getImageUri(Bitmap bitmap, Rect crop, Context context, String tag) {
+        clearOldCacheFiles(context);
         Bitmap croppedBitmap = cropBitmap(bitmap, crop);
         int cropHash = crop == null ? 0 : crop.hashCode();
-        String baseName = "image_" + bitmap.hashCode() + "_" + cropHash + ".png";
-        File file = new File(context.getCacheDir(), baseName);
+        String baseName = BASE_NAME + bitmap.hashCode() + "_" + cropHash + ".png";
+        File parent = new File(context.getCacheDir(), SUB_FOLDER);
+        parent.mkdir();
+        File file = new File(parent, baseName);
 
         try (FileOutputStream fos = new FileOutputStream(file)) {
             croppedBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
@@ -158,15 +171,30 @@
             intent = new Intent();
         }
         ClipData clipdata = new ClipData(new ClipDescription("content",
-                new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
+                new String[]{"image/png"}),
                 new ClipData.Item(uri));
         intent.setAction(Intent.ACTION_SEND)
                 .setComponent(null)
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
                 .setType("image/png")
-                .setFlags(FLAG_GRANT_READ_URI_PERMISSION)
                 .putExtra(Intent.EXTRA_STREAM, uri)
                 .setClipData(clipdata);
         return new Intent[]{Intent.createChooser(intent, null).addFlags(FLAG_ACTIVITY_NEW_TASK)};
     }
+
+    private static void clearOldCacheFiles(Context context) {
+        THREAD_POOL_EXECUTOR.execute(() -> {
+            File parent = new File(context.getCacheDir(), SUB_FOLDER);
+            File[] files = parent.listFiles((File f, String s) -> s.startsWith(BASE_NAME));
+            if (files != null) {
+                for (File file: files) {
+                    if (file.lastModified() + FILE_LIFE < System.currentTimeMillis()) {
+                        file.delete();
+                    }
+                }
+            }
+        });
+
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index cefab1b..ae19d73 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -58,21 +58,6 @@
     }
 
     /**
-     * Gets the scale that should be applied to the TaskView so that it matches the target
-     * TODO: Remove this method
-     */
-    public static float getTaskScale(RecentsOrientedState orientedState,
-            float srcWidth, float srcHeight, float targetWidth, float targetHeight) {
-        if (orientedState == null
-                || orientedState.isHomeRotationAllowed()
-                || orientedState.isDisplayPhoneNatural()) {
-            return srcWidth / targetWidth;
-        } else {
-            return srcHeight / targetHeight;
-        }
-    }
-
-    /**
      * Recursively sets view and all children enabled/disabled.
      * @param view Top most parent view to change.
      * @param enabled True = enable, False = disable.
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index 1abe903..0fdc684 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -115,7 +115,7 @@
                     HotseatPredictionController client = mLauncher.getHotseatPredictionController();
                     if (mFromAllApps && finalState == NORMAL && client.hasPredictions()) {
                         if (incrementEventCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
-                            client.showDiscoveryTip();
+                            client.showEdu();
                             stateManager.removeStateListener(this);
                         }
                     }
@@ -144,7 +144,7 @@
                 @Override
                 public void onStateTransitionComplete(LauncherState finalState) {
                     if (finalState == NORMAL) {
-                        if (mCount == MAX_NUM_SWIPES_TO_TRIGGER_EDU) {
+                        if (mCount >= MAX_NUM_SWIPES_TO_TRIGGER_EDU) {
                             if (getOpenView(mLauncher, TYPE_ALL_APPS_EDU) == null) {
                                 AllAppsEduView.show(launcher);
                             }
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index 8bd2281..b359f0f 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -33,6 +33,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Matrix;
@@ -47,6 +48,7 @@
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -56,6 +58,7 @@
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
+import com.android.systemui.shared.system.ConfigurationCompat;
 
 import java.lang.annotation.Retention;
 import java.util.function.IntConsumer;
@@ -87,7 +90,7 @@
 
     private @SurfaceRotation int mTouchRotation = ROTATION_0;
     private @SurfaceRotation int mDisplayRotation = ROTATION_0;
-    private @SurfaceRotation int mLauncherRotation = ROTATION_0;
+    private @SurfaceRotation int mRecentsActivityRotation = ROTATION_0;
 
     // Launcher activity supports multiple orientation, but fallback activity does not
     private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY = 1 << 0;
@@ -105,6 +108,9 @@
     private static final int FLAG_ROTATION_WATCHER_ENABLED = 1 << 6;
     // Enable home rotation for UI tests, ignoring home rotation value from prefs
     private static final int FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING = 1 << 7;
+    // Whether the swipe gesture is running, so the recents would stay locked in the
+    // current orientation
+    private static final int FLAG_SWIPE_UP_NOT_RUNNING = 1 << 8;
 
     private static final int MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE =
             FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY
@@ -114,22 +120,21 @@
     // multi-window is enabled as in that case, activity itself rotates.
     private static final int VALUE_ROTATION_WATCHER_ENABLED =
             MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE | FLAG_SYSTEM_ROTATION_ALLOWED
-                    | FLAG_ROTATION_WATCHER_SUPPORTED | FLAG_ROTATION_WATCHER_ENABLED;
-
-    private SysUINavigationMode.NavigationModeChangeListener mNavModeChangeListener =
-            newMode -> setFlag(FLAG_ROTATION_WATCHER_SUPPORTED, newMode != TWO_BUTTONS);
+                    | FLAG_ROTATION_WATCHER_SUPPORTED | FLAG_ROTATION_WATCHER_ENABLED
+                    | FLAG_SWIPE_UP_NOT_RUNNING;
 
     private final Context mContext;
     private final ContentResolver mContentResolver;
     private final SharedPreferences mSharedPrefs;
     private final OrientationEventListener mOrientationListener;
-    private final BaseActivityInterface mSizeStrategy;
 
     private final Matrix mTmpMatrix = new Matrix();
 
     private int mFlags;
     private int mPreviousRotation = ROTATION_0;
 
+    @Nullable private Configuration mActivityConfiguration;
+
     /**
      * @param rotationChangeListener Callback for receiving rotation events when rotation watcher
      *                              is enabled
@@ -140,7 +145,6 @@
         mContext = context;
         mContentResolver = context.getContentResolver();
         mSharedPrefs = Utilities.getPrefs(context);
-        mSizeStrategy = sizeStrategy;
         mOrientationListener = new OrientationEventListener(context) {
             @Override
             public void onOrientationChanged(int degrees) {
@@ -161,10 +165,20 @@
         if (originalSmallestWidth < 600) {
             mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
         }
+        mFlags |= FLAG_SWIPE_UP_NOT_RUNNING;
         initFlags();
     }
 
     /**
+     * Sets the configuration for the recents activity, which could affect the activity's rotation
+     * @see #update(int, int)
+     */
+    public boolean setActivityConfiguration(Configuration activityConfiguration) {
+        mActivityConfiguration = activityConfiguration;
+        return update(mTouchRotation, mDisplayRotation);
+    }
+
+    /**
      * Sets if the host is in multi-window mode
      */
     public void setMultiWindowMode(boolean isMultiWindow) {
@@ -172,6 +186,13 @@
     }
 
     /**
+     * Sets if the swipe up gesture is currently running or not
+     */
+    public void setGestureActive(boolean isGestureActive) {
+        setFlag(FLAG_SWIPE_UP_NOT_RUNNING, !isGestureActive);
+    }
+
+    /**
      * Sets the appropriate {@link PagedOrientationHandler} for {@link #mOrientationHandler}
      * @param touchRotation The rotation the nav bar region that is touched is in
      * @param displayRotation Rotation of the display/device
@@ -181,23 +202,19 @@
      */
     public boolean update(
             @SurfaceRotation int touchRotation, @SurfaceRotation int displayRotation) {
-        if (!isMultipleOrientationSupportedByDevice()) {
-            return false;
-        }
-
-        int launcherRotation = inferLauncherRotation(displayRotation);
+        int recentsActivityRotation = inferRecentsActivityRotation(displayRotation);
         if (mDisplayRotation == displayRotation
                 && mTouchRotation == touchRotation
-                && mLauncherRotation == launcherRotation) {
+                && mRecentsActivityRotation == recentsActivityRotation) {
             return false;
         }
 
-        mLauncherRotation = launcherRotation;
+        mRecentsActivityRotation = recentsActivityRotation;
         mDisplayRotation = displayRotation;
         mTouchRotation = touchRotation;
         mPreviousRotation = touchRotation;
 
-        if (mLauncherRotation == mTouchRotation || canLauncherRotate()) {
+        if (mRecentsActivityRotation == mTouchRotation || canRecentsActivityRotate()) {
             mOrientationHandler = PagedOrientationHandler.PORTRAIT;
             if (DEBUG) {
                 Log.d(TAG, "current RecentsOrientedState: " + this);
@@ -219,9 +236,11 @@
     }
 
     @SurfaceRotation
-    private int inferLauncherRotation(@SurfaceRotation int displayRotation) {
-        if (!isMultipleOrientationSupportedByDevice() || isHomeRotationAllowed()) {
-            return displayRotation;
+    private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) {
+        if (isRecentsActivityRotationAllowed()) {
+            return mActivityConfiguration == null
+                    ? displayRotation
+                    : ConfigurationCompat.getWindowConfigurationRotation(mActivityConfiguration);
         } else {
             return ROTATION_0;
         }
@@ -229,7 +248,8 @@
 
     private void setFlag(int mask, boolean enabled) {
         boolean wasRotationEnabled = !TestProtocol.sDisableSensorRotation
-                && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED;
+                && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED
+                && !canRecentsActivityRotate();
         if (enabled) {
             mFlags |= mask;
         } else {
@@ -237,7 +257,8 @@
         }
 
         boolean isRotationEnabled = !TestProtocol.sDisableSensorRotation
-                && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED;
+                && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED
+                && !canRecentsActivityRotate();
         if (wasRotationEnabled != isRotationEnabled) {
             UI_HELPER_EXECUTOR.execute(() -> {
                 if (isRotationEnabled) {
@@ -268,10 +289,9 @@
 
     private void initFlags() {
         SysUINavigationMode.Mode currentMode = SysUINavigationMode.getMode(mContext);
-        if (mOrientationListener.canDetectOrientation() &&
-                currentMode != TWO_BUTTONS) {
-            mFlags |= FLAG_ROTATION_WATCHER_SUPPORTED;
-        }
+        boolean rotationWatcherSupported = mOrientationListener.canDetectOrientation() &&
+                currentMode != TWO_BUTTONS;
+        setFlag(FLAG_ROTATION_WATCHER_SUPPORTED, rotationWatcherSupported);
 
         // initialize external flags
         updateAutoRotateSetting();
@@ -288,9 +308,6 @@
             mContentResolver.registerContentObserver(
                     Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
                     false, mSystemAutoRotateObserver);
-            SysUINavigationMode.Mode currentMode =
-                    SysUINavigationMode.INSTANCE.get(mContext)
-                            .addModeChangeListener(mNavModeChangeListener);
         }
         initFlags();
     }
@@ -302,8 +319,6 @@
         if (isMultipleOrientationSupportedByDevice()) {
             mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
             mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
-            SysUINavigationMode.INSTANCE.get(mContext)
-                    .removeModeChangeListener(mNavModeChangeListener);
         }
         setRotationWatcherEnabled(false);
     }
@@ -323,8 +338,8 @@
     }
 
     @SurfaceRotation
-    public int getLauncherRotation() {
-        return mLauncherRotation;
+    public int getRecentsActivityRotation() {
+        return mRecentsActivityRotation;
     }
 
     public boolean isMultipleOrientationSupportedByDevice() {
@@ -332,14 +347,21 @@
                 == MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE;
     }
 
-    public boolean isHomeRotationAllowed() {
-        return (mFlags & (FLAG_HOME_ROTATION_ALLOWED_IN_PREFS | FLAG_MULTIWINDOW_ROTATION_ALLOWED))
-                != 0 ||
-                (mFlags & FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING) != 0;
+    public boolean isRecentsActivityRotationAllowed() {
+        // Activity rotation is allowed if the multi-simulated-rotation is not supported
+        // (fallback recents or tablets) or activity rotation is enabled by various settings.
+        return ((mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
+                != MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
+                || (mFlags & (FLAG_HOME_ROTATION_ALLOWED_IN_PREFS
+                        | FLAG_MULTIWINDOW_ROTATION_ALLOWED
+                        | FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING)) != 0;
     }
 
-    public boolean canLauncherRotate() {
-        return (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0 && isHomeRotationAllowed();
+    /**
+     * Returns true if the activity can rotate, if allowed by system rotation settings
+     */
+    public boolean canRecentsActivityRotate() {
+        return (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0 && isRecentsActivityRotationAllowed();
     }
 
     /**
@@ -349,20 +371,6 @@
         setFlag(FLAG_ROTATION_WATCHER_ENABLED, isEnabled);
     }
 
-    public int getTouchRotationDegrees() {
-        switch (mTouchRotation) {
-            case ROTATION_90:
-                return 90;
-            case ROTATION_180:
-                return 180;
-            case ROTATION_270:
-                return 270;
-            case ROTATION_0:
-            default:
-                return 0;
-        }
-    }
-
     /**
      * Returns the scale and pivot so that the provided taskRect can fit the provided full size
      */
@@ -521,8 +529,8 @@
                     extractObjectNameAndAddress(mOrientationHandler.toString())
                 + " mDisplayRotation=" + mDisplayRotation
                 + " mTouchRotation=" + mTouchRotation
-                + " mLauncherRotation=" + mLauncherRotation
-                + " mHomeRotation=" + isHomeRotationAllowed()
+                + " mRecentsActivityRotation=" + mRecentsActivityRotation
+                + " isRecentsActivityRotationAllowed=" + isRecentsActivityRotationAllowed()
                 + " mSystemRotation=" + systemRotationOn
                 + " mFlags=" + mFlags
                 + "]";
@@ -534,7 +542,8 @@
     public DeviceProfile getLauncherDeviceProfile() {
         InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
         // TODO also check the natural orientation is landscape or portrait
-        return  (mLauncherRotation == ROTATION_90 || mLauncherRotation == ROTATION_270)
+        return  (mRecentsActivityRotation == ROTATION_90
+                || mRecentsActivityRotation == ROTATION_270)
                 ? idp.landscapeProfile
                 : idp.portraitProfile;
     }
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index bd8ab08..b9e0f62 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -131,6 +131,10 @@
                     TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString()).
                     getString("result"));
         }
+        // b/143488140
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
     }
 
     // b/143488140
diff --git a/res/layout/system_shortcut.xml b/res/layout/system_shortcut.xml
index 4b7097a..881df1b 100644
--- a/res/layout/system_shortcut.xml
+++ b/res/layout/system_shortcut.xml
@@ -22,7 +22,7 @@
     android:theme="@style/PopupItem" >
 
     <com.android.launcher3.BubbleTextView
-        style="@style/BaseIcon"
+        style="@style/BaseIconUnBounded"
         android:id="@+id/bubble_text"
         android:background="?android:attr/selectableItemBackground"
         android:gravity="start|center_vertical"
@@ -30,6 +30,7 @@
         android:paddingStart="@dimen/deep_shortcuts_text_padding_start"
         android:paddingEnd="@dimen/popup_padding_end"
         android:textSize="14sp"
+        android:maxLines="2"
         android:textColor="?android:attr/textColorPrimary"
         launcher:iconDisplay="shortcut_popup"
         launcher:layoutHorizontal="true"
diff --git a/res/layout/work_mode_switch.xml b/res/layout/work_mode_switch.xml
index b5237db..31953c7 100644
--- a/res/layout/work_mode_switch.xml
+++ b/res/layout/work_mode_switch.xml
@@ -17,7 +17,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    style="@style/PrimaryMediumText"
+    style="@style/PrimaryHeadline"
     android:id="@+id/work_mode_toggle"
     android:drawableStart="@drawable/ic_corp"
     android:drawablePadding="16dp"
@@ -25,6 +25,7 @@
     android:textColor="?attr/workProfileOverlayTextColor"
     android:layout_alignParentBottom="true"
     android:ellipsize="end"
+    android:elevation="10dp"
     android:gravity="start"
     android:lines="1"
     android:showText="false"
diff --git a/res/values/config.xml b/res/values/config.xml
index 4cbc597..ca25325 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -144,6 +144,10 @@
     <item name="staggered_stiffness" type="dimen" format="float">150</item>
     <dimen name="unlock_staggered_velocity_dp_per_s">3dp</dimen>
 
+    <item name="hint_scale_damping_ratio" type="dimen" format="float">0.7</item>
+    <item name="hint_scale_stiffness" type="dimen" format="float">200</item>
+    <dimen name="hint_scale_velocity_dp_per_s">0.3dp</dimen>
+
     <!-- Swipe up to home related -->
     <dimen name="swipe_up_fling_min_visible_change">18dp</dimen>
     <dimen name="swipe_up_y_overshoot">10dp</dimen>
@@ -175,6 +179,10 @@
 
         <item>@dimen/swipe_up_fling_min_visible_change</item>
         <item>@dimen/swipe_up_y_overshoot</item>
+
+        <item>@dimen/hint_scale_damping_ratio</item>
+        <item>@dimen/hint_scale_stiffness</item>
+        <item>@dimen/hint_scale_velocity_dp_per_s</item>
     </array>
 
     <string-array name="live_wallpapers_remove_sysui_scrims">
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ac00488..25f21f3 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -55,7 +55,7 @@
         <item name="iconOnlyShortcutColor">?android:attr/textColorSecondary</item>
         <item name="workProfileOverlayTextColor">#FF212121</item>
         <item name="eduHalfSheetBGColor">?android:attr/colorAccent</item>
-        <item name="disabledIconAlpha">.36</item>
+        <item name="disabledIconAlpha">.54</item>
 
         <item name="android:windowTranslucentStatus">false</item>
         <item name="android:windowTranslucentNavigation">false</item>
@@ -69,7 +69,7 @@
     <style name="LauncherTheme.DarkMainColor" parent="@style/LauncherTheme">
         <item name="folderFillColor">#FF3C4043</item> <!-- 100% GM2 800 -->
         <item name="folderTextColor">?attr/workspaceTextColor</item>
-        <item name="disabledIconAlpha">.24</item>
+        <item name="disabledIconAlpha">.254</item>
 
     </style>
 
@@ -117,7 +117,7 @@
     <style name="LauncherTheme.Dark.DarkMainColor" parent="@style/LauncherTheme.Dark">
         <item name="folderFillColor">#FF3C4043</item> <!-- 100% GM2 800 -->
         <item name="folderTextColor">@android:color/white</item>
-        <item name="disabledIconAlpha">.24</item>
+        <item name="disabledIconAlpha">.54</item>
     </style>
 
     <style name="LauncherTheme.Dark.DarkText" parent="@style/LauncherTheme.Dark">
@@ -187,28 +187,31 @@
     </style>
 
     <style name="AllAppsTheme">
-        <item name="disabledIconAlpha">.24</item>
+        <item name="disabledIconAlpha">.54</item>
     </style>
 
     <style name="AllAppsTheme.Dark">
-        <item name="disabledIconAlpha">.28</item>
+        <item name="disabledIconAlpha">.54</item>
     </style>
 
-    <!-- Base theme for BubbleTextView and sub classes -->
-    <style name="BaseIcon" parent="@android:style/TextAppearance.DeviceDefault">
+
+    <style name="BaseIconUnBounded" parent="@android:style/TextAppearance.DeviceDefault">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">match_parent</item>
         <item name="android:layout_gravity">center</item>
         <item name="android:focusable">true</item>
         <item name="android:gravity">center_horizontal</item>
-        <item name="android:lines">1</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
         <item name="android:defaultFocusHighlightEnabled">false</item>
-
         <!-- No shadows in the base theme -->
         <item name="android:shadowRadius">0</item>
     </style>
 
+    <!-- Base theme for BubbleTextView and sub classes -->
+    <style name="BaseIcon" parent="BaseIconUnBounded">
+        <item name="android:lines">1</item>
+    </style>
+
     <!-- Icon displayed on the workspace -->
     <style name="BaseIcon.Workspace" >
         <item name="android:shadowRadius">2.0</item>
@@ -243,7 +246,6 @@
     <style name="DropTargetButton" parent="DropTargetButtonBase" />
 
     <style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
-    <style name="PrimaryMediumText" parent="@android:style/TextAppearance.DeviceDefault.Medium"/>
     <style name="PrimaryHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
 
     <style name="TextTitle" parent="@android:style/TextAppearance.DeviceDefault" />
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
index b171712..a8e0cb3 100644
--- a/robolectric_tests/config/robolectric.properties
+++ b/robolectric_tests/config/robolectric.properties
@@ -5,6 +5,7 @@
     com.android.launcher3.shadows.LShadowAppWidgetManager \
     com.android.launcher3.shadows.LShadowBackupManager \
     com.android.launcher3.shadows.LShadowBitmap \
+    com.android.launcher3.shadows.LShadowDisplay \
     com.android.launcher3.shadows.LShadowLauncherApps \
     com.android.launcher3.shadows.LShadowTypeface \
     com.android.launcher3.shadows.LShadowUserManager \
diff --git a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
index d927ffc..b7ba106 100644
--- a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.folder;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -61,16 +62,16 @@
         ArrayList<WorkspaceItemInfo> list = new ArrayList<>();
         list.add(mItem1);
         list.add(mItem2);
-        FolderNameInfo[] nameInfos =
-                new FolderNameInfo[FolderNameProvider.SUGGEST_MAX];
+        FolderNameInfos nameInfos = new FolderNameInfos();
         new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos);
-        assertEquals("Work", nameInfos[0].getLabel());
+        assertEquals("Work", nameInfos.getLabels()[0]);
 
-        nameInfos[0] = new FolderNameInfo("candidate1", 0.9);
-        nameInfos[1] = new FolderNameInfo("candidate2", 0.8);
-        nameInfos[2] = new FolderNameInfo("candidate3", 0.7);
+        nameInfos.setLabel(0, "candidate1", 1.0f);
+        nameInfos.setLabel(1, "candidate2", 1.0f);
+        nameInfos.setLabel(2, "candidate3", 1.0f);
         new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos);
-        assertEquals("Work", nameInfos[3].getLabel());
-
+        assertEquals("Work", nameInfos.getLabels()[3]);
+        assertTrue(nameInfos.hasSuggestions());
+        assertTrue(nameInfos.hasPrimary());
     }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java
new file mode 100644
index 0000000..3813fa1
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java
@@ -0,0 +1,54 @@
+/*
+ * 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.shadows;
+
+import static org.robolectric.shadow.api.Shadow.directlyOn;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Display;
+
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadows.ShadowDisplay;
+
+/**
+ * Extension of {@link ShadowDisplay} with missing shadow methods
+ */
+@Implements(value = Display.class)
+public class LShadowDisplay extends ShadowDisplay {
+
+    private final Rect mInsets = new Rect();
+
+    @RealObject Display realObject;
+
+    /**
+     * Sets the insets for the display
+     */
+    public void setInsets(Rect insets) {
+        mInsets.set(insets);
+    }
+
+    @Override
+    protected void getCurrentSizeRange(Point outSmallestSize, Point outLargestSize) {
+        directlyOn(realObject, Display.class).getCurrentSizeRange(outSmallestSize, outLargestSize);
+        outSmallestSize.x -= mInsets.left + mInsets.right;
+        outLargestSize.x -= mInsets.left + mInsets.right;
+
+        outSmallestSize.y -= mInsets.top + mInsets.bottom;
+        outLargestSize.y -= mInsets.top + mInsets.bottom;
+    }
+}
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 7d80d81..310c306 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -37,8 +37,6 @@
 
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.logging.StatsLogUtils;
-import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.SystemUiController;
@@ -52,7 +50,7 @@
 /**
  * Launcher BaseActivity
  */
-public abstract class BaseActivity extends Activity implements LogStateProvider, ActivityContext {
+public abstract class BaseActivity extends Activity implements ActivityContext {
 
     private static final String TAG = "BaseActivity";
 
@@ -146,13 +144,11 @@
         return mDeviceProfile;
     }
 
-    public int getCurrentState() { return StatsLogUtils.LAUNCHER_STATE_BACKGROUND; }
-
     public void modifyUserEvent(LauncherLogProto.LauncherEvent event) {}
 
     public final StatsLogManager getStatsLogManager() {
         if (mStatsLogManager == null) {
-            mStatsLogManager = StatsLogManager.newInstance(this, this);
+            mStatsLogManager = StatsLogManager.newInstance(this);
         }
         return mStatsLogManager;
     }
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
index c1aed98..b27abc4 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.Executors;
 
 /**
  * Interface defining an object that can receive a drag.
@@ -84,7 +85,9 @@
 
         public DragObject(Context context) {
             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-                folderNameProvider = FolderNameProvider.newInstance(context);
+                Executors.MODEL_EXECUTOR.post(() -> {
+                    folderNameProvider = FolderNameProvider.newInstance(context);
+                });
             }
         }
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index ec32e62..0970dae 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -38,6 +38,10 @@
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONSTOP;
+import static com.android.launcher3.logging.StatsLogManager.containerTypeToAtomState;
 import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
 import static com.android.launcher3.popup.SystemShortcut.INSTALL;
 import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
@@ -110,8 +114,9 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.keyboard.CustomActionsPopup;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
+import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.logging.StatsLogUtils;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.model.BgDataModel.Callbacks;
@@ -260,7 +265,6 @@
 
     @Thunk
     Workspace mWorkspace;
-    private View mLauncherView;
     @Thunk
     DragLayer mDragLayer;
     private DragController mDragController;
@@ -363,6 +367,7 @@
         LauncherAppState app = LauncherAppState.getInstance(this);
         mOldConfig = new Configuration(getResources().getConfiguration());
         mModel = app.getModel();
+
         mRotationHelper = new RotationHelper(this);
         InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
         initDeviceProfile(idp);
@@ -382,8 +387,7 @@
                 appWidgetId -> getWorkspace().removeWidget(appWidgetId));
         mAppWidgetHost.startListening();
 
-        mLauncherView = LayoutInflater.from(this).inflate(R.layout.launcher, null);
-
+        inflateRootView(R.layout.launcher);
         setupViews();
         mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
 
@@ -420,7 +424,7 @@
         // For handling default keys
         setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
 
-        setContentView(mLauncherView);
+        setContentView(getRootView());
         getRootView().dispatchInsets();
 
         // Listen for broadcasts
@@ -520,12 +524,6 @@
     }
 
     @Override
-    public void reapplyUi(boolean cancelCurrentAnimation) {
-        getRootView().dispatchInsets();
-        super.reapplyUi(cancelCurrentAnimation);
-    }
-
-    @Override
     public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
         onIdpChanged(idp);
     }
@@ -581,11 +579,6 @@
         return mStateManager;
     }
 
-    @Override
-    public <T extends View> T findViewById(int id) {
-        return mLauncherView.findViewById(id);
-    }
-
     private LauncherCallbacks mLauncherCallbacks;
 
     /**
@@ -932,13 +925,32 @@
 
 
     private void logStopAndResume(int command) {
+        int pageIndex = mWorkspace.isOverlayShown() ? -1 : mWorkspace.getCurrentPage();
         int containerType = mStateManager.getState().containerType;
+
+        StatsLogManager.EventEnum event;
+        StatsLogManager.StatsLogger logger = getStatsLogManager().logger();
+        if (command == Action.Command.RESUME) {
+            logger.withSrcState(LAUNCHER_STATE_BACKGROUND)
+                .withDstState(containerTypeToAtomState(mStateManager.getState().containerType));
+            event = LAUNCHER_ONRESUME;
+        } else { /* command == Action.Command.STOP */
+            logger.withSrcState(containerTypeToAtomState(mStateManager.getState().containerType))
+                    .withDstState(LAUNCHER_STATE_BACKGROUND);
+            event = LAUNCHER_ONSTOP;
+        }
+
         if (containerType == ContainerType.WORKSPACE && mWorkspace != null) {
             getUserEventDispatcher().logActionCommand(command,
-                    containerType, -1, mWorkspace.isOverlayShown() ? -1 : 0);
+                    containerType, -1, pageIndex);
+            logger.withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                    .setWorkspace(
+                            LauncherAtom.WorkspaceContainer.newBuilder()
+                                    .setPageIndex(pageIndex)).build());
         } else {
             getUserEventDispatcher().logActionCommand(command, containerType, -1);
         }
+        logger.log(event);
     }
 
     private void scheduleDeferredCheck() {
@@ -1118,10 +1130,6 @@
         mHotseat = findViewById(R.id.hotseat);
         mHotseat.setWorkspace(mWorkspace);
 
-        mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
-
         // Setup the drag layer
         mDragLayer.setup(mDragController, mWorkspace);
 
@@ -1335,11 +1343,6 @@
     }
 
     @Override
-    public LauncherRootView getRootView() {
-        return (LauncherRootView) mLauncherView;
-    }
-
-    @Override
     public DragLayer getDragLayer() {
         return mDragLayer;
     }
@@ -1857,16 +1860,6 @@
     }
 
     @Override
-    public int getCurrentState() {
-        if (mStateManager.getState() == LauncherState.ALL_APPS) {
-            return StatsLogUtils.LAUNCHER_STATE_ALLAPPS;
-        } else if (mStateManager.getState() == OVERVIEW) {
-            return StatsLogUtils.LAUNCHER_STATE_OVERVIEW;
-        }
-        return StatsLogUtils.LAUNCHER_STATE_HOME;
-    }
-
-    @Override
     public boolean startActivitySafely(View v, Intent intent, ItemInfo item,
             @Nullable String sourceContainer) {
         if (!hasBeenResumed()) {
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index 6951ff2..51504ce 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -10,6 +10,8 @@
 import android.view.ViewDebug;
 import android.view.WindowInsets;
 
+import com.android.launcher3.statemanager.StatefulActivity;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -17,7 +19,7 @@
 
     private final Rect mTempRect = new Rect();
 
-    private final Launcher mLauncher;
+    private final StatefulActivity mActivity;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
@@ -31,17 +33,17 @@
 
     public LauncherRootView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mLauncher = Launcher.getLauncher(context);
+        mActivity = StatefulActivity.fromContext(context);
     }
 
     private void handleSystemWindowInsets(Rect insets) {
         // Update device profile before notifying th children.
-        mLauncher.getDeviceProfile().updateInsets(insets);
+        mActivity.getDeviceProfile().updateInsets(insets);
         boolean resetState = !insets.equals(mInsets);
         setInsets(insets);
 
         if (resetState) {
-            mLauncher.getStateManager().reapplyState(true /* cancelCurrentAnimation */);
+            mActivity.getStateManager().reapplyState(true /* cancelCurrentAnimation */);
         }
     }
 
@@ -63,7 +65,7 @@
     }
 
     public void dispatchInsets() {
-        mLauncher.getDeviceProfile().updateInsets(mInsets);
+        mActivity.getDeviceProfile().updateInsets(mInsets);
         super.setInsets(mInsets);
     }
 
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 31bfc09..c78df62 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -226,7 +226,16 @@
      * 0 means completely zoomed in, without blurs. 1 is zoomed out, with blurs.
      */
     public final float getDepth(Context context) {
-        if (BaseDraggingActivity.fromContext(context).getDeviceProfile().isMultiWindowMode) {
+        return getDepth(context,
+                BaseDraggingActivity.fromContext(context).getDeviceProfile().isMultiWindowMode);
+    }
+
+    /**
+     * Returns the amount of blur and wallpaper zoom for this state with {@param isMultiWindowMode}.
+     * @see #getDepth(Context).
+     */
+    public final float getDepth(Context context, boolean isMultiWindowMode) {
+        if (isMultiWindowMode) {
             return 0;
         }
         return getDepthUnchecked(context);
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index dc1ff66..e29faac 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -71,7 +71,9 @@
 public abstract class PagedView<T extends View & PageIndicator> extends ViewGroup {
     private static final String TAG = "PagedView";
     private static final boolean DEBUG = false;
+    public static final boolean DEBUG_FAILED_QUICKSWITCH = false;
 
+    public static final int ACTION_MOVE_ALLOW_EASY_FLING = MotionEvent.ACTION_MASK - 1;
     public static final int INVALID_PAGE = -1;
     protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE;
 
@@ -89,14 +91,16 @@
     // The following constants need to be scaled based on density. The scaled versions will be
     // assigned to the corresponding member variables below.
     private static final int FLING_THRESHOLD_VELOCITY = 500;
+    private static final int EASY_FLING_THRESHOLD_VELOCITY = 400;
     private static final int MIN_SNAP_VELOCITY = 1500;
     private static final int MIN_FLING_VELOCITY = 250;
 
     private boolean mFreeScroll = false;
 
-    protected int mFlingThresholdVelocity;
-    protected int mMinFlingVelocity;
-    protected int mMinSnapVelocity;
+    protected final int mFlingThresholdVelocity;
+    protected final int mEasyFlingThresholdVelocity;
+    protected final int mMinFlingVelocity;
+    protected final int mMinSnapVelocity;
 
     protected boolean mFirstLayout = true;
 
@@ -118,12 +122,17 @@
     private float mLastMotion;
     private float mLastMotionRemainder;
     private float mTotalMotion;
+    // Used in special cases where the fling checks can be relaxed for an intentional gesture
+    private boolean mAllowEasyFling;
     protected PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT;
 
     protected int[] mPageScrolls;
     private boolean mIsBeingDragged;
 
+    // The amount of movement to begin scrolling
     protected int mTouchSlop;
+    // The amount of movement to begin paging
+    protected int mPageSlop;
     private int mMaximumVelocity;
     protected boolean mAllowOverScroll = true;
 
@@ -170,24 +179,19 @@
 
         setHapticFeedbackEnabled(false);
         mIsRtl = Utilities.isRtl(getResources());
-        init();
-    }
 
-    /**
-     * Initializes various states for this workspace.
-     */
-    protected void init() {
-        Context context = getContext();
         mScroller = new OverScroller(context);
         setDefaultInterpolator(Interpolators.SCROLL);
         mCurrentPage = 0;
 
         final ViewConfiguration configuration = ViewConfiguration.get(context);
-        mTouchSlop = configuration.getScaledPagingTouchSlop();
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mPageSlop = configuration.getScaledPagingTouchSlop();
         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
 
         float density = getResources().getDisplayMetrics().density;
         mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * density);
+        mEasyFlingThresholdVelocity = (int) (EASY_FLING_THRESHOLD_VELOCITY * density);
         mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * density);
         mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * density);
 
@@ -913,6 +917,7 @@
                 mDownMotionPrimary = mLastMotion = mOrientationHandler.getPrimaryDirection(ev, 0);
                 mLastMotionRemainder = 0;
                 mTotalMotion = 0;
+                mAllowEasyFling = false;
                 mActivePointerId = ev.getPointerId(0);
 
                 updateIsBeingDraggedOnTouchDown();
@@ -944,7 +949,7 @@
     private void updateIsBeingDraggedOnTouchDown() {
         // mScroller.isFinished should be false when being flinged.
         final int xDist = Math.abs(mScroller.getFinalPos() - mScroller.getCurrPos());
-        final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop / 3);
+        final boolean finishedScrolling = (mScroller.isFinished() || xDist < mPageSlop / 3);
 
         if (finishedScrolling) {
             mIsBeingDragged = false;
@@ -977,7 +982,7 @@
         final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev, pointerIndex);
         final int diff = (int) Math.abs(primaryDirection - mLastMotion);
         final int touchSlop = Math.round(touchSlopScale * mTouchSlop);
-        boolean moved = diff > touchSlop;
+        boolean moved = diff > touchSlop || ev.getAction() == ACTION_MOVE_ALLOW_EASY_FLING;
 
         if (moved) {
             // Scroll if the user moved far enough along the X axis
@@ -1160,6 +1165,7 @@
             mDownMotionPrimary = mLastMotion = mOrientationHandler.getPrimaryDirection(ev, 0);
             mLastMotionRemainder = 0;
             mTotalMotion = 0;
+            mAllowEasyFling = false;
             mActivePointerId = ev.getPointerId(0);
             if (mIsBeingDragged) {
                 onScrollInteractionBegin();
@@ -1167,8 +1173,14 @@
             }
             break;
 
-            case MotionEvent.ACTION_MOVE:
-                if (mIsBeingDragged) {
+        case ACTION_MOVE_ALLOW_EASY_FLING:
+            // Start scrolling immediately
+            determineScrollingStart(ev);
+            mAllowEasyFling = true;
+            break;
+
+        case MotionEvent.ACTION_MOVE:
+            if (mIsBeingDragged) {
                 // Scroll to follow the motion event
                 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
 
@@ -1214,9 +1226,14 @@
                     SIGNIFICANT_MOVE_THRESHOLD;
 
                 mTotalMotion += Math.abs(mLastMotion + mLastMotionRemainder - primaryDirection);
-                boolean isFling = mTotalMotion > mTouchSlop && shouldFlingForVelocity(velocity);
+                boolean passedSlop = mAllowEasyFling || mTotalMotion > mPageSlop;
+                boolean isFling = passedSlop && shouldFlingForVelocity(velocity);
                 boolean isDeltaLeft = mIsRtl ? delta > 0 : delta < 0;
                 boolean isVelocityLeft = mIsRtl ? velocity > 0 : velocity < 0;
+                if (DEBUG_FAILED_QUICKSWITCH && !isFling && mAllowEasyFling) {
+                    Log.d("Quickswitch", "isFling=false vel=" + velocity
+                            + " threshold=" + mEasyFlingThresholdVelocity);
+                }
 
                 if (!mFreeScroll) {
                     // In the case that the page is moved far to one direction and then is flung
@@ -1316,7 +1333,8 @@
     }
 
     protected boolean shouldFlingForVelocity(int velocity) {
-        return Math.abs(velocity) > mFlingThresholdVelocity;
+        float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity;
+        return Math.abs(velocity) > threshold;
     }
 
     private void resetTouchState() {
@@ -1393,8 +1411,7 @@
     }
 
     private void onSecondaryPointerUp(MotionEvent ev) {
-        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
-                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerIndex = ev.getActionIndex();
         final int pointerId = ev.getPointerId(pointerIndex);
         if (pointerId == mActivePointerId) {
             // This was our active pointer going up. Choose a new
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index fb58f21..1441e0b 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -28,6 +28,9 @@
 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -79,6 +82,7 @@
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
 import com.android.launcher3.logging.UserEventDispatcher;
@@ -415,7 +419,7 @@
 
         // Always enter the spring loaded mode
         mLauncher.getStateManager().goToState(SPRING_LOADED);
-        mStatsLogManager.logger().withItemInfo(dragObject.originalDragInfo)
+        mStatsLogManager.logger().withItemInfo(dragObject.dragInfo)
                 .withInstanceId(dragObject.logInstanceId)
                 .log(LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED);
     }
@@ -999,6 +1003,15 @@
             if (!mOverlayShown) {
                 mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                         Action.Direction.LEFT, ContainerType.WORKSPACE, 0);
+                mLauncher.getStatsLogManager().logger()
+                        .withSrcState(LAUNCHER_STATE_HOME)
+                        .withDstState(LAUNCHER_STATE_HOME)
+                        .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                                .setWorkspace(
+                                        LauncherAtom.WorkspaceContainer.newBuilder()
+                                                .setPageIndex(0))
+                                .build())
+                        .log(LAUNCHER_SWIPELEFT);
             }
             mOverlayShown = true;
             // Not announcing the overlay page for accessibility since it announces itself.
@@ -1008,6 +1021,15 @@
                 if (!ued.isPreviousHomeGesture()) {
                     mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                         Action.Direction.RIGHT, ContainerType.WORKSPACE, -1);
+                    mLauncher.getStatsLogManager().logger()
+                            .withSrcState(LAUNCHER_STATE_HOME)
+                            .withDstState(LAUNCHER_STATE_HOME)
+                            .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                                    .setWorkspace(
+                                            LauncherAtom.WorkspaceContainer.newBuilder()
+                                                    .setPageIndex(-1))
+                                    .build())
+                            .log(LAUNCHER_SWIPERIGHT);
                 }
             } else if (Float.compare(mOverlayTranslation, 0f) != 0) {
                 // When arriving to 0 overscroll from non-zero overscroll, announce page for
@@ -1099,9 +1121,20 @@
     protected void notifyPageSwitchListener(int prevPage) {
         super.notifyPageSwitchListener(prevPage);
         if (prevPage != mCurrentPage) {
-            int swipeDirection = (prevPage < mCurrentPage) ? Action.Direction.RIGHT : Action.Direction.LEFT;
+            int swipeDirection = (prevPage < mCurrentPage)
+                    ? Action.Direction.RIGHT : Action.Direction.LEFT;
+            StatsLogManager.EventEnum event = (prevPage < mCurrentPage)
+                    ? LAUNCHER_SWIPERIGHT : LAUNCHER_SWIPELEFT;
             mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                     swipeDirection, ContainerType.WORKSPACE, prevPage);
+            mLauncher.getStatsLogManager().logger()
+                    .withSrcState(LAUNCHER_STATE_HOME)
+                    .withDstState(LAUNCHER_STATE_HOME)
+                    .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                            .setWorkspace(
+                                    LauncherAtom.WorkspaceContainer.newBuilder()
+                                            .setPageIndex(prevPage)).build())
+                    .log(event);
         }
     }
 
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 06a73db..cd938e1 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE;
+
 import static com.android.launcher3.LauncherAnimUtils.DRAWABLE_ALPHA;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
@@ -23,11 +25,13 @@
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.LauncherState.FLAG_HAS_SYS_UI_SCRIM;
 import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_HAS_BACKGROUNDS;
+import static com.android.launcher3.LauncherState.HINT_STATE;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
+import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.ZOOM_OUT;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
-import static com.android.launcher3.graphics.WorkspaceAndHotseatScrim.SCRIM_PROGRESS;
+import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
 import static com.android.launcher3.graphics.WorkspaceAndHotseatScrim.SYSUI_PROGRESS;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
@@ -35,6 +39,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
 
+import android.animation.ValueAnimator;
 import android.view.View;
 import android.view.animation.Interpolator;
 
@@ -43,8 +48,11 @@
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
 import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.DynamicResource;
+import com.android.systemui.plugins.ResourceProvider;
 
 /**
  * Manages the animations between each of the workspace states.
@@ -104,17 +112,32 @@
         View qsbView = qsbScaleView.getSearchView();
         if (playAtomicComponent) {
             Interpolator scaleInterpolator = config.getInterpolator(ANIM_WORKSPACE_SCALE, ZOOM_OUT);
-            propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
+            LauncherState fromState = mLauncher.getStateManager().getState();
+            boolean shouldSpring = propertySetter instanceof PendingAnimation
+                    && fromState == HINT_STATE && state == NORMAL;
+            if (shouldSpring) {
+                ((PendingAnimation) propertySetter).add(getSpringScaleAnimator(mLauncher,
+                        mWorkspace, mNewScale));
+            } else {
+                propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
+            }
 
             setPivotToScaleWithWorkspace(hotseat);
             setPivotToScaleWithWorkspace(qsbScaleView);
             float hotseatScale = hotseatScaleAndTranslation.scale;
-            Interpolator hotseatScaleInterpolator = config.getInterpolator(ANIM_HOTSEAT_SCALE,
-                    scaleInterpolator);
-            propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale,
-                    hotseatScaleInterpolator);
-            propertySetter.setFloat(qsbScaleView, SCALE_PROPERTY, qsbScaleAndTranslation.scale,
-                    hotseatScaleInterpolator);
+            if (shouldSpring) {
+                PendingAnimation pa = (PendingAnimation) propertySetter;
+                pa.add(getSpringScaleAnimator(mLauncher, hotseat, hotseatScale));
+                pa.add(getSpringScaleAnimator(mLauncher, qsbScaleView,
+                        qsbScaleAndTranslation.scale));
+            } else {
+                Interpolator hotseatScaleInterpolator = config.getInterpolator(ANIM_HOTSEAT_SCALE,
+                        scaleInterpolator);
+                propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale,
+                        hotseatScaleInterpolator);
+                propertySetter.setFloat(qsbScaleView, SCALE_PROPERTY, qsbScaleAndTranslation.scale,
+                        hotseatScaleInterpolator);
+            }
 
             float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
             propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, fadeInterpolator);
@@ -191,4 +214,24 @@
                     pageAlpha, fadeInterpolator);
         }
     }
+
+    /**
+     * Returns a spring based animator for the scale property of {@param v}.
+     */
+    public static ValueAnimator getSpringScaleAnimator(Launcher launcher, View v, float scale) {
+        ResourceProvider rp = DynamicResource.provider(launcher);
+        float damping = rp.getFloat(R.dimen.hint_scale_damping_ratio);
+        float stiffness = rp.getFloat(R.dimen.hint_scale_stiffness);
+        float velocityPxPerS = rp.getDimension(R.dimen.hint_scale_velocity_dp_per_s);
+
+        return new SpringAnimationBuilder(v.getContext())
+                .setStiffness(stiffness)
+                .setDampingRatio(damping)
+                .setMinimumVisibleChange(MIN_VISIBLE_CHANGE_SCALE)
+                .setEndValue(scale)
+                .setStartValue(SCALE_PROPERTY.get(v))
+                .setStartVelocity(velocityPxPerS)
+                .build(v, SCALE_PROPERTY);
+
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 8df6f90..4f53d45 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -126,7 +126,7 @@
     public static final BooleanFlag ENABLE_HYBRID_HOTSEAT = getDebugFlag(
             "ENABLE_HYBRID_HOTSEAT", true, "Fill gaps in hotseat with predicted apps");
 
-    public static final BooleanFlag HOTSEAT_MIGRATE_TO_FOLDER = new DeviceFlag(
+    public static final BooleanFlag HOTSEAT_MIGRATE_TO_FOLDER = getDebugFlag(
             "HOTSEAT_MIGRATE_TO_FOLDER", false, "Should move hotseat items into a folder");
 
     public static final BooleanFlag ENABLE_DEEP_SHORTCUT_ICON_CACHE = getDebugFlag(
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 7998c2d..d01e189 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -26,10 +26,6 @@
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
-import static com.android.launcher3.model.data.FolderInfo.FLAG_MANUAL_FOLDER_NAME;
-
-import static java.util.Arrays.asList;
-import static java.util.Optional.ofNullable;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -37,7 +33,6 @@
 import android.annotation.SuppressLint;
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
-import android.content.Intent;
 import android.graphics.Canvas;
 import android.graphics.Path;
 import android.graphics.Rect;
@@ -81,7 +76,10 @@
 import com.android.launcher3.dragndrop.DragController.DragListener;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.logger.LauncherAtom.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
 import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.FolderInfo.FolderListener;
@@ -99,7 +97,9 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
+import java.util.StringJoiner;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Represents a set of icons chosen by the user or generated by the system.
@@ -109,6 +109,12 @@
         View.OnFocusChangeListener, DragListener, ExtendedEditText.OnBackKeyListener {
     private static final String TAG = "Launcher.Folder";
     private static final boolean DEBUG = false;
+
+    /**
+     * Used for separating folder title when logging together.
+     */
+    private static final CharSequence FOLDER_LABEL_DELIMITER = "~";
+
     /**
      * We avoid measuring {@link #mContent} with a 0 width or height, as this
      * results in CellLayout being measured as UNSPECIFIED, which it does not support.
@@ -155,6 +161,8 @@
     protected final Launcher mLauncher;
     protected DragController mDragController;
     public FolderInfo mInfo;
+    private CharSequence mFromTitle;
+    private FromState mFromLabelState;
 
     @Thunk FolderIcon mFolderIcon;
 
@@ -315,11 +323,7 @@
     public void startEditingFolderName() {
         post(() -> {
             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-                ofNullable(mInfo)
-                        .map(info -> info.suggestedFolderNames)
-                        .map(folderNames -> (FolderNameInfo[]) folderNames
-                                .getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS))
-                        .ifPresent(this::showLabelSuggestions);
+                showLabelSuggestions();
             }
             mFolderName.setHint("");
             mIsEditingName = true;
@@ -334,12 +338,8 @@
         if (DEBUG) {
             Log.d(TAG, "onBackKey newTitle=" + newTitle);
         }
-        mInfo.setTitle(newTitle);
-        mInfo.fromCustom = mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
-        mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, !mInfo.getAcceptedSuggestionIndex().isPresent(),
-                mLauncher.getModelWriter());
+        mInfo.setTitle(newTitle, mLauncher.getModelWriter());
         mFolderIcon.onTitleChanged(newTitle);
-        mLauncher.getModelWriter().updateItemInDatabase(mInfo);
 
         if (TextUtils.isEmpty(mInfo.title)) {
             mFolderName.setHint(R.string.folder_hint_text);
@@ -415,6 +415,8 @@
 
     void bind(FolderInfo info) {
         mInfo = info;
+        mFromTitle = info.title;
+        mFromLabelState = info.getFromLabelState();
         ArrayList<WorkspaceItemInfo> children = info.contents;
         Collections.sort(children, ITEM_POS_COMPARATOR);
         updateItemLocationsInDatabaseBatch(true);
@@ -448,32 +450,24 @@
      * Show suggested folder title in FolderEditText if the first suggestion is non-empty, push
      * rest of the suggestions to InputMethodManager.
      */
-    private void showLabelSuggestions(FolderNameInfo[] nameInfos) {
-        if (nameInfos == null) {
+    private void showLabelSuggestions() {
+        if (mInfo.suggestedFolderNames == null) {
             return;
         }
-        // Open the Folder and Keyboard when the first or second suggestion is valid non-empty
-        // string.
-        boolean shouldOpen = nameInfos.length > 0 && nameInfos[0] != null && !isEmpty(
-                nameInfos[0].getLabel())
-                || nameInfos.length > 1 && nameInfos[1] != null && !isEmpty(
-                nameInfos[1].getLabel());
-
-        if (shouldOpen) {
+        if (mInfo.suggestedFolderNames.hasSuggestions()) {
             // update the primary suggestion if the folder name is empty.
             if (isEmpty(mFolderName.getText())) {
-                CharSequence firstLabel = nameInfos[0] == null ? "" : nameInfos[0].getLabel();
-                if (!isEmpty(firstLabel)) {
+                if (mInfo.suggestedFolderNames.hasPrimary()) {
                     mFolderName.setHint("");
-                    mFolderName.setText(firstLabel);
+                    mFolderName.setText(mInfo.suggestedFolderNames.getLabels()[0]);
                     mFolderName.selectAll();
                 }
             }
             mFolderName.showKeyboard();
             mFolderName.displayCompletions(
-                    asList(nameInfos).subList(0, nameInfos.length).stream()
+                    Stream.of(mInfo.suggestedFolderNames.getLabels())
                             .filter(Objects::nonNull)
-                            .map(s -> s.getLabel().toString())
+                            .map(Object::toString)
                             .filter(s -> !s.isEmpty())
                             .filter(s -> !s.equalsIgnoreCase(mFolderName.getText().toString()))
                             .collect(Collectors.toList()));
@@ -1005,16 +999,14 @@
         if (!items.isEmpty()) {
             mLauncher.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0);
         }
-        if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && !isBind) {
+        if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && !isBind
+                && total > 1 /* no need to update if there's one icon */) {
             Executors.MODEL_EXECUTOR.post(() -> {
-                FolderNameInfo[] nameInfos =
-                        new FolderNameInfo[FolderNameProvider.SUGGEST_MAX];
+                FolderNameInfos nameInfos = new FolderNameInfos();
                 FolderNameProvider fnp = FolderNameProvider.newInstance(getContext());
                 fnp.getSuggestedFolderName(
                         getContext(), mInfo.contents, nameInfos);
-                mInfo.suggestedFolderNames = new Intent().putExtra(
-                        FolderInfo.EXTRA_FOLDER_SUGGESTIONS,
-                        nameInfos);
+                mInfo.suggestedFolderNames = nameInfos;
             });
         }
     }
@@ -1441,10 +1433,38 @@
     public void onFocusChange(View v, boolean hasFocus) {
         if (v == mFolderName) {
             if (hasFocus) {
+                mFromLabelState = mInfo.getFromLabelState();
+                mFromTitle = mInfo.title;
                 startEditingFolderName();
             } else {
-                mStatsLogManager.logger().withItemInfo(mInfo).log(LAUNCHER_FOLDER_LABEL_UPDATED);
-                logFolderLabelState();
+                StatsLogger statsLogger = mStatsLogManager.logger()
+                        .withItemInfo(mInfo)
+                        .withFromState(mFromLabelState);
+
+                // If the folder label is suggested, it is logged to improve prediction model.
+                // When both old and new labels are logged together delimiter is used.
+                StringJoiner labelInfoBuilder = new StringJoiner(FOLDER_LABEL_DELIMITER);
+                if (mFromLabelState.equals(FromState.FROM_SUGGESTED)) {
+                    labelInfoBuilder.add(mFromTitle);
+                }
+
+                ToState toLabelState;
+                if (mFromTitle != null && mFromTitle.equals(mInfo.title)) {
+                    toLabelState = ToState.UNCHANGED;
+                } else {
+                    toLabelState = mInfo.getToLabelState();
+                    if (toLabelState.toString().startsWith("TO_SUGGESTION")) {
+                        labelInfoBuilder.add(mInfo.title);
+                    }
+                }
+                statsLogger.withToState(toLabelState);
+
+                if (labelInfoBuilder.length() > 0) {
+                    statsLogger.withEditText(labelInfoBuilder.toString());
+                }
+
+                statsLogger.log(LAUNCHER_FOLDER_LABEL_UPDATED);
+                logFolderLabelState(mFromLabelState, toLabelState);
                 mFolderName.dispatchBackKey();
             }
         }
@@ -1649,8 +1669,8 @@
      * @deprecated This method is only used for log validation and soon will be removed.
      */
     @Deprecated
-    public void logFolderLabelState() {
+    public void logFolderLabelState(FromState fromState, ToState toState) {
         mLauncher.getUserEventDispatcher()
-                .logLauncherEvent(mInfo.getFolderLabelStateLauncherEvent());
+                .logLauncherEvent(mInfo.getFolderLabelStateLauncherEvent(fromState, toState));
     }
 }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index b40b1e2..75275b2 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -16,11 +16,11 @@
 
 package com.android.launcher3.folder;
 
-import static android.text.TextUtils.isEmpty;
-
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -63,11 +63,14 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.icons.DotRenderer;
+import com.android.launcher3.logger.LauncherAtom.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.FolderInfo.FolderListener;
+import com.android.launcher3.model.data.FolderInfo.LabelState;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.touch.ItemClickHandler;
@@ -81,6 +84,7 @@
 import java.util.List;
 import java.util.function.Predicate;
 
+
 /**
  * An icon that can appear on in the workspace representing an {@link Folder}.
  */
@@ -406,10 +410,9 @@
             if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true);
             final int finalIndex = index;
 
-            FolderNameInfo[] nameInfos =
-                    new FolderNameInfo[FolderNameProvider.SUGGEST_MAX];
+            FolderNameInfos nameInfos = new FolderNameInfos();
             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-                Executors.UI_HELPER_EXECUTOR.post(() -> {
+                Executors.MODEL_EXECUTOR.post(() -> {
                     d.folderNameProvider.getSuggestedFolderName(
                             getContext(), mInfo.contents, nameInfos);
                     showFinalView(finalIndex, item, nameInfos, d.logInstanceId);
@@ -423,12 +426,11 @@
     }
 
     private void showFinalView(int finalIndex, final WorkspaceItemInfo item,
-            FolderNameInfo[] nameInfos, InstanceId instanceId) {
+            FolderNameInfos nameInfos, InstanceId instanceId) {
         postDelayed(() -> {
             mPreviewItemManager.hidePreviewItem(finalIndex, false);
             mFolder.showItem(item);
             setLabelSuggestion(nameInfos, instanceId);
-            mFolder.logFolderLabelState();
             invalidate();
         }, DROP_IN_ANIMATION_DURATION);
     }
@@ -436,23 +438,45 @@
     /**
      * Set the suggested folder name.
      */
-    public void setLabelSuggestion(FolderNameInfo[] nameInfos, InstanceId instanceId) {
+    public void setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId) {
         if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
             return;
         }
-        if (!isEmpty(mFolderName.getText().toString())
-                || mInfo.hasOption(FolderInfo.FLAG_MANUAL_FOLDER_NAME)) {
+        if (!mInfo.getLabelState().equals(LabelState.UNLABELED)) {
             return;
         }
-        if (nameInfos == null || nameInfos[0] == null || isEmpty(nameInfos[0].getLabel())) {
+        if (nameInfos == null || !nameInfos.hasSuggestions()) {
+            StatsLogManager.newInstance(getContext()).logger()
+                    .withInstanceId(instanceId)
+                    .withItemInfo(mInfo)
+                    .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS);
             return;
         }
-        mInfo.setTitle(nameInfos[0].getLabel());
-        StatsLogManager.newInstance(getContext()).logger().withItemInfo(mInfo)
-                .withInstanceId(instanceId).log(LAUNCHER_FOLDER_LABEL_UPDATED);
+        if (!nameInfos.hasPrimary()) {
+            StatsLogManager.newInstance(getContext()).logger()
+                    .withInstanceId(instanceId)
+                    .withItemInfo(mInfo)
+                    .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY);
+            return;
+        }
+        CharSequence newTitle = nameInfos.getLabels()[0];
+        FromState fromState = mInfo.getFromLabelState();
+
+        mInfo.setTitle(newTitle, mFolder.mLauncher.getModelWriter());
         onTitleChanged(mInfo.title);
         mFolder.mFolderName.setText(mInfo.title);
-        mFolder.mLauncher.getModelWriter().updateItemInDatabase(mInfo);
+
+        // Logging for folder creation flow
+        StatsLogManager.newInstance(getContext()).logger()
+                .withInstanceId(instanceId)
+                .withItemInfo(mInfo)
+                .withFromState(fromState)
+                .withToState(ToState.TO_SUGGESTION0)
+                // When LAUNCHER_FOLDER_LABEL_UPDATED event.edit_text does not have delimiter,
+                // event is assumed to be folder creation on the server side.
+                .withEditText(newTitle.toString())
+                .log(LAUNCHER_FOLDER_AUTO_LABELED);
+        mFolder.logFolderLabelState(fromState, ToState.TO_SUGGESTION0);
     }
 
 
diff --git a/src/com/android/launcher3/folder/FolderNameInfo.java b/src/com/android/launcher3/folder/FolderNameInfo.java
deleted file mode 100644
index 1841cd9..0000000
--- a/src/com/android/launcher3/folder/FolderNameInfo.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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.folder;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-
-import androidx.annotation.NonNull;
-
-/**
- * Information about a single label suggestions of the Folder.
- */
-
-public final class FolderNameInfo implements Parcelable {
-    private final double mScore;
-    private final CharSequence mLabel;
-
-    /**
-     * Create a simple completion with label.
-     *
-     * @param label The text that should be inserted into the editor and pushed to
-     *              InputMethodManager suggestions.
-     * @param score The score for the label between 0.0 and 1.0.
-     */
-    public FolderNameInfo(CharSequence label, double score) {
-        mScore = score;
-        mLabel = label;
-    }
-
-    private FolderNameInfo(Parcel source) {
-        mScore = source.readDouble();
-        mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
-    }
-
-    public CharSequence getLabel() {
-        return mLabel;
-    }
-
-    public double getScore() {
-        return mScore;
-    }
-
-    /**
-     * Used to package this object into a {@link Parcel}.
-     *
-     * @param dest  The {@link Parcel} to be written.
-     * @param flags The flags used for parceling.
-     */
-    public void writeToParcel(Parcel dest, int flags) {
-        dest.writeDouble(mScore);
-        TextUtils.writeToParcel(mLabel, dest, flags);
-    }
-
-    /**
-     * Used to make this class parcelable.
-     */
-    @NonNull
-    public static final Parcelable.Creator<FolderNameInfo> CREATOR =
-            new Parcelable.Creator<FolderNameInfo>() {
-                public FolderNameInfo createFromParcel(Parcel source) {
-                    return new FolderNameInfo(source);
-                }
-
-                public FolderNameInfo[] newArray(int size) {
-                    return new FolderNameInfo[size];
-                }
-            };
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    @NonNull
-    public String toString() {
-        return String.format("%s:%.2f", mLabel, mScore);
-    }
-}
diff --git a/src/com/android/launcher3/folder/FolderNameInfos.java b/src/com/android/launcher3/folder/FolderNameInfos.java
new file mode 100644
index 0000000..457ae87
--- /dev/null
+++ b/src/com/android/launcher3/folder/FolderNameInfos.java
@@ -0,0 +1,117 @@
+/*
+ * 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.folder;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Information about a  label suggestions of a Folder.
+ */
+
+public class FolderNameInfos {
+    public static final int SUCCESS = 1;
+    public static final int HAS_PRIMARY = 1 << 1;
+    public static final int HAS_SUGGESTIONS = 1 << 2;
+    public static final int ERROR_NO_PROVIDER = 1 << 3;
+    public static final int ERROR_APP_LOOKUP_FAILED = 1 << 4;
+    public static final int ERROR_ALL_APP_LOOKUP_FAILED = 1 << 5;
+    public static final int ERROR_NO_LABELS_GENERATED = 1 << 6;
+    public static final int ERROR_LABEL_LOOKUP_FAILED = 1 << 7;
+    public static final int ERROR_ALL_LABEL_LOOKUP_FAILED = 1 << 8;
+    public static final int ERROR_NO_PACKAGES = 1 << 9;
+
+    private int mStatus;
+    private final CharSequence[] mLabels;
+    private final Float[] mScores;
+
+    public FolderNameInfos() {
+        mStatus = 0;
+        mLabels = new CharSequence[FolderNameProvider.SUGGEST_MAX];
+        mScores = new Float[FolderNameProvider.SUGGEST_MAX];
+    }
+
+    /**
+     * set the status of FolderNameInfos.
+     */
+    public void setStatus(int statusBit) {
+        mStatus = mStatus | statusBit;
+    }
+
+    /**
+     * returns status of FolderNameInfos generations.
+     */
+    public int status() {
+        return mStatus;
+    }
+
+    /**
+     * return true if the first suggestion is a Primary suggestion.
+     */
+    public boolean hasPrimary() {
+        return (mStatus & HAS_PRIMARY) > 0 && (mLabels[0] != null);
+    }
+
+    /**
+     * return true if there is at least one valid suggestion.
+     */
+    public boolean hasSuggestions() {
+        for (CharSequence l : mLabels) {
+            if (l != null && !TextUtils.isEmpty(l)) return true;
+        }
+        return false;
+    }
+
+    /**
+     * assign label and score in the specified index.
+     */
+    public void setLabel(int index, CharSequence label, Float score) {
+        if (index < mLabels.length) {
+            mLabels[index] = label;
+            mScores[index] = score;
+        }
+    }
+
+    /**
+     * returns true if the label is found in label suggestions/
+     */
+    public boolean contains(CharSequence label) {
+        return Arrays.stream(mLabels)
+                .filter(Objects::nonNull)
+                .anyMatch(l -> l.toString().equalsIgnoreCase(label.toString()));
+    }
+
+
+    public CharSequence[] getLabels() {
+        return mLabels;
+    }
+
+    public Float[] getScores() {
+        return mScores;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return String.format("status=%s, labels=%s", Integer.toBinaryString(mStatus),
+                Arrays.toString(mLabels));
+    }
+}
+
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index 7731e6e..d166e27 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.ResourceBasedOverride;
 
 import java.util.ArrayList;
@@ -64,6 +65,7 @@
     public static FolderNameProvider newInstance(Context context) {
         FolderNameProvider fnp = Overrides.getObject(FolderNameProvider.class,
                 context.getApplicationContext(), R.string.folder_name_provider_class);
+        Preconditions.assertWorkerThread();
         fnp.load(context);
 
         return fnp;
@@ -71,6 +73,7 @@
 
     public static FolderNameProvider newInstance(Context context, List<AppInfo> appInfos,
             IntSparseArrayMap<FolderInfo> folderInfos) {
+        Preconditions.assertWorkerThread();
         FolderNameProvider fnp = Overrides.getObject(FolderNameProvider.class,
                 context.getApplicationContext(), R.string.folder_name_provider_class);
         fnp.load(appInfos, folderInfos);
@@ -93,10 +96,10 @@
      */
     public void getSuggestedFolderName(Context context,
             ArrayList<WorkspaceItemInfo> workspaceItemInfos,
-            FolderNameInfo[] nameInfos) {
-
+            FolderNameInfos nameInfos) {
+        Preconditions.assertWorkerThread();
         if (DEBUG) {
-            Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(nameInfos));
+            Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString());
         }
         // If all the icons are from work profile,
         // Then, suggest "Work" as the folder name
@@ -121,7 +124,7 @@
             info.ifPresent(i -> setAsFirstSuggestion(nameInfos, i.title.toString()));
         }
         if (DEBUG) {
-            Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(nameInfos));
+            Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString());
         }
     }
 
@@ -135,39 +138,37 @@
                 .findAny();
     }
 
-    private void setAsFirstSuggestion(FolderNameInfo[] nameInfos, CharSequence label) {
-        if (nameInfos.length == 0 || contains(nameInfos, label)) {
+    private void setAsFirstSuggestion(FolderNameInfos nameInfos, CharSequence label) {
+        if (nameInfos == null || nameInfos.contains(label)) {
             return;
         }
-        for (int i = nameInfos.length - 1; i > 0; i--) {
-            if (nameInfos[i - 1] != null && !TextUtils.isEmpty(nameInfos[i - 1].getLabel())) {
-                nameInfos[i] = nameInfos[i - 1];
+        nameInfos.setStatus(FolderNameInfos.HAS_PRIMARY);
+        nameInfos.setStatus(FolderNameInfos.HAS_SUGGESTIONS);
+        CharSequence[] labels = nameInfos.getLabels();
+        Float[] scores = nameInfos.getScores();
+        for (int i = labels.length - 1; i > 0; i--) {
+            if (labels[i - 1] != null && !TextUtils.isEmpty(labels[i - 1])) {
+                nameInfos.setLabel(i, labels[i - 1], scores[i - 1]);
             }
         }
-        nameInfos[0] = new FolderNameInfo(label, 1.0);
+        nameInfos.setLabel(0, label, 1.0f);
     }
 
-    private void setAsLastSuggestion(FolderNameInfo[] nameInfos, CharSequence label) {
-        if (nameInfos.length == 0 || contains(nameInfos, label)) {
+    private void setAsLastSuggestion(FolderNameInfos nameInfos, CharSequence label) {
+        if (nameInfos == null || nameInfos.contains(label)) {
             return;
         }
-
-        for (int i = 0; i < nameInfos.length; i++) {
-            if (nameInfos[i] == null || TextUtils.isEmpty(nameInfos[i].getLabel())) {
-                nameInfos[i] = new FolderNameInfo(label, 1.0);
+        nameInfos.setStatus(FolderNameInfos.HAS_PRIMARY);
+        nameInfos.setStatus(FolderNameInfos.HAS_SUGGESTIONS);
+        CharSequence[] labels = nameInfos.getLabels();
+        for (int i = 0; i < labels.length; i++) {
+            if (labels[i] == null || TextUtils.isEmpty(labels[i])) {
+                nameInfos.setLabel(i, label, 1.0f);
                 return;
             }
         }
         // Overwrite the last suggestion.
-        int lastIndex = nameInfos.length - 1;
-        nameInfos[lastIndex] = new FolderNameInfo(label, 1.0);
-    }
-
-    private boolean contains(FolderNameInfo[] nameInfos, CharSequence label) {
-        return Arrays.stream(nameInfos)
-                .filter(Objects::nonNull)
-                .anyMatch(nameInfo -> nameInfo.getLabel().toString().equalsIgnoreCase(
-                        label.toString()));
+        nameInfos.setLabel(labels.length - 1, label, 1.0f);
     }
 
     private class FolderNameWorker extends BaseModelUpdateTask {
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 8fd779d..8e23b65 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -15,15 +15,22 @@
  */
 package com.android.launcher3.logging;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_CLOSE_DOWN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_OPEN_UP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE;
+
 import android.content.Context;
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.logger.LauncherAtom;
-import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
+import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.userevent.LauncherLogProto;
 import com.android.launcher3.util.ResourceBasedOverride;
 
 /**
@@ -34,6 +41,54 @@
  */
 public class StatsLogManager implements ResourceBasedOverride {
 
+    public static final int LAUNCHER_STATE_UNSPECIFIED = 0;
+    public static final int LAUNCHER_STATE_BACKGROUND = 1;
+    public static final int LAUNCHER_STATE_HOME = 2;
+    public static final int LAUNCHER_STATE_OVERVIEW = 3;
+    public static final int LAUNCHER_STATE_ALLAPPS = 4;
+    public static final int LAUNCHER_STATE_UNCHANGED = 5;
+
+    /**
+     * Returns proper launcher state enum for {@link StatsLogManager}
+     * (to be removed during UserEventDispatcher cleanup)
+     */
+    public static int containerTypeToAtomState(int containerType) {
+        switch (containerType) {
+            case LauncherLogProto.ContainerType.ALLAPPS_VALUE:
+                return LAUNCHER_STATE_ALLAPPS;
+            case LauncherLogProto.ContainerType.OVERVIEW_VALUE:
+                return LAUNCHER_STATE_OVERVIEW;
+            case LauncherLogProto.ContainerType.WORKSPACE_VALUE:
+                return LAUNCHER_STATE_HOME;
+            case LauncherLogProto.ContainerType.APP_VALUE:
+                return LAUNCHER_STATE_BACKGROUND;
+        }
+        return LAUNCHER_STATE_UNSPECIFIED;
+    }
+
+    /**
+     * Returns event enum based on the two {@link ContainerType} transition information when
+     * swipe gesture happens.
+     * (to be removed during UserEventDispatcher cleanup)
+     */
+    public static EventEnum getLauncherAtomEvent(int startContainerType,
+            int targetContainerType, EventEnum fallbackEvent) {
+        if (startContainerType == LauncherLogProto.ContainerType.WORKSPACE.getNumber()
+                && targetContainerType == LauncherLogProto.ContainerType.WORKSPACE.getNumber()) {
+            return LAUNCHER_HOME_GESTURE;
+        } else if (startContainerType != LauncherLogProto.ContainerType.TASKSWITCHER.getNumber()
+                && targetContainerType == LauncherLogProto.ContainerType.TASKSWITCHER.getNumber()) {
+            return LAUNCHER_OVERVIEW_GESTURE;
+        } else if (startContainerType != LauncherLogProto.ContainerType.ALLAPPS.getNumber()
+                && targetContainerType == LauncherLogProto.ContainerType.ALLAPPS.getNumber()) {
+            return LAUNCHER_ALLAPPS_OPEN_UP;
+        } else if (startContainerType == LauncherLogProto.ContainerType.ALLAPPS.getNumber()
+                && targetContainerType != LauncherLogProto.ContainerType.ALLAPPS.getNumber()) {
+            return LAUNCHER_ALLAPPS_CLOSE_DOWN;
+        }
+        return fallbackEvent; // TODO fix
+    }
+
     public interface EventEnum {
         int getId();
     }
@@ -67,8 +122,16 @@
                 + "resulting in a new folder creation")
         LAUNCHER_ITEM_DROP_FOLDER_CREATED(386),
 
-        @UiEvent(doc = "User action resulted in or manually updated the folder label to "
-                + "new/same value.")
+        @UiEvent(doc = "Folder's label is automatically assigned.")
+        LAUNCHER_FOLDER_AUTO_LABELED(591),
+
+        @UiEvent(doc = "Could not auto-label a folder because primary suggestion is null or empty.")
+        LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY(592),
+
+        @UiEvent(doc = "Could not auto-label a folder because no suggestions exist.")
+        LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS(593),
+
+        @UiEvent(doc = "User manually updated the folder label.")
         LAUNCHER_FOLDER_LABEL_UPDATED(460),
 
         @UiEvent(doc = "User long pressed on the workspace empty space.")
@@ -145,12 +208,75 @@
         @UiEvent(doc = "Hotseat education tip shown")
         LAUNCHER_HOTSEAT_EDU_ONLY_TIP(482),
 
+        /**
+         * @deprecated LauncherUiChanged.rank field is repurposed to store all apps rank, so no
+         * separate event is required.
+         */
+        @Deprecated
         @UiEvent(doc = "App launch ranking logged for all apps predictions")
         LAUNCHER_ALL_APPS_RANKED(552),
 
         @UiEvent(doc = "App launch ranking logged for hotseat predictions)")
-        LAUNCHER_HOTSEAT_RANKED(553);
+        LAUNCHER_HOTSEAT_RANKED(553),
+        @UiEvent(doc = "Launcher is now in background. e.g., Screen off event")
+        LAUNCHER_ONSTOP(562),
+
+        @UiEvent(doc = "Launcher is now in foreground. e.g., Screen on event, back button")
+        LAUNCHER_ONRESUME(563),
+
+        @UiEvent(doc = "User swipes or fling in LEFT direction on workspace.")
+        LAUNCHER_SWIPELEFT(564),
+
+        @UiEvent(doc = "User swipes or fling in RIGHT direction on workspace.")
+        LAUNCHER_SWIPERIGHT(565),
+
+        @UiEvent(doc = "User swipes or fling in UP direction in unknown way.")
+        LAUNCHER_UNKNOWN_SWIPEUP(566),
+
+        @UiEvent(doc = "User swipes or fling in DOWN direction in unknown way.")
+        LAUNCHER_UNKNOWN_SWIPEDOWN(567),
+
+        @UiEvent(doc = "User swipes or fling in UP direction to open apps drawer.")
+        LAUNCHER_ALLAPPS_OPEN_UP(568),
+
+        @UiEvent(doc = "User swipes or fling in DOWN direction to close apps drawer.")
+        LAUNCHER_ALLAPPS_CLOSE_DOWN(569),
+
+        @UiEvent(doc = "User swipes or fling in UP direction and hold from the bottom bazel area")
+        LAUNCHER_OVERVIEW_GESTURE(570),
+
+        @UiEvent(doc = "User swipes or fling in LEFT direction on the bottom bazel area.")
+        LAUNCHER_QUICKSWITCH_LEFT(571),
+
+        @UiEvent(doc = "User swipes or fling in RIGHT direction on the bottom bazel area.")
+        LAUNCHER_QUICKSWITCH_RIGHT(572),
+
+        @UiEvent(doc = "User swipes or fling in DOWN direction on the bottom bazel area.")
+        LAUNCHER_SWIPEDOWN_NAVBAR(573),
+
+        @UiEvent(doc = "User swipes or fling in UP direction from bottom bazel area.")
+        LAUNCHER_HOME_GESTURE(574),
+
+        @UiEvent(doc = "User's workspace layout information is snapshot in the background.")
+        LAUNCHER_WORKSPACE_SNAPSHOT(579),
+
+        @UiEvent(doc = "User tapped on the screenshot button on overview)")
+        LAUNCHER_OVERVIEW_ACTIONS_SCREENSHOT(580),
+
+        @UiEvent(doc = "User tapped on the select button on overview)")
+        LAUNCHER_OVERVIEW_ACTIONS_SELECT(581),
+
+        @UiEvent(doc = "User tapped on the share button on overview")
+        LAUNCHER_OVERVIEW_ACTIONS_SHARE(582),
+
+        @UiEvent(doc = "User tapped on the close button in select mode")
+        LAUNCHER_SELECT_MODE_CLOSE(583),
+
+        @UiEvent(doc = "User tapped on the highlight items in select mode")
+        LAUNCHER_SELECT_MODE_ITEM(584);
+
         // ADD MORE
+
         private final int mId;
 
         LauncherEvent(int id) {
@@ -223,6 +349,37 @@
         }
 
         /**
+         * Sets FromState field of log message.
+         */
+        default StatsLogger withFromState(FromState fromState) {
+            return this;
+        }
+
+        /**
+         * Sets ToState field of log message.
+         */
+        default StatsLogger withToState(ToState toState) {
+            return this;
+        }
+
+        /**
+         * Sets editText field of log message.
+         */
+        default StatsLogger withEditText(String editText) {
+            return this;
+        }
+
+        /**
+         * Sets the final value for container related fields of log message.
+         *
+         * By default container related fields are derived from {@link ItemInfo}, this method would
+         * override those values.
+         */
+        default StatsLogger withContainerInfo(ContainerInfo containerInfo) {
+            return this;
+        }
+
+        /**
          * Builds the final message and logs it as {@link EventEnum}.
          */
         default void log(EventEnum event) {
@@ -237,42 +394,16 @@
         };
     }
 
-    protected LogStateProvider mStateProvider;
-
     /**
      * Creates a new instance of {@link StatsLogManager} based on provided context.
      */
     public static StatsLogManager newInstance(Context context) {
-        return newInstance(context, null);
-    }
-
-    public static StatsLogManager newInstance(Context context, LogStateProvider stateProvider) {
         StatsLogManager mgr = Overrides.getObject(StatsLogManager.class,
                 context.getApplicationContext(), R.string.stats_log_manager_class);
-        mgr.mStateProvider = stateProvider;
         return mgr;
     }
 
     /**
-     * Logs an event.
-     *
-     * @param event an enum implementing EventEnum interface.
-     * @param atomInfo item typically containing app or task launch related information.
-     */
-    public void log(EventEnum event, InstanceId instanceId, LauncherAtom.ItemInfo atomInfo) {
-    }
-
-    /**
-     * Logs an event and accompanying {@link LauncherState}s.
-     *
-     * @param event an enum implementing EventEnum interface.
-     * @param launcherAtomItemInfo item typically containing app or task launch related information.
-     */
-    public void log(EventEnum event, @Nullable LauncherAtom.ItemInfo launcherAtomItemInfo,
-            int srcState, int dstState) {
-    }
-
-    /**
      * Log an event with ranked-choice information along with package. Does nothing if event.getId()
      * <= 0.
      *
diff --git a/src/com/android/launcher3/logging/StatsLogUtils.java b/src/com/android/launcher3/logging/StatsLogUtils.java
index 10d88e5..a5cc7ea 100644
--- a/src/com/android/launcher3/logging/StatsLogUtils.java
+++ b/src/com/android/launcher3/logging/StatsLogUtils.java
@@ -6,28 +6,13 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 
 import java.util.ArrayList;
 
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.DEFAULT_CONTAINERTYPE;
-
 public class StatsLogUtils {
-
-    // Defined in android.stats.launcher.nano
-    // As they cannot be linked in this file, defining again.
-    public final static int LAUNCHER_STATE_BACKGROUND = 0;
-    public final static int LAUNCHER_STATE_HOME = 1;
-    public final static int LAUNCHER_STATE_OVERVIEW = 2;
-    public final static int LAUNCHER_STATE_ALLAPPS = 3;
-
     private final static int MAXIMUM_VIEW_HIERARCHY_LEVEL = 5;
 
-    public interface LogStateProvider {
-        int getCurrentState();
-    }
-
     /**
      * Implemented by containers to provide a container source for a given child.
      */
@@ -61,20 +46,4 @@
         }
         return null;
     }
-
-    public static int getContainerTypeFromState(int state) {
-        int containerType = DEFAULT_CONTAINERTYPE;
-        switch (state) {
-            case StatsLogUtils.LAUNCHER_STATE_ALLAPPS:
-                containerType = ContainerType.ALLAPPS;
-                break;
-            case StatsLogUtils.LAUNCHER_STATE_HOME:
-                containerType = ContainerType.WORKSPACE;
-                break;
-            case StatsLogUtils.LAUNCHER_STATE_OVERVIEW:
-                containerType = ContainerType.OVERVIEW;
-                break;
-        }
-        return containerType;
-    }
 }
diff --git a/src/com/android/launcher3/model/AppLaunchTracker.java b/src/com/android/launcher3/model/AppLaunchTracker.java
index 13ab033..629a0ee 100644
--- a/src/com/android/launcher3/model/AppLaunchTracker.java
+++ b/src/com/android/launcher3/model/AppLaunchTracker.java
@@ -40,6 +40,7 @@
     public static final String CONTAINER_ALL_APPS = Integer.toString(ContainerType.ALLAPPS);
     public static final String CONTAINER_PREDICTIONS = Integer.toString(ContainerType.PREDICTION);
     public static final String CONTAINER_SEARCH = Integer.toString(ContainerType.SEARCHRESULT);
+    public static final String CONTAINER_OVERVIEW = Integer.toString(ContainerType.OVERVIEW);
 
 
     public static final MainThreadInitializedObject<AppLaunchTracker> INSTANCE =
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index f2073ef..102ec31 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -59,7 +59,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderGridOrganizer;
-import com.android.launcher3.folder.FolderNameInfo;
+import com.android.launcher3.folder.FolderNameInfos;
 import com.android.launcher3.folder.FolderNameProvider;
 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.icons.ComponentWithLabelAndIcon.ComponentWithIconCachingLogic;
@@ -953,13 +953,12 @@
 
         synchronized (mBgDataModel) {
             for (int i = 0; i < mBgDataModel.folders.size(); i++) {
-                FolderNameInfo[] suggestionInfos =
-                        new FolderNameInfo[FolderNameProvider.SUGGEST_MAX];
+                FolderNameInfos suggestionInfos = new FolderNameInfos();
                 FolderInfo info = mBgDataModel.folders.valueAt(i);
                 if (info.suggestedFolderNames == null) {
                     provider.getSuggestedFolderName(mApp.getContext(), info.contents,
                             suggestionInfos);
-                    info.suggestedFolderNames = new Intent().putExtra("suggest", suggestionInfos);
+                    info.suggestedFolderNames = suggestionInfos;
                 }
             }
         }
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index 8f577b5..05ce06a 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -22,6 +22,7 @@
 
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.logger.LauncherAtom.Attribute.EMPTY_LABEL;
 import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL;
 import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL;
 import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_CUSTOM;
@@ -29,18 +30,16 @@
 import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
 import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_SUGGESTED;
 
-import static java.util.Arrays.stream;
-import static java.util.Optional.ofNullable;
-
-import android.content.Intent;
 import android.os.Process;
-import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.folder.FolderNameInfo;
+import com.android.launcher3.folder.FolderNameInfos;
 import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logger.LauncherAtom.Attribute;
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
 import com.android.launcher3.model.ModelWriter;
@@ -51,10 +50,7 @@
 import com.android.launcher3.util.ContentWriter;
 
 import java.util.ArrayList;
-import java.util.Objects;
-import java.util.Optional;
 import java.util.OptionalInt;
-import java.util.StringJoiner;
 import java.util.stream.IntStream;
 
 
@@ -82,25 +78,35 @@
 
     public static final int FLAG_MANUAL_FOLDER_NAME = 0x00000008;
 
+    /**
+     * Different states of folder label.
+     */
+    public enum LabelState {
+        // Folder's label is not yet assigned( i.e., title == null). Eligible for auto-labeling.
+        UNLABELED(Attribute.UNLABELED),
+
+        // Folder's label is empty(i.e., title == ""). Not eligible for auto-labeling.
+        EMPTY(EMPTY_LABEL),
+
+        // Folder's label is one of the non-empty suggested values.
+        SUGGESTED(SUGGESTED_LABEL),
+
+        // Folder's label is non-empty, manually entered by the user
+        // and different from any of suggested values.
+        MANUAL(MANUAL_LABEL);
+
+        private final LauncherAtom.Attribute mLogAttribute;
+
+        LabelState(Attribute logAttribute) {
+            this.mLogAttribute = logAttribute;
+        }
+    }
+
     public static final String EXTRA_FOLDER_SUGGESTIONS = "suggest";
 
     public int options;
 
-    public Intent suggestedFolderNames;
-
-    // Represents the title before current.
-    // Primarily used for logging purpose.
-    private CharSequence mPreviousTitle;
-
-    // True if the title before was manually entered, suggested otherwise.
-    // Primarily used for logging purpose.
-    public boolean fromCustom;
-
-    /**
-     * Used for separating {@link #mPreviousTitle} and {@link #title} when concatenating them
-     * for logging.
-     */
-    private static final CharSequence FOLDER_LABEL_DELIMITER = "=>";
+    public FolderNameInfos suggestedFolderNames;
 
     /**
      * The apps and shortcuts
@@ -198,8 +204,7 @@
 
     @Override
     protected String dumpProperties() {
-        return super.dumpProperties()
-                + " manuallyTypedTitle=" + hasOption(FLAG_MANUAL_FOLDER_NAME);
+        return String.format("%s; labelState=%s", super.dumpProperties(), getLabelState());
     }
 
     @Override
@@ -207,19 +212,41 @@
         return getDefaultItemInfoBuilder()
                 .setFolderIcon(LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size()))
                 .setRank(rank)
-                .setAttribute(fromCustom ? MANUAL_LABEL : SUGGESTED_LABEL)
+                .setAttribute(getLabelState().mLogAttribute)
                 .setContainerInfo(getContainerInfo())
                 .build();
     }
 
     @Override
-    public void setTitle(CharSequence title) {
-        mPreviousTitle = this.title;
+    public void setTitle(@Nullable CharSequence title, ModelWriter modelWriter) {
+        // Updating label from null to empty is considered as false touch.
+        // Retaining null title(ie., UNLABELED state) allows auto-labeling when new items added.
+        if (isEmpty(title) && this.title == null) {
+            return;
+        }
+
+        // Updating title to same value does not change any states.
+        if (title != null && title == this.title) {
+            return;
+        }
+
         this.title = title;
+        LabelState newLabelState =
+                title == null ? LabelState.UNLABELED
+                        : title.length() == 0 ? LabelState.EMPTY :
+                                getAcceptedSuggestionIndex().isPresent() ? LabelState.SUGGESTED
+                                        : LabelState.MANUAL;
+        setOption(FLAG_MANUAL_FOLDER_NAME, newLabelState.equals(LabelState.MANUAL), modelWriter);
     }
 
-    public CharSequence getPreviousTitle() {
-        return mPreviousTitle;
+    /**
+     * Returns current state of the current folder label.
+     */
+    public LabelState getLabelState() {
+        return title == null ? LabelState.UNLABELED
+                : title.length() == 0 ? LabelState.EMPTY :
+                        hasOption(FLAG_MANUAL_FOLDER_NAME) ? LabelState.MANUAL
+                                : LabelState.SUGGESTED;
     }
 
     @Override
@@ -235,30 +262,7 @@
      */
     @Override
     public LauncherAtom.ItemInfo buildProto() {
-        FromState fromFolderLabelState = getFromFolderLabelState();
-        ToState toFolderLabelState = getToFolderLabelState();
-        LauncherAtom.FolderIcon.Builder folderIconBuilder = LauncherAtom.FolderIcon.newBuilder()
-                .setCardinality(contents.size())
-                .setFromLabelState(fromFolderLabelState)
-                .setToLabelState(toFolderLabelState);
-
-        // If the folder label is suggested, it is logged to improve prediction model.
-        // When both old and new labels are logged together delimiter is used.
-        StringJoiner labelInfoBuilder = new StringJoiner(FOLDER_LABEL_DELIMITER);
-        if (fromFolderLabelState.equals(FromState.FROM_SUGGESTED)) {
-            labelInfoBuilder.add(mPreviousTitle);
-        }
-        if (toFolderLabelState.toString().startsWith("TO_SUGGESTION")) {
-            labelInfoBuilder.add(title);
-        }
-        if (labelInfoBuilder.length() > 0) {
-            folderIconBuilder.setLabelInfo(labelInfoBuilder.toString());
-        }
-
-        return getDefaultItemInfoBuilder()
-                .setFolderIcon(folderIconBuilder)
-                .setContainerInfo(getContainerInfo())
-                .build();
+        return buildProto(null);
     }
 
     /**
@@ -267,47 +271,58 @@
     public OptionalInt getAcceptedSuggestionIndex() {
         String newLabel = checkNotNull(title,
                 "Expected valid folder label, but found null").toString();
-        return getSuggestedLabels()
-                .map(suggestionsArray ->
-                        IntStream.range(0, suggestionsArray.length)
-                                .filter(
-                                        index -> !isEmpty(suggestionsArray[index])
-                                                && newLabel.equalsIgnoreCase(
-                                                suggestionsArray[index]))
-                                .sequential()
-                                .findFirst()
-                ).orElse(OptionalInt.empty());
-
+        if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
+            return OptionalInt.empty();
+        }
+        CharSequence[] labels = suggestedFolderNames.getLabels();
+        return IntStream.range(0, labels.length)
+                .filter(index -> !isEmpty(labels[index])
+                        && newLabel.equalsIgnoreCase(
+                        labels[index].toString()))
+                .sequential()
+                .findFirst();
     }
 
-    private LauncherAtom.ToState getToFolderLabelState() {
+    /**
+     * Returns {@link FromState} based on current {@link #title}.
+     */
+    public LauncherAtom.FromState getFromLabelState() {
+        switch (getLabelState()){
+            case EMPTY:
+                return LauncherAtom.FromState.FROM_EMPTY;
+            case MANUAL:
+                return LauncherAtom.FromState.FROM_CUSTOM;
+            case SUGGESTED:
+                return LauncherAtom.FromState.FROM_SUGGESTED;
+            case UNLABELED:
+            default:
+                return LauncherAtom.FromState.FROM_STATE_UNSPECIFIED;
+        }
+    }
+
+    /**
+     * Returns {@link ToState} based on current {@link #title}.
+     */
+    public LauncherAtom.ToState getToLabelState() {
         if (title == null) {
             return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
         }
 
-        if (title.equals(mPreviousTitle)) {
-            return LauncherAtom.ToState.UNCHANGED;
-        }
-
         if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
             return title.length() > 0
                     ? LauncherAtom.ToState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED
                     : LauncherAtom.ToState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED;
         }
 
-        Optional<String[]> suggestedLabels = getSuggestedLabels();
-        boolean isEmptySuggestions = suggestedLabels
-                .map(labels -> stream(labels).allMatch(TextUtils::isEmpty))
-                .orElse(true);
-        if (isEmptySuggestions) {
+        // TODO: if suggestedFolderNames is null then it infrastructure issue, not
+        // ranking issue. We should log these appropriately.
+        if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
             return title.length() > 0
                     ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS
                     : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
         }
 
-        boolean hasValidPrimary = suggestedLabels
-                .map(labels -> !isEmpty(labels[0]))
-                .orElse(false);
+        boolean hasValidPrimary = suggestedFolderNames != null && suggestedFolderNames.hasPrimary();
         if (title.length() == 0) {
             return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY
                     : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
@@ -335,31 +350,6 @@
                 // fall through
         }
         return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
-
-    }
-
-    private LauncherAtom.FromState getFromFolderLabelState() {
-        return mPreviousTitle == null
-                ? LauncherAtom.FromState.FROM_STATE_UNSPECIFIED
-                : mPreviousTitle.length() == 0
-                        ? LauncherAtom.FromState.FROM_EMPTY
-                        : fromCustom
-                                ? LauncherAtom.FromState.FROM_CUSTOM
-                                : LauncherAtom.FromState.FROM_SUGGESTED;
-    }
-
-    private Optional<String[]> getSuggestedLabels() {
-        return ofNullable(suggestedFolderNames)
-                .map(folderNames ->
-                        (FolderNameInfo[])
-                                folderNames.getParcelableArrayExtra(EXTRA_FOLDER_SUGGESTIONS))
-                .map(folderNameInfoArray ->
-                        stream(folderNameInfoArray)
-                                .filter(Objects::nonNull)
-                                .map(FolderNameInfo::getLabel)
-                                .filter(Objects::nonNull)
-                                .map(CharSequence::toString)
-                                .toArray(String[]::new));
     }
 
     /**
@@ -368,7 +358,8 @@
      * @deprecated This method is used only for validation purpose and soon will be removed.
      */
     @Deprecated
-    public LauncherLogProto.LauncherEvent getFolderLabelStateLauncherEvent() {
+    public LauncherLogProto.LauncherEvent getFolderLabelStateLauncherEvent(FromState fromState,
+            ToState toState) {
         return LauncherLogProto.LauncherEvent.newBuilder()
                 .setAction(LauncherLogProto.Action
                         .newBuilder()
@@ -377,8 +368,8 @@
                         .newBuilder()
                         .setType(Target.Type.ITEM)
                         .setItemType(LauncherLogProto.ItemType.EDITTEXT)
-                        .setFromFolderLabelState(convertFolderLabelState(getFromFolderLabelState()))
-                        .setToFolderLabelState(convertFolderLabelState(getToFolderLabelState())))
+                        .setFromFolderLabelState(convertFolderLabelState(fromState))
+                        .setToFolderLabelState(convertFolderLabelState(toState)))
                 .addSrcTarget(Target.newBuilder()
                         .setType(Target.Type.CONTAINER)
                         .setContainerType(LauncherLogProto.ContainerType.FOLDER)
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index 66c3cbb..0d3ddad 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -52,6 +52,7 @@
 import com.android.launcher3.logger.LauncherAtom.SettingsContainer;
 import com.android.launcher3.logger.LauncherAtom.ShortcutsContainer;
 import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer;
+import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.util.ContentWriter;
 
 import java.util.Optional;
@@ -343,7 +344,7 @@
         return itemBuilder;
     }
 
-    ContainerInfo getContainerInfo() {
+    protected ContainerInfo getContainerInfo() {
         switch (container) {
             case CONTAINER_HOTSEAT:
                 return ContainerInfo.newBuilder()
@@ -405,7 +406,10 @@
         return itemInfo;
     }
 
-    public void setTitle(CharSequence title) {
+    /**
+     * Sets the title of the item and writes to DB model if needed.
+     */
+    public void setTitle(CharSequence title, ModelWriter modelWriter) {
         this.title = title;
     }
 }
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 0a1607c..dbe5f42 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -18,10 +18,13 @@
 import static com.android.launcher3.LauncherState.FLAG_NON_INTERACTIVE;
 
 import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
 
 import androidx.annotation.CallSuper;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.LauncherRootView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
@@ -38,6 +41,8 @@
     private final Runnable mHandleDeferredResume = this::handleDeferredResume;
     private boolean mDeferredResumePending;
 
+    private LauncherRootView mRootView;
+
     /**
      * Create handlers to control the property changes for this activity
      */
@@ -55,6 +60,23 @@
      */
     public abstract StateManager<STATE_TYPE> getStateManager();
 
+    protected void inflateRootView(int layoutId) {
+        mRootView = (LauncherRootView) LayoutInflater.from(this).inflate(layoutId, null);
+        mRootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+    }
+
+    @Override
+    public final LauncherRootView getRootView() {
+        return mRootView;
+    }
+
+    @Override
+    public <T extends View> T findViewById(int id) {
+        return mRootView.findViewById(id);
+    }
+
     /**
      * Called when transition to the state starts
      */
@@ -87,6 +109,7 @@
      * the transition if requested.
      */
     public void reapplyUi(boolean cancelCurrentAnimation) {
+        getRootView().dispatchInsets();
         getStateManager().reapplyState(cancelCurrentAnimation);
     }
 
diff --git a/src/com/android/launcher3/states/HintState.java b/src/com/android/launcher3/states/HintState.java
index 9ea8436..b8a184f 100644
--- a/src/com/android/launcher3/states/HintState.java
+++ b/src/com/android/launcher3/states/HintState.java
@@ -39,8 +39,18 @@
     }
 
     @Override
+    protected float getDepthUnchecked(Context context) {
+        return 0.15f;
+    }
+
+    @Override
+    public float getOverviewScrimAlpha(Launcher launcher) {
+        return 0.4f;
+    }
+
+    @Override
     public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(0.9f, 0, 0);
+        return new ScaleAndTranslation(0.92f, 0, 0);
     }
 
     @Override
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index f0e0557..2a4f887 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -35,7 +35,7 @@
             | FLAG_HIDE_BACK_BUTTON;
 
     public SpringLoadedState(int id) {
-        super(id, ContainerType.OVERVIEW, STATE_FLAGS);
+        super(id, ContainerType.WORKSPACE, STATE_FLAGS);
     }
 
     @Override
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index e786f07..38b3712 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -15,27 +15,17 @@
  */
 package com.android.launcher3.testing;
 
-import static android.graphics.Bitmap.Config.ARGB_8888;
-
 import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
 import android.graphics.Insets;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Debug;
-import android.system.Os;
-import android.util.Log;
-import android.view.View;
 import android.view.WindowInsets;
 
-import androidx.annotation.Keep;
-
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
@@ -44,10 +34,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.util.ResourceBasedOverride;
 
-import java.util.LinkedList;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -65,7 +52,6 @@
     protected Context mContext;
     protected DeviceProfile mDeviceProfile;
     protected LauncherAppState mLauncherAppState;
-    private static LinkedList mLeaks;
 
     public void init(Context context) {
         mContext = context;
@@ -77,15 +63,6 @@
     public Bundle call(String method) {
         final Bundle response = new Bundle();
         switch (method) {
-            case TestProtocol.REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT: {
-                return getLauncherUIProperty(Bundle::putInt, l -> {
-                    final float progress = LauncherState.OVERVIEW.getVerticalProgress(l)
-                            - LauncherState.ALL_APPS.getVerticalProgress(l);
-                    final float distance = l.getAllAppsController().getShiftRange() * progress;
-                    return (int) distance;
-                });
-            }
-
             case TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT: {
                 return getLauncherUIProperty(Bundle::putInt, l -> {
                     final float progress = LauncherState.NORMAL.getVerticalProgress(l)
@@ -99,14 +76,6 @@
                 return getUIProperty(Bundle::putBoolean, t -> isLauncherInitialized(), () -> true);
             }
 
-            case TestProtocol.REQUEST_ENABLE_DEBUG_TRACING:
-                TestProtocol.sDebugTracing = true;
-                break;
-
-            case TestProtocol.REQUEST_DISABLE_DEBUG_TRACING:
-                TestProtocol.sDebugTracing = false;
-                break;
-
             case TestProtocol.REQUEST_FREEZE_APP_LIST:
                 return getLauncherUIProperty(Bundle::putBoolean, l -> {
                     l.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
@@ -118,11 +87,6 @@
                     return true;
                 });
 
-            case TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS: {
-                return getLauncherUIProperty(Bundle::putInt,
-                        l -> l.getAppsView().getAppsStore().getDeferUpdatesFlags());
-            }
-
             case TestProtocol.REQUEST_APPS_LIST_SCROLL_Y: {
                 return getLauncherUIProperty(Bundle::putInt,
                         l -> l.getAppsView().getActiveRecyclerView().getCurrentScrollY());
@@ -137,59 +101,19 @@
                 }, this::getCurrentActivity);
             }
 
-            case TestProtocol.REQUEST_PID: {
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, Os.getpid());
-                break;
-            }
-
-            case TestProtocol.REQUEST_TOTAL_PSS_KB: {
-                runGcAndFinalizersSync();
-                Debug.MemoryInfo mem = new Debug.MemoryInfo();
-                Debug.getMemoryInfo(mem);
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, mem.getTotalPss());
-                break;
-            }
-
-            case TestProtocol.REQUEST_JAVA_LEAK: {
-                if (mLeaks == null) mLeaks = new LinkedList();
-
-                // Allocate and dirty the memory.
-                final int leakSize = 1024 * 1024;
-                final byte[] bytes = new byte[leakSize];
-                for (int i = 0; i < leakSize; i += 239) {
-                    bytes[i] = (byte) (i % 256);
-                }
-                mLeaks.add(bytes);
-                break;
-            }
-
-            case TestProtocol.REQUEST_NATIVE_LEAK: {
-                if (mLeaks == null) mLeaks = new LinkedList();
-
-                // Allocate and dirty a bitmap.
-                final Bitmap bitmap = Bitmap.createBitmap(512, 512, ARGB_8888);
-                bitmap.eraseColor(Color.RED);
-                mLeaks.add(bitmap);
-                break;
-            }
-
-            case TestProtocol.REQUEST_VIEW_LEAK: {
-                if (mLeaks == null) mLeaks = new LinkedList();
-                mLeaks.add(new View(mContext));
-                break;
-            }
-
             case TestProtocol.REQUEST_ICON_HEIGHT: {
                 response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
                         mDeviceProfile.allAppsCellHeightPx);
-                break;
+                return response;
             }
 
             case TestProtocol.REQUEST_MOCK_SENSOR_ROTATION:
                 TestProtocol.sDisableSensorRotation = true;
-                break;
+                return response;
+
+            default:
+                return null;
         }
-        return response;
     }
 
     protected boolean isLauncherInitialized() {
@@ -201,22 +125,6 @@
         return Launcher.ACTIVITY_TRACKER.getCreatedActivity();
     }
 
-    private static void runGcAndFinalizersSync() {
-        Runtime.getRuntime().gc();
-        Runtime.getRuntime().runFinalization();
-
-        final CountDownLatch fence = new CountDownLatch(1);
-        createFinalizationObserver(fence);
-        try {
-            do {
-                Runtime.getRuntime().gc();
-                Runtime.getRuntime().runFinalization();
-            } while (!fence.await(100, TimeUnit.MILLISECONDS));
-        } catch (InterruptedException ex) {
-            throw new RuntimeException(ex);
-        }
-    }
-
     /**
      * Returns the result by getting a Launcher property on UI thread
      */
@@ -257,21 +165,4 @@
          */
         void set(Bundle b, String key, T value);
     }
-
-    // Create the observer in the scope of a method to minimize the chance that
-    // it remains live in a DEX/machine register at the point of the fence guard.
-    // This must be kept to avoid R8 inlining it.
-    @Keep
-    private static void createFinalizationObserver(CountDownLatch fence) {
-        new Object() {
-            @Override
-            protected void finalize() throws Throwable {
-                try {
-                    fence.countDown();
-                } finally {
-                    super.finalize();
-                }
-            }
-        };
-    }
 }
diff --git a/src/com/android/launcher3/testing/TestLogging.java b/src/com/android/launcher3/testing/TestLogging.java
index d522d81..51e0819 100644
--- a/src/com/android/launcher3/testing/TestLogging.java
+++ b/src/com/android/launcher3/testing/TestLogging.java
@@ -21,9 +21,17 @@
 
 import com.android.launcher3.Utilities;
 
+import java.util.function.BiConsumer;
+
 public final class TestLogging {
+    private static BiConsumer<String, String> sEventConsumer;
+
     private static void recordEventSlow(String sequence, String event) {
         Log.d(TestProtocol.TAPL_EVENTS_TAG, sequence + " / " + event);
+        final BiConsumer<String, String> eventConsumer = sEventConsumer;
+        if (eventConsumer != null) {
+            eventConsumer.accept(sequence, event);
+        }
     }
 
     public static void recordEvent(String sequence, String event) {
@@ -43,4 +51,8 @@
             recordEventSlow(sequence, message + ": " + event);
         }
     }
+
+    static void setEventConsumer(BiConsumer<String, String> consumer) {
+        sEventConsumer = consumer;
+    }
 }
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 519b1b9..3ca08fd 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -88,6 +88,10 @@
     public static final String REQUEST_NATIVE_LEAK = "native-leak";
     public static final String REQUEST_VIEW_LEAK = "view-leak";
     public static final String REQUEST_RECENT_TASKS_LIST = "recent-tasks-list";
+    public static final String REQUEST_START_EVENT_LOGGING = "start-event-logging";
+    public static final String REQUEST_GET_TEST_EVENTS = "get-test-events";
+    public static final String REQUEST_STOP_EVENT_LOGGING = "stop-event-logging";
+    public static final String REQUEST_CLEAR_DATA = "clear-data";
 
     public static boolean sDebugTracing = false;
     public static final String REQUEST_ENABLE_DEBUG_TRACING = "enable-debug-tracing";
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 171c5ee..3c78b08 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -21,6 +21,8 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_NON_ATOMIC;
@@ -42,12 +44,14 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.FlingBlockCheck;
 import com.android.launcher3.util.TouchController;
 
@@ -298,11 +302,11 @@
     public boolean onDrag(float displacement, MotionEvent ev) {
         if (!mIsLogContainerSet) {
             if (mStartState == ALL_APPS) {
-                mStartContainerType = LauncherLogProto.ContainerType.ALLAPPS;
+                mStartContainerType = ContainerType.ALLAPPS;
             } else if (mStartState == NORMAL) {
                 mStartContainerType = getLogContainerTypeForNormalState(ev);
             } else if (mStartState == OVERVIEW) {
-                mStartContainerType = LauncherLogProto.ContainerType.TASKSWITCHER;
+                mStartContainerType = ContainerType.TASKSWITCHER;
             }
             mIsLogContainerSet = true;
         }
@@ -559,10 +563,22 @@
         // Transition complete. log the action
         mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
                 getDirectionForLog(), mDetector.getDownX(), mDetector.getDownY(),
-                mStartContainerType,
-                mStartState.containerType,
+                mStartContainerType /* e.g., hotseat */,
+                mStartState.containerType /* e.g., workspace */,
                 targetState.containerType,
                 mLauncher.getWorkspace().getCurrentPage());
+        mLauncher.getStatsLogManager().logger()
+                .withSrcState(StatsLogManager.containerTypeToAtomState(mStartState.containerType))
+                .withDstState(StatsLogManager.containerTypeToAtomState(targetState.containerType))
+                .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                        .setWorkspace(
+                                LauncherAtom.WorkspaceContainer.newBuilder()
+                                        .setPageIndex(mLauncher.getWorkspace().getCurrentPage()))
+                        .build())
+                .log(StatsLogManager.getLauncherAtomEvent(mStartState.containerType,
+                            targetState.containerType, mToState.ordinal > mFromState.ordinal
+                                    ? LAUNCHER_UNKNOWN_SWIPEUP
+                                    : LAUNCHER_UNKNOWN_SWIPEDOWN));
     }
 
     protected void clearState() {
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
index 90a1c82..d4e074c 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.java
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -38,6 +38,7 @@
     public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
     public static final String ALL_APPS_COUNT = "launcher.all_apps_count";
     public static final String HOTSEAT_DISCOVERY_TIP_COUNT = "launcher.hotseat_discovery_tip_count";
+    public static final String HOTSEAT_LONGPRESS_TIP_SEEN = "launcher.hotseat_longpress_tip_seen";
 
     /**
      * Events that either have happened or have not (booleans).
@@ -99,6 +100,13 @@
     }
 
     /**
+     * Marks on-boarding preference boolean at true
+     */
+    public void markChecked(String flag) {
+        mSharedPrefs.edit().putBoolean(flag, true).apply();
+    }
+
+    /**
      * Add 1 to the given event count, if we haven't already reached the max count.
      * @return Whether we have now reached the max count.
      */
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 7cce044..858e183 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -294,22 +294,15 @@
         // Limits UI tests affecting tests running after them.
         mLauncher.waitForLauncherInitialized();
         if (mLauncherPid != 0) {
-            assertEquals("Launcher crashed, pid mismatch:", mLauncherPid, mLauncher.getPid());
+            assertEquals("Launcher crashed, pid mismatch:",
+                    mLauncherPid, mLauncher.getPid().intValue());
         }
         checkDetectedLeaks(mLauncher);
     }
 
-    protected void clearLauncherData() throws IOException, InterruptedException {
-        if (TestHelpers.isInLauncherProcess()) {
-            LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
-                    LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
-            resetLoaderState();
-        } else {
-            clearPackageData(mDevice.getLauncherPackageName());
-            mLauncher.enableDebugTracing();
-            mLauncherPid = mLauncher.getPid();
-            mLauncher.waitForLauncherInitialized();
-        }
+    protected void clearLauncherData() {
+        mLauncher.clearLauncherData();
+        mLauncher.waitForLauncherInitialized();
     }
 
     /**
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 808be66..b6c17df 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -18,6 +18,7 @@
 
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
@@ -244,8 +245,8 @@
     }
 
     private void verifyNotFrozen(String message) {
-        mLauncher.assertEquals(message, 0, mLauncher.getTestInfo(
-                TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS).
-                getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD));
+        final Bundle testInfo = mLauncher.getTestInfo(TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS);
+        if (testInfo == null) return;
+        mLauncher.assertEquals(message, 0, testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD));
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 7ddb492..5cf5d0f 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -165,9 +165,7 @@
 
     private Consumer<ContainerType> mOnSettledStateAction;
 
-    private static LogEventChecker sEventChecker;
-    // True if there is an gesture in progress that needs event verification.
-    private static boolean sCheckingEvents;
+    private LogEventChecker mEventChecker;
 
     private boolean mCheckEventsForSuccessfulGestures = false;
     private Runnable mOnLauncherCrashed;
@@ -437,15 +435,16 @@
     }
 
     private String formatErrorWithEvents(String message, boolean checkEvents) {
-        if (sCheckingEvents) {
-            sCheckingEvents = false;
+        if (mEventChecker != null) {
+            final LogEventChecker eventChecker = mEventChecker;
+            mEventChecker = null;
             if (checkEvents) {
-                final String eventMismatch = sEventChecker.verify(0, false);
+                final String eventMismatch = eventChecker.verify(0, false);
                 if (eventMismatch != null) {
                     message = message + ", having produced " + eventMismatch;
                 }
             } else {
-                sEventChecker.finishNoWait();
+                eventChecker.finishNoWait();
             }
         }
 
@@ -1309,8 +1308,9 @@
                 getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
-    public int getPid() {
-        return getTestInfo(TestProtocol.REQUEST_PID).getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    public Integer getPid() {
+        final Bundle testInfo = getTestInfo(TestProtocol.REQUEST_PID);
+        return testInfo != null ? testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD) : null;
     }
 
     public void produceJavaLeak() {
@@ -1335,16 +1335,19 @@
         return tasks;
     }
 
+    public void clearLauncherData() {
+        getTestInfo(TestProtocol.REQUEST_CLEAR_DATA);
+    }
+
     public Closable eventsCheck() {
-        Assert.assertTrue("Nested event checking", !sCheckingEvents);
+        Assert.assertTrue("Nested event checking", mEventChecker == null);
         disableSensorRotation();
-        final int initialPid = getPid();
-        if (sEventChecker == null) sEventChecker = new LogEventChecker();
-        sEventChecker.start();
-        sCheckingEvents = true;
+        final Integer initialPid = getPid();
+        final LogEventChecker eventChecker = new LogEventChecker(this);
+        if (eventChecker.start()) mEventChecker = eventChecker;
 
         return () -> {
-            if (initialPid != getPid()) {
+            if (initialPid != null && initialPid.intValue() != getPid()) {
                 if (mOnLauncherCrashed != null) mOnLauncherCrashed.run();
                 checkForAnomaly();
                 Assert.fail(
@@ -1352,10 +1355,10 @@
                                 formatErrorWithEvents("Launcher crashed", false)));
             }
 
-            if (sCheckingEvents) {
-                sCheckingEvents = false;
+            if (mEventChecker != null) {
+                mEventChecker = null;
                 if (mCheckEventsForSuccessfulGestures) {
-                    final String message = sEventChecker.verify(WAIT_TIME_MS, true);
+                    final String message = eventChecker.verify(WAIT_TIME_MS, true);
                     if (message != null) {
                         dumpDiagnostics();
                         checkForAnomaly();
@@ -1363,7 +1366,7 @@
                                 "http://go/tapl : successful gesture produced " + message));
                     }
                 } else {
-                    sEventChecker.finishNoWait();
+                    eventChecker.finishNoWait();
                 }
             }
         };
@@ -1374,7 +1377,11 @@
     }
 
     void expectEvent(String sequence, Pattern expected) {
-        if (sCheckingEvents) sEventChecker.expectPattern(sequence, expected);
+        if (mEventChecker != null) {
+            mEventChecker.expectPattern(sequence, expected);
+        } else {
+            Log.d(TAG, "Expecting: " + sequence + " / " + expected);
+        }
     }
 
     Rect getVisibleBounds(UiObject2 object) {
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
index 79d20ac..4440b82 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -15,169 +15,81 @@
  */
 package com.android.launcher3.tapl;
 
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-
-import android.util.Log;
+import android.os.SystemClock;
 
 import com.android.launcher3.testing.TestProtocol;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Semaphore;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
- * Utility class to read log on a background thread.
+ * Utility class to verify expected events.
  */
 public class LogEventChecker {
 
-    private static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
-            ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<sequence>[a-zA-Z]+) / (?<event>.*)");
-
-    private static final String START_PREFIX = "START_READER ";
-    private static final String FINISH_PREFIX = "FINISH_READER ";
-    private static final String SKIP_EVENTS_TAG = "b/153670015";
-
-    private volatile CountDownLatch mFinished;
+    private final LauncherInstrumentation mLauncher;
 
     // Map from an event sequence name to an ordered list of expected events in that sequence.
     private final ListMap<Pattern> mExpectedEvents = new ListMap<>();
 
-    private final ListMap<String> mEvents = new ListMap<>();
-    private final Semaphore mEventsCounter = new Semaphore(0);
-
-    private volatile String mStartCommand;
-    private volatile String mFinishCommand;
-
-    LogEventChecker() {
-        final Thread thread = new Thread(this::onRun, "log-reader-thread");
-        thread.setPriority(Thread.NORM_PRIORITY);
-        thread.start();
+    LogEventChecker(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
     }
 
-    void start() {
-        if (mFinished != null) {
-            try {
-                mFinished.await();
-            } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-            } finally {
-                mFinished = null;
-            }
-        }
-        mEvents.clear();
-        Log.d(SKIP_EVENTS_TAG, "Cleared events");
+    boolean start() {
         mExpectedEvents.clear();
-        mEventsCounter.drainPermits();
-        final String id = UUID.randomUUID().toString();
-        mStartCommand = START_PREFIX + id;
-        mFinishCommand = FINISH_PREFIX + id;
-        Log.d(SKIP_EVENTS_TAG, "Expected finish command: " + mFinishCommand);
-        Log.d(TestProtocol.TAPL_EVENTS_TAG, mStartCommand);
-    }
-
-    private void onRun() {
-        while (true) readEvents();
-    }
-
-    private void readEvents() {
-        try {
-            // Note that we use Runtime.exec to start the log reading process instead of running
-            // it via UIAutomation, so that we can directly access the "Process" object and
-            // ensure that the instrumentation is not stuck forever.
-            final String cmd = "logcat -s " + TestProtocol.TAPL_EVENTS_TAG;
-
-            final Process logcatProcess = Runtime.getRuntime().exec(cmd);
-            try (BufferedReader reader = new BufferedReader(new InputStreamReader(
-                    logcatProcess.getInputStream()))) {
-                while (true) {
-                    // Skip everything before the next start command.
-                    for (; ; ) {
-                        final String event = reader.readLine();
-                        if (event == null) {
-                            Log.d(SKIP_EVENTS_TAG, "Read a null line while waiting for start");
-                            return;
-                        }
-                        if (event.contains(mStartCommand)) {
-                            Log.d(SKIP_EVENTS_TAG, "Read start: " + event);
-                            break;
-                        }
-                    }
-
-                    // Store all actual events until the finish command.
-                    for (; ; ) {
-                        final String event = reader.readLine();
-                        if (event == null) {
-                            Log.d(SKIP_EVENTS_TAG, "Read a null line after waiting for start");
-                            mEventsCounter.drainPermits();
-                            mEvents.clear();
-                            return;
-                        }
-                        if (event.contains(mFinishCommand)) {
-                            mFinished.countDown();
-                            Log.d(SKIP_EVENTS_TAG, "Read finish: " + event);
-                            break;
-                        } else {
-                            final Matcher matcher = EVENT_LOG_ENTRY.matcher(event);
-                            if (matcher.find()) {
-                                mEvents.add(matcher.group("sequence"), matcher.group("event"));
-                                Log.d(SKIP_EVENTS_TAG, "Read event: " + event);
-                                mEventsCounter.release();
-                            } else {
-                                Log.d(SKIP_EVENTS_TAG, "Read something unexpected: " + event);
-                            }
-                        }
-                    }
-                }
-            } finally {
-                logcatProcess.destroyForcibly();
-            }
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
+        return mLauncher.getTestInfo(TestProtocol.REQUEST_START_EVENT_LOGGING) != null;
     }
 
     void expectPattern(String sequence, Pattern pattern) {
         mExpectedEvents.add(sequence, pattern);
     }
 
-    private void finishSync(long waitForExpectedCountMs) {
-        try {
-            // Wait until Launcher generates the expected number of events.
-            int expectedCount = mExpectedEvents.entrySet()
+    // Waits for the expected number of events and returns them.
+    private ListMap<String> finishSync(long waitForExpectedCountMs) {
+        final long startTime = SystemClock.uptimeMillis();
+        // Event strings with '/' separating the sequence and the event.
+        ArrayList<String> rawEvents;
+
+        while (true) {
+            rawEvents = mLauncher.getTestInfo(TestProtocol.REQUEST_GET_TEST_EVENTS)
+                    .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+            final int expectedCount = mExpectedEvents.entrySet()
                     .stream().mapToInt(e -> e.getValue().size()).sum();
-            mEventsCounter.tryAcquire(expectedCount, waitForExpectedCountMs, MILLISECONDS);
-            finishNoWait();
-            mFinished.await();
-            mFinished = null;
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
+            if (rawEvents.size() >= expectedCount
+                    || SystemClock.uptimeMillis() > startTime + waitForExpectedCountMs) {
+                break;
+            }
+            SystemClock.sleep(100);
         }
+
+        finishNoWait();
+
+        // Parse raw events into a map.
+        final ListMap<String> eventSequences = new ListMap<>();
+        for (String rawEvent : rawEvents) {
+            final String[] split = rawEvent.split("/");
+            eventSequences.add(split[0], split[1]);
+        }
+        return eventSequences;
     }
 
     void finishNoWait() {
-        mFinished = new CountDownLatch(1);
-        Log.d(TestProtocol.TAPL_EVENTS_TAG, mFinishCommand);
+        mLauncher.getTestInfo(TestProtocol.REQUEST_STOP_EVENT_LOGGING);
     }
 
     String verify(long waitForExpectedCountMs, boolean successfulGesture) {
-        finishSync(waitForExpectedCountMs);
+        final ListMap<String> actualEvents = finishSync(waitForExpectedCountMs);
 
         final StringBuilder sb = new StringBuilder();
         boolean hasMismatches = false;
         for (Map.Entry<String, List<Pattern>> expectedEvents : mExpectedEvents.entrySet()) {
             String sequence = expectedEvents.getKey();
 
-            List<String> actual = new ArrayList<>(mEvents.getNonNull(sequence));
-            Log.d(SKIP_EVENTS_TAG, "Verifying events");
+            List<String> actual = new ArrayList<>(actualEvents.getNonNull(sequence));
             final int mismatchPosition = getMismatchPosition(expectedEvents.getValue(), actual);
             hasMismatches = hasMismatches
                     || mismatchPosition != -1 && !ignoreMistatch(successfulGesture, sequence);
@@ -189,7 +101,7 @@
                     mismatchPosition);
         }
         // Check for unexpected event sequences in the actual data.
-        for (String actualNamedSequence : mEvents.keySet()) {
+        for (String actualNamedSequence : actualEvents.keySet()) {
             if (!mExpectedEvents.containsKey(actualNamedSequence)) {
                 hasMismatches = hasMismatches
                         || !ignoreMistatch(successfulGesture, actualNamedSequence);
@@ -197,7 +109,7 @@
                         sb,
                         actualNamedSequence,
                         new ArrayList<>(),
-                        mEvents.get(actualNamedSequence),
+                        actualEvents.get(actualNamedSequence),
                         0);
             }
         }