Reposition the stack to a similar position upon rotation.

Fixes: 128691406
Test: atest SystemUITests
Change-Id: I2d73c88f06d759c2b7b71dc77d008a1e026ee959
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 0fcc950..af9fd1d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -34,6 +34,7 @@
 import android.app.Notification;
 import android.content.Context;
 import android.content.pm.ParceledListSlice;
+import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -135,6 +136,9 @@
     // Used for determining view rect for touch interaction
     private Rect mTempRect = new Rect();
 
+    /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */
+    private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
+
     /**
      * Listener to be notified when some states of the bubbles change.
      */
@@ -254,6 +258,14 @@
         }
     }
 
+    @Override
+    public void onConfigChanged(Configuration newConfig) {
+        if (mStackView != null && newConfig != null && newConfig.orientation != mOrientation) {
+            mStackView.onOrientationChanged();
+            mOrientation = newConfig.orientation;
+        }
+    }
+
     /**
      * Set a listener to be notified when some states of the bubbles change.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 424cd55..024e564 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -138,6 +138,16 @@
     private Runnable mHideFlyout =
             () -> mFlyout.animate().alpha(0f).withEndAction(() -> mFlyout.setVisibility(GONE));
 
+    /** Layout change listener that moves the stack to the nearest valid position on rotation. */
+    private OnLayoutChangeListener mMoveStackToValidPositionOnLayoutListener;
+    /** Whether the stack was on the left side of the screen prior to rotation. */
+    private boolean mWasOnLeftBeforeRotation = false;
+    /**
+     * How far down the screen the stack was before rotation, in terms of percentage of the way down
+     * the allowable region. Defaults to -1 if not set.
+     */
+    private float mVerticalPosPercentBeforeRotation = -1;
+
     private int mBubbleSize;
     private int mBubblePadding;
     private int mExpandedAnimateXDistance;
@@ -304,6 +314,15 @@
             return view.onApplyWindowInsets(insets);
         });
 
+        mMoveStackToValidPositionOnLayoutListener =
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                    if (mVerticalPosPercentBeforeRotation >= 0) {
+                        mStackAnimationController.moveStackToSimilarPositionAfterRotation(
+                                mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation);
+                    }
+                    removeOnLayoutChangeListener(mMoveStackToValidPositionOnLayoutListener);
+                };
+
         // This must be a separate OnDrawListener since it should be called for every draw.
         getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
     }
@@ -318,6 +337,18 @@
         }
     }
 
+    /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
+    public void onOrientationChanged() {
+        final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion();
+        mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide();
+        mVerticalPosPercentBeforeRotation =
+                (mStackAnimationController.getStackPosition().y - allowablePos.top)
+                        / (allowablePos.bottom - allowablePos.top);
+        addOnLayoutChangeListener(mMoveStackToValidPositionOnLayoutListener);
+
+        hideFlyoutImmediate();
+    }
+
     @Override
     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
         getBoundsOnScreen(outRect);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index b953f27..74a6b60 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -252,6 +252,21 @@
     }
 
     /**
+     * Moves the stack in response to rotation. We keep it in the most similar position by keeping
+     * it on the same side, and positioning it the same percentage of the way down the screen
+     * (taking status bar/nav bar into account by using the allowable region's height).
+     */
+    public void moveStackToSimilarPositionAfterRotation(boolean wasOnLeft, float verticalPercent) {
+        final RectF allowablePos = getAllowableStackPositionRegion();
+        final float allowableRegionHeight = allowablePos.bottom - allowablePos.top;
+
+        final float x = wasOnLeft ? allowablePos.left : allowablePos.right;
+        final float y = (allowableRegionHeight * verticalPercent) + allowablePos.top;
+
+        setStackPosition(new PointF(x, y));
+    }
+
+    /**
      * Flings the first bubble along the given property's axis, using the provided configuration
      * values. When the animation ends - either by hitting the min/max, or by friction sufficiently
      * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final