Merge "Add back screenshot button to Launcher3" into sc-v2-dev
diff --git a/Android.bp b/Android.bp
index 60ef5b1..bab994a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -258,6 +258,10 @@
         "go/res",
         "go/quickstep/res",
     ],
+    // Note the ordering here is important when it comes to resource
+    // overriding. We want the most specific resource overrides defined
+    // in QuickstepResLib to take precendece, so it should be the final
+    // dependency. See b/205278434 for how this can go wrong.
     static_libs: [
         "Launcher3CommonDepsLib",
         "QuickstepResLib",
@@ -283,11 +287,15 @@
     libs: [
         "framework-statsd",
     ],
+    // Note the ordering here is important when it comes to resource
+    // overriding. We want the most specific resource overrides defined
+    // in QuickstepResLib to take precendece, so it should be the final
+    // dependency. See b/208647810 for how this can go wrong.
     static_libs: [
-        "QuickstepResLib",
         "SystemUI-statsd",
         "SystemUISharedLib",
         "Launcher3CommonDepsLib",
+        "QuickstepResLib",
     ],
     manifest: "quickstep/AndroidManifest.xml",
     platform_apis: true,
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 151b8e4..a4bbae0 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -218,10 +218,10 @@
     <string name="taskbar_edu_closed">Taskbar education closed</string>
     <!-- Text in dialog that lets a user know how they can use the taskbar to switch apps on their device.
          [CHAR_LIMIT=60] -->
-    <string name="taskbar_edu_switch_apps" translatable="false">Use the taskbar to switch apps</string>
+    <string name="taskbar_edu_switch_apps">Use the taskbar to switch apps</string>
     <!-- Text in dialog that lets a user know how they can use the taskbar to use multiple apps at once on their device.
          [CHAR_LIMIT=60] -->
-    <string name="taskbar_edu_splitscreen" translatable="false">Drag to the side to use two apps at once</string>
+    <string name="taskbar_edu_splitscreen">Drag to the side to use two apps at once</string>
     <!-- Text in dialog that lets a user know how they can hide the taskbar on their device.
          [CHAR_LIMIT=60] -->
     <string name="taskbar_edu_stashing">Touch &amp; hold to hide the taskbar</string>
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 319833d..ce1e8b6b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -233,14 +233,6 @@
         mPropertyHolders.forEach(StatePropertyHolder::endAnimation);
     }
 
-    public void onDestroy() {
-        mPropertyHolders.clear();
-        mControllers.rotationButtonController.unregisterListeners();
-        if (mFloatingRotationButton != null) {
-            mFloatingRotationButton.hide();
-        }
-    }
-
     private void initButtons(ViewGroup navContainer, ViewGroup endContainer,
             TaskbarNavButtonController navButtonController) {
 
@@ -466,6 +458,14 @@
         }
     }
 
+    public void onDestroy() {
+        mPropertyHolders.clear();
+        mControllers.rotationButtonController.unregisterListeners();
+        if (mFloatingRotationButton != null) {
+            mFloatingRotationButton.hide();
+        }
+    }
+
     private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback {
         @Override
         public void onVisibilityChanged(boolean isVisible) {
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 8a30aad..5541a46 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -115,8 +115,6 @@
     }
 
     private HomeAnimationFactory createIconHomeAnimationFactory(View workspaceView) {
-        final ResourceProvider rp = DynamicResource.provider(mActivity);
-        final float transY = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp));
         RectF iconLocation = new RectF();
         FloatingIconView floatingIconView = getFloatingIconView(mActivity, workspaceView,
                 true /* hideOriginal */, iconLocation, false /* isOpening */);
@@ -127,19 +125,15 @@
 
         return new FloatingViewHomeAnimationFactory(floatingIconView) {
 
-            // There is a delay in loading the icon, so we need to keep the window
-            // opaque until it is ready.
-            private boolean mIsFloatingIconReady = false;
-
             @Nullable
             @Override
             protected View getViewIgnoredInWorkspaceRevealAnimation() {
                 return workspaceView;
             }
 
+            @NonNull
             @Override
             public RectF getWindowTargetRect() {
-                super.getWindowTargetRect();
                 return iconLocation;
             }
 
@@ -152,15 +146,6 @@
             }
 
             @Override
