Move bubbles away from the IME if needed.

This involves adding the PinnedStackListenerForwarder, so that sysui can have multiple pinned stack listeners listening for updates from the WM. This looked easier and simpler than modifying all the WM code to support multiple listeners. We're also planning to integrate PIP and bubbles at some point, so that they're aware of each other and move together. At that time, we can simply delete the forwarder and use a single listener again, without modifying WM code.

Test: atest SystemUITests
Change-Id: Ie2f9f937fe0a19cac5a1ae83d83698db8d53aba2
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/PinnedStackListenerForwarder.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/PinnedStackListenerForwarder.java
new file mode 100644
index 0000000..3ae2df5b
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/PinnedStackListenerForwarder.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2019 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.systemui.shared.system;
+
+import android.content.pm.ParceledListSlice;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.view.IPinnedStackController;
+import android.view.IPinnedStackListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * PinnedStackListener that simply forwards all calls to each listener added via
+ * {@link #addListener}. This is necessary since calling
+ * {@link com.android.server.wm.WindowManagerService#registerPinnedStackListener} replaces any
+ * previously set listener.
+ */
+public class PinnedStackListenerForwarder extends IPinnedStackListener.Stub {
+    private List<IPinnedStackListener> mListeners = new ArrayList<>();
+
+    /** Adds a listener to receive updates from the WindowManagerService. */
+    public void addListener(IPinnedStackListener listener) {
+        mListeners.add(listener);
+    }
+
+    /** Removes a listener so it will no longer receive updates from the WindowManagerService. */
+    public void removeListener(IPinnedStackListener listener) {
+        mListeners.remove(listener);
+    }
+
+    @Override
+    public void onListenerRegistered(IPinnedStackController controller) throws RemoteException {
+        for (IPinnedStackListener listener : mListeners) {
+            listener.onListenerRegistered(controller);
+        }
+    }
+
+    @Override
+    public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds,
+            boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation)
+            throws RemoteException {
+        for (IPinnedStackListener listener : mListeners) {
+            listener.onMovementBoundsChanged(
+                    insetBounds, normalBounds, animatingBounds,
+                    fromImeAdjustment, fromShelfAdjustment, displayRotation);
+        }
+    }
+
+    @Override
+    public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) throws RemoteException {
+        for (IPinnedStackListener listener : mListeners) {
+            listener.onImeVisibilityChanged(imeVisible, imeHeight);
+        }
+    }
+
+    @Override
+    public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)
+            throws RemoteException {
+        for (IPinnedStackListener listener : mListeners) {
+            listener.onShelfVisibilityChanged(shelfVisible, shelfHeight);
+        }
+    }
+
+    @Override
+    public void onMinimizedStateChanged(boolean isMinimized) throws RemoteException {
+        for (IPinnedStackListener listener : mListeners) {
+            listener.onMinimizedStateChanged(isMinimized);
+        }
+    }
+
+    @Override
+    public void onActionsChanged(ParceledListSlice actions) throws RemoteException {
+        for (IPinnedStackListener listener : mListeners) {
+            listener.onActionsChanged(actions);
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java
index 8a251ae..10996e88 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java
@@ -27,6 +27,7 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.IPinnedStackListener;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 
@@ -81,6 +82,13 @@
 
     private static final WindowManagerWrapper sInstance = new WindowManagerWrapper();
 
+    /**
+     * Forwarder to which we can add multiple pinned stack listeners. Each listener will receive
+     * updates from the window manager service.
+     */
+    private PinnedStackListenerForwarder mPinnedStackListenerForwarder =
+            new PinnedStackListenerForwarder();
+
     public static WindowManagerWrapper getInstance() {
         return sInstance;
     }
@@ -199,4 +207,14 @@
             Log.w(TAG, "Failed to register docked stack listener");
         }
     }
