Merge "Replace onTaskMovedToFront with a transition observer in bubbles" into main
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 8400dde..645a961 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.bubbles;
 
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED;
 import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
@@ -115,6 +114,7 @@
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.taskview.TaskView;
 import com.android.wm.shell.taskview.TaskViewTransitions;
+import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -143,6 +143,8 @@
     // Should match with PhoneWindowManager
     private static final String SYSTEM_DIALOG_REASON_KEY = "reason";
     private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav";
+    private static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
+    private static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
 
     /**
      * Common interface to send updates to bubble views.
@@ -182,6 +184,7 @@
     private final ShellTaskOrganizer mTaskOrganizer;
     private final DisplayController mDisplayController;
     private final TaskViewTransitions mTaskViewTransitions;
+    private final Transitions mTransitions;
     private final SyncTransactionQueue mSyncQueue;
     private final ShellController mShellController;
     private final ShellCommandHandler mShellCommandHandler;
@@ -282,6 +285,7 @@
             @ShellMainThread Handler mainHandler,
             @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
+            Transitions transitions,
             SyncTransactionQueue syncQueue,
             IWindowManager wmService,
             BubbleProperties bubbleProperties) {
@@ -317,6 +321,7 @@
                         com.android.internal.R.dimen.importance_ring_stroke_width));
         mDisplayController = displayController;
         mTaskViewTransitions = taskViewTransitions;
+        mTransitions = transitions;
         mOneHandedOptional = oneHandedOptional;
         mDragAndDropController = dragAndDropController;
         mSyncQueue = syncQueue;
@@ -416,23 +421,9 @@
             }
         }, mMainHandler);
 
-        mTaskStackListener.addListener(new TaskStackListenerCallback() {
-            @Override
-            public void onTaskMovedToFront(int taskId) {
-                mMainExecutor.execute(() -> {
-                    int expandedId = INVALID_TASK_ID;
-                    if (mStackView != null && mStackView.getExpandedBubble() != null
-                            && isStackExpanded()
-                            && !mStackView.isExpansionAnimating()
-                            && !mStackView.isSwitchAnimating()) {
-                        expandedId = mStackView.getExpandedBubble().getTaskId();
-                    }
-                    if (expandedId != INVALID_TASK_ID && expandedId != taskId) {
-                        mBubbleData.setExpanded(false);
-                    }
-                });
-            }
+        mTransitions.registerObserver(new BubblesTransitionObserver(this, mBubbleData));
 
+        mTaskStackListener.addListener(new TaskStackListenerCallback() {
             @Override
             public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
                     boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
@@ -883,8 +874,10 @@
 
             String action = intent.getAction();
             String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
-            if ((Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)
-                    && SYSTEM_DIALOG_REASON_GESTURE_NAV.equals(reason))
+            boolean validReasonToCollapse = SYSTEM_DIALOG_REASON_RECENT_APPS.equals(reason)
+                    || SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)
+                    || SYSTEM_DIALOG_REASON_GESTURE_NAV.equals(reason);
+            if ((Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action) && validReasonToCollapse)
                     || Intent.ACTION_SCREEN_OFF.equals(action)) {
                 mMainExecutor.execute(() -> collapseStack());
             }
@@ -1961,6 +1954,15 @@
         }
     }
 
+    /**
+     * Returns whether the stack is animating or not.
+     */
+    public boolean isStackAnimating() {
+        return mStackView != null
+                && (mStackView.isExpansionAnimating()
+                || mStackView.isSwitchAnimating());
+    }
+
     @VisibleForTesting
     @Nullable
     public BubbleStackView getStackView() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java