-            public boolean keepWindowOpaque() {
-                if (mIsFloatingIconReady || floatingIconView.isVisibleToUser()) {
-                    mIsFloatingIconReady = true;
-                    return false;
-                }
-                return true;
-            }
-
-            @Override
             public void update(RectF currentRect, float progress, float radius) {
                 super.update(currentRect, progress, radius);
                 floatingIconView.update(1f /* alpha */, 255 /* fgAlpha */, currentRect, progress,
@@ -215,11 +200,6 @@
             }
 
             @Override
-            public boolean keepWindowOpaque() {
-                return false;
-            }
-
-            @Override
             public void update(RectF currentRect, float progress, float radius) {
                 super.update(currentRect, progress, radius);
                 final float fallbackBackgroundAlpha =
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index c5f4a53..097850f 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -36,6 +36,7 @@
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.StagedSplitBounds;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.function.Consumer;
@@ -219,6 +220,26 @@
         return newTasks;
     }
 
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "RecentTasksList:");
+        writer.println(prefix + "  mChangeId=" + mChangeId);
+        writer.println(prefix + "  mResultsUi=[id=" + mResultsUi.mRequestId + ", tasks=");
+        for (GroupTask task : mResultsUi) {
+            writer.println(prefix + "    t1=" + task.task1.key.id
+                    + " t2=" + (task.hasMultipleTasks() ? task.task2.key.id : "-1"));
+        }
+        writer.println(prefix + "  ]");
+        int currentUserId = Process.myUserHandle().getIdentifier();
+        ArrayList<GroupedRecentTaskInfo> rawTasks =
+                mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
+        writer.println(prefix + "  rawTasks=[");
+        for (GroupedRecentTaskInfo task : rawTasks) {
+            writer.println(prefix + "    t1=" + task.mTaskInfo1.taskId
+                    + " t2=" + (task.mTaskInfo2 != null ? task.mTaskInfo2.taskId : "-1"));
+        }
+        writer.println(prefix + "  ]");
+    }
+
     private static class TaskLoadResult extends ArrayList<GroupTask> {
 
         final int mRequestId;
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index e539a8c..5d77a6e 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -43,6 +43,7 @@
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -220,6 +221,11 @@
         mThumbnailChangeListeners.remove(listener);
     }
 
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "RecentsModel:");
+        mTaskList.dump("  ", writer);
+    }
+
     /**
      * Listener for receiving various task properties changes
      */
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index b36cb0a..8e9b668 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -183,8 +183,6 @@
 
         public void setAnimation(RectFSpringAnim anim) { }
 
-        public boolean keepWindowOpaque() { return false; }
-
         public void update(RectF currentRect, float progress, float radius) { }
 
         public void onCancel() { }
@@ -338,9 +336,6 @@
             mMatrix.setRectToRect(mCropRectF, mWindowCurrentRect, ScaleToFit.FILL);
             float cornerRadius = Utilities.mapRange(progress, mStartRadius, mEndRadius);
             float alpha = mAnimationFactory.getWindowAlpha(progress);
-            if (mAnimationFactory.keepWindowOpaque()) {
-                alpha = 1f;
-            }
             mLocalTransformParams
                     .setTargetAlpha(alpha)
                     .setCornerRadius(cornerRadius);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 6c623bc..67e7f88 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -83,14 +83,16 @@
         MAIN_EXECUTOR.execute(() -> clearProxy());
     };
 
-    // Save the listeners passed into the proxy since when set/register these listeners,
-    // setProxy may not have been called, eg. OverviewProxyService is not connected yet.
-    private IPipAnimationListener mPendingPipAnimationListener;
-    private ISplitScreenListener mPendingSplitScreenListener;
-    private IStartingWindowListener mPendingStartingWindowListener;
-    private ISmartspaceCallback mPendingSmartspaceCallback;
-    private IRecentTasksListener mPendingRecentTasksListener;
-    private final ArrayList<RemoteTransitionCompat> mPendingRemoteTransitions = new ArrayList<>();
+    // Save the listeners passed into the proxy since OverviewProxyService may not have been bound
+    // yet, and we'll need to set/register these listeners with SysUI when they do.  Note that it is
+    // up to the caller to clear the listeners to prevent leaks as these can be held indefinitely
+    // in case SysUI needs to rebind.
+    private IPipAnimationListener mPipAnimationListener;
+    private ISplitScreenListener mSplitScreenListener;
+    private IStartingWindowListener mStartingWindowListener;
+    private ISmartspaceCallback mSmartspaceCallback;
+    private IRecentTasksListener mRecentTasksListener;
+    private final ArrayList<RemoteTransitionCompat> mRemoteTransitions = new ArrayList<>();
 
     // Used to dedupe calls to SystemUI
     private int mLastShelfHeight;
