Merge "Makes all ArrowPopups AccessibilityTargets." into ub-launcher3-master
diff --git a/Android.bp b/Android.bp
index cb695df..e132854 100644
--- a/Android.bp
+++ b/Android.bp
@@ -47,3 +47,14 @@
     },
     static_libs: ["libprotobuf-java-lite"],
 }
+
+java_library {
+    name: "LauncherPluginLib",
+
+    static_libs: ["PluginCoreLib"],
+
+    srcs: ["src_plugins/**/*.java"],
+
+    sdk_version: "current",
+    min_sdk_version: "28",
+}
diff --git a/Android.mk b/Android.mk
index c066a12..9cfcf17 100644
--- a/Android.mk
+++ b/Android.mk
@@ -17,24 +17,6 @@
 LOCAL_PATH := $(call my-dir)
 
 #
-# Build rule for plugin lib (needed to write a plugin).
-#
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT2_ONLY := true
-LOCAL_MODULE_TAGS := optional
-LOCAL_STATIC_JAVA_LIBRARIES:= PluginCoreLib
-
-LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src_plugins)
-
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 28
-LOCAL_MODULE := LauncherPluginLib
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#
 # Build rule for Launcher3 dependencies lib.
 #
 include $(CLEAR_VARS)
diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java
index 7690b9d..3b3dc01 100644
--- a/go/src/com/android/launcher3/model/WidgetsModel.java
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.model;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.os.UserHandle;
 
@@ -68,4 +69,9 @@
     public void onPackageIconsUpdated(Set<String> packageNames, UserHandle user,
             LauncherAppState app) {
     }
+
+    public WidgetItem getWidgetProviderInfoByProviderName(
+            ComponentName providerName) {
+        return null;
+    }
 }
\ No newline at end of file
diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto
index ec1d55b..d08d851 100644
--- a/protos/launcher_log.proto
+++ b/protos/launcher_log.proto
@@ -151,6 +151,10 @@
   DISMISS_PREDICTION = 21;
   HYBRID_HOTSEAT_ACCEPTED = 22;
   HYBRID_HOTSEAT_CANCELED = 23;
