Fix a bug showing the background through a punch hole when switching bubbles.

Bug: 126856255
Test: Manual. Recorded screen and verified.
Change-Id: I2d29fc30b192c0d28c7d7e28b45ec7e01385a912
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index eb85589..e0ed111 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -118,6 +118,7 @@
     private final BubbleTaskStackListener mTaskStackListener;
     private BubbleStateChangeListener mStateChangeListener;
     private BubbleExpandListener mExpandListener;
+    @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
 
     private BubbleData mBubbleData;
     private BubbleStackView mStackView;
@@ -178,7 +179,12 @@
 
     @Inject
     public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
-            BubbleData data) {
+                            BubbleData data) {
+        this(context, statusBarWindowController, data, null /* synchronizer */);
+    }
+
+    public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
+            BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer) {
         mContext = context;
 
         mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
@@ -206,6 +212,7 @@
         }
 
         mBubbleData = data;
+        mSurfaceSynchronizer = synchronizer;
     }
 
     /**
@@ -297,7 +304,7 @@
             mStackView.updateBubble(notif, updatePosition);
         } else {
             if (mStackView == null) {
-                mStackView = new BubbleStackView(mContext, mBubbleData);
+                mStackView = new BubbleStackView(mContext, mBubbleData, mSurfaceSynchronizer);
                 ViewGroup sbv = mStatusBarWindowController.getStatusBarView();
                 // XXX: Bug when you expand the shade on top of expanded bubble, there is no scrim
                 // between bubble and the shade
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 888e3fe..e150ac9 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -19,6 +19,7 @@
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
+import android.annotation.NonNull;
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -32,6 +33,7 @@
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
 import android.util.StatsLog;
+import android.view.Choreographer;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -67,6 +69,43 @@
     private static final String TAG = "BubbleStackView";
     private static final boolean DEBUG = false;
 
+    /**
+     * Interface to synchronize {@link View} state and the screen.
+     *
+     * {@hide}
+     */
+    interface SurfaceSynchronizer {
+        /**
+         * Wait until requested change on a {@link View} is reflected on the screen.
+         *
+         * @param callback callback to run after the change is reflected on the screen.
+         */
+        void syncSurfaceAndRun(Runnable callback);
+    }
+
+    private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
+            new SurfaceSynchronizer() {
+        @Override
+        public void syncSurfaceAndRun(Runnable callback) {
+            Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
+                // Just wait 2 frames. There is no guarantee, but this is usually enough time that
+                // the requested change is reflected on the screen.
+                // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and
+                // surfaces, rewrite this logic with them.
+                private int mFrameWait = 2;
+
+                @Override
+                public void doFrame(long frameTimeNanos) {
+                    if (--mFrameWait > 0) {
+                        Choreographer.getInstance().postFrameCallback(this);
+                    } else {
+                        callback.run();
+                    }
+                }
+            });
+        }
+    };
+
     private Point mDisplaySize;
 
     private final SpringAnimation mExpandedViewXAnim;
@@ -79,7 +118,6 @@
 
     private FrameLayout mExpandedViewContainer;
 
-
     private int mBubbleSize;
     private int mBubblePadding;
     private int mExpandedAnimateXDistance;
@@ -129,7 +167,11 @@
         }
     };
 
-    public BubbleStackView(Context context, BubbleData data) {
+    @NonNull private final SurfaceSynchronizer mSurfaceSynchronizer;
+
+
+    public BubbleStackView(Context context, BubbleData data,
+                           @Nullable SurfaceSynchronizer synchronizer) {
         super(context);
 
         mBubbleData = data;
@@ -160,6 +202,7 @@
 
         mStackAnimationController = new StackAnimationController();
         mExpandedAnimationController = new ExpandedAnimationController(mDisplaySize);
+        mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
 
         mBubbleContainer = new PhysicsAnimationLayout(context);
         mBubbleContainer.setMaxRenderedChildren(
@@ -314,17 +357,28 @@
             // If we weren't previously expanded we should animate open.
             animateExpansion(true /* expand */);
             logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
+            mExpandedBubble.entry.setShowInShadeWhenBubble(false);
+            notifyExpansionChanged(mExpandedBubble.entry, true /* expanded */);
         } else {
-            // Otherwise just update the views
-            // TODO: probably animate / page to expanded one
-            updateExpandedBubble();
-            updatePointerPosition();
-            requestUpdate();
-            logBubbleEvent(prevBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
-            logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
+            // Make the container of the expanded view transparent before removing the expanded view
+            // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
+            // expanded view becomes visible on the screen. See b/126856255
+            mExpandedViewContainer.setAlpha(0.0f);
+
+            mSurfaceSynchronizer.syncSurfaceAndRun(new Runnable() {
+                @Override
+                public void run() {
+                    updateExpandedBubble();
+                    updatePointerPosition();
+                    requestUpdate();
+                    logBubbleEvent(prevBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
+                    logBubbleEvent(mExpandedBubble,
+                            StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
+                    mExpandedBubble.entry.setShowInShadeWhenBubble(false);
+                    notifyExpansionChanged(mExpandedBubble.entry, true /* expanded */);
+                }
+            });
         }
-        mExpandedBubble.entry.setShowInShadeWhenBubble(false);
-        notifyExpansionChanged(mExpandedBubble.entry, true /* expanded */);
     }
 
     /**
@@ -767,6 +821,7 @@
             mExpandedViewContainer.addView(mExpandedBubble.expandedView);
             mExpandedBubble.expandedView.populateExpandedView();
             mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
+            mExpandedViewContainer.setAlpha(1.0f);
         }
     }