Merge "Allow Workspace Scrim to be colored per state" into sc-dev
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 5b1b59b..fb67645 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -79,6 +79,7 @@
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.views.FloatingIconView;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskViewUtils;
@@ -86,6 +87,7 @@
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.views.FloatingWidgetView;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.ActivityCompat;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
@@ -100,7 +102,9 @@
 import com.android.systemui.shared.system.WindowManagerWrapper;
 import com.android.wm.shell.startingsurface.IStartingWindowListener;
 
+import java.util.ArrayList;
 import java.util.LinkedHashMap;
+import java.util.List;
 
 /**
  * Manages the opening and closing app transitions from Launcher
@@ -160,6 +164,9 @@
 
     private static final int MAX_NUM_TASKS = 5;
 
+    // Cross-fade duration between App Widget and App
+    private static final int WIDGET_CROSSFADE_DURATION_MILLIS = 125;
+
     protected final BaseQuickstepLauncher mLauncher;
 
     private final DragLayer mDragLayer;
@@ -349,6 +356,29 @@
         }
     }
 
+    private void composeWidgetLaunchAnimator(
+            @NonNull AnimatorSet anim,
+            @NonNull LauncherAppWidgetHostView v,
+            @NonNull RemoteAnimationTargetCompat[] appTargets,
+            @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
+            @NonNull RemoteAnimationTargetCompat[] nonAppTargets) {
+        mLauncher.getStateManager().setCurrentAnimation(anim);
+
+        Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets));
+        anim.play(getOpeningWindowAnimatorsForWidget(v, appTargets, wallpaperTargets, nonAppTargets,
+                windowTargetBounds, areAllTargetsTranslucent(appTargets)));
+
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mLauncher.addOnResumeCallback(() ->
+                        ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH,
+                                mLauncher.getStateManager().getState().getDepth(
+                                        mLauncher)).start());
+            }
+        });
+    }
+
     /**
      * Return the window bounds of the opening target.
      * In multiwindow mode, we need to get the final size of the opening app window target to help
@@ -457,32 +487,29 @@
             alpha.setInterpolator(LINEAR);
             launcherAnimator.play(alpha);
 
+            List<View> viewsToAnimate = new ArrayList<>();
+
             Workspace workspace = mLauncher.getWorkspace();
-            View currentPage = ((CellLayout) workspace.getChildAt(workspace.getCurrentPage()))
-                    .getShortcutsAndWidgets();
-            View hotseat = mLauncher.getHotseat();
-            View qsb = mLauncher.findViewById(R.id.search_container_all_apps);
+            workspace.getVisiblePages().forEach(
+                    view -> viewsToAnimate.add(((CellLayout) view).getShortcutsAndWidgets()));
 
-            currentPage.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-            hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-            qsb.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            viewsToAnimate.add(mLauncher.getHotseat());
+            // Add QSB
+            viewsToAnimate.add(mLauncher.findViewById(R.id.search_container_all_apps));
 
-            launcherAnimator.play(ObjectAnimator.ofFloat(currentPage, View.TRANSLATION_Y, trans));
-            launcherAnimator.play(ObjectAnimator.ofFloat(hotseat, View.TRANSLATION_Y, trans));
-            launcherAnimator.play(ObjectAnimator.ofFloat(qsb, View.TRANSLATION_Y, trans));
+            viewsToAnimate.forEach(view -> {
+                view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                launcherAnimator.play(ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, trans));
+            });
 
             // Pause page indicator animations as they lead to layer trashing.
             mLauncher.getWorkspace().getPageIndicator().pauseAnimations();
 
             endListener = () -> {
-                currentPage.setTranslationY(0);
-                hotseat.setTranslationY(0);
-                qsb.setTranslationY(0);
-
-                currentPage.setLayerType(View.LAYER_TYPE_NONE, null);
-                hotseat.setLayerType(View.LAYER_TYPE_NONE, null);
-                qsb.setLayerType(View.LAYER_TYPE_NONE, null);
-
+                viewsToAnimate.forEach(view -> {
+                    view.setTranslationY(0);
+                    view.setLayerType(View.LAYER_TYPE_NONE, null);
+                });
                 mDragLayerAlpha.setValue(1f);
                 mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd();
             };
@@ -737,6 +764,112 @@
             }
         });
 
+        animatorSet.playTogether(appAnimator, getBackgroundAnimator(appTargets));
+        return animatorSet;
+    }
+
+    private Animator getOpeningWindowAnimatorsForWidget(LauncherAppWidgetHostView v,
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets,
+            RemoteAnimationTargetCompat[] nonAppTargets, Rect windowTargetBounds,
+            boolean appTargetsAreTranslucent) {
+        final RectF widgetBackgroundBounds = new RectF();
+        final Rect appWindowCrop = new Rect();
+        final Matrix matrix = new Matrix();
+
+        final float finalWindowRadius = mDeviceProfile.isMultiWindowMode
+                ? 0 : getWindowCornerRadius(mLauncher.getResources());
+        final FloatingWidgetView floatingView = FloatingWidgetView.getFloatingWidgetView(mLauncher,
+                v, widgetBackgroundBounds, windowTargetBounds, finalWindowRadius);
+        final float initialWindowRadius = supportsRoundedCornersOnWindows(mLauncher.getResources())
+                ? floatingView.getInitialCornerRadius() : 0;
+
+        RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets,
+                wallpaperTargets, nonAppTargets, MODE_OPENING);
+        SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(floatingView);
+        openingTargets.addReleaseCheck(surfaceApplier);
+
+        AnimatorSet animatorSet = new AnimatorSet();
+        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+        appAnimator.setDuration(APP_LAUNCH_DURATION);
+        appAnimator.setInterpolator(LINEAR);
+        appAnimator.addListener(floatingView);
+        appAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                openingTargets.release();
+            }
+        });
+        floatingView.setFastFinishRunnable(animatorSet::end);
+
+        appAnimator.addUpdateListener(new MultiValueUpdateListener() {
+            float mAppWindowScale = 1;
+            final FloatProp mWidgetForegroundAlpha = new FloatProp(1 /* start */,
+                    0 /* end */, 0 /* delay */,
+                    WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
+            final FloatProp mWidgetFallbackBackgroundAlpha = new FloatProp(0 /* start */,
+                    1 /* end */, 0 /* delay */, 75 /* duration */, LINEAR);
+            final FloatProp mPreviewAlpha = new FloatProp(0 /* start */, 1 /* end */,
+                    WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* delay */,
+                    WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
+            final FloatProp mWindowRadius = new FloatProp(initialWindowRadius, finalWindowRadius,
+                    0 /* start */, RADIUS_DURATION, LINEAR);
+            final FloatProp mCornerRadiusProgress = new FloatProp(0, 1, 0, RADIUS_DURATION, LINEAR);
+
+            // Window & widget background positioning bounds
+            final FloatProp mDx = new FloatProp(widgetBackgroundBounds.centerX(),
+                    windowTargetBounds.centerX(), 0 /* delay */, APP_LAUNCH_CURVED_DURATION,
+                    EXAGGERATED_EASE);
+            final FloatProp mDy = new FloatProp(widgetBackgroundBounds.centerY(),
+                    windowTargetBounds.centerY(), 0 /* delay */, APP_LAUNCH_DURATION,
+                    EXAGGERATED_EASE);
+            final FloatProp mWidth = new FloatProp(widgetBackgroundBounds.width(),
+                    windowTargetBounds.width(), 0 /* delay */, APP_LAUNCH_DURATION,
+                    EXAGGERATED_EASE);
+            final FloatProp mHeight = new FloatProp(widgetBackgroundBounds.height(),
+                    windowTargetBounds.height(), 0 /* delay */, APP_LAUNCH_DURATION,
+                    EXAGGERATED_EASE);
+
+            @Override
+            public void onUpdate(float percent) {
+                widgetBackgroundBounds.set(mDx.value - mWidth.value / 2f,
+                        mDy.value - mHeight.value / 2f, mDx.value + mWidth.value / 2f,
+                        mDy.value + mHeight.value / 2f);
+                // Set app window scaling factor to match widget background width
+                mAppWindowScale = widgetBackgroundBounds.width() / windowTargetBounds.width();
+                // Crop scaled app window to match widget
+                appWindowCrop.set(0 /* left */, 0 /* top */,
+                        Math.round(windowTargetBounds.width()) /* right */,
+                        Math.round(widgetBackgroundBounds.height() / mAppWindowScale) /* bottom */);
+                matrix.setTranslate(widgetBackgroundBounds.left, widgetBackgroundBounds.top);
+                matrix.postScale(mAppWindowScale, mAppWindowScale, widgetBackgroundBounds.left,
+                        widgetBackgroundBounds.top);
+
+                SurfaceParams[] params = new SurfaceParams[appTargets.length];
+                float floatingViewAlpha = appTargetsAreTranslucent ? 1 - mPreviewAlpha.value : 1;
+                for (int i = appTargets.length - 1; i >= 0; i--) {
+                    RemoteAnimationTargetCompat target = appTargets[i];
+                    SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
+                    if (target.mode == MODE_OPENING) {
+                        floatingView.update(widgetBackgroundBounds, floatingViewAlpha,
+                                mWidgetForegroundAlpha.value, mWidgetFallbackBackgroundAlpha.value,
+                                mCornerRadiusProgress.value);
+                        builder.withMatrix(matrix)
+                                .withWindowCrop(appWindowCrop)
+                                .withAlpha(mPreviewAlpha.value)
+                                .withCornerRadius(mWindowRadius.value / mAppWindowScale);
+                    }
+                    params[i] = builder.build();
+                }
+                surfaceApplier.scheduleApply(params);
+            }
+        });
+
+        animatorSet.playTogether(appAnimator, getBackgroundAnimator(appTargets));
+        return animatorSet;
+    }
+
+    private ObjectAnimator getBackgroundAnimator(RemoteAnimationTargetCompat[] appTargets) {
         // When launching an app from overview that doesn't map to a task, we still want to just
         // blur the wallpaper instead of the launcher surface as well
         boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW;
@@ -754,9 +887,7 @@
                 }
             });
         }