+  OVERVIEW_ACTIONS_SHARE_BUTTON = 24;
+  OVERVIEW_ACTIONS_SCREENSHOT_BUTTON = 25;
+  OVERVIEW_ACTIONS_SELECT_BUTTON = 26;
+  SELECT_MODE_CLOSE_BUTTON = 27;
 }
 
 enum TipType {
diff --git a/protos/launcher_trace.proto b/protos/launcher_trace.proto
new file mode 100644
index 0000000..c6f3543
--- /dev/null
+++ b/protos/launcher_trace.proto
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package com.android.launcher3.tracing;
+
+option java_multiple_files = true;
+
+message LauncherTraceProto {
+
+    optional TouchInteractionServiceProto touch_interaction_service = 1;
+}
+
+message TouchInteractionServiceProto {
+
+    optional bool service_connected = 1;
+}
diff --git a/protos/launcher_trace_file.proto b/protos/launcher_trace_file.proto
new file mode 100644
index 0000000..6ce182a
--- /dev/null
+++ b/protos/launcher_trace_file.proto
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+import "launcher_trace.proto";
+
+package com.android.launcher3.tracing;
+
+option java_multiple_files = true;
+
+/* represents a file full of launcher trace entries.
+   Encoded, it should start with 0x9 0x4C 0x4E 0x43 0x48 0x52 0x54 0x52 0x43 (.LNCHRTRC), such
+   that they can be easily identified. */
+message LauncherTraceFileProto {
+
+    /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
+       (this is needed because enums have to be 32 bits and there's no nice way to put 64bit
+        constants into .proto files. */
+    enum MagicNumber {
+        INVALID = 0;
+        MAGIC_NUMBER_L = 0x48434E4C;  /* LNCH (little-endian ASCII) */
+        MAGIC_NUMBER_H = 0x43525452;  /* RTRC (little-endian ASCII) */
+    }
+
+    optional fixed64 magic_number = 1;  /* Must be the first field, set to value in MagicNumber */
+    repeated LauncherTraceEntryProto entry = 2;
+}
+
+/* one launcher trace entry. */
+message LauncherTraceEntryProto {
+    /* required: elapsed realtime in nanos since boot of when this entry was logged */
+    optional fixed64 elapsed_realtime_nanos = 1;
+
+    optional LauncherTraceProto launcher = 3;
+}
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 7ce4d74..a06a2dd 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -47,10 +47,7 @@
             </intent-filter>
         </service>
 
-        <!-- STOPSHIP: Change exported to false once all the integration is complete.
-        It is set to true so that the activity can be started from command line -->
         <activity android:name="com.android.quickstep.RecentsActivity"
-            android:exported="true"
             android:excludeFromRecents="true"
             android:launchMode="singleTask"
             android:clearTaskOnLaunch="true"
diff --git a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
index 7f1425b..a572cad 100644
--- a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
+++ b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
@@ -14,12 +14,17 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.quickstep.views.LauncherRecentsView
+<com.android.launcher3.InsettableFrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:theme="@style/HomeScreenElementTheme"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:accessibilityPaneTitle="@string/accessibility_recent_apps"
-    android:visibility="invisible" />
\ No newline at end of file
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+    <com.android.quickstep.views.LauncherRecentsView
+        android:id="@+id/overview_panel_recents"
+        android:theme="@style/HomeScreenElementTheme"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        android:accessibilityPaneTitle="@string/accessibility_recent_apps"
+        android:visibility="invisible" />
+</com.android.launcher3.InsettableFrameLayout>
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index a1c8378..7895bac 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -37,6 +37,7 @@
 
 import android.graphics.Rect;
 import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.AbstractFloatingView;
@@ -47,6 +48,7 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.SysUINavigationMode;
@@ -143,6 +145,10 @@
     public void onStateTransitionEnd(Launcher launcher) {
         launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
         DiscoveryBounce.showForOverviewIfNeeded(launcher);
+        RecentsView recentsView = launcher.getOverviewPanel();
+        AccessibilityManagerCompat.sendCustomAccessibilityEvent(
+                recentsView.getPageAt(recentsView.getCurrentPage()),
+                AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
     }
 
     @Override
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 7ff8969..3601af2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -428,6 +428,10 @@
         // rounding at the end of the animation.
         float startRadius = mAppWindowAnimationHelper.getCurrentCornerRadius();
         float endRadius = startRect.width() / 6f;
+
+        float startTransformProgress = mTransformParams.getProgress();
+        float endTransformProgress = 1;
+
         // We want the window alpha to be 0 once this threshold is met, so that the
         // FolderIconView can be seen morphing into the icon shape.
         final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f;
@@ -437,8 +441,10 @@
             public void onUpdate(RectF currentRect, float progress) {
                 homeAnim.setPlayFraction(progress);
 
-                mTransformParams.setProgress(progress)
-                        .setCurrentRectAndTargetAlpha(currentRect, getWindowAlpha(progress));
+                mTransformParams.setProgress(
+                        Utilities.mapRange(progress, startTransformProgress, endTransformProgress))
+                        .setCurrentRect(currentRect)
+                        .setTargetAlpha(getWindowAlpha(progress));
                 if (isFloatingIconView) {
                     mTransformParams.setCornerRadius(endRadius * progress + startRadius
                             * (1f - progress));
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
index bfe5738..8d73591 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
@@ -163,7 +163,7 @@
                 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
                     List<SurfaceParams> surfaceParamsList = new ArrayList<>();
                     // Append the surface transform params for the app that's being opened.
-                    Collections.addAll(surfaceParamsList, inOutHelper.getSurfaceParams(params));
+                    Collections.addAll(surfaceParamsList, inOutHelper.computeSurfaceParams(params));
 
                     AppWindowAnimationHelper liveTileAnimationHelper =
                             v.getRecentsView().getClipAnimationHelper();
@@ -173,14 +173,14 @@
                                 v.getRecentsView().getLiveTileParams(true /* mightNeedToRefill */);
                         if (liveTileParams != null) {
                             SurfaceParams[] liveTileSurfaceParams =
-                                    liveTileAnimationHelper.getSurfaceParams(liveTileParams);
+                                    liveTileAnimationHelper.computeSurfaceParams(liveTileParams);
                             if (liveTileSurfaceParams != null) {
                                 Collections.addAll(surfaceParamsList, liveTileSurfaceParams);
                             }
                         }
                     }
                     // Apply surface transform using the surface params list.
-                    AppWindowAnimationHelper.applySurfaceParams(params.syncTransactionApplier,
+                    AppWindowAnimationHelper.applySurfaceParams(params.getSyncTransactionApplier(),
                             surfaceParamsList.toArray(new SurfaceParams[surfaceParamsList.size()]));
                     // Get the task bounds for the app that's being opened after surface transform
                     // update.
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 a597458..28e8fb6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -28,6 +28,7 @@
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT;
 
 import android.annotation.TargetApi;
@@ -63,6 +64,8 @@
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.tracing.nano.LauncherTraceProto;
+import com.android.launcher3.tracing.nano.TouchInteractionServiceProto;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
@@ -76,6 +79,7 @@
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AssistantUtilities;
+import com.android.quickstep.util.ProtoTracer;
 import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.IOverviewProxy;
@@ -85,6 +89,7 @@
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.RecentsAnimationListener;
+import com.android.systemui.shared.tracing.ProtoTraceable;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -113,7 +118,8 @@
  * Service connected by system-UI for handling touch interaction.
  */
 @TargetApi(Build.VERSION_CODES.Q)
-public class TouchInteractionService extends Service implements PluginListener<OverscrollPlugin> {
+public class TouchInteractionService extends Service implements PluginListener<OverscrollPlugin>,
+        ProtoTraceable<LauncherTraceProto> {
 
     private static final String TAG = "TouchInteractionService";
 
@@ -275,6 +281,7 @@
         mDeviceState = new RecentsAnimationDeviceState(this);
         mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
         mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
+        ProtoTracer.INSTANCE.get(this).add(this);
 
         sConnected = true;
     }
@@ -381,6 +388,13 @@
             SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(
                     mDeviceState.getSystemUiStateFlags());
             mOverviewComponentObserver.onSystemUiStateChanged();
+
+            // Update the tracing state
+            if ((mDeviceState.getSystemUiStateFlags() & SYSUI_STATE_TRACING_ENABLED) != 0) {
+                ProtoTracer.INSTANCE.get(TouchInteractionService.this).start();
+            } else {
+                ProtoTracer.INSTANCE.get(TouchInteractionService.this).stop();
+            }
         }
     }
 
@@ -403,6 +417,8 @@
         disposeEventHandlers();
         mDeviceState.destroy();
         SystemUiProxy.INSTANCE.get(this).setProxy(null);
+        ProtoTracer.INSTANCE.get(TouchInteractionService.this).stop();
+        ProtoTracer.INSTANCE.get(this).remove(this);
 
         sConnected = false;
         super.onDestroy();
@@ -723,6 +739,9 @@
             pw.println("  resumed=" + resumed);
             pw.println("  mConsumer=" + mConsumer.getName());
             ActiveGestureLog.INSTANCE.dump("", pw);
+            pw.println("ProtoTrace:");
+            pw.println("  file="
+                    + ProtoTracer.INSTANCE.get(TouchInteractionService.this).getTraceFile());
         }
     }
 
@@ -781,4 +800,13 @@
     public void onPluginDisconnected(OverscrollPlugin overscrollPlugin) {
         mOverscrollPlugin = null;
     }
+
+    @Override
+    public void writeToProto(LauncherTraceProto proto) {
+        if (proto.touchInteractionService == null) {
+            proto.touchInteractionService = new TouchInteractionServiceProto();
+        }
+        proto.touchInteractionService.serviceConnected = true;
+        proto.touchInteractionService.serviceConnected = true;
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
index 9e29238..5a9c2fe 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
@@ -60,23 +60,23 @@
     private final Rect mSourceStackBounds = new Rect();
     // The insets of the source app
     private final Rect mSourceInsets = new Rect();
-    // The source app bounds with the source insets applied, in the source app window coordinates
+    // The source app bounds with the source insets applied, in the device coordinates
     private final RectF mSourceRect = new RectF();
-    // The bounds of the task view in launcher window coordinates
+    // The bounds of the task view in device coordinates
     private final RectF mTargetRect = new RectF();
+    // The bounds of the app window (between mSourceRect and mTargetRect) in device coordinates
+    private final RectF mCurrentRect = new RectF();
     // The insets to be used for clipping the app window, which can be larger than mSourceInsets
     // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
     // app window coordinates.
     private final RectF mSourceWindowClipInsets = new RectF();
-    // The insets to be used for clipping the app window. For live tile, we don't transform the clip
-    // relative to the target rect.
-    private final RectF mSourceWindowClipInsetsForLiveTile = new RectF();
+    // The clip rect in source app window coordinates. The app window surface will only be drawn
+    // within these bounds. This clip rect starts at the full mSourceStackBounds, and insets by
+    // mSourceWindowClipInsets as the transform progress goes to 1.
+    private final RectF mCurrentClipRectF = new RectF();
 
     // The bounds of launcher (not including insets) in device coordinates
     public final Rect mHomeStackBounds = new Rect();
-
-    // The clip rect in source app window coordinates
-    private final RectF mClipRectF = new RectF();
     private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
     private final Matrix mTmpMatrix = new Matrix();
     private final Rect mTmpRect = new Rect();
@@ -146,7 +146,6 @@
                 Math.max(scaledTargetRect.top, 0),
                 Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
                 Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
-        mSourceWindowClipInsetsForLiveTile.set(mSourceWindowClipInsets);
         mSourceRect.set(scaledTargetRect);
     }
 
@@ -156,25 +155,29 @@
     }
 
     public RectF applyTransform(TransformParams params) {
-        SurfaceParams[] surfaceParams = getSurfaceParams(params);
+        SurfaceParams[] surfaceParams = computeSurfaceParams(params);
         if (surfaceParams == null) {
             return null;
         }
-        applySurfaceParams(params.syncTransactionApplier, surfaceParams);
-        return params.currentRect;
+        applySurfaceParams(params.mSyncTransactionApplier, surfaceParams);
+        return mCurrentRect;
     }
 
-    public SurfaceParams[] getSurfaceParams(TransformParams params) {
-        if (params.targetSet == null) {
+    /**
+     * Updates this AppWindowAnimationHelper's state based on the given TransformParams, and returns
+     * the SurfaceParams to apply via {@link SyncRtSurfaceTransactionApplierCompat#applyParams}.
+     */
+    public SurfaceParams[] computeSurfaceParams(TransformParams params) {
+        if (params.mTargetSet == null) {
             return null;
         }
 
-        float progress = Utilities.boundToRange(params.progress, 0, 1);
+        float progress = Utilities.boundToRange(params.mProgress, 0, 1);
         updateCurrentRect(params);
 
-        SurfaceParams[] surfaceParams = new SurfaceParams[params.targetSet.unfilteredApps.length];
-        for (int i = 0; i < params.targetSet.unfilteredApps.length; i++) {
-            RemoteAnimationTargetCompat app = params.targetSet.unfilteredApps[i];
+        SurfaceParams[] surfaceParams = new SurfaceParams[params.mTargetSet.unfilteredApps.length];
+        for (int i = 0; i < params.mTargetSet.unfilteredApps.length; i++) {
+            RemoteAnimationTargetCompat app = params.mTargetSet.unfilteredApps[i];
             mTmpMatrix.setTranslate(app.position.x, app.position.y);
             Rect crop = mTmpRect;
             crop.set(app.sourceContainerBounds);
@@ -182,17 +185,17 @@
             float alpha;
             int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
             float cornerRadius = 0f;
-            float scale = Math.max(params.currentRect.width(), mTargetRect.width()) / crop.width();
-            if (app.mode == params.targetSet.targetMode) {
-                alpha = mTaskAlphaCallback.getAlpha(app, params.targetAlpha);
+            float scale = Math.max(mCurrentRect.width(), mTargetRect.width()) / crop.width();
+            if (app.mode == params.mTargetSet.targetMode) {
+                alpha = mTaskAlphaCallback.getAlpha(app, params.mTargetAlpha);
                 if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
-                    mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
+                    mTmpMatrix.setRectToRect(mSourceRect, mCurrentRect, ScaleToFit.FILL);
                     mTmpMatrix.postTranslate(app.position.x, app.position.y);
-                    mClipRectF.roundOut(crop);
+                    mCurrentClipRectF.roundOut(crop);
                     if (mSupportsRoundedCornersOnWindows) {
-                        if (params.cornerRadius > -1) {
-                            cornerRadius = params.cornerRadius;
-                            scale = params.currentRect.width() / crop.width();
+                        if (params.mCornerRadius > -1) {
+                            cornerRadius = params.mCornerRadius;
+                            scale = mCurrentRect.width() / crop.width();
                         } else {
                             float windowCornerRadius = mUseRoundedCornersOnWindows
                                     ? mWindowCornerRadius : 0;
@@ -206,14 +209,14 @@
                             && app.isNotInRecents) {
                         alpha = 1 - Interpolators.DEACCEL_2_5.getInterpolation(progress);
                     }
-                } else if (params.targetSet.hasRecents) {
+                } else if (params.mTargetSet.hasRecents) {
                     // If home has a different target then recents, reverse anim the
                     // home target.
-                    alpha = 1 - (progress * params.targetAlpha);
+                    alpha = 1 - (progress * params.mTargetAlpha);
                 }
             } else {
                 alpha = mBaseAlphaCallback.getAlpha(app, progress);
-                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.launcherOnTop) {
+                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.mLauncherOnTop) {
                     crop = null;
                     layer = Integer.MAX_VALUE;
                 }
@@ -228,31 +231,33 @@
     }
 
     public RectF updateCurrentRect(TransformParams params) {
-        float progress = params.progress;
-        if (params.currentRect == null) {
-            RectF currentRect;
+        if (params.mCurrentRect != null) {
+            mCurrentRect.set(params.mCurrentRect);
+        } else {
             mTmpRectF.set(mTargetRect);
-            Utilities.scaleRectFAboutCenter(mTmpRectF, params.offsetScale);
-            currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
-            currentRect.offset(params.offsetX, 0);
-
-            // Don't clip past progress > 1.
-            progress = Math.min(1, progress);
-            final RectF sourceWindowClipInsets = params.forLiveTile
-                    ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets;
-            mClipRectF.left = sourceWindowClipInsets.left * progress;
-            mClipRectF.top = sourceWindowClipInsets.top * progress;
-            mClipRectF.right =
-                    mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress);
-            mClipRectF.bottom =
-                    mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress);
-            params.setCurrentRectAndTargetAlpha(currentRect, 1);
+            Utilities.scaleRectFAboutCenter(mTmpRectF, params.mOffsetScale);
+            mCurrentRect.set(mRectFEvaluator.evaluate(params.mProgress, mSourceRect, mTmpRectF));
+            mCurrentRect.offset(params.mOffsetX, 0);
         }
-        return params.currentRect;
+
+        updateClipRect(params);
+
+        return mCurrentRect;
+    }
+
+    private void updateClipRect(TransformParams params) {
+        // Don't clip past progress > 1.
+        float progress = Math.min(1, params.mProgress);
+        mCurrentClipRectF.left = mSourceWindowClipInsets.left * progress;
+        mCurrentClipRectF.top = mSourceWindowClipInsets.top * progress;
+        mCurrentClipRectF.right =
+                mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress);
+        mCurrentClipRectF.bottom =
+                mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress);
     }
 
     public RectF getCurrentRectWithInsets() {
-        mTmpMatrix.mapRect(mCurrentRectWithInsets, mClipRectF);
+        mTmpMatrix.mapRect(mCurrentRectWithInsets, mCurrentClipRectF);
         return mCurrentRectWithInsets;
     }
 
@@ -384,75 +389,152 @@
     }
 
     public static class TransformParams {
-        float progress;
-        public float offsetX;
-        public float offsetScale;
-        public @Nullable RectF currentRect;
-        float targetAlpha;
-        boolean forLiveTile;
-        float cornerRadius;
-        boolean launcherOnTop;
-
-        public RemoteAnimationTargets targetSet;
-        public SyncRtSurfaceTransactionApplierCompat syncTransactionApplier;
+        private float mProgress;
+        private float mOffsetX;
+        private float mOffsetScale;
+        private @Nullable RectF mCurrentRect;
+        private float mTargetAlpha;
+        private float mCornerRadius;
+        private boolean mLauncherOnTop;
+        private RemoteAnimationTargets mTargetSet;
+        private SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier;
 
         public TransformParams() {
-            progress = 0;
-            offsetX = 0;
-            offsetScale = 1;
-            currentRect = null;
-            targetAlpha = 0;
-            forLiveTile = false;
-            cornerRadius = -1;
-            launcherOnTop = false;
+            mProgress = 0;
+            mOffsetX = 0;
+            mOffsetScale = 1;
+            mCurrentRect = null;
+            mTargetAlpha = 1;
+            mCornerRadius = -1;
+            mLauncherOnTop = false;
         }
 
+        /**
+         * Sets the progress of the transformation, where 0 is the source and 1 is the target. We
+         * automatically adjust properties such as currentRect and cornerRadius based on this
+         * progress, unless they are manually overridden by setting them on this TransformParams.
+         */
         public TransformParams setProgress(float progress) {
-            this.progress = progress;
-            this.currentRect = null;
+            mProgress = progress;
             return this;
         }
 
+        /**
+         * Sets the corner radius of the transformed window, in pixels. If unspecified (-1), we
+         * simply interpolate between the window's corner radius to the task view's corner radius,
+         * based on {@link #mProgress}.
+         */
         public TransformParams setCornerRadius(float cornerRadius) {
-            this.cornerRadius = cornerRadius;
+            mCornerRadius = cornerRadius;
             return this;
         }
 
-        public TransformParams setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha) {
-            this.currentRect = currentRect;
-            this.targetAlpha = targetAlpha;
+        /**
+         * Sets the current rect to show the transformed window, in device coordinates. This gives
+         * the caller manual control of where to show the window. If unspecified (null), we
+         * interpolate between {@link AppWindowAnimationHelper#mSourceRect} and
+         * {@link AppWindowAnimationHelper#mTargetRect}, based on {@link #mProgress}.
+         */
+        public TransformParams setCurrentRect(RectF currentRect) {
+            mCurrentRect = currentRect;
             return this;
         }
 
+        /**
+         * Specifies the alpha of the transformed window. Default is 1.
+         */
+        public TransformParams setTargetAlpha(float targetAlpha) {
+            mTargetAlpha = targetAlpha;
+            return this;
+        }
+
+        /**
+         * If {@link #mCurrentRect} is null (i.e. {@link #setCurrentRect(RectF)} hasn't overridden
+         * the default), then offset the current rect by this amount after computing the rect based
+         * on {@link #mProgress}.
+         */
         public TransformParams setOffsetX(float offsetX) {
-            this.offsetX = offsetX;
+            mOffsetX = offsetX;
             return this;
         }
 
+        /**
+         * If {@link #mCurrentRect} is null (i.e. {@link #setCurrentRect(RectF)} hasn't overridden
+         * the default), then scale the current rect by this amount after computing the rect based
+         * on {@link #mProgress}.
+         */
         public TransformParams setOffsetScale(float offsetScale) {
-            this.offsetScale = offsetScale;
+            mOffsetScale = offsetScale;
             return this;
         }
 
-        public TransformParams setForLiveTile(boolean forLiveTile) {
-            this.forLiveTile = forLiveTile;
-            return this;
-        }
-
+        /**
+         * If true, sets the crop = null and layer = Integer.MAX_VALUE for targets that don't match
+         * {@link #mTargetSet}.targetMode. (Currently only does this when live tiles are enabled.)
+         */
         public TransformParams setLauncherOnTop(boolean launcherOnTop) {
-            this.launcherOnTop = launcherOnTop;
+            mLauncherOnTop = launcherOnTop;
             return this;
         }
 
+        /**
+         * Specifies the set of RemoteAnimationTargetCompats that are included in the transformation
+         * that these TransformParams help compute. These TransformParams generally only apply to
+         * the targetSet.apps which match the targetSet.targetMode (e.g. the MODE_CLOSING app when
+         * swiping to home).
+         */
         public TransformParams setTargetSet(RemoteAnimationTargets targetSet) {
-            this.targetSet = targetSet;
+            mTargetSet = targetSet;
             return this;
         }
 
+        /**
+         * Sets the SyncRtSurfaceTransactionApplierCompat that will apply the SurfaceParams that
+         * are computed based on these TransformParams.
+         */
         public TransformParams setSyncTransactionApplier(
                 SyncRtSurfaceTransactionApplierCompat applier) {
-            this.syncTransactionApplier = applier;
+            mSyncTransactionApplier = applier;
             return this;
         }
+
+        // Pubic getters so outside packages can read the values.
+
+        public float getProgress() {
+            return mProgress;
+        }
+
+        public float getOffsetX() {
+            return mOffsetX;
+        }
+
+        public float getOffsetScale() {
+            return mOffsetScale;
+        }
+
+        @Nullable
+        public RectF getCurrentRect() {
+            return mCurrentRect;
+        }
+
+        public float getTargetAlpha() {
+            return mTargetAlpha;
+        }
+
+        public float getCornerRadius() {
+            return mCornerRadius;
+        }
+
+        public boolean isLauncherOnTop() {
+            return mLauncherOnTop;
+        }
+
+        public RemoteAnimationTargets getTargetSet() {
+            return mTargetSet;
+        }
+
+        public SyncRtSurfaceTransactionApplierCompat getSyncTransactionApplier() {
+            return mSyncTransactionApplier;
+        }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ProtoTracer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ProtoTracer.java
new file mode 100644
index 0000000..190763a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ProtoTracer.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static com.android.launcher3.tracing.nano.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_H;
+import static com.android.launcher3.tracing.nano.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_L;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import com.android.launcher3.tracing.nano.LauncherTraceProto;
+import com.android.launcher3.tracing.nano.LauncherTraceEntryProto;
+import com.android.launcher3.tracing.nano.LauncherTraceFileProto;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.systemui.shared.tracing.FrameProtoTracer;
+import com.android.systemui.shared.tracing.FrameProtoTracer.ProtoTraceParams;
+import com.android.systemui.shared.tracing.ProtoTraceable;
+import com.google.protobuf.nano.MessageNano;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Queue;
+
+
+/**
+ * Controller for coordinating winscope proto tracing.
+ */
+public class ProtoTracer implements ProtoTraceParams<MessageNano,
+        LauncherTraceFileProto, LauncherTraceEntryProto, LauncherTraceProto> {
+
+    public static final MainThreadInitializedObject<ProtoTracer> INSTANCE =
+            new MainThreadInitializedObject<>(ProtoTracer::new);
+
+    private static final String TAG = "ProtoTracer";
+    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+
+    private final Context mContext;
+    private final FrameProtoTracer<MessageNano,
+            LauncherTraceFileProto, LauncherTraceEntryProto, LauncherTraceProto> mProtoTracer;
+
+    public ProtoTracer(Context context) {
+        mContext = context;
+        mProtoTracer = new FrameProtoTracer<>(this);
+    }
+
+    @Override
+    public File getTraceFile() {
+        return new File(mContext.getFilesDir(), "launcher_trace.pb");
+    }
+
+    @Override
+    public LauncherTraceFileProto getEncapsulatingTraceProto() {
+        return new LauncherTraceFileProto();
+    }
+
+    @Override
+    public LauncherTraceEntryProto updateBufferProto(LauncherTraceEntryProto reuseObj,
+            ArrayList<ProtoTraceable<LauncherTraceProto>> traceables) {
+        LauncherTraceEntryProto proto = reuseObj != null
+                ? reuseObj
+                : new LauncherTraceEntryProto();
+        proto.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
+        proto.launcher = proto.launcher != null ? proto.launcher : new LauncherTraceProto();
+        for (ProtoTraceable t : traceables) {
+            t.writeToProto(proto.launcher);
+        }
+        return proto;
+    }
+
+    @Override
+    public byte[] serializeEncapsulatingProto(LauncherTraceFileProto encapsulatingProto,
+            Queue<LauncherTraceEntryProto> buffer) {
+        encapsulatingProto.magicNumber = MAGIC_NUMBER_VALUE;
+        encapsulatingProto.entry = buffer.toArray(new LauncherTraceEntryProto[0]);
+        return MessageNano.toByteArray(encapsulatingProto);
+    }
+
+    @Override
+    public byte[] getProtoBytes(MessageNano proto) {
+        return MessageNano.toByteArray(proto);
+    }
+
+    @Override
+    public int getProtoSize(MessageNano proto) {
+        return proto.getCachedSize();
+    }
+
+    public void start() {
+        mProtoTracer.start();
+    }
+
+    public void stop() {
+        mProtoTracer.stop();
+    }
+
+    public void add(ProtoTraceable<LauncherTraceProto> traceable) {
+        mProtoTracer.add(traceable);
+    }
+
+    public void remove(ProtoTraceable<LauncherTraceProto> traceable) {
+        mProtoTracer.remove(traceable);
+    }
+
+    public void scheduleFrameUpdate() {
+        mProtoTracer.scheduleFrameUpdate();
+    }
+
+    public void update() {
+        mProtoTracer.update();
+    }
+}
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 1bbb3f5..3e106aa 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
@@ -200,8 +200,8 @@
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (tv.isRunningTask()) {
                 mTransformParams.setProgress(1 - progress)
-                        .setSyncTransactionApplier(mSyncTransactionApplier)
-                        .setForLiveTile(true);
+                        .setCurrentRect(null)
+                        .setSyncTransactionApplier(mSyncTransactionApplier);
                 mAppWindowAnimationHelper.applyTransform(mTransformParams);
             } else {
                 redrawLiveTile(true);
@@ -267,7 +267,8 @@
             }
             mTempRectF.set(mTempRect);
             mTransformParams.setProgress(1f)
-                    .setCurrentRectAndTargetAlpha(mTempRectF, taskView.getAlpha())
+                    .setCurrentRect(mTempRectF)
+                    .setTargetAlpha(taskView.getAlpha())
                     .setSyncTransactionApplier(mSyncTransactionApplier)
                     .setTargetSet(mRecentsAnimationTargets)
                     .setLauncherOnTop(true);
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 ef1698e..fe78a84 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
@@ -92,6 +92,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.anim.SpringObjectAnimator;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -972,6 +973,10 @@
         TaskView runningTask = getRunningTaskView();
         if (runningTask != null) {
             runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
+            if (!isHidden) {
+                AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask,
+                        AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 15503b8..07d2381 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -194,7 +194,7 @@
 
         if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(this)) {
             // Overview is above all other launcher elements, including qsb, so move it to the top.
-            getOverviewPanel().bringToFront();
+            getOverviewPanelContainer().bringToFront();
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index d8b10b6..7d52571 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep.util;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_LSQ_VELOCITY_PROVIDER;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.view.MotionEvent;
@@ -85,7 +87,8 @@
         mForcePauseTimeout = new Alarm();
         mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
         mMakePauseHarderToTrigger = makePauseHarderToTrigger;
-        mVelocityProvider = new LinearVelocityProvider(axis);
+        mVelocityProvider = ENABLE_LSQ_VELOCITY_PROVIDER.get()
+                ? new LSqVelocityProvider(axis) : new LinearVelocityProvider(axis);
     }
 
     /**
@@ -106,8 +109,6 @@
     /**
      * Computes velocity and acceleration to determine whether the motion is paused.
      * @param ev The motion being tracked.
-     *
-     * TODO: Use historical positions as well, e.g. {@link MotionEvent#getHistoricalY(int, int)}.
      */
     public void addPosition(MotionEvent ev) {
         addPosition(ev, 0);
@@ -248,4 +249,137 @@
             mPreviousPosition = null;
         }
     }
+
+    /**
+     * Java implementation of {@link android.view.VelocityTracker} using the Least Square (deg 2)
+     * algorithm.
+     */
+    private static class LSqVelocityProvider implements VelocityProvider {
+
+        // Maximum age of a motion event to be considered when calculating the velocity.
+        private static final long HORIZON_MS = 100;
+        // Number of samples to keep.
+        private static final int HISTORY_SIZE = 20;
+
+        // Position history are stored in a circular array
+        private final float[] mHistoricTimes = new float[HISTORY_SIZE];
+        private final float[] mHistoricPos = new float[HISTORY_SIZE];
+        private int mHistoryCount = 0;
+        private int mHistoryStart = 0;
+
+        private final int mAxis;
+
+        LSqVelocityProvider(int axis) {
+            mAxis = axis;
+        }
+
+        @Override
+        public void clear() {
+            mHistoryCount = mHistoryStart = 0;
+        }
+
+        private void addPositionAndTime(float eventTime, float eventPosition) {
+            mHistoricTimes[mHistoryStart] = eventTime;
+            mHistoricPos[mHistoryStart] = eventPosition;
+            mHistoryStart++;
+            if (mHistoryStart >= HISTORY_SIZE) {
+                mHistoryStart = 0;
+            }
+            mHistoryCount = Math.min(HISTORY_SIZE, mHistoryCount + 1);
+        }
+
+        @Override
+        public Float addMotionEvent(MotionEvent ev, int pointer) {
+            // Add all historic points
+            int historyCount = ev.getHistorySize();
+            for (int i = 0; i < historyCount; i++) {
+                addPositionAndTime(
+                        ev.getHistoricalEventTime(i), ev.getHistoricalAxisValue(mAxis, pointer, i));
+            }
+
+            // Start index for the last position (about to be added)
+            int eventStartIndex = mHistoryStart;
+            addPositionAndTime(ev.getEventTime(), ev.getAxisValue(mAxis, pointer));
+            return solveUnweightedLeastSquaresDeg2(eventStartIndex);
+        }
+
+        /**
+         * Solves the instantaneous velocity.
+         * Based on solveUnweightedLeastSquaresDeg2 in VelocityTracker.cpp
+         */
+        private Float solveUnweightedLeastSquaresDeg2(final int pointPos) {
+            final float eventTime = mHistoricTimes[pointPos];
+
+            float sxi = 0, sxiyi = 0, syi = 0, sxi2 = 0, sxi3 = 0, sxi2yi = 0, sxi4 = 0;
+            int count = 0;
+            for (int i = 0; i < mHistoryCount; i++) {
+                int index = pointPos - i;
+                if (index < 0) {
+                    index += HISTORY_SIZE;
+                }
+
+                float time = mHistoricTimes[index];
+                float age = eventTime - time;
+                if (age > HORIZON_MS) {
+                    break;
+                }
+                count++;
+                float xi = -age;
+
+                float yi = mHistoricPos[index];
+                float xi2 = xi * xi;
+                float xi3 = xi2 * xi;
+                float xi4 = xi3 * xi;
+                float xiyi = xi * yi;
+                float xi2yi = xi2 * yi;
+
+                sxi += xi;
+                sxi2 += xi2;
+                sxiyi += xiyi;
+                sxi2yi += xi2yi;
+                syi += yi;
+                sxi3 += xi3;
+                sxi4 += xi4;
+            }
+
+            if (count < 3) {
+                // Too few samples
+                if (count == 2) {
+                    int endPos = pointPos - 1;
+                    if (endPos < 0) {
+                        endPos += HISTORY_SIZE;
+                    }
+                    float denominator = eventTime - mHistoricTimes[endPos];
+                    if (denominator != 0) {
+                        return (eventTime - mHistoricPos[endPos]) / denominator;
+
+                    }
+                }
+                return null;
+            }
+
+            float Sxx = sxi2 - sxi * sxi / count;
+            float Sxy = sxiyi - sxi * syi / count;
+            float Sxx2 = sxi3 - sxi * sxi2 / count;
+            float Sx2y = sxi2yi - sxi2 * syi / count;
+            float Sx2x2 = sxi4 - sxi2 * sxi2 / count;
+
+            float denominator = Sxx * Sx2x2 - Sxx2 * Sxx2;
+            if (denominator == 0) {
+                // division by 0 when computing velocity
+                return null;
+            }
+            // Compute a
+            // float numerator = Sx2y*Sxx - Sxy*Sxx2;
+
+            // Compute b
+            float numerator = Sxy * Sx2x2 - Sx2y * Sxx2;
+            float b = numerator / denominator;
+
+            // Compute c
+            // float c = syi/count - b * sxi/count - a * sxi2/count;
+
+            return b;
+        }
+    }
 }
diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml
index cca899b..6c66897 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -44,9 +44,8 @@
             layout="@layout/hotseat" />
 
         <include
-            android:id="@+id/overview_panel"
-            layout="@layout/overview_panel"
-            android:visibility="gone" />
+            android:id="@+id/overview_panel_container"
+            layout="@layout/overview_panel"/>
 
         <!-- Keep these behind the workspace so that they are not visible when
          we go into AllApps -->
diff --git a/res/layout/overview_panel.xml b/res/layout/overview_panel.xml
index bdd5d23..7fff711 100644
--- a/res/layout/overview_panel.xml
+++ b/res/layout/overview_panel.xml
@@ -14,7 +14,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<Space
+<FrameLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
+      android:id="@+id/overview_panel_recents"
       android:layout_width="0dp"
-      android:layout_height="0dp" />
\ No newline at end of file
+      android:layout_height="0dp"
+      android:visibility="gone" />
\ No newline at end of file
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 3fc8de2..33f5a95 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -80,6 +80,7 @@
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.OvershootInterpolator;
+import android.widget.FrameLayout;
 import android.widget.Toast;
 
 import androidx.annotation.Nullable;
@@ -273,6 +274,7 @@
 
     // UI and state for the overview panel
     private View mOverviewPanel;
+    private FrameLayout mOverviewPanelContainer;
 
     @Thunk
     boolean mWorkspaceLoading = true;
@@ -1143,7 +1145,8 @@
         mFocusHandler = mDragLayer.getFocusIndicatorHelper();
         mWorkspace = mDragLayer.findViewById(R.id.workspace);
         mWorkspace.initParentViews(mDragLayer);
-        mOverviewPanel = findViewById(R.id.overview_panel);
+        mOverviewPanel = findViewById(R.id.overview_panel_recents);
+        mOverviewPanelContainer = findViewById(R.id.overview_panel_container);
         mHotseat = findViewById(R.id.hotseat);
 
         mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@@ -1386,6 +1389,10 @@
         return (T) mOverviewPanel;
     }
 
+    public FrameLayout getOverviewPanelContainer() {
+        return mOverviewPanelContainer;
+    }
+
     public DropTargetBar getDropTargetBar() {
         return mDropTargetBar;
     }
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 4c6e1f3..01893e9 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -374,6 +374,8 @@
     protected void onPageEndTransition() {
         mWasInOverscroll = false;
         AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
+        AccessibilityManagerCompat.sendCustomAccessibilityEvent(getPageAt(mCurrentPage),
+                AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
     }
 
     protected int getUnboundedScrollX() {
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 28579c1..db4bef0 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -18,11 +18,14 @@
 
 import android.content.Context;
 import android.os.Bundle;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.Utilities;
 import com.android.launcher3.testing.TestProtocol;
 
@@ -37,11 +40,13 @@
         return isAccessibilityEnabled(context);
     }
 
-    public static void sendCustomAccessibilityEvent(View target, int type, String text) {
+    public static void sendCustomAccessibilityEvent(View target, int type, @Nullable String text) {
         if (isObservedEventType(target.getContext(), type)) {
             AccessibilityEvent event = AccessibilityEvent.obtain(type);
             target.onInitializeAccessibilityEvent(event);
-            event.getText().add(text);
+            if (!TextUtils.isEmpty(text)) {
+                event.getText().add(text);
+            }
             getManager(target.getContext()).sendAccessibilityEvent(event);
         }
     }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 6413044..3d8a9d7 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -135,6 +135,10 @@
             "ENABLE_UNIVERSAL_SMARTSPACE", false,
             "Replace Smartspace with a version rendered by System UI.");
 
+    public static final BooleanFlag ENABLE_LSQ_VELOCITY_PROVIDER = getDebugFlag(
+            "ENABLE_LSQ_VELOCITY_PROVIDER", false,
+            "Use Least Square algorithm for motion pause detection.");
+
     public static void initialize(Context context) {
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index c72509e..e33d89f 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -33,6 +33,7 @@
 
 import static java.util.Arrays.asList;
 import static java.util.Arrays.stream;
+import static java.util.Optional.ofNullable;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -95,6 +96,7 @@
 import com.android.launcher3.userevent.LauncherLogProto.LauncherEvent;
 import com.android.launcher3.userevent.LauncherLogProto.Target;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.ClipPathView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -322,12 +324,11 @@
         post(() -> {
             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
                 if (isEmpty(mFolderName.getText())) {
-                    FolderNameInfo[] nameInfos =
-                            (FolderNameInfo[]) mInfo.suggestedFolderNames.getParcelableArrayExtra(
-                                    FolderInfo.EXTRA_FOLDER_SUGGESTIONS);
-                    if (nameInfos != null) {
-                        showLabelSuggestion(nameInfos, false);
-                    }
+                    ofNullable(mInfo)
+                            .map(info -> info.suggestedFolderNames)
+                            .map(folderNames -> (FolderNameInfo[]) folderNames
+                                    .getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS))
+                            .ifPresent(nameInfos -> showLabelSuggestion(nameInfos, false));
                 }
             }
             mFolderName.setHint("");
@@ -345,7 +346,7 @@
         }
 
         mInfo.title = newTitle;
-        mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, mFolderName.isEnteredCompose(),
+        mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, getAcceptedSuggestionIndex() < 0,
                 mLauncher.getModelWriter());
         mFolderIcon.onTitleChanged(newTitle);
         mLauncher.getModelWriter().updateItemInDatabase(mInfo);
@@ -426,7 +427,7 @@
         mInfo = info;
         ArrayList<WorkspaceItemInfo> children = info.contents;
         Collections.sort(children, ITEM_POS_COMPARATOR);
-        updateItemLocationsInDatabaseBatch();
+        updateItemLocationsInDatabaseBatch(true);
 
         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
         if (lp == null) {
@@ -436,19 +437,15 @@
         }
         mItemsInvalidated = true;
         mInfo.addListener(this);
+        mPreviousLabel = mInfo.title.toString();
+        mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
 
         if (!isEmpty(mInfo.title)) {
             mFolderName.setText(mInfo.title);
-            mPreviousLabel = mInfo.title.toString();
-            mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
             mFolderName.setHint(null);
         } else {
             mFolderName.setText("");
-            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-                mFolderName.setHint("");
-            } else {
-                mFolderName.setHint(R.string.folder_hint_text);
-            }
+            mFolderName.setHint(R.string.folder_hint_text);
         }
         // In case any children didn't come across during loading, clean up the folder accordingly
         mFolderIcon.post(() -> {
@@ -464,8 +461,6 @@
      */
     public void showSuggestedTitle(FolderNameInfo[] nameInfos) {
         if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-            mInfo.suggestedFolderNames = new Intent().putExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS,
-                    nameInfos);
             if (isEmpty(mFolderName.getText().toString())
                     && !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME)) {
                 showLabelSuggestion(nameInfos, true);
@@ -985,7 +980,7 @@
 
         // Reordering may have occured, and we need to save the new item locations. We do this once
         // at the end to prevent unnecessary database operations.
-        updateItemLocationsInDatabaseBatch();
+        updateItemLocationsInDatabaseBatch(false);
         // Use the item count to check for multi-page as the folder UI may not have
         // been refreshed yet.
         if (getItemCount() <= mContent.itemsPerPage()) {
@@ -995,7 +990,7 @@
         }
     }
 
-    private void updateItemLocationsInDatabaseBatch() {
+    private void updateItemLocationsInDatabaseBatch(boolean isBind) {
         FolderGridOrganizer verifier = new FolderGridOrganizer(
                 mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
 
@@ -1011,6 +1006,18 @@
         if (!items.isEmpty()) {
             mLauncher.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0);
         }
+        if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && !isBind) {
+            Executors.MODEL_EXECUTOR.post(() -> {
+                FolderNameInfo[] nameInfos =
+                        new FolderNameInfo[FolderNameProvider.SUGGEST_MAX];
+                FolderNameProvider fnp = FolderNameProvider.newInstance(getContext());
+                fnp.getSuggestedFolderName(
+                        getContext(), mInfo.contents, nameInfos);
+                mInfo.suggestedFolderNames = new Intent().putExtra(
+                        FolderInfo.EXTRA_FOLDER_SUGGESTIONS,
+                        nameInfos);
+            });
+        }
     }
 
     public void notifyDrop() {
@@ -1315,7 +1322,7 @@
             // We only need to update the locations if it doesn't get handled in
             // #onDropCompleted.
             if (d.dragSource != this) {
-                updateItemLocationsInDatabaseBatch();
+                updateItemLocationsInDatabaseBatch(false);
             }
         }
 
@@ -1356,7 +1363,7 @@
         verifier.updateRankAndPos(item, rank);
         mLauncher.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
                 item.cellY);
-        updateItemLocationsInDatabaseBatch();
+        updateItemLocationsInDatabaseBatch(false);
 
         if (mContent.areViewsBound()) {
             mContent.createAndAddViewForRank(item, rank);
@@ -1647,26 +1654,8 @@
                 checkNotNull(mFolderName.getText().toString(),
                         "Expected valid folder label, but found null");
 
-        Optional<String[]> suggestedLabels = Optional.ofNullable(
-                (FolderNameInfo[]) mInfo.suggestedFolderNames
-                        .getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS))
-                .map(folderNameInfoArray ->
-                        stream(folderNameInfoArray)
-                                .filter(Objects::nonNull)
-                                .map(FolderNameInfo::getLabel)
-                                .map(CharSequence::toString)
-                                .toArray(String[]::new));
-
-
-        int accepted_suggestion_index = suggestedLabels
-                .map(folderNameInfoArray ->
-                        IntStream.range(0, folderNameInfoArray.length)
-                                .filter(index -> newLabel.equalsIgnoreCase(
-                                        folderNameInfoArray[index]))
-                                .findFirst()
-                                .orElse(-1)
-                ).orElse(-1);
-
+        Optional<String[]> suggestedLabels = getSuggestedLabels();
+        int accepted_suggestion_index = getAcceptedSuggestionIndex();
         boolean hasValidPrimary = suggestedLabels
                 .map(labels -> labels.length > 0 && !isEmpty(labels[0]))
                 .orElse(false);
@@ -1695,6 +1684,39 @@
                         : Target.ToFolderLabelState.valueOf("TO_CUSTOM" + suggestionsSuffix);
     }
 