new file mode 100644
index 0000000..9e8a385
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.bubbles;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
+import android.app.ActivityManager;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
+
+/**
+ * Observer used to identify tasks that are opening or moving to front. If a bubble activity is
+ * currently opened when this happens, we'll collapse the bubbles.
+ */
+public class BubblesTransitionObserver implements Transitions.TransitionObserver {
+
+    private BubbleController mBubbleController;
+    private BubbleData mBubbleData;
+
+    public BubblesTransitionObserver(BubbleController controller,
+            BubbleData bubbleData) {
+        mBubbleController = controller;
+        mBubbleData = bubbleData;
+    }
+
+    @Override
+    public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction) {
+        for (TransitionInfo.Change change : info.getChanges()) {
+            final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+            // We only care about opens / move to fronts when bubbles are expanded & not animating.
+            if (taskInfo == null
+                    || taskInfo.taskId == INVALID_TASK_ID
+                    || !TransitionUtil.isOpeningType(change.getMode())
+                    || mBubbleController.isStackAnimating()
+                    || !mBubbleData.isExpanded()
+                    || mBubbleData.getSelectedBubble() == null) {
+                continue;
+            }
+            int expandedId = mBubbleData.getSelectedBubble().getTaskId();
+            // If the task id that's opening is the same as the expanded bubble, skip collapsing
+            // because it is our bubble that is opening.
+            if (expandedId != INVALID_TASK_ID && expandedId != taskInfo.taskId) {
+                mBubbleData.setExpanded(false);
+            }
+        }
+    }
+
+    @Override
+    public void onTransitionStarting(@NonNull IBinder transition) {
+
+    }
+
+    @Override
+    public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
+
+    }
+
+    @Override
+    public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {
+
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 93ce91f..c641e87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -167,6 +167,7 @@
             @ShellMainThread Handler mainHandler,
             @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
+            Transitions transitions,
             SyncTransactionQueue syncQueue,
             IWindowManager wmService) {
         return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
@@ -176,7 +177,8 @@
                 statusBarService, windowManager, windowManagerShellWrapper, userManager,
                 launcherApps, logger, taskStackListener, organizer, positioner, displayController,
                 oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
-                taskViewTransitions, syncQueue, wmService, ProdBubbleProperties.INSTANCE);
+                taskViewTransitions, transitions, syncQueue, wmService,
+                ProdBubbleProperties.INSTANCE);
     }
 
     //
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java
new file mode 100644
index 0000000..9655f97
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.bubbles;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.IWindowContainerToken;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.TransitionInfoBuilder;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests of {@link BubblesTransitionObserver}.
+ */
+@SmallTest
+public class BubblesTransitionObserverTest {
+
+    @Mock
+    private BubbleController mBubbleController;
+    @Mock
+    private BubbleData mBubbleData;
+
+    @Mock
+    private IBinder mTransition;
+    @Mock
+    private SurfaceControl.Transaction mStartT;
+    @Mock
+    private SurfaceControl.Transaction mFinishT;
+
+    @Mock
+    private Bubble mBubble;
+
+    private BubblesTransitionObserver mTransitionObserver;
+
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mTransitionObserver = new BubblesTransitionObserver(mBubbleController, mBubbleData);
+    }
+
+    @Test
+    public void testOnTransitionReady_open_collapsesStack() {
+        when(mBubbleData.isExpanded()).thenReturn(true);
+        when(mBubbleData.getSelectedBubble()).thenReturn(mBubble);
+        when(mBubble.getTaskId()).thenReturn(1);
+        when(mBubbleController.isStackAnimating()).thenReturn(false);
+
+        TransitionInfo info = createTransitionInfo(TRANSIT_OPEN, createTaskInfo(2));
+
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        verify(mBubbleData).setExpanded(eq(false));
+    }
+
+    @Test
+    public void testOnTransitionReady_toFront_collapsesStack() {
+        when(mBubbleData.isExpanded()).thenReturn(true);
+        when(mBubbleData.getSelectedBubble()).thenReturn(mBubble);
+        when(mBubble.getTaskId()).thenReturn(1);
+        when(mBubbleController.isStackAnimating()).thenReturn(false);
+
+        TransitionInfo info = createTransitionInfo(TRANSIT_TO_FRONT, createTaskInfo(2));
+
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        verify(mBubbleData).setExpanded(eq(false));
+    }
+
+    @Test
+    public void testOnTransitionReady_noTaskInfo_skip() {
+        when(mBubbleData.isExpanded()).thenReturn(true);
+        when(mBubbleData.getSelectedBubble()).thenReturn(mBubble);
+        when(mBubble.getTaskId()).thenReturn(1);
+        when(mBubbleController.isStackAnimating()).thenReturn(false);
+
+        // Null task info
+        TransitionInfo info = createTransitionInfo(TRANSIT_TO_FRONT, null /* taskInfo */);
+
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        verify(mBubbleData, never()).setExpanded(eq(false));
+    }
+
+    @Test
+    public void testOnTransitionReady_noTaskId_skip() {
+        when(mBubbleData.isExpanded()).thenReturn(true);
+        when(mBubbleData.getSelectedBubble()).thenReturn(mBubble);
+        when(mBubble.getTaskId()).thenReturn(1);
+        when(mBubbleController.isStackAnimating()).thenReturn(false);
+
+        // Invalid task id
+        TransitionInfo info = createTransitionInfo(TRANSIT_TO_FRONT,
+                createTaskInfo(INVALID_TASK_ID));
+
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        verify(mBubbleData, never()).setExpanded(eq(false));
+    }
+
+    @Test
+    public void testOnTransitionReady_notOpening_skip() {
+        when(mBubbleData.isExpanded()).thenReturn(true);
+        when(mBubbleData.getSelectedBubble()).thenReturn(mBubble);
+        when(mBubble.getTaskId()).thenReturn(1);
+        when(mBubbleController.isStackAnimating()).thenReturn(false);
+
+        // Transits that aren't opening
+        TransitionInfo info = createTransitionInfo(TRANSIT_CHANGE, createTaskInfo(2));
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        info = createTransitionInfo(TRANSIT_CLOSE, createTaskInfo(3));
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        info = createTransitionInfo(TRANSIT_TO_BACK, createTaskInfo(4));
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        verify(mBubbleData, never()).setExpanded(eq(false));
+    }
+
+    @Test
+    public void testOnTransitionReady_stackAnimating_skip() {
+        when(mBubbleData.isExpanded()).thenReturn(true);
+        when(mBubbleData.getSelectedBubble()).thenReturn(mBubble);
+        when(mBubble.getTaskId()).thenReturn(1);
+        when(mBubbleController.isStackAnimating()).thenReturn(true); // Stack is animating
+
+        TransitionInfo info = createTransitionInfo(TRANSIT_OPEN, createTaskInfo(2));
+
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        verify(mBubbleData, never()).setExpanded(eq(false));
+    }
+
+    @Test
+    public void testOnTransitionReady_stackNotExpanded_skip() {
+        when(mBubbleData.isExpanded()).thenReturn(false); // Stack is not expanded
+        when(mBubbleData.getSelectedBubble()).thenReturn(mBubble);
+        when(mBubble.getTaskId()).thenReturn(1);
+        when(mBubbleController.isStackAnimating()).thenReturn(false);
+
+        TransitionInfo info = createTransitionInfo(TRANSIT_TO_FRONT, createTaskInfo(2));
+
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        verify(mBubbleData, never()).setExpanded(eq(false));
+    }
+
+    @Test
+    public void testOnTransitionReady_noSelectedBubble_skip() {
+        when(mBubbleData.isExpanded()).thenReturn(true);
+        when(mBubbleData.getSelectedBubble()).thenReturn(null); // No selected bubble
+        when(mBubble.getTaskId()).thenReturn(1);
+        when(mBubbleController.isStackAnimating()).thenReturn(false);
+
+        TransitionInfo info = createTransitionInfo(TRANSIT_OPEN, createTaskInfo(2));
+
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        verify(mBubbleData, never()).setExpanded(eq(false));
+    }
+
+    @Test
+    public void testOnTransitionReady_openingMatchesExpanded_skip() {
+        when(mBubbleData.isExpanded()).thenReturn(true);
+        when(mBubbleData.getSelectedBubble()).thenReturn(mBubble);
+        when(mBubble.getTaskId()).thenReturn(1);
+        when(mBubbleController.isStackAnimating()).thenReturn(false);
+
+        // What's moving to front is same as the opened bubble
+        TransitionInfo info = createTransitionInfo(TRANSIT_TO_FRONT, createTaskInfo(1));
+
+        mTransitionObserver.onTransitionReady(mTransition, info, mStartT, mFinishT);
+
+        verify(mBubbleData, never()).setExpanded(eq(false));
+    }
+
+    private ActivityManager.RunningTaskInfo createTaskInfo(int taskId) {
+        final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+        taskInfo.taskId = taskId;
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        return taskInfo;
+    }
+
+    private TransitionInfo createTransitionInfo(int changeType,
+            ActivityManager.RunningTaskInfo info) {
+        final TransitionInfo.Change change = new TransitionInfo.Change(
+                new WindowContainerToken(mock(IWindowContainerToken.class)),
+                mock(SurfaceControl.class));
+        change.setMode(changeType);
+        change.setTaskInfo(info);
+
+        return new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+                .addChange(change).build();
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index ac9cfb3..1b623a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -420,6 +420,7 @@
                 syncExecutor,
                 mock(Handler.class),
                 mTaskViewTransitions,
+                mTransitions,
                 mock(SyncTransactionQueue.class),
                 mock(IWindowManager.class),
                 mBubbleProperties);
