Merge "Import translations. DO NOT MERGE ANYWHERE"
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index a07f27f..0bfeaac 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -71,6 +71,7 @@
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.ViewRootImpl;
+import android.view.ViewTreeObserver;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 
@@ -256,7 +257,7 @@
 
     /**
      * @return ActivityOptions with remote animations that controls how the window of the opening
-     *         targets are displayed.
+     * targets are displayed.
      */
     public ActivityOptionsWrapper getActivityLaunchOptions(View v) {
         boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
@@ -285,7 +286,7 @@
      * may not always be correct as we may resolve the opening app to a task when the animation
      * starts.
      *
-     * @param v the view to launch from
+     * @param v       the view to launch from
      * @param targets apps that are opening/closing
      * @return true if the app is launching from recents, false if it most likely is not
      */
@@ -298,9 +299,9 @@
     /**
      * Composes the animations for a launch from the recents list.
      *
-     * @param anim the animator set to add to
-     * @param v the launching view
-     * @param appTargets the apps that are opening/closing
+     * @param anim            the animator set to add to
+     * @param v               the launching view
+     * @param appTargets      the apps that are opening/closing
      * @param launcherClosing true if the launcher app is closing
      */
     protected void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
@@ -327,9 +328,9 @@
     /**
      * Compose the animations for a launch from the app icon.
      *
-     * @param anim the animation to add to
-     * @param v the launching view with the icon
-     * @param appTargets the list of opening/closing apps
+     * @param anim            the animation to add to
+     * @param v               the launching view with the icon
+     * @param appTargets      the list of opening/closing apps
      * @param launcherClosing true if launcher is closing
      */
     private void composeIconLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
@@ -367,7 +368,8 @@
                 public void onAnimationStart(Animator animation) {
                     mLauncher.addOnResumeCallback(() ->
                             ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH,
-                            mLauncher.getStateManager().getState().getDepth(mLauncher)).start());
+                                    mLauncher.getStateManager().getState().getDepth(
+                                            mLauncher)).start());
                 }
             });
         }
@@ -444,7 +446,7 @@
      *
      * @param isAppOpening True when this is called when an app is opening.
      *                     False when this is called when an app is closing.