+    private Optional<String[]> getSuggestedLabels() {
+        return ofNullable(mInfo)
+            .map(info -> info.suggestedFolderNames)
+            .map(
+                folderNames ->
+                    (FolderNameInfo[])
+                        folderNames.getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS))
+            .map(
+                folderNameInfoArray ->
+                    stream(folderNameInfoArray)
+                        .filter(Objects::nonNull)
+                        .map(FolderNameInfo::getLabel)
+                        .filter(Objects::nonNull)
+                        .map(CharSequence::toString)
+                        .toArray(String[]::new));
+    }
+
+    private int getAcceptedSuggestionIndex() {
+        String newLabel =
+                checkNotNull(mFolderName.getText().toString(),
+                        "Expected valid folder label, but found null");
+
+        return getSuggestedLabels()
+                .map(suggestionsArray ->
+                        IntStream.range(0, suggestionsArray.length)
+                                .filter(index -> newLabel.equalsIgnoreCase(
+                                        suggestionsArray[index]))
+                                .findFirst()
+                                .orElse(-1)
+                ).orElse(-1);
+
+    }
+
 
     private Target.Builder newEditTextTargetBuilder() {
         return Target.newBuilder().setType(Target.Type.ITEM).setItemType(ItemType.EDITTEXT);
diff --git a/src/com/android/launcher3/folder/FolderNameEditText.java b/src/com/android/launcher3/folder/FolderNameEditText.java
index 7e11b18..edf2c70 100644
--- a/src/com/android/launcher3/folder/FolderNameEditText.java
+++ b/src/com/android/launcher3/folder/FolderNameEditText.java
@@ -102,13 +102,6 @@
         mEnteredCompose = value;
     }
 
