Merge "Fixes NPE when creating folder with null suggestedFolderNames." into ub-launcher3-master
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_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/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..b71fede 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,10 +60,12 @@
     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.
@@ -71,12 +73,13 @@
     // 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();
@@ -156,25 +159,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 +189,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 +213,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 +235,35 @@
     }
 
     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);
+        final RectF sourceWindowClipInsets = params.mForLiveTile
+                ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets;
+        mCurrentClipRectF.left = sourceWindowClipInsets.left * progress;
+        mCurrentClipRectF.top = sourceWindowClipInsets.top * progress;
+        mCurrentClipRectF.right =
+                mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress);
+        mCurrentClipRectF.bottom =
+                mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress);
     }
 
     public RectF getCurrentRectWithInsets() {
-        mTmpMatrix.mapRect(mCurrentRectWithInsets, mClipRectF);
+        mTmpMatrix.mapRect(mCurrentRectWithInsets, mCurrentClipRectF);
         return mCurrentRectWithInsets;
     }
 
@@ -384,75 +395,168 @@
     }
 
     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 boolean mForLiveTile;
+        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;
+            mForLiveTile = false;
+            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;
         }
 
+        /**
+         * Specifies whether we should clip the source window based on
+         * {@link AppWindowAnimationHelper#mSourceWindowClipInsetsForLiveTile} rather than
+         * {@link AppWindowAnimationHelper#mSourceWindowClipInsets} as {@link #mProgress} goes to 1.
+         */
         public TransformParams setForLiveTile(boolean forLiveTile) {
-            this.forLiveTile = forLiveTile;
+            mForLiveTile = 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 boolean isForLiveTile() {
+            return mForLiveTile;
+        }
+
+        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..d705cc0 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,6 +200,7 @@
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (tv.isRunningTask()) {
                 mTransformParams.setProgress(1 - progress)
+                        .setCurrentRect(null)
                         .setSyncTransactionApplier(mSyncTransactionApplier)
                         .setForLiveTile(true);
                 mAppWindowAnimationHelper.applyTransform(mTransformParams);
@@ -267,7 +268,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/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/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 810c91a..6fe6739 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -88,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..b3b887d 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");
@@ -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;
+        }
+    }
+}