-     * @param startDelay Start delay duration.
+     * @param startDelay   Start delay duration.
      */
     private Pair<AnimatorSet, Runnable> getLauncherContentAnimator(boolean isAppOpening,
             int startDelay) {
@@ -452,12 +454,12 @@
         Runnable endListener;
 
         float[] alphas = isAppOpening
-                ? new float[] {1, 0}
-                : new float[] {0, 1};
+                ? new float[]{1, 0}
+                : new float[]{0, 1};
 
         float[] scales = isAppOpening
-                ? new float[] {1, mContentScale}
-                : new float[] {mContentScale, 1};
+                ? new float[]{1, mContentScale}
+                : new float[]{mContentScale, 1};
 
         if (mLauncher.isInState(ALL_APPS)) {
             // All Apps in portrait mode is full screen, so we only animate AllAppsContainerView.
@@ -549,7 +551,7 @@
     /**
      * Compose recents view alpha and translation Y animation when launcher opens/closes apps.
      *
-     * @param anim the animator set to add to
+     * @param anim   the animator set to add to
      * @param alphas the alphas to animate to over time
      * @param scales the scale values to animator to over time
      * @return listener to run when the animation ends
@@ -702,7 +704,7 @@
 
                 float scaledCropWidth = windowCropWidth * scale;
                 float scaledCropHeight = windowCropHeight * scale;
-                float offsetX  = (scaledCropWidth - iconWidth) / 2;
+                float offsetX = (scaledCropWidth - iconWidth) / 2;
                 float offsetY = (scaledCropHeight - iconHeight) / 2;
 
                 // Calculate the window position to match the icon position.
@@ -1092,7 +1094,7 @@
 
     /**
      * @return Runner that plays when user goes to Launcher
-     *         ie. pressing home, swiping up from nav bar.
+     * ie. pressing home, swiping up from nav bar.
      */
     RemoteAnimationFactory createWallpaperOpenRunner(boolean fromUnlock) {
         return new WallpaperOpenLauncherAnimationRunner(mHandler, fromUnlock);
@@ -1224,7 +1226,24 @@
         anim.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationStart(Animator animation) {
-                InteractionJankMonitorWrapper.begin(mDragLayer, cuj);
+                mDragLayer.getViewTreeObserver().addOnDrawListener(
+                        new ViewTreeObserver.OnDrawListener() {
+                            boolean mHandled = false;
+
+                            @Override
+                            public void onDraw() {
+                                if (mHandled) {
+                                    return;
+                                }
+                                mHandled = true;
+
+                                InteractionJankMonitorWrapper.begin(mDragLayer, cuj);
+
+                                mDragLayer.post(() ->
+                                        mDragLayer.getViewTreeObserver().removeOnDrawListener(
+                                                this));
+                            }
+                        });
                 super.onAnimationStart(animation);
             }
 
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 1c178ad..bb5e493 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -147,7 +147,9 @@
                     if (recentsView != null) {
                         RemoteAnimationTargetCompat[] apps = new RemoteAnimationTargetCompat[1];
                         apps[0] = appearedTaskTarget;
-                        recentsView.launchSideTaskInLiveTileMode(appearedTaskTarget.taskId, apps);
+                        recentsView.launchSideTaskInLiveTileMode(appearedTaskTarget.taskId, apps,
+                                new RemoteAnimationTargetCompat[0] /* wallpaper */,
+                                new RemoteAnimationTargetCompat[0] /* nonApps */);
                         return;
                     }
                 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 3a445c3..eec0a71 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -141,6 +141,7 @@
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
+import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
@@ -853,11 +854,13 @@
     public void launchSideTaskInLiveTileModeForRestartedApp(int taskId) {
         if (mRunningTaskId != -1 && mRunningTaskId == taskId &&
                 getLiveTileParams().getTargetSet().findTask(taskId) != null) {
-            launchSideTaskInLiveTileMode(taskId, getLiveTileParams().getTargetSet().apps);
+            RemoteAnimationTargets targets = getLiveTileParams().getTargetSet();
+            launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers, targets.nonApps);
         }
     }
 
-    public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTargetCompat[] apps) {
+    public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTargetCompat[] apps,
+            RemoteAnimationTargetCompat[] wallpaper, RemoteAnimationTargetCompat[] nonApps) {
         AnimatorSet anim = new AnimatorSet();
         TaskView taskView = getTaskView(taskId);
         if (taskView == null || !isTaskViewVisible(taskView)) {
@@ -886,11 +889,8 @@
                 }
             });
         } else {
-            TaskViewUtils.composeRecentsLaunchAnimator(
-                    anim, taskView, apps,
-                    mLiveTileParams.getTargetSet().wallpapers,
-                    mLiveTileParams.getTargetSet().nonApps, true /* launcherClosing */,
-                    mActivity.getStateManager(), this,
+            TaskViewUtils.composeRecentsLaunchAnimator(anim, taskView, apps, wallpaper, nonApps,
+                    true /* launcherClosing */, mActivity.getStateManager(), this,
                     getDepthController());
         }
         anim.start();
diff --git a/res/color-night-v31/popup_color_first.xml b/res/color-night-v31/popup_shade_first.xml
similarity index 100%
rename from res/color-night-v31/popup_color_first.xml
rename to res/color-night-v31/popup_shade_first.xml
diff --git a/res/color-night-v31/popup_color_second.xml b/res/color-night-v31/popup_shade_second.xml
similarity index 100%
rename from res/color-night-v31/popup_color_second.xml
rename to res/color-night-v31/popup_shade_second.xml
diff --git a/res/color-night-v31/popup_color_third.xml b/res/color-night-v31/popup_shade_third.xml
similarity index 100%
rename from res/color-night-v31/popup_color_third.xml
rename to res/color-night-v31/popup_shade_third.xml
diff --git a/res/color-v31/popup_color_first.xml b/res/color-v31/popup_shade_first.xml
similarity index 100%
rename from res/color-v31/popup_color_first.xml
rename to res/color-v31/popup_shade_first.xml
diff --git a/res/color-v31/popup_color_second.xml b/res/color-v31/popup_shade_second.xml
similarity index 100%
rename from res/color-v31/popup_color_second.xml
rename to res/color-v31/popup_shade_second.xml
diff --git a/res/color-v31/popup_color_third.xml b/res/color-v31/popup_shade_third.xml
similarity index 100%
rename from res/color-v31/popup_color_third.xml
rename to res/color-v31/popup_shade_third.xml
diff --git a/res/color/popup_color_second.xml b/res/color/popup_color_second.xml
deleted file mode 100644
index d9999a2..0000000
--- a/res/color/popup_color_second.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 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.
--->
-<selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item android:color="?attr/popupColorPrimary" />
-</selector>
diff --git a/res/color/popup_color_first.xml b/res/color/popup_shade_first.xml
similarity index 93%
copy from res/color/popup_color_first.xml
copy to res/color/popup_shade_first.xml
index d9999a2..151190b 100644
--- a/res/color/popup_color_first.xml
+++ b/res/color/popup_shade_first.xml
@@ -14,5 +14,5 @@
      limitations under the License.
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item android:color="?attr/popupColorPrimary" />
+    <item android:color="?attr/popupShadeFirst" />
 </selector>
diff --git a/res/color/popup_color_first.xml b/res/color/popup_shade_second.xml
similarity index 93%
rename from res/color/popup_color_first.xml
rename to res/color/popup_shade_second.xml
index d9999a2..8660850 100644
--- a/res/color/popup_color_first.xml
+++ b/res/color/popup_shade_second.xml
@@ -14,5 +14,5 @@
      limitations under the License.
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item android:color="?attr/popupColorPrimary" />
+    <item android:color="?attr/popupShadeSecond" />
 </selector>
diff --git a/res/color/popup_color_third.xml b/res/color/popup_shade_third.xml
similarity index 93%
rename from res/color/popup_color_third.xml
rename to res/color/popup_shade_third.xml
index d7e9e79..9544728 100644
--- a/res/color/popup_color_third.xml
+++ b/res/color/popup_shade_third.xml
@@ -14,5 +14,5 @@
      limitations under the License.
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item android:color="?attr/popupColorPrimary" />
+    <item android:color="?attr/popupShadeThird" />
 </selector>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index ed1db30..00cf31c 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -25,6 +25,9 @@
     <attr name="popupColorPrimary" format="color" />
     <attr name="popupColorSecondary" format="color" />
     <attr name="popupColorTertiary" format="color" />
+    <attr name="popupShadeFirst" format="color" />
+    <attr name="popupShadeSecond" format="color" />
+    <attr name="popupShadeThird" format="color" />
     <attr name="isMainColorDark" format="boolean" />
     <attr name="isWorkspaceDarkText" format="boolean" />
     <attr name="workspaceTextColor" format="color" />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 76e821d..76fd1d7 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -53,6 +53,13 @@
     <color name="popup_color_secondary_dark">#202124</color>
     <color name="popup_color_tertiary_dark">#757575</color> <!-- Gray 600 -->
 
+    <color name="popup_shade_first_light">#F9F9F9</color>
+    <color name="popup_shade_second_light">#F1F1F1</color>
+    <color name="popup_shade_third_light">#E2E2E2</color>
+    <color name="popup_shade_first_dark">#303030</color>
+    <color name="popup_shade_second_dark">#262626</color>
+    <color name="popup_shade_third_dark">#1B1B1B</color>
+
     <color name="popup_notification_dot_light">#FFF</color>
     <color name="popup_notification_dot_dark">#757575</color>
 
@@ -62,8 +69,8 @@
     <color name="folder_hint_text_color_light">#FFF</color>
     <color name="folder_hint_text_color_dark">#FF000000</color>
 
-    <color name="folder_background_light">#FFFFFF</color>
-    <color name="folder_background_dark">#FF3C4043</color>
+    <color name="folder_background_light">#F9F9F9</color>
+    <color name="folder_background_dark">#464746</color>
 
     <color name="folder_dot_color">?attr/colorPrimary</color>
 
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 1536ec1..09c8b9b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -37,6 +37,9 @@
         <item name="popupColorPrimary">@color/popup_color_primary_light</item>
         <item name="popupColorSecondary">@color/popup_color_secondary_light</item>
         <item name="popupColorTertiary">@color/popup_color_tertiary_light</item>
+        <item name="popupShadeFirst">@color/popup_shade_first_light</item>
+        <item name="popupShadeSecond">@color/popup_shade_second_light</item>
+        <item name="popupShadeThird">@color/popup_shade_third_light</item>
         <item name="popupNotificationDotColor">@color/popup_notification_dot_light</item>
         <item name="isMainColorDark">false</item>
         <item name="isWorkspaceDarkText">false</item>
@@ -100,6 +103,9 @@
         <item name="popupColorSecondary">@color/popup_color_secondary_dark</item>
         <item name="popupColorTertiary">@color/popup_color_tertiary_dark</item>
         <item name="popupNotificationDotColor">@color/popup_notification_dot_dark</item>
+        <item name="popupShadeFirst">@color/popup_shade_first_dark</item>
+        <item name="popupShadeSecond">@color/popup_shade_second_dark</item>
+        <item name="popupShadeThird">@color/popup_shade_third_dark</item>
         <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
         <item name="folderDotColor">@color/folder_dot_color</item>
         <item name="folderFillColor">@color/folder_background_dark</item>
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index b918826..1fe50f8 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -102,6 +102,10 @@
     public static final BooleanFlag ENABLE_DEVICE_SEARCH = new DeviceFlag(
             "ENABLE_DEVICE_SEARCH", true, "Allows on device search in all apps");
 
+    public static final BooleanFlag ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING = new DeviceFlag(
+            "ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING", true,
+            "Allows on device search in all apps logging");
+
     public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(
             "IME_STICKY_SNACKBAR_EDU", true, "Show sticky IME edu in AllApps");
 
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 2305c60..d076be6 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -196,7 +196,7 @@
         icon.mFolderName.setText(folderInfo.title);
         icon.mFolderName.setCompoundDrawablePadding(0);
         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
-        lp.topMargin = grid.cellYPaddingPx + grid.iconSizePx + grid.iconDrawablePaddingPx;
+        lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
 
         icon.setTag(folderInfo);
         icon.setOnClickListener(ItemClickHandler.INSTANCE);
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index a89fb3b..b7fe348 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -183,12 +183,12 @@
 
         if (isAboveAnotherSurface) {
             mColors = new int[] {
-                    getColorStateList(context, R.color.popup_color_first).getDefaultColor()};
+                    getColorStateList(context, R.color.popup_shade_first).getDefaultColor()};
         } else {
             mColors = new int[] {
-                    getColorStateList(context, R.color.popup_color_first).getDefaultColor(),
-                    getColorStateList(context, R.color.popup_color_second).getDefaultColor(),
-                    getColorStateList(context, R.color.popup_color_third).getDefaultColor()};
+                    getColorStateList(context, R.color.popup_shade_first).getDefaultColor(),
+                    getColorStateList(context, R.color.popup_shade_second).getDefaultColor(),
+                    getColorStateList(context, R.color.popup_shade_third).getDefaultColor()};
         }
     }
 
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 731de25..5e57df6 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -65,6 +65,7 @@
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.launcher3.util.rule.LauncherActivityRule;
+import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
 