-
-        animatorSet.playTogether(appAnimator, backgroundRadiusAnim);
-        return animatorSet;
+        return backgroundRadiusAnim;
     }
 
     /**
@@ -1120,9 +1251,13 @@
             boolean launcherClosing =
                     launcherIsATargetWithMode(appTargets, MODE_CLOSING);
 
+            final boolean launchingFromWidget = mV instanceof LauncherAppWidgetHostView;
             final boolean launchingFromRecents = isLaunchingFromRecents(mV, appTargets);
             final boolean launchingFromTaskbar = mLauncher.isViewInTaskbar(mV);
-            if (launchingFromRecents) {
+            if (launchingFromWidget) {
+                composeWidgetLaunchAnimator(anim, (LauncherAppWidgetHostView) mV, appTargets,
+                        wallpaperTargets, nonAppTargets);
+            } else if (launchingFromRecents) {
                 composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets, nonAppTargets,
                         launcherClosing);
             } else if (launchingFromTaskbar) {
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 2a6e478..c47300c 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -92,7 +92,7 @@
     };
 
     private static final String TAG = "OrientationTouchTransformer";
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
 
     private static final int QUICKSTEP_ROTATION_UNINITIALIZED = -1;
 
@@ -163,6 +163,10 @@
 
     void setNavigationMode(SysUINavigationMode.Mode newMode, Info info,
             Resources newRes) {
+        if (DEBUG) {
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "setNavigationMode new: " + newMode
+                    + " oldMode: " + mMode + " " + this);
+        }
         if (mMode == newMode) {
             return;
         }
@@ -254,10 +258,18 @@
 
         mCurrentDisplay = new CurrentDisplay(region.realSize, region.rotation);
         OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplay);
+        if (DEBUG) {
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "cached region: " + regionToKeep
+                    + " mCurrentDisplay: " + mCurrentDisplay + " " + this);
+        }
         if (regionToKeep == null) {
             regionToKeep = createRegionForDisplay(region);
         }
         mSwipeTouchRegions.clear();
+        if (DEBUG) {
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "adding region: " + regionToKeep
+                    + " mCurrentDisplay: " + mCurrentDisplay + " " + this);
+        }
         mSwipeTouchRegions.put(mCurrentDisplay, regionToKeep);
         updateAssistantRegions(regionToKeep);
     }
@@ -273,7 +285,8 @@
 
     private OrientationRectF createRegionForDisplay(Info display) {
         if (DEBUG) {
-            Log.d(TAG, "creating rotation region for: " + mCurrentDisplay.rotation);
+            Log.d(TAG, "creating rotation region for: " + mCurrentDisplay.rotation
+            + " with mode: " + mMode + " displayRotation: " + display.rotation);
         }
 
         Point size = display.realSize;
@@ -287,14 +300,19 @@
         } else {
             mAssistantLeftRegion.setEmpty();
             mAssistantRightRegion.setEmpty();
+            int navbarSize = getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
+            if (DEBUG) {
+                Log.d(TestProtocol.NO_SWIPE_TO_HOME, "else case mode: " + mMode
+                        + " getNavbarSize: " + navbarSize + " rotation: " + rotation + " " + this);
+            }
             switch (rotation) {
                 case Surface.ROTATION_90:
                     orientationRectF.left = orientationRectF.right
-                            - getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
+                            - navbarSize;
                     break;
                 case Surface.ROTATION_270:
                     orientationRectF.right = orientationRectF.left
-                            + getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
+                            + navbarSize;
                     break;
                 default:
                     orientationRectF.top = orientationRectF.bottom - touchHeight;
@@ -339,7 +357,7 @@
     boolean touchInValidSwipeRegions(float x, float y) {
         if (TestProtocol.sDebugTracing) {
             Log.d(TestProtocol.NO_SWIPE_TO_HOME, "touchInValidSwipeRegions " + x + "," + y + " in "
-                    + mLastRectTouched);
+                    + mLastRectTouched + " this: " + this);
         }
         if (mLastRectTouched != null) {
             return mLastRectTouched.contains(x, y);
@@ -462,7 +480,8 @@
                 if (DEBUG) {
                     Log.d(TAG, "Transforming rotation due to forceTransform, "
                             + "mCurrentRotation: " + mCurrentDisplay.rotation
-                            + "mRotation: " + mRotation);
+                            + "mRotation: " + mRotation
+                            + " this: " + this);
                 }
                 event.transform(mTmpMatrix);
                 return true;
@@ -473,9 +492,10 @@
 
             if (DEBUG) {
                 Log.d(TAG, "original: " + event.getX() + ", " + event.getY()
-                                + " new: " + mTmpPoint[0] + ", " + mTmpPoint[1]
-                                + " rect: " + this + " forceTransform: " + forceTransform
-                                + " contains: " + contains(mTmpPoint[0], mTmpPoint[1]));
+                        + " new: " + mTmpPoint[0] + ", " + mTmpPoint[1]
+                        + " rect: " + this + " forceTransform: " + forceTransform
+                        + " contains: " + contains(mTmpPoint[0], mTmpPoint[1])
+                        + " this: " + this);
             }
 
             if (contains(mTmpPoint[0], mTmpPoint[1])) {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index b4f1330..ef09957 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -54,6 +54,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.Surface;
 
@@ -61,6 +62,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
@@ -127,14 +129,27 @@
     private boolean mIsUserSetupComplete;
 
     public RecentsAnimationDeviceState(Context context) {
+        this(context, false);
+    }
+
+    /**
+     * @param isInstanceForTouches {@code true} if this is the persistent instance being used for
+     *                                   gesture touch handling
+     */
+    public RecentsAnimationDeviceState(Context context, boolean isInstanceForTouches) {
         mContext = context;
         mDisplayController = DisplayController.INSTANCE.get(context);
         mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
         mDisplayId = mDisplayController.getInfo().id;
         mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
         runOnDestroy(() -> mDisplayController.removeChangeListener(this));
-        mRotationTouchHelper = new RotationTouchHelper(context, mDisplayController);
-        runOnDestroy(mRotationTouchHelper::destroy);
+        mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context);
+        if (isInstanceForTouches) {
+            // rotationTouchHelper doesn't get initialized after being destroyed, so only destroy
+            // if primary TouchInteractionService instance needs to be destroyed.
+            mRotationTouchHelper.init();
+            runOnDestroy(mRotationTouchHelper::destroy);
+        }
 
         // Register for user unlocked if necessary
         mIsUserUnlocked = context.getSystemService(UserManager.class)