+
+    /**
+     * Adds a pinned stack listener, which will receive updates from the window manager service
+     * along with any other pinned stack listeners that were added via this method.
+     */
+    public void addPinnedStackListener(IPinnedStackListener listener) throws RemoteException {
+        mPinnedStackListenerForwarder.addListener(listener);
+        WindowManagerGlobal.getWindowManagerService().registerPinnedStackListener(
+                DEFAULT_DISPLAY, mPinnedStackListenerForwarder);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 0832296..e0d3ace 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -30,12 +30,15 @@
 import android.app.INotificationManager;
 import android.app.Notification;
 import android.content.Context;
+import android.content.pm.ParceledListSlice;
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 import android.view.Display;
+import android.view.IPinnedStackController;
+import android.view.IPinnedStackListener;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
@@ -48,6 +51,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.WindowManagerWrapper;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
@@ -172,6 +176,12 @@
         mTaskStackListener = new BubbleTaskStackListener();
         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
 
+        try {
+            WindowManagerWrapper.getInstance().addPinnedStackListener(new BubblesImeListener());
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+
         mBubbleData = data;
     }
 
@@ -543,4 +553,37 @@
         return Settings.Secure.getInt(context.getContentResolver(),
                 ENABLE_BUBBLES, 1) != 0;
     }
+
+    /** PinnedStackListener that dispatches IME visibility updates to the stack. */
+    private class BubblesImeListener extends IPinnedStackListener.Stub {
+
+        @Override
+        public void onListenerRegistered(IPinnedStackController controller) throws RemoteException {
+        }
+
+        @Override
+        public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds,
+                Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment,
+                int displayRotation) throws RemoteException {}
+
+        @Override
+        public void onImeVisibilityChanged(boolean imeVisible, int imeHeight)
+                throws RemoteException {
+            if (mStackView != null) {
+                mStackView.post(() -> {
+                    mStackView.onImeVisibilityChanged(imeVisible, imeHeight);
+                });
+            }
+        }
+
+        @Override
+        public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)
+                throws RemoteException {}
+
+        @Override
+        public void onMinimizedStateChanged(boolean isMinimized) throws RemoteException {}
+
+        @Override
+        public void onActionsChanged(ParceledListSlice actions) throws RemoteException {}
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 5546e4c..31558f1 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -100,6 +100,7 @@
     private int mExpandedAnimateYDistance;
     private int mStatusBarHeight;
     private int mPipDismissHeight;
+    private int mImeOffset;
 
     private Bubble mExpandedBubble;
     private boolean mIsExpanded;
@@ -162,6 +163,7 @@
                 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
         mPipDismissHeight = mContext.getResources().getDimensionPixelSize(
                 R.dimen.pip_dismiss_gradient_height);
+        mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
 
         mDisplaySize = new Point();
         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
@@ -550,8 +552,15 @@
                 : null;
     }
 
-    public PointF getStackPosition() {
-        return mStackAnimationController.getStackPosition();
+    /** Moves the bubbles out of the way if they're going to be over the keyboard. */
+    public void onImeVisibilityChanged(boolean visible, int height) {
+        if (!mIsExpanded) {
+            if (visible) {
+                mStackAnimationController.updateBoundsForVisibleImeAndAnimate(height + mImeOffset);
+            } else {
+                mStackAnimationController.updateBoundsForInvisibleImeAndAnimate();
+            }
+        }
     }
 
     /** Called when a drag operation on an individual bubble has started. */
@@ -808,6 +817,10 @@
                 .floatValue();
     }
 
+    public PointF getStackPosition() {
+        return mStackAnimationController.getStackPosition();
+    }
+
     /**
      * Logs the bubble UI event.
      *
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 7dfb21c..f47fbe0 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -66,6 +66,15 @@
      */
     private PointF mStackPosition = new PointF();
 
+    /** The height of the most recently visible IME. */
+    private float mImeHeight = 0f;
+
+    /**
+     * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
+     * IME is not visible or the user moved the stack since the IME became visible.
+     */
+    private float mPreImeY = Float.MIN_VALUE;
+
     /**
      * Animations on the stack position itself, which would have been started in
      * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to
@@ -108,6 +117,10 @@
      * it with the 'following' effect.
      */
     public void moveFirstBubbleWithStackFollowing(float x, float y) {
+        // If we manually move the bubbles with the IME open, clear the return point since we don't
+        // want the stack to snap away from the new position.
+        mPreImeY = Float.MIN_VALUE;
+
         moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x);
         moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y);
     }