@@ -207,6 +208,9 @@
     public ShellCommandRule mDisableHeadsUpNotification =
             ShellCommandRule.disableHeadsUpNotification();
 
+    @Rule
+    public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
+
     protected void clearPackageData(String pkg) throws IOException, InterruptedException {
         final CountDownLatch count = new CountDownLatch(2);
         final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 06bc26a..4dd44f4 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.tapl.AppIconMenuItem;
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.launcher3.widget.picker.WidgetsRecyclerView;
@@ -92,6 +93,7 @@
     }
 
     @Test
+    @ScreenRecord //b/187080582
     public void testDevicePressMenu() throws Exception {
         mDevice.pressMenu();
         mDevice.waitForIdle();
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 822fefc..745dc22 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -42,6 +42,7 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.Wait.Condition;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.ShellCommandRule;
 
 import org.junit.Before;
@@ -77,6 +78,7 @@
     public void testEmpty() throws Throwable { /* needed while the broken tests are being fixed */ }
 
     @Test
+    @ScreenRecord  //b/192010616
     public void testPinWidgetNoConfig() throws Throwable {
         runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
                 ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
@@ -85,6 +87,7 @@
     }
 
     @Test
+    @ScreenRecord  //b/192005114
     public void testPinWidgetNoConfig_customPreview() throws Throwable {
         // Command to set custom preview
         Intent command = RequestPinItemActivity.getCommandIntent(
diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
index 4c47947..dc59bdd 100644
--- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
+++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
@@ -2,6 +2,8 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor.AutoCloseInputStream;
 import android.util.Log;
 
 import androidx.test.uiautomator.UiDevice;
@@ -12,9 +14,12 @@
 import org.junit.rules.TestWatcher;
 import org.junit.runner.Description;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 public class FailureWatcher extends TestWatcher {
     private static final String TAG = "FailureWatcher";
@@ -26,20 +31,6 @@
         mLauncher = launcher;
     }
 
-    private static void dumpViewHierarchy(UiDevice device) {
-        final ByteArrayOutputStream stream = new ByteArrayOutputStream();
-        try {
-            device.dumpWindowHierarchy(stream);
-            stream.flush();
-            stream.close();
-            for (String line : stream.toString().split("\\r?\\n")) {
-                Log.e(TAG, line.trim());
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "error dumping XML to logcat", e);
-        }
-    }
-
     @Override
     protected void succeeded(Description description) {
         super.succeeded(description);
@@ -53,22 +44,41 @@
 
     public static void onError(UiDevice device, Description description, Throwable e) {
         if (device == null) return;
-        final String pathname = getInstrumentation().getTargetContext().
-                getFilesDir().getPath() + "/TestScreenshot-" + description.getMethodName()
-                + ".png";
-        Log.e(TAG, "Failed test " + description.getMethodName() +
-                ", screenshot will be saved to " + pathname +
-                ", track trace is below, UI object dump is further below:\n" +
-                Log.getStackTraceString(e));
-        dumpViewHierarchy(device);
+        final File parentFile = getInstrumentation().getTargetContext().getFilesDir();
+        final File sceenshot = new File(parentFile,
+                "TestScreenshot-" + description.getMethodName() + ".png");
+        final File hierarchy = new File(parentFile,
+                "Hierarchy-" + description.getMethodName() + ".zip");
 
-        try {
-            final String dumpsysResult = device.executeShellCommand(
-                    "dumpsys activity service TouchInteractionService");
-            Log.d(TAG, "TouchInteractionService: " + dumpsysResult);
-        } catch (IOException ex) {
+        // Dump window hierarchy
+        try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(hierarchy))) {
+            out.putNextEntry(new ZipEntry("bugreport.txt"));
+            dumpStringCommand("dumpsys window windows", out);
+            dumpStringCommand("dumpsys package", out);
+            dumpStringCommand("dumpsys activity service TouchInteractionService", out);
+            out.closeEntry();
+
+            out.putNextEntry(new ZipEntry("visible_windows.zip"));
+            dumpCommand("cmd window dump-visible-window-views", out);
+            out.closeEntry();
+        } catch (IOException ex) { }
+
+        Log.e(TAG, "Failed test " + description.getMethodName()
+                + ",\nscreenshot will be saved to " + sceenshot
+                + ",\nUI dump at: " + hierarchy
+                + " (use go/web-hv to open the dump file)", e);
+        device.takeScreenshot(sceenshot);
+    }
+
+    private static void dumpStringCommand(String cmd, OutputStream out) throws IOException {
+        out.write(("\n\n" + cmd + "\n").getBytes());
+        dumpCommand(cmd, out);
+    }
+
+    private static void dumpCommand(String cmd, OutputStream out) throws IOException {
+        try (AutoCloseInputStream in = new AutoCloseInputStream(getInstrumentation()
+                .getUiAutomation().executeShellCommand(cmd))) {
+            FileUtils.copy(in, out);
         }
-
-        device.takeScreenshot(new File(pathname));
     }
 }
diff --git a/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
new file mode 100644
index 0000000..00b1cdd
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 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.util.rule;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.test.uiautomator.UiDevice;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.File;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Rule which captures a screen record for a test.
+ * After adding this rule to the test class, apply the annotation @ScreenRecord to individual tests
+ */
+public class ScreenRecordRule implements TestRule {
+
+    private static final String TAG = "ScreenRecordRule";
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        if (description.getAnnotation(ScreenRecord.class) == null) {
+            return base;
+        }
+
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                Instrumentation inst = getInstrumentation();
+                UiAutomation automation = inst.getUiAutomation();
+                UiDevice device = UiDevice.getInstance(inst);
+
+                File outputFile = new File(inst.getTargetContext().getFilesDir(),
+                        "screenrecord-" + description.getMethodName() + ".mp4");
+                device.executeShellCommand("killall screenrecord");
+                ParcelFileDescriptor output =
+                        automation.executeShellCommand("screenrecord " + outputFile);
+                String screenRecordPid = device.executeShellCommand("pidof screenrecord");
+                try {
+                    base.evaluate();
+                } finally {
+                    device.executeShellCommand("kill -INT " + screenRecordPid);
+                    Log.e(TAG, "Screenrecord captured at: " + outputFile);
+                    output.close();
+                }
+            }
+        };
+    }
+
+    /**
+     * Interface to indicate that the test should capture screenrecord
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface ScreenRecord { }
+}