-    protected boolean isEnteredCompose() {
-        if (DEBUG) {
-            Log.d(TAG, "isEnteredCompose " + mEnteredCompose);
-        }
-        return mEnteredCompose;
-    }
-
     private class FolderNameEditTextInputConnection extends InputConnectionWrapper {
 
         FolderNameEditTextInputConnection(InputConnection target, boolean mutable) {
diff --git a/src/com/android/launcher3/folder/FolderNameInfo.java b/src/com/android/launcher3/folder/FolderNameInfo.java
index ecbe46c..1287219 100644
--- a/src/com/android/launcher3/folder/FolderNameInfo.java
+++ b/src/com/android/launcher3/folder/FolderNameInfo.java
@@ -84,6 +84,6 @@
     @Override
     @NonNull
     public String toString() {
-        return mLabel.toString() + ":" + mScore;
+        return String.format("%s:%.2f", mLabel, mScore);
     }
 }
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index def76e8..a429af2 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -25,6 +25,7 @@
 
 import android.annotation.TargetApi;
 import android.app.Fragment;
+import android.appwidget.AppWidgetHostView;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.TypedArray;
@@ -55,6 +56,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -72,6 +74,8 @@
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.LoaderResults;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
 
@@ -248,6 +252,16 @@
             addInScreenFromBind(folderIcon, info);
         }
 