@@ -214,6 +229,7 @@
      * Cleans up all the registered listeners and receivers.
      */
     public void destroy() {
+        Log.d(TestProtocol.NO_SWIPE_TO_HOME, "destroying RADS", new Throwable());
         for (Runnable r : mOnDestroyActions) {
             r.run();
         }
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index f4688a1..fd0de42 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -24,6 +24,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.OrientationEventListener;
 
@@ -31,6 +32,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
+import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -43,10 +45,13 @@
         SysUINavigationMode.NavigationModeChangeListener,
         DisplayInfoChangeListener {
 
-    private final OrientationTouchTransformer mOrientationTouchTransformer;
-    private final DisplayController mDisplayController;
-    private final SysUINavigationMode mSysUiNavMode;
-    private final int mDisplayId;
+    public static final MainThreadInitializedObject<RotationTouchHelper> INSTANCE =
+            new MainThreadInitializedObject<>(RotationTouchHelper::new);
+
+    private OrientationTouchTransformer mOrientationTouchTransformer;
+    private DisplayController mDisplayController;
+    private SysUINavigationMode mSysUiNavMode;
+    private int mDisplayId;
     private int mDisplayRotation;
 
     private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
@@ -117,25 +122,46 @@
      */
     private boolean mInOverview;
     private boolean mTaskListFrozen;
-
-
     private final Context mContext;
 
-    public RotationTouchHelper(Context context, DisplayController displayController) {
+    /**
+     * Keeps track of whether destroy has been called for this instance. Mainly used for TAPL tests
+     * where multiple instances of RotationTouchHelper are being created. b/177316094
+     */
+    private boolean mNeedsInit = true;
+
+    private RotationTouchHelper(Context context) {
         mContext = context;
-        mDisplayController = displayController;
+        Log.d(TestProtocol.NO_SWIPE_TO_HOME, "RotationTouchHelper ctor init? " + mNeedsInit
+                + " " + this);
+        if (mNeedsInit) {
+            init();
+        }
+    }
+
+    public void init() {
+        if (!mNeedsInit) {
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "Did not need init? " + " " + this);
+            return;
+        }
+        Log.d(TestProtocol.NO_SWIPE_TO_HOME, "RotationTouchHelper init() " + this,
+                new Throwable());
+        mDisplayController = DisplayController.INSTANCE.get(mContext);
         Resources resources = mContext.getResources();
-        mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
+        mSysUiNavMode = SysUINavigationMode.INSTANCE.get(mContext);
         mDisplayId = mDisplayController.getInfo().id;
 
         mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
                 () -> QuickStepContract.getWindowCornerRadius(resources));
 
         // Register for navigation mode changes
-        onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(this));
+        SysUINavigationMode.Mode newMode = mSysUiNavMode.addModeChangeListener(this);
+        Log.d(TestProtocol.NO_SWIPE_TO_HOME, "AddedModeChangeListener: " + this +
+                " currentMode: " + newMode);
+        onNavigationModeChanged(newMode);
         runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
 
-        mOrientationListener = new OrientationEventListener(context) {
+        mOrientationListener = new OrientationEventListener(mContext) {
             @Override
             public void onOrientationChanged(int degrees) {
                 int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
@@ -154,6 +180,7 @@
                 }
             }
         };
+        mNeedsInit = false;
     }
 
     private void setupOrientationSwipeHandler() {
@@ -176,9 +203,11 @@
      * Cleans up all the registered listeners and receivers.
      */
     public void destroy() {
+        Log.d(TestProtocol.NO_SWIPE_TO_HOME, "destroying " + this);
         for (Runnable r : mOnDestroyActions) {
             r.run();
         }
+        mNeedsInit = true;
     }
 
     public boolean isTaskListFrozen() {
@@ -223,6 +252,7 @@
 
     @Override
     public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+        Log.d(TestProtocol.NO_SWIPE_TO_HOME, "nav mode changed: " + newMode);
         mDisplayController.removeChangeListener(this);
         mDisplayController.addChangeListener(this);
         onDisplayInfoChanged(mContext, mDisplayController.getInfo(), CHANGE_ALL);
@@ -374,4 +404,8 @@
         pw.println("  displayRotation=" + getDisplayRotation());
         mOrientationTouchTransformer.dump(pw);
     }
+
+    public OrientationTouchTransformer getOrientationTouchTransformer() {
+        return mOrientationTouchTransformer;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index b6dad2d..c87cd17 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -86,6 +86,7 @@
     @UiThread
     public RecentsAnimationCallbacks startRecentsAnimation(GestureState gestureState,
             Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) {
+        Log.d("b/186444448", "startRecentsAnimation");
         // Notify if recents animation is still running
         if (mController != null) {
             String msg = "New recents animation started before old animation completed";
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index f3fe0b4..c74f53b 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -300,7 +300,9 @@
         // Everything else should be initialized in onUserUnlocked() below.
         mMainChoreographer = Choreographer.getInstance();
         mAM = ActivityManagerWrapper.getInstance();
-        mDeviceState = new RecentsAnimationDeviceState(this);
+        mDeviceState = new RecentsAnimationDeviceState(this, true);
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "RADS OTT instance: " +
+                    mDeviceState.getRotationTouchHelper().getOrientationTouchTransformer());
         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
         mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
         mDeviceState.addOneHandedModeChangedCallback(this::onOneHandedModeOverlayChanged);
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
new file mode 100644
index 0000000..f74aa55
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
@@ -0,0 +1,191 @@
+/*
+ * 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.quickstep.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.RemoteViews.RemoteViewOutlineProvider;
+
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.RoundedCornerEnforcement;
+
+import java.util.stream.IntStream;
+
+/**
+ * Mimics the appearance of the background view of a {@link LauncherAppWidgetHostView} through a
+ * an App Widget activity launch animation.
+ */
+@TargetApi(Build.VERSION_CODES.S)
+final class FloatingWidgetBackgroundView extends View {
+    private final ColorDrawable mFallbackDrawable = new ColorDrawable();
+    private final DrawableProperties mForegroundProperties = new DrawableProperties();
+    private final DrawableProperties mBackgroundProperties = new DrawableProperties();
+
+    private Drawable mOriginalForeground;
+    private Drawable mOriginalBackground;
+    private float mFinalRadius;
+    private float mInitialOutlineRadius;
+    private float mOutlineRadius;
+    private boolean mIsUsingFallback;
+    private View mSourceView;
+
+    FloatingWidgetBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius);
+            }
+        });
+        setClipToOutline(true);
+    }
+
+    void init(LauncherAppWidgetHostView hostView, View backgroundView, float finalRadius) {
+        mFinalRadius = finalRadius;
+        mSourceView = backgroundView;
+        mInitialOutlineRadius = getOutlineRadius(hostView, backgroundView);
+        mIsUsingFallback = false;
+        if (isSupportedDrawable(backgroundView.getForeground())) {
+            mOriginalForeground = backgroundView.getForeground();
+            mForegroundProperties.init(
+                    mOriginalForeground.getConstantState().newDrawable().mutate());
+            setForeground(mForegroundProperties.mDrawable);
+            mSourceView.setForeground(null);
+        }
+        if (isSupportedDrawable(backgroundView.getBackground())) {
+            mOriginalBackground = backgroundView.getBackground();
+            mBackgroundProperties.init(
+                    mOriginalBackground.getConstantState().newDrawable().mutate());
+            setBackground(mBackgroundProperties.mDrawable);
+            mSourceView.setBackground(null);
+        } else if (mOriginalForeground == null) {
+            mFallbackDrawable.setColor(Themes.getColorBackground(backgroundView.getContext()));
+            setBackground(mFallbackDrawable);
+            mIsUsingFallback = true;
+        }
+    }
+
+    /** Update the animated properties of the drawables. */
+    void update(float cornerRadiusProgress, float fallbackAlpha) {
+        if (isUninitialized()) return;
+        mOutlineRadius = mInitialOutlineRadius + (mFinalRadius - mInitialOutlineRadius)
+                * cornerRadiusProgress;
+        mForegroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress);
+        mBackgroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress);
+        setAlpha(mIsUsingFallback ? fallbackAlpha : 1f);
+    }
+
+    /** Restores the drawables to the source view. */
+    void finish() {
+        if (isUninitialized()) return;
+        mSourceView.setForeground(mOriginalForeground);
+        mSourceView.setBackground(mOriginalBackground);
+    }
+
+    void recycle() {
+        mSourceView = null;
+        mOriginalForeground = null;
+        mOriginalBackground = null;
+        mOutlineRadius = 0;
+        mFinalRadius = 0;
+        setForeground(null);
+        setBackground(null);
+    }
+
+    /** Get the largest of drawable corner radii or background view outline radius. */
+    float getMaximumRadius() {
+        if (isUninitialized()) return 0;
+        return Math.max(mInitialOutlineRadius, Math.max(getMaxRadius(mOriginalForeground),
+                getMaxRadius(mOriginalBackground)));
+    }
+
+    private boolean isUninitialized() {
+        return mSourceView == null;
+    }
+
+    /** Returns the maximum corner radius of {@param drawable}. */
+    private static float getMaxRadius(Drawable drawable) {
+        if (!(drawable instanceof GradientDrawable)) return 0;
+        float[] cornerRadii = ((GradientDrawable) drawable).getCornerRadii();
+        float cornerRadius = ((GradientDrawable) drawable).getCornerRadius();
+        double radiiMax = cornerRadii == null ? 0 : IntStream.range(0, cornerRadii.length)
+                .mapToDouble(i -> cornerRadii[i]).max().orElse(0);
+        return Math.max(cornerRadius, (float) radiiMax);
+    }
+
+    /** Returns whether the given drawable type is supported. */
+    private static boolean isSupportedDrawable(Drawable drawable) {
+        return drawable instanceof ColorDrawable || (drawable instanceof GradientDrawable
+                && ((GradientDrawable) drawable).getShape() == GradientDrawable.RECTANGLE);
+    }
+
+    /** Corner radius from source view's outline, or enforced view. */
+    private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) {
+        if (RoundedCornerEnforcement.isRoundedCornerEnabled()
+                && hostView.hasEnforcedCornerRadius()) {
+            return hostView.getEnforcedCornerRadius();
+        } else if (v.getOutlineProvider() instanceof RemoteViewOutlineProvider
+                && v.getClipToOutline()) {
+            return ((RemoteViewOutlineProvider) v.getOutlineProvider()).getRadius();
+        }
+        return 0;
+    }
+
+    /** Stores and modifies a drawable's properties through an animation. */
+    private static class DrawableProperties {
+        private Drawable mDrawable;
+        private float mOriginalRadius;
+        private float[] mOriginalRadii;
+        private final float[] mTmpRadii = new float[8];
+
+        /** Store a drawable's animated properties. */
+        void init(Drawable drawable) {
+            mDrawable = drawable;
+            if (!(drawable instanceof GradientDrawable)) return;
+            mOriginalRadius = ((GradientDrawable) drawable).getCornerRadius();
+            mOriginalRadii = ((GradientDrawable) drawable).getCornerRadii();
+        }
+
+        /**
+         * Update the drawable for the given animation state.
+         *
+         * @param finalRadius the radius of each corner when {@param progress} is 1
+         * @param progress    the linear progress of the corner radius from its original value to
+         *                    {@param finalRadius}
+         */
+        void updateDrawable(float finalRadius, float progress) {
+            if (!(mDrawable instanceof GradientDrawable)) return;
+            GradientDrawable d = (GradientDrawable) mDrawable;
+            if (mOriginalRadii != null) {
+                for (int i = 0; i < mOriginalRadii.length; i++) {
+                    mTmpRadii[i] = mOriginalRadii[i] + (finalRadius - mOriginalRadii[i]) * progress;
+                }
+                d.setCornerRadii(mTmpRadii);
+            } else {
+                d.setCornerRadius(mOriginalRadius + (finalRadius - mOriginalRadius) * progress);
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
new file mode 100644
index 0000000..d23884c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
@@ -0,0 +1,250 @@
+/*
+ * 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.quickstep.views;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.GhostView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.views.ListenerView;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.RoundedCornerEnforcement;
+
+/** A view that mimics an App Widget through a launch animation. */
+@TargetApi(Build.VERSION_CODES.S)
+public class FloatingWidgetView extends FrameLayout implements AnimatorListener {
+    private static final Matrix sTmpMatrix = new Matrix();
+
+    private final Launcher mLauncher;
+    private final ListenerView mListenerView;
+    private final FloatingWidgetBackgroundView mBackgroundView;
+    private final RectF mBackgroundOffset = new RectF();
+
+    private LauncherAppWidgetHostView mAppWidgetView;
+    private View mAppWidgetBackgroundView;
+    private RectF mBackgroundPosition;
+    private GhostView mForegroundOverlayView;
+
+    private Runnable mEndRunnable;
+    private Runnable mFastFinishRunnable;
+
+    public FloatingWidgetView(Context context) {
+        this(context, null);
+    }
+
+    public FloatingWidgetView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public FloatingWidgetView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+        mListenerView = new ListenerView(context, attrs);
+        mBackgroundView = new FloatingWidgetBackgroundView(context, attrs, defStyleAttr);
+        addView(mBackgroundView);
+        setWillNotDraw(false);
+    }
+
+    @Override
+    public void onAnimationEnd(Animator animator) {
+        Runnable endRunnable = mEndRunnable;
+        mEndRunnable = null;
+        if (endRunnable != null) {
+            endRunnable.run();
+        }
+    }
+
+    @Override
+    public void onAnimationStart(Animator animator) {
+    }
+
+    @Override
+    public void onAnimationCancel(Animator animator) {
+    }
+
+    @Override
+    public void onAnimationRepeat(Animator animator) {
+    }
+
+    /** Sets a runnable that is called after a call to {@link #fastFinish()}. */
+    public void setFastFinishRunnable(Runnable runnable) {
+        mFastFinishRunnable = runnable;
+    }
+
+    /** Callback at the end or early exit of the animation. */
+    public void fastFinish() {
+        if (isUninitialized()) return;
+        Runnable fastFinishRunnable = mFastFinishRunnable;
+        if (fastFinishRunnable != null) {
+            fastFinishRunnable.run();
+        }
+        Runnable endRunnable = mEndRunnable;
+        mEndRunnable = null;
+        if (endRunnable != null) {
+            endRunnable.run();
+        }
+    }
+
+    private void init(DragLayer dragLayer, LauncherAppWidgetHostView originalView,
+            RectF widgetBackgroundPosition, Rect windowTargetBounds, float windowCornerRadius) {
+        mAppWidgetView = originalView;
+        mAppWidgetView.beginDeferringUpdates();
+        mBackgroundPosition = widgetBackgroundPosition;
+        mEndRunnable = () -> finish(dragLayer);
+
+        mAppWidgetBackgroundView = RoundedCornerEnforcement.findBackground(mAppWidgetView);
+        if (mAppWidgetBackgroundView == null) {
+            mAppWidgetBackgroundView = mAppWidgetView;
+        }
+
+        getRelativePosition(mAppWidgetBackgroundView, dragLayer, mBackgroundPosition);
+        getRelativePosition(mAppWidgetBackgroundView, mAppWidgetView, mBackgroundOffset);
+        mBackgroundView.init(mAppWidgetView, mAppWidgetBackgroundView, windowCornerRadius);
+        // Layout call before GhostView creation so that the overlaid view isn't clipped
+        layout(0, 0, windowTargetBounds.width(), windowTargetBounds.height());
+        mForegroundOverlayView = GhostView.addGhost(mAppWidgetView, this);
+        positionViews();
+
+        mListenerView.setListener(this::fastFinish);
+        dragLayer.addView(mListenerView);
+    }
+
+    /**
+     * Updates the position and opacity of the floating widget's components.
+     *
+     * @param backgroundPosition      the new position of the widget's background relative to the
+     *                                {@link FloatingWidgetView}'s parent
+     * @param floatingWidgetAlpha     the overall opacity of the {@link FloatingWidgetView}
+     * @param foregroundAlpha         the opacity of the foreground layer
+     * @param fallbackBackgroundAlpha the opacity of the fallback background used when the App
+     *                                Widget doesn't have a background
+     * @param cornerRadiusProgress    progress of the corner radius animation, where 0 is the
+     *                                original radius and 1 is the window radius
+     */
+    public void update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha,
+            float fallbackBackgroundAlpha, float cornerRadiusProgress) {
+        if (isUninitialized()) return;
+        setAlpha(floatingWidgetAlpha);
+        mBackgroundView.update(cornerRadiusProgress, fallbackBackgroundAlpha);
+        mAppWidgetView.setAlpha(foregroundAlpha);
+        mBackgroundPosition = backgroundPosition;
+        positionViews();
+    }
+
+    /** Sets the layout parameters of the floating view and its background view child. */
+    private void positionViews() {
+        LayoutParams layoutParams = (LayoutParams) getLayoutParams();
+        layoutParams.setMargins(0, 0, 0, 0);
+        setLayoutParams(layoutParams);
+
+        // FloatingWidgetView layout is forced LTR
+        mBackgroundView.setTranslationX(mBackgroundPosition.left);
+        mBackgroundView.setTranslationY(mBackgroundPosition.top);
+        LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams();
+        backgroundParams.leftMargin = 0;
+        backgroundParams.topMargin = 0;
+        backgroundParams.width = (int) mBackgroundPosition.width();
+        backgroundParams.height = (int) mBackgroundPosition.height();
+        mBackgroundView.setLayoutParams(backgroundParams);
+
+        sTmpMatrix.reset();
+        float foregroundScale = mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth();
+        sTmpMatrix.setTranslate(-mBackgroundOffset.left - mAppWidgetView.getLeft(),
+                -mBackgroundOffset.top - mAppWidgetView.getTop());
+        sTmpMatrix.postScale(foregroundScale, foregroundScale);
+        sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top);
+        mForegroundOverlayView.setMatrix(sTmpMatrix);
+    }
+
+    private void finish(DragLayer dragLayer) {
+        mAppWidgetView.setAlpha(1f);
+        GhostView.removeGhost(mAppWidgetView);
+        ((ViewGroup) dragLayer.getParent()).removeView(this);
+        dragLayer.removeView(mListenerView);
+        mBackgroundView.finish();
+        mAppWidgetView.endDeferringUpdates();
+        recycle();
+        mLauncher.getViewCache().recycleView(R.layout.floating_widget_view, this);
+    }
+
+    public float getInitialCornerRadius() {
+        return mBackgroundView.getMaximumRadius();
+    }
+
+    private boolean isUninitialized() {
+        return mForegroundOverlayView == null;
+    }
+
+    private void recycle() {
+        mEndRunnable = null;
+        mFastFinishRunnable = null;
+        mBackgroundPosition = null;
+        mListenerView.setListener(null);
+        mAppWidgetView = null;
+        mForegroundOverlayView = null;
+        mAppWidgetBackgroundView = null;
+        mBackgroundView.recycle();
+    }
+
+    /**
+     * Configures and returns a an instance of {@link FloatingWidgetView} matching the appearance of
+     * {@param originalView}.
+     *
+     * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's
+     *                                 background bounds
+     * @param windowTargetBounds       the bounds of the window when launched
+     * @param windowCornerRadius       the corner radius of the window
+     */
+    public static FloatingWidgetView getFloatingWidgetView(Launcher launcher,
+            LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition,
+            Rect windowTargetBounds, float windowCornerRadius) {
+        final DragLayer dragLayer = launcher.getDragLayer();
+        ViewGroup parent = (ViewGroup) dragLayer.getParent();
+        FloatingWidgetView floatingView =
+                launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent);
+        floatingView.recycle();
+
+        floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowTargetBounds,
+                windowCornerRadius);
+        parent.addView(floatingView);
+        return floatingView;
+    }
+
+    private static void getRelativePosition(View descendant, View ancestor, RectF position) {
+        float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()};
+        Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points,
+                false /* includeRootScroll */);
+        position.set(
+                Math.min(points[0], points[2]),
+                Math.min(points[1], points[3]),
+                Math.max(points[0], points[2]),
+                Math.max(points[1], points[3]));
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 41076f3..596f746 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1277,23 +1277,20 @@
         }
 
         float accumulatedTranslationX = 0;