@@ -167,29 +169,23 @@
         mRecentTasks = recentTasks;
         linkToDeath();
         // re-attach the listeners once missing due to setProxy has not been initialized yet.
-        if (mPendingPipAnimationListener != null && mPip != null) {
-            setPinnedStackAnimationListener(mPendingPipAnimationListener);
-            mPendingPipAnimationListener = null;
+        if (mPipAnimationListener != null && mPip != null) {
+            setPinnedStackAnimationListener(mPipAnimationListener);
         }
-        if (mPendingSplitScreenListener != null && mSplitScreen != null) {
-            registerSplitScreenListener(mPendingSplitScreenListener);
-            mPendingSplitScreenListener = null;
+        if (mSplitScreenListener != null && mSplitScreen != null) {
+            registerSplitScreenListener(mSplitScreenListener);
         }
-        if (mPendingStartingWindowListener != null && mStartingWindow != null) {
-            setStartingWindowListener(mPendingStartingWindowListener);
-            mPendingStartingWindowListener = null;
+        if (mStartingWindowListener != null && mStartingWindow != null) {
+            setStartingWindowListener(mStartingWindowListener);
         }
-        if (mPendingSmartspaceCallback != null && mSmartspaceTransitionController != null) {
-            setSmartspaceCallback(mPendingSmartspaceCallback);
-            mPendingSmartspaceCallback = null;
+        if (mSmartspaceCallback != null && mSmartspaceTransitionController != null) {
+            setSmartspaceCallback(mSmartspaceCallback);
         }
-        for (int i = mPendingRemoteTransitions.size() - 1; i >= 0; --i) {
-            registerRemoteTransition(mPendingRemoteTransitions.get(i));
+        for (int i = mRemoteTransitions.size() - 1; i >= 0; --i) {
+            registerRemoteTransition(mRemoteTransitions.get(i));
         }
-        mPendingRemoteTransitions.clear();
-        if (mPendingRecentTasksListener != null && mRecentTasks != null) {
-            registerRecentTasksListener(mPendingRecentTasksListener);
-            mPendingRecentTasksListener = null;
+        if (mRecentTasksListener != null && mRecentTasks != null) {
+            registerRecentTasksListener(mRecentTasksListener);
         }
 
         if (mPendingSetNavButtonAlpha != null) {
@@ -513,9 +509,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setPinnedStackAnimationListener", e);
             }
-        } else {
-            mPendingPipAnimationListener = listener;
         }
+        mPipAnimationListener = listener;
     }
 
     public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
@@ -553,9 +548,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerSplitScreenListener");
             }
-        } else {
-            mPendingSplitScreenListener = listener;
         }
+        mSplitScreenListener = listener;
     }
 
     public void unregisterSplitScreenListener(ISplitScreenListener listener) {
@@ -566,7 +560,7 @@
                 Log.w(TAG, "Failed call unregisterSplitScreenListener");
             }
         }
-        mPendingSplitScreenListener = null;
+        mSplitScreenListener = null;
     }
 
     /** Start multiple tasks in split-screen simultaneously. */
@@ -687,8 +681,9 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerRemoteTransition");
             }
-        } else {
-            mPendingRemoteTransitions.add(remoteTransition);
+        }
+        if (!mRemoteTransitions.contains(remoteTransition)) {
+            mRemoteTransitions.add(remoteTransition);
         }
     }
 
@@ -700,7 +695,7 @@
                 Log.w(TAG, "Failed call registerRemoteTransition");
             }
         }
-        mPendingRemoteTransitions.remove(remoteTransition);
+        mRemoteTransitions.remove(remoteTransition);
     }
 
     //
@@ -717,9 +712,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setStartingWindowListener", e);
             }
-        } else {
-            mPendingStartingWindowListener = listener;
         }
+        mStartingWindowListener = listener;
     }
 
     //
@@ -733,9 +727,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setStartingWindowListener", e);
             }
-        } else {
-            mPendingSmartspaceCallback = callback;
         }
+        mSmartspaceCallback = callback;
     }
 
     //