@@ -189,6 +202,44 @@
     }
 
     /**
+     * Save the IME height so that the allowable stack bounds reflect the now-visible IME, and
+     * animate the stack out of the way if necessary.
+     */
+    public void updateBoundsForVisibleImeAndAnimate(int imeHeight) {
+        mImeHeight = imeHeight;
+
+        final float maxBubbleY = getAllowableStackPositionRegion().bottom;
+        if (mStackPosition.y > maxBubbleY && mPreImeY == Float.MIN_VALUE) {
+            mPreImeY = mStackPosition.y;
+
+            springFirstBubbleWithStackFollowing(
+                    DynamicAnimation.TRANSLATION_Y,
+                    getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
+                            .setStiffness(SpringForce.STIFFNESS_LOW),
+                    /* startVel */ 0f,
+                    maxBubbleY);
+        }
+    }
+
+    /**
+     * Clear the IME height from the bounds and animate the stack back to its original position,
+     * assuming it wasn't moved in the meantime.
+     */
+    public void updateBoundsForInvisibleImeAndAnimate() {
+        mImeHeight = 0;
+
+        if (mPreImeY > Float.MIN_VALUE) {
+            springFirstBubbleWithStackFollowing(
+                    DynamicAnimation.TRANSLATION_Y,
+                    getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
+                        .setStiffness(SpringForce.STIFFNESS_LOW),
+                    /* startVel */ 0f,
+                    mPreImeY);
+            mPreImeY = Float.MIN_VALUE;
+        }
+    }
+
+    /**
      * Returns the region within which the stack is allowed to rest. This goes slightly off the left
      * and right sides of the screen, below the status bar/cutout and above the navigation bar.
      * While the stack is not allowed to rest outside of these bounds, it can temporarily be
@@ -228,6 +279,7 @@
                     mLayout.getHeight()
                             - mIndividualBubbleSize
                             - mBubblePadding
+                            - (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePadding : 0f)
                             - Math.max(
                             insets.getSystemWindowInsetBottom(),
                             insets.getDisplayCutout() != null
@@ -389,8 +441,8 @@
             float vel, float finalPosition) {
 
         Log.d(TAG, String.format("Springing %s to final position %f.",
-                        PhysicsAnimationLayout.getReadablePropertyName(property),
-                        finalPosition));
+                PhysicsAnimationLayout.getReadablePropertyName(property),
+                finalPosition));
 
         StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
         SpringAnimation springAnimation =
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
index 3346ad2..1740290 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.pip.phone;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.IActivityManager;
@@ -33,8 +31,6 @@
 import android.util.Pair;
 import android.view.IPinnedStackController;
 import android.view.IPinnedStackListener;
-import android.view.IWindowManager;
-import android.view.WindowManagerGlobal;
 
 import com.android.systemui.Dependency;
 import com.android.systemui.UiOffloadThread;
@@ -57,7 +53,6 @@
     private Context mContext;
     private IActivityManager mActivityManager;
     private IActivityTaskManager mActivityTaskManager;
-    private IWindowManager mWindowManager;
     private Handler mHandler = new Handler();
 
     private final PinnedStackListener mPinnedStackListener = new PinnedStackListener();
@@ -178,10 +173,9 @@
         mContext = context;
         mActivityManager = ActivityManager.getService();
         mActivityTaskManager = ActivityTaskManager.getService();
-        mWindowManager = WindowManagerGlobal.getWindowManagerService();
 
         try {
-            mWindowManager.registerPinnedStackListener(DEFAULT_DISPLAY, mPinnedStackListener);
+            WindowManagerWrapper.getInstance().addPinnedStackListener(mPinnedStackListener);
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to register pinned stack listener", e);
         }