-        float[] fullscreenTranslations = new float[taskCount];
         for (int i = 0; i < taskCount; i++) {
             TaskView taskView = getTaskViewAt(i);
             taskView.updateTaskSize();
-            fullscreenTranslations[i] += accumulatedTranslationX;
+            taskView.getPrimaryFullscreenTranslationProperty().set(taskView,
+                    accumulatedTranslationX);
+            taskView.getSecondaryFullscreenTranslationProperty().set(taskView, 0f);
             // Compensate space caused by TaskView scaling.
             float widthDiff =
                     taskView.getLayoutParams().width * (1 - taskView.getFullscreenScale());
             // Compensate page spacing widening caused by RecentsView scaling.
             widthDiff += mPageSpacing * (1 - 1 / mFullscreenScale);
-            float fullscreenTranslationX = mIsRtl ? widthDiff : -widthDiff;
-            accumulatedTranslationX += fullscreenTranslationX;
+            accumulatedTranslationX += mIsRtl ? widthDiff : -widthDiff;
         }
 
-        for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).setFullscreenTranslationX(fullscreenTranslations[i]);
-        }
         mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX);
 
         updateGridProperties();
@@ -1600,8 +1597,9 @@
      * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
      */
     public void onSwipeUpAnimationSuccess() {
+        Log.d("b/186444448", "onSwipeUpAnimationSuccess");
         if (getRunningTaskView() != null) {
-            animateUpRunningTaskIconScale(0f);
+            animateUpRunningTaskIconScale();
         }
         setSwipeDownShouldLaunchApp(true);
     }