@@ -749,9 +742,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerRecentTasksListener", e);
             }
-        } else {
-            mPendingRecentTasksListener = listener;
         }
+        mRecentTasksListener = listener;
     }
 
     public void unregisterRecentTasksListener(IRecentTasksListener listener) {
@@ -762,7 +754,7 @@
                 Log.w(TAG, "Failed call unregisterRecentTasksListener");
             }
         }
-        mPendingRecentTasksListener = null;
+        mRecentTasksListener = null;
     }
 
     public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId) {
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 2c7fd71..f6f2cf9 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -973,6 +973,7 @@
             pw.println("  resumed=" + resumed);
             pw.println("  mConsumer=" + mConsumer.getName());
             ActiveGestureLog.INSTANCE.dump("", pw);
+            RecentsModel.INSTANCE.get(this).dump("", pw);
             pw.println("ProtoTrace:");
             pw.println("  file=" + ProtoTracer.INSTANCE.get(this).getTraceFile());
         }
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 9311261..b215ef1 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -249,7 +249,7 @@
         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
 
         getPagedOrientationHandler().setSplitIconParams(mIconView, mIconView2,
-                taskIconHeight, mSnapshotView.getWidth(), mSnapshotView.getHeight(),
+                taskIconHeight, mSnapshotView.getMeasuredWidth(), mSnapshotView.getMeasuredHeight(),
                 isRtl, deviceProfile, mSplitBoundsConfig);
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index fcc6272..0294828 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -16,8 +16,6 @@
 
 package com.android.quickstep.views;
 
-import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
-
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -35,7 +33,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
-import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.quickstep.SysUINavigationMode;
@@ -155,7 +152,7 @@
     public void setInsets(Rect insets) {
         mInsets.set(insets);
         updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
-        updatePaddingAndTranslations();
+        updateHorizontalPadding();
     }
 
     public void updateHiddenFlags(@ActionsHiddenFlags int visibilityFlags, boolean enable) {
@@ -198,37 +195,8 @@
         return mMultiValueAlpha.getProperty(INDEX_FULLSCREEN_ALPHA);
     }
 
-    /**
-     * Aligns OverviewActionsView vertically with and offsets horizontal position based on
-     * 3 button nav container in taskbar.
-     */
-    private void updatePaddingAndTranslations() {
-        boolean alignFor3ButtonTaskbar = mDp.isTaskbarPresent &&
-                SysUINavigationMode.getMode(getContext()) == THREE_BUTTONS;
-        if (alignFor3ButtonTaskbar) {
-            // Add extra horizontal spacing
-            int additionalPadding = ApiWrapper.getHotseatEndOffset(getContext());
-            if (isLayoutRtl()) {
-                setPadding(mInsets.left + additionalPadding, 0, mInsets.right, 0);
-            } else {
-                setPadding(mInsets.left, 0, mInsets.right + additionalPadding, 0);
-            }
-
-            // Align vertically, using taskbar height + mDp.taskbarOffsetY() to guestimate
-            // where the button nav top is
-            View startActionView = findViewById(R.id.action_screenshot);
-            int marginBottom = getOverviewActionsBottomMarginPx(
-                    SysUINavigationMode.getMode(getContext()), mDp);
-            int actionsTop = (mDp.heightPx - marginBottom - mInsets.bottom);
-            int navTop = mDp.heightPx - (mDp.taskbarSize + mDp.getTaskbarOffsetY());
-            int transY = navTop - actionsTop
-                    + ((mDp.taskbarSize - startActionView.getHeight()) / 2);
-            setTranslationY(transY);
-        } else {
-            setPadding(mInsets.left, 0, mInsets.right, 0);
-            setTranslationX(0);
-            setTranslationY(0);
-        }
+    private void updateHorizontalPadding() {
+        setPadding(mInsets.left, 0, mInsets.right, 0);
     }
 
     /** Updates vertical margins for different navigation mode or configuration changes. */
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 8c98c38..2ad586d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1148,7 +1148,7 @@
             for (int i = 0; i < taskCount; i++) {
                 View v = getTaskViewAt(i);
                 if (!(v instanceof GroupedTaskView)) {
-                    continue;
+                    return;
                 }
                 GroupedTaskView gtv = (GroupedTaskView) v;
                 gtv.onTaskListVisibilityChanged(false);
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3efaddc..f3c2143 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -300,6 +300,7 @@
     <dimen name="snackbar_elevation">3dp</dimen>
     <dimen name="snackbar_min_text_size">12sp</dimen>
     <dimen name="snackbar_max_text_size">14sp</dimen>
+    <dimen name="snackbar_max_width">504dp</dimen>
 
 <!-- Developer Options -->
     <dimen name="developer_options_filter_margins">10dp</dimen>
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index b9f1b66..cb1ba7d 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -541,17 +541,18 @@
 
         if (deviceProfile.isLandscape) {
             primaryIconParams.gravity = TOP | START;
-            primaryIconView.setTranslationX(primarySnapshotWidth - primaryIconView.getWidth());
+            primaryIconView.setTranslationX(
+                    primarySnapshotWidth - primaryIconView.getMeasuredWidth());
             primaryIconView.setTranslationY(0);
             secondaryIconParams.gravity = TOP | START;
             secondaryIconView.setTranslationX(primarySnapshotWidth + dividerBar);
         } else {
             primaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
-            primaryIconView.setTranslationX(-(primaryIconView.getWidth()) / 2f);
+            primaryIconView.setTranslationX(-(primaryIconView.getMeasuredWidth()) / 2f);
             primaryIconView.setTranslationY(0);
 
             secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
-            secondaryIconView.setTranslationX(secondaryIconView.getWidth() / 2f);
+            secondaryIconView.setTranslationX(secondaryIconView.getMeasuredWidth() / 2f);
         }
         secondaryIconView.setTranslationY(0);
         primaryIconView.setLayoutParams(primaryIconParams);
diff --git a/src/com/android/launcher3/views/Snackbar.java b/src/com/android/launcher3/views/Snackbar.java
index f945819..e582114 100644
--- a/src/com/android/launcher3/views/Snackbar.java
+++ b/src/com/android/launcher3/views/Snackbar.java
@@ -88,9 +88,14 @@
         int maxMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_max_margin_left_right);
         int minMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_min_margin_left_right);
         int marginBottom = res.getDimensionPixelSize(R.dimen.snackbar_margin_bottom);