@@ -511,6 +512,11 @@
     }
 
     @Test
+    public void instantiateController_registerTransitionObserver() {
+        verify(mTransitions).registerObserver(any());
+    }
+
+    @Test
     public void testAddBubble() {
         mBubbleController.updateBubble(mBubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
@@ -1470,6 +1476,34 @@
     }
 
     @Test
+    public void testBroadcastReceiverCloseDialogs_reasonHomeKey() {
+        spyOn(mContext);
+        mBubbleController.updateBubble(mBubbleEntry);
+        mBubbleData.setExpanded(true);
+
+        verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
+                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
+        Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        i.putExtra("reason", "homekey");
+        mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
+        assertStackCollapsed();
+    }
+
+    @Test
+    public void testBroadcastReceiverCloseDialogs_reasonRecentsKey() {
+        spyOn(mContext);
+        mBubbleController.updateBubble(mBubbleEntry);
+        mBubbleData.setExpanded(true);
+
+        verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
+                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
+        Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        i.putExtra("reason", "recentapps");
+        mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
+        assertStackCollapsed();
+    }
+
+    @Test
     public void testBroadcastReceiver_screenOff() {
         spyOn(mContext);
         mBubbleController.updateBubble(mBubbleEntry);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 5855347..9ad234e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -43,6 +43,7 @@
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.taskview.TaskViewTransitions;
+import com.android.wm.shell.transition.Transitions;
 
 import java.util.Optional;
 
@@ -74,6 +75,7 @@
             ShellExecutor shellMainExecutor,
             Handler shellMainHandler,
             TaskViewTransitions taskViewTransitions,
+            Transitions transitions,
             SyncTransactionQueue syncQueue,
             IWindowManager wmService,
             BubbleProperties bubbleProperties) {
@@ -82,7 +84,8 @@
                 windowManagerShellWrapper, userManager, launcherApps, bubbleLogger,
                 taskStackListener, shellTaskOrganizer, positioner, displayController,
                 oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
-                new SyncExecutor(), taskViewTransitions, syncQueue, wmService, bubbleProperties);
+                new SyncExecutor(), taskViewTransitions, transitions,
+                syncQueue, wmService, bubbleProperties);
         setInflateSynchronously(true);
         onInit();
     }