@@ -1664,6 +1662,7 @@
      * Called when a gesture from an app has finished, and the animation to the target has ended.
      */
     public void onGestureAnimationEnd() {
+        Log.d("b/186444448", "onGestureEnd");
         mGestureActive = false;
         if (mOrientationState.setGestureActive(false)) {
             updateOrientationHandler();
@@ -1677,7 +1676,8 @@
         setRunningTaskHidden(false);
         animateUpRunningTaskIconScale();
 
-        if (!showAsGrid() || getFocusedTaskView() != null) {
+        if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS
+                && (!showAsGrid() || getFocusedTaskView() != null)) {
             animateActionsViewIn();
         }
 
@@ -1817,15 +1817,13 @@
     }
 
     public void animateUpRunningTaskIconScale() {
-        animateUpRunningTaskIconScale(0);
-    }
-
-    public void animateUpRunningTaskIconScale(float startProgress) {
         mRunningTaskIconScaledDown = false;
         TaskView firstTask = getRunningTaskView();
+        Log.d("b/186444448", "animateUpRunningTaskIconScale: firstTask="
+                + (firstTask != null ? "t:" + firstTask.getTask() : null));
         if (firstTask != null) {
+            firstTask.setIconScaleAnimStartProgress(0f);
             firstTask.animateIconScaleAndDimIntoView();
-            firstTask.setIconScaleAnimStartProgress(startProgress);
         }
     }
 
@@ -1997,29 +1995,12 @@
                 gridTranslationAnimators.add(taskDismissAnimator);
             }
             taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX);