+        int absoluteMaxWidth = res.getDimensionPixelSize(R.dimen.snackbar_max_width);
         Rect insets = activity.getDeviceProfile().getInsets();
-        int maxWidth = dragLayer.getWidth() - minMarginLeftRight * 2 - insets.left - insets.right;
-        int minWidth = dragLayer.getWidth() - maxMarginLeftRight * 2 - insets.left - insets.right;
+        int maxWidth = Math.min(
+                dragLayer.getWidth() - minMarginLeftRight * 2 - insets.left - insets.right,
+                absoluteMaxWidth);
+        int minWidth = Math.min(
+                dragLayer.getWidth() - maxMarginLeftRight * 2 - insets.left - insets.right,
+                absoluteMaxWidth);
         params.width = minWidth;
         params.setMargins(0, 0, 0, marginBottom + insets.bottom);
 
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index c90d283..41c7c37 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -80,12 +80,8 @@
         assertTrue(message, failed);
     }
 
-    private int pagesPerScreen() {
-        return mLauncher.isTwoPanels() ? 2 : 1;
-    }
-
-    private boolean isWorkspaceScrollable(Launcher launcher) {
-        return launcher.getWorkspace().getPageCount() > pagesPerScreen();
+    public static boolean isWorkspaceScrollable(Launcher launcher) {
+        return launcher.getWorkspace().getPageCount() > launcher.getWorkspace().getPanelCount();
     }
 
     private int getCurrentWorkspacePage(Launcher launcher) {
@@ -192,7 +188,7 @@
         executeOnLauncher(
                 launcher -> assertEquals(
                         "Ensuring workspace scrollable didn't switch to next screen",
-                        pagesPerScreen(), getCurrentWorkspacePage(launcher)));
+                        workspace.pagesPerScreen(), getCurrentWorkspacePage(launcher)));
         executeOnLauncher(
                 launcher -> assertTrue("ensureScrollable didn't make workspace scrollable",
                         isWorkspaceScrollable(launcher)));
@@ -209,7 +205,7 @@
         workspace.flingForward();
         executeOnLauncher(
                 launcher -> assertEquals("Flinging forward didn't switch workspace to next screen",
-                        pagesPerScreen(), getCurrentWorkspacePage(launcher)));
+                        workspace.pagesPerScreen(), getCurrentWorkspacePage(launcher)));
         assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL));
 
         // Test starting a workspace app.
