Merge "Import translations. DO NOT MERGE ANYWHERE" into udc-d1-dev
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index f5202b7..3a6566a 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -147,6 +147,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Consumer;
 
@@ -662,6 +663,14 @@
         } else {
             runningTasks = mGestureState.getRunningTask().getPlaceholderTasks();
         }
+
+        // Safeguard against any null tasks being sent to recents view, happens when quickswitching
+        // very quickly w/ split tasks because TopTaskTracker provides stale information compared to
+        // actual running tasks in the recents animation.
+        // TODO(b/236226779), Proper fix (ag/22237143)
+        if (Arrays.stream(runningTasks).anyMatch(Objects::isNull)) {
+            return;
+        }
         mRecentsView.onGestureAnimationStart(runningTasks, mDeviceState.getRotationTouchHelper());
     }
 
@@ -915,7 +924,12 @@
         if (DesktopTaskView.DESKTOP_MODE_SUPPORTED && targets.hasDesktopTasks()) {
             mRemoteTargetHandles = mTargetGluer.assignTargetsForDesktop(targets);
         } else {
+            int untrimmedAppCount = mRemoteTargetHandles.length;
             mRemoteTargetHandles = mTargetGluer.assignTargetsForSplitScreen(targets);
+            if (mRemoteTargetHandles.length < untrimmedAppCount && mIsSwipeForSplit) {
+                updateIsGestureForSplit(mRemoteTargetHandles.length);
+                setupRecentsViewUi();
+            }
         }
         mRecentsAnimationController = controller;
         mRecentsAnimationTargets = targets;
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index d9c269a..84b90b9 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -29,12 +29,15 @@
 import com.android.quickstep.views.DesktopTaskView;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * Glues together the necessary components to animate a remote target using a
  * {@link TaskViewSimulator}
  */
 public class RemoteTargetGluer {
+    private static final int DEFAULT_NUM_HANDLES = 2;
+
     private RemoteTargetHandle[] mRemoteTargetHandles;
     private SplitBounds mSplitBounds;
 
@@ -62,8 +65,9 @@
             }
         }
 
-        int[] splitIds = TopTaskTracker.INSTANCE.get(context).getRunningSplitTaskIds();
-        init(context, sizingStrategy, splitIds.length == 2 ? 2 : 1, false /* forDesktop */);
+        // Assume 2 handles needed for split, scale down as needed later on when we actually
+        // get remote targets
+        init(context, sizingStrategy, DEFAULT_NUM_HANDLES, false /* forDesktop */);
     }
 
     private void init(Context context, BaseActivityInterface sizingStrategy, int numHandles,
@@ -108,6 +112,17 @@
      * the left/top task, index 1 right/bottom.
      */
     public RemoteTargetHandle[] assignTargetsForSplitScreen(RemoteAnimationTargets targets) {
+        // Resize the mRemoteTargetHandles array since we started assuming split screen, but
+        // targets.apps is the ultimate source of truth here
+        long appCount = Arrays.stream(targets.apps)
+                .filter(app -> app.mode == targets.targetMode)
+                .count();
+        if (appCount < mRemoteTargetHandles.length) {
+            RemoteTargetHandle[] newHandles = new RemoteTargetHandle[(int) appCount];
+            System.arraycopy(mRemoteTargetHandles, 0/*src*/, newHandles, 0/*dst*/, (int) appCount);
+            mRemoteTargetHandles = newHandles;
+        }
+
         if (mRemoteTargetHandles.length == 1) {
             // If we're not in split screen, the splitIds count doesn't really matter since we
             // should always hit this case.
@@ -233,6 +248,14 @@
                 targets.targetMode);
     }
 
+    /**
+     * The object returned by this is may be modified in
+     * {@link #assignTargetsForSplitScreen(RemoteAnimationTargets)}, specifically the length of the
+     * array may be shortened based on the number of RemoteAnimationTargets present.
+     * <p>
+     * This can be accessed at any time, however the count will be more accurate if accessed after
+     * calling one of the respective assignTargets*() methods
+     */
     public RemoteTargetHandle[] getRemoteTargetHandles() {
         return mRemoteTargetHandles;
     }
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index 1b4fdc4..25ac47a 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -82,7 +82,8 @@
         mContext = context;
         mDeviceState = deviceState;
         mGestureState = gestureState;
-        mIsSwipeForSplit = TopTaskTracker.INSTANCE.get(context).getRunningSplitTaskIds().length > 1;
+        updateIsGestureForSplit(TopTaskTracker.INSTANCE.get(context)
+                .getRunningSplitTaskIds().length);
 
         mTargetGluer = new RemoteTargetGluer(mContext, mGestureState.getActivityInterface());
         mRemoteTargetHandles = mTargetGluer.getRemoteTargetHandles();
@@ -280,6 +281,10 @@
         return out;
     }
 