-            taskView.setNonFullscreenTranslationX(snappedTaskFullscreenScrollAdjustment);
+            taskView.getPrimaryNonFullscreenTranslationProperty().set(taskView,
+                    snappedTaskFullscreenScrollAdjustment);
+            taskView.getSecondaryNonFullscreenTranslationProperty().set(taskView, 0f);
         }
         AnimatorSet gridTranslationAnimatorSet = new AnimatorSet();
         gridTranslationAnimatorSet.playTogether(gridTranslationAnimators);
-        gridTranslationAnimatorSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            // Allow the actions view to display again once in focus mode
-            public void onAnimationEnd(Animator animation) {
-                super.onAnimationEnd(animation);
-                if (getFocusedTaskView() == null) {
-                    mActionsView.getScrollAlpha().setValue(1);
-                }
-            }
-
-            @Override
-            // Hide the actions view if not in focus mode
-            public void onAnimationStart(Animator animation) {
-                super.onAnimationStart(animation);
-                if (getFocusedTaskView() == null) {
-                    mActionsView.getScrollAlpha().setValue(0);
-                }
-            }
-        });
         gridTranslationAnimatorSet.start();
 
         // Use the accumulated translation of the row containing the last task.
@@ -2322,7 +2303,8 @@
                         snapToPageImmediately(pageToSnapTo);
                         // Grid got messed up, reapply.
                         updateGridProperties(taskView, draggedIndex - mTaskViewStartIndex);
-                        if (showAsGrid() && getFocusedTaskView() == null) {
+                        if (showAsGrid() && getFocusedTaskView() == null
+                                && mActionsView.getVisibilityAlpha().getValue() == 1) {
                             animateActionsViewOut();
                         }
                     }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 35acdd1..3349b74 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -250,6 +250,58 @@
                 }
             };
 