diff --git a/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java
new file mode 100644
index 0000000..b048cd4
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.ui.workspace;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.TaplTestsLauncher3;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Tests for two panel workspace.
+ *
+ * Note running these tests will clear the workspace on the device.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest {
+
+    Workspace mWorkspace;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        TaplTestsLauncher3.initialize(this);
+        mWorkspace = mLauncher.getWorkspace();
+    }
+
+    @Test
+    public void testDragIconToRightPanel() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Chrome"), 1);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Maps", "Play Store");
+            assertItemsOnPage(launcher, 1, "Chrome");
+        });
+    }
+
+    @Test
+    public void testDragIconToPage2() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 2, "Maps");
+            assertPageEmpty(launcher, 3);
+        });
+    }
+
+    @Test
+    public void testDragIconToPage3() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Phone"), 3);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+            assertPageEmpty(launcher, 2);
+            assertItemsOnPage(launcher, 3, "Phone");
+        });
+    }
+
+
+    @Test
+    public void testEmptyPageDoesNotGetRemovedIfPagePairIsNotEmpty() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 3);
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Chrome"), 0);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 2, "Chrome");
+            assertItemsOnPage(launcher, 3, "Maps");
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), -1);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertItemsOnPage(launcher, 1, "Maps");
+            assertItemsOnPage(launcher, 2, "Chrome");
+            assertPageEmpty(launcher, 3);
+        });
+
+        // Move Chrome to the right panel as well, to make sure pages are not deleted whichever
+        // page is the empty one
+        mWorkspace.flingForward();
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Chrome"), 1);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertItemsOnPage(launcher, 1, "Maps");
+            assertPageEmpty(launcher, 2);
+            assertItemsOnPage(launcher, 3, "Chrome");
+        });
+    }
+
+
+    @Test
+    public void testEmptyPagesGetRemovedIfBothPagesAreEmpty() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Play Store"), 2);
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Camera"), 1);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Maps");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 2, "Play Store");
+            assertItemsOnPage(launcher, 3, "Camera");
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Camera"), -1);
+        mWorkspace.flingForward();
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Play Store"), -2);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertItemsOnPage(launcher, 1, "Camera");
+        });
+    }
+
+    @Test
+    public void testMiddleEmptyPagesGetRemoved() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2);
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Messages"), 3);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3, 4, 5);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 2, "Maps");
+            assertPageEmpty(launcher, 3);
+            assertPageEmpty(launcher, 4);
+            assertItemsOnPage(launcher, 5, "Messages");
+        });
+
+        mWorkspace.flingBackward();
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 4, 5);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 4, "Maps");
+            assertItemsOnPage(launcher, 5, "Messages");
+        });
+    }
+
+    private void assertPageEmpty(Launcher launcher, int pageId) {
+        CellLayout page = launcher.getWorkspace().getScreenWithId(pageId);
+        assertNotNull("Page " + pageId + " does NOT exist.", page);
+        assertEquals("Page " + pageId + " is NOT empty. Number of items on the page:", 0,
+                page.getShortcutsAndWidgets().getChildCount());
+    }
+
+    private void assertPagesExist(Launcher launcher, int... pageIds) {
+        int pageCount = launcher.getWorkspace().getPageCount();
+        assertEquals("Existing page count does NOT match.", pageIds.length, pageCount);
+        for (int i = 0; i < pageCount; i++) {
+            CellLayout page = (CellLayout) launcher.getWorkspace().getPageAt(i);
+            int pageId = launcher.getWorkspace().getIdForScreen(page);
+            assertEquals("The page's id at index " + i + " does NOT match.", pageId,
+                    pageIds[i]);
+        }
+    }
+
+    private void assertItemsOnPage(Launcher launcher, int pageId, String... itemTitles) {
+        Set<String> itemTitleSet = Arrays.stream(itemTitles).collect(Collectors.toSet());
+        CellLayout page = launcher.getWorkspace().getScreenWithId(pageId);
+        int itemCount = page.getShortcutsAndWidgets().getChildCount();
+        for (int i = 0; i < itemCount; i++) {
+            ItemInfo itemInfo = (ItemInfo) page.getShortcutsAndWidgets().getChildAt(i).getTag();
+            if (itemInfo != null) {
+                assertTrue("There was an extra item on page " + pageId + ": " + itemInfo.title,
+                        itemTitleSet.remove(itemInfo.title));
+            }
+        }
+        assertTrue("Could NOT find some of the items on page " + pageId + ": "
+                        + itemTitleSet.stream().collect(Collectors.joining(",")),
+                itemTitleSet.isEmpty());
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 0145690..d9f5cc8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -145,16 +145,7 @@
             if (!isWorkspaceScrollable(workspace)) {
                 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                         "dragging icon to a second page of workspace to make it scrollable")) {
-                    dragIconToWorkspace(
-                            mLauncher,
-                            getHotseatAppIcon("Chrome"),
-                            new Point(mLauncher.getDevice().getDisplayWidth(),
-                                    mLauncher.getVisibleBounds(workspace).centerY()),
-                            "popup_container",
-                            false,
-                            false,
-                            () -> mLauncher.expectEvent(
-                                    TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT));
+                    dragIcon(workspace, getHotseatAppIcon("Chrome"), pagesPerScreen());
                     verifyActiveContainer();
                 }
             }