+        private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
+            WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
+                    info.providerName);
+            AppWidgetHostView view = new AppWidgetHostView(mContext);
+            view.setAppWidget(-1, widgetItem.widgetInfo);
+            view.updateAppWidget(null);
+            view.setTag(info);
+            addInScreenFromBind(view, info);
+        }
+
         private void dispatchVisibilityAggregated(View view, boolean isVisible) {
             // Similar to View.dispatchVisibilityAggregated implementation.
             final boolean thisVisible = view.getVisibility() == VISIBLE;
@@ -272,9 +286,9 @@
                         mContext).getModel();
                 final WorkspaceItemsInfoFetcher fetcher = new WorkspaceItemsInfoFetcher();
                 launcherModel.enqueueModelUpdateTask(fetcher);
-                ArrayList<ItemInfo> workspaceItems;
+                WorkspaceResult workspaceResult;
                 try {
-                    workspaceItems = fetcher.mTask.get(5, TimeUnit.SECONDS);
+                    workspaceResult = fetcher.mTask.get(5, TimeUnit.SECONDS);
                 } catch (InterruptedException | ExecutionException | TimeoutException e) {
                     Log.d(TAG, "Error fetching workspace items info", e);
                     return;
@@ -284,9 +298,14 @@
                 // items
                 ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
                 ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
+                ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
+                ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
 
-                filterCurrentWorkspaceItems(0 /* currentScreenId */, workspaceItems,
-                        currentWorkspaceItems, otherWorkspaceItems);
+                filterCurrentWorkspaceItems(0 /* currentScreenId */,
+                        workspaceResult.mWorkspaceItems, currentWorkspaceItems,
+                        otherWorkspaceItems);
+                filterCurrentWorkspaceItems(0 /* currentScreenId */, workspaceResult.mAppWidgets,
+                        currentAppWidgets, otherAppWidgets);
                 sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems);
 
                 for (ItemInfo itemInfo : currentWorkspaceItems) {
@@ -303,6 +322,17 @@
                             break;
                     }
                 }