+    private static final FloatProperty<TaskView> FULLSCREEN_TRANSLATION_X =
+            new FloatProperty<TaskView>("fullscreenTranslationX") {
+                @Override
+                public void setValue(TaskView taskView, float v) {
+                    taskView.setFullscreenTranslationX(v);
+                }
+
+                @Override
+                public Float get(TaskView taskView) {
+                    return taskView.mFullscreenTranslationX;
+                }
+            };
+
+    private static final FloatProperty<TaskView> FULLSCREEN_TRANSLATION_Y =
+            new FloatProperty<TaskView>("fullscreenTranslationY") {
+                @Override
+                public void setValue(TaskView taskView, float v) {
+                    taskView.setFullscreenTranslationY(v);
+                }
+
+                @Override
+                public Float get(TaskView taskView) {
+                    return taskView.mFullscreenTranslationY;
+                }
+            };
+
+    private static final FloatProperty<TaskView> NON_FULLSCREEN_TRANSLATION_X =
+            new FloatProperty<TaskView>("nonFullscreenTranslationX") {
+                @Override
+                public void setValue(TaskView taskView, float v) {
+                    taskView.setNonFullscreenTranslationX(v);
+                }
+
+                @Override
+                public Float get(TaskView taskView) {
+                    return taskView.mNonFullscreenTranslationX;
+                }
+            };
+
+    private static final FloatProperty<TaskView> NON_FULLSCREEN_TRANSLATION_Y =
+            new FloatProperty<TaskView>("nonFullscreenTranslationY") {
+                @Override
+                public void setValue(TaskView taskView, float v) {
+                    taskView.setNonFullscreenTranslationY(v);
+                }
+
+                @Override
+                public Float get(TaskView taskView) {
+                    return taskView.mNonFullscreenTranslationY;
+                }
+            };
+
     private static final FloatProperty<TaskView> COLOR_TINT =
             new FloatProperty<TaskView>("colorTint") {
                 @Override
@@ -284,9 +336,11 @@
     private float mTaskResistanceTranslationY;
     // The following translation variables should only be used in the same orientation as Launcher.
     private float mFullscreenTranslationX;
+    private float mFullscreenTranslationY;
     // Applied as a complement to fullscreenTranslation, for adjusting the carousel overview, or the
     // in transition carousel before forming the grid on tablets.
     private float mNonFullscreenTranslationX;
+    private float mNonFullscreenTranslationY;
     private float mBoxTranslationY;
     // The following grid translations scales with mGridProgress.
     private float mGridTranslationX;
@@ -740,6 +794,8 @@
     }
 
     public void animateIconScaleAndDimIntoView() {
+        Log.d("b/186444448", "animateIconScaleAndDimIntoView: startProgress="
+                + mIconScaleAnimStartProgress);
         if (mIconAndDimAnimator != null) {
             mIconAndDimAnimator.cancel();
         }
@@ -749,6 +805,7 @@
         mIconAndDimAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
+                Log.d("b/186444448", "animateIconScaleAndDimIntoView: end");
                 mIconAndDimAnimator = null;
             }
         });
@@ -786,8 +843,9 @@
 
     @Override
     public void onRecycle() {
-        mFullscreenTranslationX = mNonFullscreenTranslationX =
-                mGridTranslationX = mGridTranslationY = mBoxTranslationY = 0f;
+        mFullscreenTranslationX = mFullscreenTranslationY = mNonFullscreenTranslationX =
+                mNonFullscreenTranslationY = mGridTranslationX = mGridTranslationY =
+                        mBoxTranslationY = 0f;
         resetViewTransforms();
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
@@ -929,16 +987,26 @@
         applyTranslationY();
     }
 