@@ -163,6 +154,48 @@
         }
     }
 
+    /**
+     * Returns the number of pages that are visible on the screen simultaneously.
+     */
+    public int pagesPerScreen() {
+        return mLauncher.isTwoPanels() ? 2 : 1;
+    }
+
+    /**
+     * Drags an icon to the (currentPage + pageDelta) page if the page already exists.
+     * If the target page doesn't exist, the icon will be put onto an existing page that is the
+     * closest to the target page.
+     *
+     * @param appIcon   - icon to drag.
+     * @param pageDelta - how many pages should the icon be dragged from the current page.
+     *                    It can be a negative value.
+     */
+    public void dragIcon(AppIcon appIcon, int pageDelta) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final UiObject2 workspace = verifyActiveContainer();
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "dragging icon to page with delta: " + pageDelta)) {
+                dragIcon(workspace, appIcon, pageDelta);
+                verifyActiveContainer();
+            }
+        }
+    }
+
+    private void dragIcon(UiObject2 workspace, AppIcon appIcon, int pageDelta) {
+        int pageWidth = mLauncher.getDevice().getDisplayWidth() / pagesPerScreen();
+        int targetX = (pageWidth / 2) + pageWidth * pageDelta;
+        dragIconToWorkspace(
+                mLauncher,
+                appIcon,
+                new Point(targetX, mLauncher.getVisibleBounds(workspace).centerY()),
+                "popup_container",
+                false,
+                false,
+                () -> mLauncher.expectEvent(
+                        TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT));
+        verifyActiveContainer();
+    }
+
     private boolean isWorkspaceScrollable(UiObject2 workspace) {
         return workspace.getChildCount() > (mLauncher.isTwoPanels() ? 2 : 1);
     }
@@ -258,10 +291,26 @@
         try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer(
                 "want to drag icon to workspace")) {
             final long downTime = SystemClock.uptimeMillis();
-            final Point dragStartCenter = dragIconToSpringLoaded(launcher, downTime,
+            Point dragStart = dragIconToSpringLoaded(launcher, downTime,
                     launchable.getObject(), longPressIndicator, expectLongClickEvents);
-            final Point targetDest = dest.get();
-            launcher.movePointer(dragStartCenter, targetDest, 10, downTime, true,
+            Point targetDest = dest.get();
+            int displayX = launcher.getRealDisplaySize().x;
+
+            // Since the destination can be on another page, we need to drag to the edge first
+            // until we reach the target page
+            while (targetDest.x > displayX || targetDest.x < 0) {
+                int edgeX = targetDest.x > 0 ? displayX : 0;
+                Point screenEdge = new Point(edgeX, targetDest.y);
+                launcher.movePointer(dragStart, screenEdge, 10, downTime, true,
+                        LauncherInstrumentation.GestureScope.INSIDE);
+                launcher.waitForIdle(); // Wait for the page change to happen
+                targetDest.x += displayX * (targetDest.x > 0 ? -1 : 1);
+                dragStart = screenEdge;
+            }
+
+            // targetDest.x is now between 0 and displayX so we found the target page,
+            // we just have to put move the icon to the destination and drop it
+            launcher.movePointer(dragStart, targetDest, 10, downTime, true,
                     LauncherInstrumentation.GestureScope.INSIDE);
             dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents);
         }