+                for (ItemInfo itemInfo : currentAppWidgets) {
+                    switch (itemInfo.itemType) {
+                        case Favorites.ITEM_TYPE_APPWIDGET:
+                        case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
+                            inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
+                                    workspaceResult.mWidgetsModel);
+                            break;
+                        default:
+                            break;
+                    }
+                }
             } else {
                 // Add hotseat icons
                 for (int i = 0; i < mIdp.numHotseatIcons; i++) {
@@ -349,10 +379,10 @@
         }
     }
 
-    private static class WorkspaceItemsInfoFetcher implements Callable<ArrayList<ItemInfo>>,
+    private static class WorkspaceItemsInfoFetcher implements Callable<WorkspaceResult>,
             LauncherModel.ModelUpdateTask {
 
-        private final FutureTask<ArrayList<ItemInfo>> mTask = new FutureTask<>(this);
+        private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
 
         private LauncherAppState mApp;
         private LauncherModel mModel;
@@ -374,14 +404,16 @@
         }
 
         @Override
-        public ArrayList<ItemInfo> call() throws Exception {
+        public WorkspaceResult call() throws Exception {
             if (!mModel.isModelLoaded()) {
                 Log.d(TAG, "Workspace not loaded, loading now");
                 mModel.startLoaderForResults(
                         new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
-                return new ArrayList<>();
+                return null;
             }
-            return mBgDataModel.workspaceItems;
+
+            return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
+                    mBgDataModel.widgetsModel);
         }
     }
 
@@ -389,4 +421,17 @@
         view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
         view.layout(0, 0, width, height);
     }