-    public void setFullscreenTranslationX(float fullscreenTranslationX) {
+    private void setFullscreenTranslationX(float fullscreenTranslationX) {
         mFullscreenTranslationX = fullscreenTranslationX;
         applyTranslationX();
     }
 
-    public void setNonFullscreenTranslationX(float nonFullscreenTranslationX) {
+    private void setFullscreenTranslationY(float fullscreenTranslationY) {
+        mFullscreenTranslationY = fullscreenTranslationY;
+        applyTranslationY();
+    }
+
+    private void setNonFullscreenTranslationX(float nonFullscreenTranslationX) {
         mNonFullscreenTranslationX = nonFullscreenTranslationX;
         applyTranslationX();
     }
 
+    private void setNonFullscreenTranslationY(float nonFullscreenTranslationY) {
+        mNonFullscreenTranslationY = nonFullscreenTranslationY;
+        applyTranslationY();
+    }
+
     public void setGridTranslationX(float gridTranslationX) {
         mGridTranslationX = gridTranslationX;
         applyTranslationX();
@@ -960,9 +1028,9 @@
     public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
         float scrollAdjustment = 0;
         if (fullscreenEnabled) {
-            scrollAdjustment += mFullscreenTranslationX;
+            scrollAdjustment += getPrimaryFullscreenTranslationProperty().get(this);
         } else {
-            scrollAdjustment += mNonFullscreenTranslationX;
+            scrollAdjustment += getPrimaryNonFullscreenTranslationProperty().get(this);
         }
         if (gridEnabled) {
             scrollAdjustment += mGridTranslationX;
@@ -1012,7 +1080,10 @@
      * change according to a temporary state (e.g. task offset).
      */
     public float getPersistentTranslationY() {
-        return getGridTrans(mGridTranslationY) + mBoxTranslationY;
+        return mBoxTranslationY
+                + getFullscreenTrans(mFullscreenTranslationY)
+                + getNonFullscreenTrans(mNonFullscreenTranslationY)
+                + getGridTrans(mGridTranslationY);
     }
 
     public FloatProperty<TaskView> getPrimaryDismissTranslationProperty() {
@@ -1035,6 +1106,26 @@
                 TASK_RESISTANCE_TRANSLATION_X, TASK_RESISTANCE_TRANSLATION_Y);
     }
 
+    public FloatProperty<TaskView> getPrimaryFullscreenTranslationProperty() {
+        return getPagedOrientationHandler().getPrimaryValue(
+                FULLSCREEN_TRANSLATION_X, FULLSCREEN_TRANSLATION_Y);
+    }
+
+    public FloatProperty<TaskView> getSecondaryFullscreenTranslationProperty() {
+        return getPagedOrientationHandler().getSecondaryValue(
+                FULLSCREEN_TRANSLATION_X, FULLSCREEN_TRANSLATION_Y);
+    }
+
+    public FloatProperty<TaskView> getPrimaryNonFullscreenTranslationProperty() {
+        return getPagedOrientationHandler().getPrimaryValue(
+                NON_FULLSCREEN_TRANSLATION_X, NON_FULLSCREEN_TRANSLATION_Y);
+    }
+
+    public FloatProperty<TaskView> getSecondaryNonFullscreenTranslationProperty() {
+        return getPagedOrientationHandler().getSecondaryValue(
+                NON_FULLSCREEN_TRANSLATION_X, NON_FULLSCREEN_TRANSLATION_Y);
+    }
+
     @Override
     public boolean hasOverlappingRendering() {
         // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
diff --git a/res/layout/floating_widget_view.xml b/res/layout/floating_widget_view.xml
new file mode 100644
index 0000000..eea7a92
--- /dev/null
+++ b/res/layout/floating_widget_view.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<com.android.quickstep.views.FloatingWidgetView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layoutDirection="ltr" />
diff --git a/res/layout/widgets_full_sheet_search_and_recommendations.xml b/res/layout/widgets_full_sheet_search_and_recommendations.xml
index e322c6c..bfce01d 100644
--- a/res/layout/widgets_full_sheet_search_and_recommendations.xml
+++ b/res/layout/widgets_full_sheet_search_and_recommendations.xml
@@ -46,7 +46,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:background="@drawable/widgets_recommendation_background"
-        android:paddingVertical="8dp"
+        android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
         android:layout_marginTop="16dp"
         android:visibility="gone"/>
 </LinearLayout>
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
index f4b4130..f20af87 100644
--- a/res/layout/widgets_list_row_header.xml
+++ b/res/layout/widgets_list_row_header.xml
@@ -35,6 +35,7 @@
         tools:src="@drawable/ic_corp"/>
 
     <LinearLayout
+        android:id="@+id/app_container"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_gravity="center_vertical"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 8a160bd..600a550 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -118,6 +118,7 @@
     <dimen name="widget_cell_horizontal_padding">16dp</dimen>
     <dimen name="widget_cell_font_size">14sp</dimen>
 
+    <dimen name="recommended_widgets_table_vertical_padding">8dp</dimen>
 
     <dimen name="widget_list_top_bottom_corner_radius">28dp</dimen>
     <dimen name="widget_list_content_corner_radius">4dp</dimen>
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index f77c740..620604a 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -72,6 +72,8 @@
 
     // Maintains a list of widget ids which are supposed to be auto advanced.
     private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
+    // Maximum duration for which updates can be deferred.
+    private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000;
 
     protected final LayoutInflater mInflater;
 
@@ -110,6 +112,9 @@
             }
         }
     };
+    private final Object mUpdateLock = new Object();
+    private long mDeferUpdatesUntilMillis = 0;
+    private RemoteViews mMostRecentRemoteViews;
 
     public LauncherAppWidgetHostView(Context context) {
         super(context);
@@ -165,6 +170,11 @@
 
     @Override
     public void updateAppWidget(RemoteViews remoteViews) {
+        synchronized (mUpdateLock) {
+            mMostRecentRemoteViews = remoteViews;
+            if (SystemClock.uptimeMillis() < mDeferUpdatesUntilMillis) return;
+        }
+
         super.updateAppWidget(remoteViews);
 
         // The provider info or the views might have changed.
@@ -198,6 +208,34 @@
         return false;
     }
 
+    /**
+     * Begin deferring the application of any {@link RemoteViews} updates made through
+     * {@link #updateAppWidget(RemoteViews)} until {@link #endDeferringUpdates()} has been called or
+     * the next {@link #updateAppWidget(RemoteViews)} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS}
+     * have elapsed.
+     */
+    public void beginDeferringUpdates() {
+        synchronized (mUpdateLock) {
+            mDeferUpdatesUntilMillis = SystemClock.uptimeMillis() + UPDATE_LOCK_TIMEOUT_MILLIS;
+        }
+    }
+
+    /**
+     * Stop deferring the application of {@link RemoteViews} updates made through
+     * {@link #updateAppWidget(RemoteViews)} and apply the most recently received update.
+     */
+    public void endDeferringUpdates() {
+        RemoteViews remoteViews;
+        synchronized (mUpdateLock) {
+            mDeferUpdatesUntilMillis = 0;
+            remoteViews = mMostRecentRemoteViews;
+            mMostRecentRemoteViews = null;
+        }
+        if (remoteViews != null) {
+            updateAppWidget(remoteViews);
+        }
+    }
+
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             DragLayer dragLayer = mLauncher.getDragLayer();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index 8794a4a..ccf3187 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -19,7 +19,10 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
+import android.os.Bundle;
 import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.CheckBox;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -94,6 +97,32 @@
         mTitle = findViewById(R.id.app_title);
         mSubtitle = findViewById(R.id.app_subtitle);
         mExpandToggle = findViewById(R.id.toggle);
+        findViewById(R.id.app_container).setAccessibilityDelegate(new AccessibilityDelegate() {
+
+            @Override
+            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+                if (mIsExpanded) {
+                    info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND);
+                    info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+                } else {
+                    info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+                    info.addAction(AccessibilityNodeInfo.ACTION_EXPAND);
+                }
+                super.onInitializeAccessibilityNodeInfo(host, info);
+            }
+
+            @Override
+            public boolean performAccessibilityAction(View host, int action, Bundle args) {
+                switch (action) {
+                    case AccessibilityNodeInfo.ACTION_EXPAND:
+                    case AccessibilityNodeInfo.ACTION_COLLAPSE:
+                        callOnClick();
+                        return true;
+                    default:
+                        return super.performAccessibilityAction(host, action, args);
+                }
+            }
+        });
     }
 
     /**
@@ -106,7 +135,9 @@
         // Use the entire touch area of this view to expand / collapse an app widgets section.
         setOnClickListener(view -> {
             setExpanded(!mIsExpanded);
-            onExpandChangeListener.onExpansionChange(mIsExpanded);
+            if (onExpandChangeListener != null) {
+                onExpandChangeListener.onExpansionChange(mIsExpanded);
+            }
         });
     }
 
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 18f1be3..2d3f1a0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -42,6 +42,7 @@
     private static final String TAG = "WidgetsRecommendationTableLayout";
     private static final float DOWN_SCALE_RATIO = 0.9f;
     private static final float MAX_DOWN_SCALE_RATIO = 0.5f;
+    private final float mWidgetsRecommendationTableVerticalPadding;
     private final float mWidgetCellTextViewsHeight;
     private final float mWidgetPreviewPadding;
 
@@ -57,6 +58,8 @@
     public WidgetsRecommendationTableLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
         // There are 1 row for title, 1 row for dimension and 2 rows for description.
+        mWidgetsRecommendationTableVerticalPadding = 2 * getResources()
+                .getDimensionPixelSize(R.dimen.widget_cell_vertical_padding);
         mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size);
         mWidgetPreviewPadding = 2 * getResources()
                 .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
@@ -143,7 +146,7 @@
             return new RecommendationTableData(List.of(), previewScale);
         }
         // A naive estimation of the widgets recommendation table height without inflation.
-        float totalHeight = 0;
+        float totalHeight = mWidgetsRecommendationTableVerticalPadding;
         DeviceProfile deviceProfile = Launcher.getLauncher(getContext()).getDeviceProfile();
         for (int i = 0; i < recommendedWidgetsInTable.size(); i++) {
             List<WidgetItem> widgetItems = recommendedWidgetsInTable.get(i);