+    protected void updateIsGestureForSplit(int targetCount) {
+        mIsSwipeForSplit = targetCount > 1;
+    }
+
     private RectFSpringAnim getWindowAnimationToHomeInternal(
             HomeAnimationFactory homeAnimationFactory, RectF targetRect,
             TransformParams transformParams, TaskViewSimulator taskViewSimulator,
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index dbe4402..97e34c5 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -116,11 +116,12 @@
             Utilities.enableRunningInTestHarnessForTests();
         }
 
+        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule();
         mOrderSensitiveRules = RuleChain
                 .outerRule(new SamplerRule())
                 .around(new NavigationModeSwitchRule(mLauncher))
-                .around(new ViewCaptureRule())
-                .around(new FailureWatcher(mDevice, mLauncher));
+                .around(viewCaptureRule)
+                .around(new FailureWatcher(mDevice, mLauncher, viewCaptureRule.getViewCapture()));
 
         mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
                 getHomeIntentInPackage(context),
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 2664988..93ea28e 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -100,7 +100,7 @@
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Ordner: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> Elemente"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Ordner: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> oder mehr Elemente"</string>
     <string name="wallpaper_button_text" msgid="8404103075899945851">"Hintergründe"</string>
-    <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Hintergrund &amp; Stil"</string>
+    <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Hintergrund und Stil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Startbildschirm bearbeiten"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Einstellungen"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Von deinem Administrator deaktiviert"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 8416815..7478968 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -50,8 +50,7 @@
     <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Personnels"</string>
     <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Professionnels"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"Conversations"</string>
-    <!-- no translation found for widget_category_note_taking (3469689394504266039) -->
-    <skip />
+    <string name="widget_category_note_taking" msgid="3469689394504266039">"Prise de note"</string>
     <string name="widget_education_header" msgid="4874760613775913787">"Renseignements utiles à portée de main"</string>
     <string name="widget_education_content" msgid="1731667670753497052">"Pour obtenir des informations sans ouvrir d\'applications, vous pouvez ajouter des widgets à votre écran d\'accueil"</string>
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Touchez pour modifier les paramètres du widget"</string>
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 617afcb..c20494d 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,6 +20,8 @@
 
 import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
 import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