+
+    private static class WorkspaceResult {
+        private final ArrayList<ItemInfo> mWorkspaceItems;
+        private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
+        private final WidgetsModel mWidgetsModel;
+
+        private WorkspaceResult(ArrayList<ItemInfo> workspaceItems,
+                ArrayList<LauncherAppWidgetInfo> appWidgets, WidgetsModel widgetsModel) {
+            mWorkspaceItems = workspaceItems;
+            mAppWidgets = appWidgets;
+            mWidgetsModel = widgetsModel;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
index 67f07b1..a3fdf8d 100644
--- a/src/com/android/launcher3/logging/FileLog.java
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -10,6 +10,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.IOUtils;
 
 import java.io.BufferedReader;
@@ -42,7 +43,7 @@
     private static Handler sHandler = null;
     private static File sLogsDirectory = null;
 
-    public static final int LOG_DAYS = 4;
+    public static final int LOG_DAYS = FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() ? 10 : 4;
 
     public static void setDir(File logsDir) {
         if (ENABLED) {
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index 199d13f..afa3f6d 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -147,7 +147,7 @@
             fillIntentInfo(event.srcTarget[0], intent, userHandle);
         }
         ItemInfo info = (ItemInfo) v.getTag();
-        if (Utilities.IS_DEBUG_DEVICE && FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
+        if (info != null && Utilities.IS_DEBUG_DEVICE && FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
             FileLog.d(TAG, "appLaunch: packageName:" + info.getTargetComponent().getPackageName()
                     + ",isWorkApp:" + (info.user != null && !Process.myUserHandle().equals(
                     userHandle)) + ",launchLocation:" + info.container);
diff --git a/src/com/android/launcher3/model/PackageItemInfo.java b/src/com/android/launcher3/model/PackageItemInfo.java
index 3ef48cd..2fc064c 100644
--- a/src/com/android/launcher3/model/PackageItemInfo.java
+++ b/src/com/android/launcher3/model/PackageItemInfo.java
@@ -19,6 +19,8 @@
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherSettings;
 
+import java.util.Objects;
+
 /**
  * Represents a {@link Package} in the widget tray section.
  */
@@ -48,4 +50,17 @@
     public PackageItemInfo clone() {
         return new PackageItemInfo(this);
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PackageItemInfo that = (PackageItemInfo) o;
+        return Objects.equals(packageName, that.packageName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(packageName);
+    }
 }
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index 282867a..ae32692 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -6,6 +6,7 @@
 import static com.android.launcher3.pm.ShortcutConfigActivityInfo.queryList;
 
 import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Process;
@@ -243,4 +244,16 @@
             }
         }
     }
+
+    public WidgetItem getWidgetProviderInfoByProviderName(
+            ComponentName providerName) {
+        ArrayList<WidgetItem> widgetsList = mWidgetsList.get(
+                new PackageItemInfo(providerName.getPackageName()));
+        for (WidgetItem item : widgetsList) {
+            if (item.componentName.equals(providerName)) {
+                return item;
+            }
+        }
+        return null;
+    }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index 5aa0090..6fe6739 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -73,11 +73,10 @@
         mDevice.pressHome();
         waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
-
         waitForLauncherCondition("Personal tab is missing",
-                launcher -> launcher.getAppsView().isPersonalTabVisible());
+                launcher -> launcher.getAppsView().isPersonalTabVisible(), 60000);
         waitForLauncherCondition("Work tab is missing",
-                launcher -> launcher.getAppsView().isWorkTabVisible());
+                launcher -> launcher.getAppsView().isWorkTabVisible(), 60000);
     }
 
     @Test
@@ -89,7 +88,7 @@
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
         waitForState("Launcher internal state didn't switch to All Apps", () -> ALL_APPS);
         getOnceNotNull("Apps view did not bind",
-                launcher -> launcher.getAppsView().getWorkFooterContainer());
+                launcher -> launcher.getAppsView().getWorkFooterContainer(), 60000);
 
         UserManager userManager = getFromLauncher(l -> l.getSystemService(UserManager.class));
         assertEquals(2, userManager.getUserProfiles().size());
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index c5fbd7c..6775521 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -69,22 +69,16 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.Deque;
-import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -97,16 +91,8 @@
     private static final String TAG = "Tapl";
     private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 20;
     private static final int GESTURE_STEP_MS = 16;
-    private static final SimpleDateFormat DATE_TIME_FORMAT =
-            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
     private static long START_TIME = System.currentTimeMillis();
 
-    static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
-            "(?<time>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] "
-                    + "[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9])"
-                    + ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<sequence>[a-zA-Z]+) / "
-                    + "(?<event>.*)");
-
     private static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
     private static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
     private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
@@ -159,7 +145,7 @@
 
     private static final String WORKSPACE_RES_ID = "workspace";
     private static final String APPS_RES_ID = "apps_view";
-    private static final String OVERVIEW_RES_ID = "overview_panel";
+    private static final String OVERVIEW_RES_ID = "overview_panel_recents";
     private static final String WIDGETS_RES_ID = "widgets_list_view";
     private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
     public static final int WAIT_TIME_MS = 10000;
@@ -176,11 +162,10 @@
 
     private Consumer<ContainerType> mOnSettledStateAction;
 
-    // Map from an event sequence name to an ordered list of expected events in that sequence.
-    // Not null when we are collecting expected events to compare with actual ones.
-    private Map<String, List<Pattern>> mExpectedEvents;
+    private static LogEventChecker sEventChecker;
+    // True if there is an gesture in progress that needs event verification.
+    private static boolean sCheckingEvents;
 