+import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
@@ -560,6 +562,7 @@
             synchronized (mLock) {
                 // Everything loaded bind the data.
                 mModelLoaded = true;
+                testLogD(WORK_TAB_MISSING, "launcher model loaded");
             }
         }
 
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 621c2ab..f88ff86 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -302,7 +302,7 @@
             "Enable widget transition animation when resizing the widgets");
 
     public static final BooleanFlag PREEMPTIVE_UNFOLD_ANIMATION_START = getDebugFlag(270397209,
-            "PREEMPTIVE_UNFOLD_ANIMATION_START", ENABLED,
+            "PREEMPTIVE_UNFOLD_ANIMATION_START", DISABLED,
             "Enables starting the unfold animation preemptively when unfolding, without"
                     + "waiting for SystemUI and then merging the SystemUI progress whenever we "
                     + "start receiving the events");
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 2ce6c78..dd82ecf 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -273,6 +273,8 @@
         // {@link #onAnimationEnd} before B reads new UI state from {@link #onAnimationStart}.
         a.addListener(new AnimatorListenerAdapter() {
             private CellLayout mCellLayout;
+
+            private boolean mFolderClipChildren;
             private boolean mFolderClipToPadding;
             private boolean mContentClipChildren;
             private boolean mContentClipToPadding;
@@ -283,12 +285,14 @@
             public void onAnimationStart(Animator animator) {
                 super.onAnimationStart(animator);
                 mCellLayout = mContent.getCurrentCellLayout();
+                mFolderClipChildren = mFolder.getClipChildren();
                 mFolderClipToPadding = mFolder.getClipToPadding();
                 mContentClipChildren = mContent.getClipChildren();
                 mContentClipToPadding = mContent.getClipToPadding();
                 mCellLayoutClipChildren = mCellLayout.getClipChildren();
                 mCellLayoutClipPadding = mCellLayout.getClipToPadding();
 
+                mFolder.setClipChildren(false);
                 mFolder.setClipToPadding(false);
                 mContent.setClipChildren(false);
                 mContent.setClipToPadding(false);
@@ -309,6 +313,7 @@
                 mFolder.mFooter.setTranslationX(0f);
                 mFolder.mFolderName.setAlpha(1f);
 
+                mFolder.setClipChildren(mFolderClipChildren);
                 mFolder.setClipToPadding(mFolderClipToPadding);
                 mContent.setClipChildren(mContentClipChildren);
                 mContent.setClipToPadding(mContentClipToPadding);
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 15f3538..17d3302 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -642,6 +642,9 @@
 
         @UiEvent(doc = "User has swiped upwards from the gesture handle to show transient taskbar.")
         LAUNCHER_TRANSIENT_TASKBAR_SHOW(1331),
+
+        @UiEvent(doc = "App launched through pending intent")
+        LAUNCHER_APP_LAUNCH_PENDING_INTENT(1394),
         ;
 
         // ADD MORE
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index 70c9802..44d32d9 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 
 import android.util.Log;
 
@@ -72,7 +73,9 @@
 
     @Override
     public final void run() {
-        if (!Objects.requireNonNull(mModel).isModelLoaded()) {
+        boolean isModelLoaded = Objects.requireNonNull(mModel).isModelLoaded();
+        testLogD(WORK_TAB_MISSING, "modelLoaded: " + isModelLoaded + " forTask: " + this);
+        if (!isModelLoaded) {
             if (DEBUG_TASKS) {
                 Log.d(TAG, "Ignoring model task since loader is pending=" + this);
             }
diff --git a/src/com/android/launcher3/util/ContentWriter.java b/src/com/android/launcher3/util/ContentWriter.java
index 7c5ef4d..9910dc2 100644
--- a/src/com/android/launcher3/util/ContentWriter.java
+++ b/src/com/android/launcher3/util/ContentWriter.java
@@ -106,7 +106,7 @@
 
     public int commit() {
         if (mCommitParams != null) {
-            mCommitParams.mDbController.update(
+            return mCommitParams.mDbController.update(
                     Favorites.TABLE_NAME, getValues(mContext),
                     mCommitParams.mWhere, mCommitParams.mSelectionArgs);
         }
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index 515a2d8..4b319e5 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON;
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_PENDING_INTENT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -305,6 +306,11 @@
         ActivityOptionsWrapper options = getActivityLaunchOptions(v, item);
         try {
             intent.send(null, 0, null, null, null, null, options.toBundle());
+            if (item != null) {
+                InstanceId instanceId = new InstanceIdSequence().newInstanceId();
+                getStatsLogManager().logger().withItemInfo(item).withInstanceId(instanceId)
+                        .log(LAUNCHER_APP_LAUNCH_PENDING_INTENT);
+            }
             return options.onEndCallback;
         } catch (PendingIntent.CanceledException e) {
             Toast.makeText(v.getContext(),
diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
index 2742882..580b4f1 100644
--- a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -105,6 +105,7 @@
                 mEnforcedRectangle);
         setOutlineProvider(mCornerRadiusEnforcementOutline);
         setClipToOutline(true);
+        invalidateOutline();
     }
 
     /** Returns the corner radius currently enforced, in pixels. */
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 723ea17..8dd1de4 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -42,7 +42,6 @@
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
 import com.android.launcher3.R;
-import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.recyclerview.ViewHolderBinder;
 import com.android.launcher3.util.LabelComparator;
 import com.android.launcher3.util.PackageUserKey;
@@ -58,7 +57,6 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
 import java.util.OptionalInt;
 import java.util.function.IntSupplier;
 import java.util.function.Predicate;
@@ -174,9 +172,6 @@
         mAllEntries.clear();
         mAllEntries.add(new WidgetListSpaceEntry());
         tempEntries.stream().sorted(mRowComparator).forEach(mAllEntries::add);
-        if (shouldClearVisibleEntries()) {
-            mVisibleEntries.clear();
-        }
         updateVisibleEntries();
     }
 
@@ -426,29 +421,6 @@
         updateVisibleEntries();
     }
 
-    /**
-     * Returns {@code true} if there is a change in {@link #mAllEntries} that results in an
-     * invalidation of {@link #mVisibleEntries}. e.g. there is change in the device language.
-     */
-    private boolean shouldClearVisibleEntries() {
-        Map<PackageUserKey, PackageItemInfo> packagesInfo =
-                mAllEntries.stream()
-                        .filter(entry -> entry instanceof WidgetsListHeaderEntry)
-                        .map(entry -> entry.mPkgItem)
-                        .collect(Collectors.toMap(
-                                entry -> PackageUserKey.fromPackageItemInfo(entry),
-                                entry -> entry));
-        for (WidgetsListBaseEntry visibleEntry: mVisibleEntries) {
-            PackageUserKey key = PackageUserKey.fromPackageItemInfo(visibleEntry.mPkgItem);
-            PackageItemInfo packageItemInfo = packagesInfo.get(key);
-            if (packageItemInfo != null
-                    && !visibleEntry.mPkgItem.title.equals(packageItemInfo.title)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     /** Comparator for sorting WidgetListRowEntry based on package title. */
     public static class WidgetListBaseRowEntryComparator implements
             Comparator<WidgetsListBaseEntry> {
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 8def7e8..b7c3aca 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.testing.shared;
 
+import android.util.Log;
+
 /**
  * Protocol for custom accessibility events for communication with UI Automation tests.
  */
@@ -161,4 +163,12 @@
     public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
     public static final String REQUEST_IS_EMULATE_DISPLAY_RUNNING = "is-emulate-display-running";
     public static final String REQUEST_EMULATE_PRINT_DEVICE = "emulate-print-device";
+
+    /** Logs {@link Log#d(String, String)} if {@link #sDebugTracing} is true. */
+    public static void testLogD(String tag, String message) {
+        if (!sDebugTracing) {
+            return;
+        }
+        Log.d(tag, message);
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 5bd28d8..d7c4ae3 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -216,10 +216,11 @@
     }
 
     protected TestRule getRulesInsideActivityMonitor() {
+        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule();
         final RuleChain inner = RuleChain
                 .outerRule(new PortraitLandscapeRunner(this))
-                .around(new ViewCaptureRule())
-                .around(new FailureWatcher(mDevice, mLauncher));
+                .around(viewCaptureRule)
+                .around(new FailureWatcher(mDevice, mLauncher, viewCaptureRule.getViewCapture()));
 
         return TestHelpers.isInLauncherProcess()
                 ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner)
diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
index 6b11fd6..7ca6a06 100644
--- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
+++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
@@ -6,8 +6,12 @@
 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.uiautomator.UiDevice;
 
+import com.android.app.viewcapture.ViewCapture;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 
@@ -28,10 +32,14 @@
     private static boolean sSavedBugreport = false;
     final private UiDevice mDevice;
     private final LauncherInstrumentation mLauncher;
+    @NonNull
+    private final ViewCapture mViewCapture;
 
-    public FailureWatcher(UiDevice device, LauncherInstrumentation launcher) {
+    public FailureWatcher(UiDevice device, LauncherInstrumentation launcher,
+            @NonNull ViewCapture viewCapture) {
         mDevice = device;
         mLauncher = launcher;
+        mViewCapture = viewCapture;
     }
 
     @Override
@@ -63,7 +71,7 @@
 
     @Override
     protected void failed(Throwable e, Description description) {
-        onError(mLauncher, description, e);
+        onError(mLauncher, description, e, mViewCapture);
     }
 
     static File diagFile(Description description, String prefix, String ext) {
@@ -74,6 +82,12 @@
 
     public static void onError(LauncherInstrumentation launcher, Description description,
             Throwable e) {
+        onError(launcher, description, e, null);
+    }
+
+    private static void onError(LauncherInstrumentation launcher, Description description,
+            Throwable e, @Nullable ViewCapture viewCapture) {
+
         final File sceenshot = diagFile(description, "TestScreenshot", "png");
         final File hierarchy = diagFile(description, "Hierarchy", "zip");
 
@@ -88,6 +102,12 @@
             out.putNextEntry(new ZipEntry("visible_windows.zip"));
             dumpCommand("cmd window dump-visible-window-views", out);
             out.closeEntry();
+
+            if (viewCapture != null) {
+                out.putNextEntry(new ZipEntry("FS/data/misc/wmtrace/failed_test.vc"));
+                viewCapture.dumpTo(out, ApplicationProvider.getApplicationContext());
+                out.closeEntry();
+            }
         } catch (Exception ignored) {
         }
 
diff --git a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
index f3fff35..0c65539 100644
--- a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
+++ b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
@@ -19,101 +19,62 @@
 import android.app.Application
 import android.media.permission.SafeCloseable
 import android.os.Bundle
-import android.util.Log
-import androidx.annotation.AnyThread
 import androidx.test.core.app.ApplicationProvider
 import com.android.app.viewcapture.SimpleViewCapture
 import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR
 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
-import java.io.File
-import java.io.FileOutputStream
-import java.util.zip.ZipEntry
-import java.util.zip.ZipOutputStream
-import org.junit.rules.TestWatcher
+import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
-private const val TAG = "ViewCaptureRule"
-
 /**
  * This JUnit TestRule registers a listener for activity lifecycle events to attach a ViewCapture
  * instance that other test rules use to dump the timelapse hierarchy upon an error during a test.
  *
  * This rule will not work in OOP tests that don't have access to the activity under test.
  */
-class ViewCaptureRule : TestWatcher() {
-    private val viewCapture = SimpleViewCapture("test-view-capture")
-    private val windowListenerCloseables = mutableListOf<SafeCloseable>()
+class ViewCaptureRule : TestRule {
+    val viewCapture = SimpleViewCapture("test-view-capture")
 
     override fun apply(base: Statement, description: Description): Statement {
-        val testWatcherStatement = super.apply(base, description)
-
         return object : Statement() {
             override fun evaluate() {
-                val lifecycleCallbacks = createLifecycleCallbacks(description)
-                with(ApplicationProvider.getApplicationContext<Application>()) {
-                    registerActivityLifecycleCallbacks(lifecycleCallbacks)
-                    try {
-                        testWatcherStatement.evaluate()
-                    } finally {
-                        unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
+                val windowListenerCloseables = mutableListOf<SafeCloseable>()
+
+                val lifecycleCallbacks =
+                    object : ActivityLifecycleCallbacksAdapter {
+                        override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
+                            super.onActivityCreated(activity, bundle)
+                            windowListenerCloseables.add(
+                                viewCapture.startCapture(
+                                    activity.window.decorView,
+                                    "${description.testClass?.simpleName}.${description.methodName}"
+                                )
+                            )
+                        }
+
+                        override fun onActivityDestroyed(activity: Activity) {
+                            super.onActivityDestroyed(activity)
+                            viewCapture.stopCapture(activity.window.decorView)
+                        }
                     }
+
+                val application = ApplicationProvider.getApplicationContext<Application>()
+                application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
+
+                try {
+                    base.evaluate()
+                } finally {
+                    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
+
+                    // Clean up ViewCapture references here rather than in onActivityDestroyed so
+                    // test code can access view hierarchy capture. onActivityDestroyed would delete
+                    // view capture data before FailureWatcher could output it as a test artifact.
+                    // This is on the main thread to avoid a race condition where the onDrawListener
+                    // is removed while onDraw is running, resulting in an IllegalStateException.
+                    MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) }
                 }
             }
         }
     }
-
-    private fun createLifecycleCallbacks(description: Description) =
-        object : ActivityLifecycleCallbacksAdapter {
-            override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
-                super.onActivityCreated(activity, bundle)
-                windowListenerCloseables.add(
-                    viewCapture.startCapture(
-                        activity.window.decorView,
-                        "${description.testClass?.simpleName}.${description.methodName}"
-                    )
-                )
-            }
-
-            override fun onActivityDestroyed(activity: Activity) {
-                super.onActivityDestroyed(activity)
-                viewCapture.stopCapture(activity.window.decorView)
-            }
-        }
-
-    override fun succeeded(description: Description) = cleanup()
-
-    /** If the test fails, this function will output the ViewCapture information. */
-    override fun failed(e: Throwable, description: Description) {
-        super.failed(e, description)
-
-        val testName = "${description.testClass.simpleName}.${description.methodName}"
-        val application: Application = ApplicationProvider.getApplicationContext()
-        val zip = File(application.filesDir, "ViewCapture-$testName.zip")
-
-        ZipOutputStream(FileOutputStream(zip)).use {
-            it.putNextEntry(ZipEntry("FS/data/misc/wmtrace/failed_test.vc"))
-            viewCapture.dumpTo(it, ApplicationProvider.getApplicationContext())
-            it.closeEntry()
-        }
-        cleanup()
-
-        Log.d(
-            TAG,
-            "Failed $testName due to ${e::class.java.simpleName}.\n" +
-                "\tUse go/web-hv to open dump file: \n\t\t${zip.absolutePath}"
-        )
-    }
-
-    /**
-     * Clean up ViewCapture references can't happen in onActivityDestroyed otherwise view
-     * hierarchies would be erased before they could be outputted.
-     *
-     * This is on the main thread to avoid a race condition where the onDrawListener is removed
-     * while onDraw is running, resulting in an IllegalStateException.
-     */
-    @AnyThread
-    private fun cleanup() {
-        MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) }
-    }
 }