-    private Date mStartRecordingTime;
     private boolean mCheckEventsForSuccessfulGestures = false;
 
     private static Pattern getTouchEventPattern(String prefix, String action) {
@@ -434,10 +419,12 @@
         log("Hierarchy dump for: " + message);
         dumpViewHierarchy();
 
-        final String eventMismatch = getEventMismatchMessage(false);
-
-        if (eventMismatch != null) {
-            message = message + ", having produced " + eventMismatch;
+        if (sCheckingEvents) {
+            sCheckingEvents = false;
+            final String eventMismatch = sEventChecker.verify(0);
+            if (eventMismatch != null) {
+                message = message + ", having produced " + eventMismatch;
+            }
         }
 
         Assert.fail(formatSystemHealthMessage(message));
@@ -1245,204 +1232,34 @@
         return tasks;
     }
 
-    // Returns actual events retrieved from logcat. The return value's key set is the set of all
-    // sequence names that actually had at least one event, and the values are lists of events in
-    // the given sequence, in the order they were recorded.
-    private Map<String, List<String>> getEvents() {
-        final Map<String, List<String>> events = new HashMap<>();
-        try {
-            // Logcat may skip events after the specified time. Querying for events starting 1 min
-            // earlier.
-            final Date startTime = new Date(mStartRecordingTime.getTime() - 60000);
-            final String logcatCommand = "logcat -d -v year --pid=" + getPid() + " -t "
-                    + DATE_TIME_FORMAT.format(startTime).replaceAll(" ", "")
-                    + " -s " + TestProtocol.TAPL_EVENTS_TAG;
-            log("Events query command: " + logcatCommand);
-            final String logcatEvents = mDevice.executeShellCommand(logcatCommand);
-            final Matcher matcher = EVENT_LOG_ENTRY.matcher(logcatEvents);
-            while (matcher.find()) {
-                // Skip events before recording start time.
-                if (DATE_TIME_FORMAT.parse(matcher.group("time"))
-                        .compareTo(mStartRecordingTime) < 0) {
-                    continue;
-                }
-
-                eventsListForSequence(matcher.group("sequence"), events).add(
-                        matcher.group("event"));
-            }
-            return events;
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        } catch (ParseException e) {
-            throw new AssertionError(e);
-        }
-    }
-
-    // Returns an event list for a given sequence, adding it to the map as needed.
-    private static <T> List<T> eventsListForSequence(
-            String sequenceName, Map<String, List<T>> events) {
-        List<T> eventSequence = events.get(sequenceName);
-        if (eventSequence == null) {
-            eventSequence = new ArrayList<>();
-            events.put(sequenceName, eventSequence);
-        }
-        return eventSequence;
-    }
-
-    private void startRecordingEvents() {
-        Assert.assertTrue("Already recording events", mExpectedEvents == null);
-        mExpectedEvents = new HashMap<>();
-        mStartRecordingTime = new Date();
-        log("startRecordingEvents: " + DATE_TIME_FORMAT.format(mStartRecordingTime));
-    }
-
-    private void stopRecordingEvents() {
-        mExpectedEvents = null;
-        mStartRecordingTime = null;
-    }
-
     public Closable eventsCheck() {
+        Assert.assertTrue("Nested event checking", !sCheckingEvents);
         if ("com.android.launcher3".equals(getLauncherPackageName())) {
             // Not checking specific Launcher3 event sequences.
             return () -> {
             };
         }
-
-        // Entering events check block.
-        startRecordingEvents();
+        sCheckingEvents = true;
+        if (sEventChecker == null) sEventChecker = new LogEventChecker();
+        sEventChecker.start();
 
         return () -> {
-            // Leaving events check block.
-            if (mExpectedEvents == null) {
-                return; // There was a failure. Noo need to report another one.
-            }
-
-            if (!mCheckEventsForSuccessfulGestures) {
-                stopRecordingEvents();
-                return;
-            }
-
-            final String message = getEventMismatchMessage(true);
-            if (message != null) {
-                Assert.fail(formatSystemHealthMessage(
-                        "http://go/tapl : successful gesture produced " + message));
+            if (sCheckingEvents) {
+                sCheckingEvents = false;
+                if (mCheckEventsForSuccessfulGestures) {
+                    final String message = sEventChecker.verify(WAIT_TIME_MS);
+                    if (message != null) {
+                        Assert.fail(formatSystemHealthMessage(
+                                "http://go/tapl : successful gesture produced " + message));
+                    }
+                } else {
+                    sEventChecker.finishNoWait();
+                }
             }
         };
     }
 
     void expectEvent(String sequence, Pattern expected) {
-        if (mExpectedEvents != null) {
-            eventsListForSequence(sequence, mExpectedEvents).add(expected);
-        }
-    }
-
-    // Returns non-null error message if the actual events in logcat don't match expected events.
-    // If we are not checking events, returns null.
-    private String getEventMismatchMessage(boolean waitForExpectedCount) {
-        if (mExpectedEvents == null) return null;
-
-        try {
-            Map<String, List<String>> actual = getEvents();
-
-            if (waitForExpectedCount) {
-                // Wait until Launcher generates the expected number of events.
-                final long endTime = SystemClock.uptimeMillis() + WAIT_TIME_MS;
-                while (SystemClock.uptimeMillis() < endTime
-                        && !receivedEnoughEvents(actual)) {
-                    SystemClock.sleep(100);
-                    actual = getEvents();
-                }
-            }
-
-            return getEventMismatchErrorMessage(actual);
-        } finally {
-            stopRecordingEvents();
-        }
-    }
-
-    // Returns whether there is a sufficient number of events in the logcat to match the expected
-    // events.
-    private boolean receivedEnoughEvents(Map<String, List<String>> actual) {
-        for (Map.Entry<String, List<Pattern>> expectedNamedSequence : mExpectedEvents.entrySet()) {
-            final List<String> actualEventSequence = actual.get(expectedNamedSequence.getKey());
-            if (actualEventSequence == null
-                    || actualEventSequence.size() < expectedNamedSequence.getValue().size()) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    // If the list of actual events matches the list of expected events, returns -1, otherwise
-    // the position of the mismatch.
-    private static int getMismatchPosition(List<Pattern> expected, List<String> actual) {
-        for (int i = 0; i < expected.size(); ++i) {
-            if (i >= actual.size()
-                    || !expected.get(i).matcher(actual.get(i)).find()) {
-                return i;
-            }
-        }
-
-        if (actual.size() > expected.size()) return expected.size();
-
-        return -1;
-    }
-
-    // Returns non-null error message if the actual events passed as a param don't match expected
-    // events.
-    private String getEventMismatchErrorMessage(Map<String, List<String>> actualEvents) {
-        final StringBuilder sb = new StringBuilder();
-
-        // Check that all expected even sequences match the actual data.
-        for (Map.Entry<String, List<Pattern>> expectedNamedSequence : mExpectedEvents.entrySet()) {
-            List<String> actualEventSequence = actualEvents.get(expectedNamedSequence.getKey());
-            if (actualEventSequence == null) actualEventSequence = new ArrayList<>();
-            final int mismatchPosition = getMismatchPosition(
-                    expectedNamedSequence.getValue(), actualEventSequence);
-            if (mismatchPosition != -1) {
-                formatSequenceWithMismatch(
-                        sb,
-                        expectedNamedSequence.getKey(),
-                        expectedNamedSequence.getValue(),
-                        actualEventSequence,
-                        mismatchPosition);
-            }
-        }
-
-        // Check for unexpected event sequences in the actual data.
-        for (Map.Entry<String, List<String>> actualNamedSequence : actualEvents.entrySet()) {
-            if (!mExpectedEvents.containsKey(actualNamedSequence.getKey())) {
-                formatSequenceWithMismatch(
-                        sb,
-                        actualNamedSequence.getKey(),
-                        new ArrayList<>(),
-                        actualNamedSequence.getValue(),
-                        0);
-            }
-        }
-
-        return sb.length() != 0 ? "mismatching events: " + sb.toString() : null;
-    }
-
-    private static void formatSequenceWithMismatch(
-            StringBuilder sb,
-            String sequenceName,
-            List<Pattern> expected,
-            List<String> actualEvents,
-            int mismatchPosition) {
-        sb.append("\n>> Sequence " + sequenceName);
-        sb.append("\n  Expected:");
-        formatEventListWithMismatch(sb, expected, mismatchPosition);
-        sb.append("\n  Actual:");
-        formatEventListWithMismatch(sb, actualEvents, mismatchPosition);
-    }
-
-    private static void formatEventListWithMismatch(StringBuilder sb, List events, int position) {
-        for (int i = 0; i < events.size(); ++i) {
-            sb.append("\n  | ");
-            sb.append(i == position ? "---> " : "     ");
-            sb.append(events.get(i).toString());
-        }
-        if (position == events.size()) sb.append("\n  | ---> (end)");
+        if (sCheckingEvents) sEventChecker.expectPattern(sequence, expected);
     }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
new file mode 100644
index 0000000..0fc88ee
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -0,0 +1,233 @@
+/*
+ * 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.tapl;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.util.Log;
+
+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.
+ */
+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 volatile CountDownLatch mFinished;
+
+    // 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();
+    }
+
+    void start() {
+        if (mFinished != null) {
+            try {
+                mFinished.await();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+            mFinished = null;
+        }
+        mEvents.clear();
+        mExpectedEvents.clear();
+        mEventsCounter.drainPermits();
+        final String id = UUID.randomUUID().toString();
+        mStartCommand = START_PREFIX + id;
+        mFinishCommand = FINISH_PREFIX + id;
+        Log.d(TestProtocol.TAPL_EVENTS_TAG, mStartCommand);
+    }
+
+    private void onRun() {
+        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;
+
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+                    Runtime.getRuntime().exec(cmd).getInputStream()))) {
+                for (;;) {
+                    // Skip everything before the next start command.
+                    for (;;) {
+                        final String event = reader.readLine();
+                        if (event.contains(TestProtocol.TAPL_EVENTS_TAG)
+                                && event.contains(mStartCommand)) {
+                            break;
+                        }
+                    }
+
+                    // Store all actual events until the finish command.
+                    for (;;) {
+                        final String event = reader.readLine();
+                        if (event.contains(TestProtocol.TAPL_EVENTS_TAG)) {
+                            if (event.contains(mFinishCommand)) {
+                                mFinished.countDown();
+                                break;
+                            } else {
+                                final Matcher matcher = EVENT_LOG_ENTRY.matcher(event);
+                                if (matcher.find()) {
+                                    mEvents.add(matcher.group("sequence"), matcher.group("event"));
+                                    mEventsCounter.release();
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    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()
+                    .stream().mapToInt(e -> e.getValue().size()).sum();
+            mEventsCounter.tryAcquire(expectedCount, waitForExpectedCountMs, MILLISECONDS);
+            finishNoWait();
+            mFinished.await();
+            mFinished = null;
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    void finishNoWait() {
+        mFinished = new CountDownLatch(1);
+        Log.d(TestProtocol.TAPL_EVENTS_TAG, mFinishCommand);
+    }
+
+    String verify(long waitForExpectedCountMs) {
+        finishSync(waitForExpectedCountMs);
+
+        final StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, List<Pattern>> expectedEvents : mExpectedEvents.entrySet()) {
+            String sequence = expectedEvents.getKey();
+
+            List<String> actual = new ArrayList<>(mEvents.getNonNull(sequence));
+            final int mismatchPosition = getMismatchPosition(expectedEvents.getValue(), actual);
+            if (mismatchPosition != -1) {
+                formatSequenceWithMismatch(
+                        sb,
+                        sequence,
+                        expectedEvents.getValue(),
+                        actual,
+                        mismatchPosition);
+            }
+        }
+        // Check for unexpected event sequences in the actual data.
+        for (String actualNamedSequence : mEvents.keySet()) {
+            if (!mExpectedEvents.containsKey(actualNamedSequence)) {
+                formatSequenceWithMismatch(
+                        sb,
+                        actualNamedSequence,
+                        new ArrayList<>(),
+                        mEvents.get(actualNamedSequence),
+                        0);
+            }
+        }
+
+        return sb.length() != 0 ? "mismatching events: " + sb.toString() : null;
+    }
+
+    // If the list of actual events matches the list of expected events, returns -1, otherwise
+    // the position of the mismatch.
+    private static int getMismatchPosition(List<Pattern> expected, List<String> actual) {
+        for (int i = 0; i < expected.size(); ++i) {
+            if (i >= actual.size()
+                    || !expected.get(i).matcher(actual.get(i)).find()) {
+                return i;
+            }
+        }
+
+        if (actual.size() > expected.size()) return expected.size();
+
+        return -1;
+    }
+
+    private static void formatSequenceWithMismatch(
+            StringBuilder sb,
+            String sequenceName,
+            List<Pattern> expected,
+            List<String> actualEvents,
+            int mismatchPosition) {
+        sb.append("\n>> Sequence " + sequenceName);
+        sb.append("\n  Expected:");
+        formatEventListWithMismatch(sb, expected, mismatchPosition);
+        sb.append("\n  Actual:");
+        formatEventListWithMismatch(sb, actualEvents, mismatchPosition);
+    }
+
+    private static void formatEventListWithMismatch(StringBuilder sb, List events, int position) {
+        for (int i = 0; i < events.size(); ++i) {
+            sb.append("\n  | ");
+            sb.append(i == position ? "---> " : "     ");
+            sb.append(events.get(i).toString());
+        }
+        if (position == events.size()) sb.append("\n  | ---> (end)");
+    }
+
+    private static class ListMap<T> extends HashMap<String, List<T>> {
+
+        void add(String key, T value) {
+            getNonNull(key).add(value);
+        }
+
+        List<T> getNonNull(String key) {
+            List<T> list = get(key);
+            if (list == null) {
+                list = new ArrayList<>();
+                put(key, list);
+            }
+            return list;
+        }
+    }
+}