Support adding task listeners for incoming tasks

- The task/transition paths currently have no guaranteed order, if
  a task listener is registered before a task appears, we store it
  as a pending listener which will be fully registered once the task
  next appears.  This pending listener is always removed whenever the
  listener is unregistered, and priority of the launch cookie is >
  than the pending listener (and the pending listener will be removed
  if a launch cookie listener is used).

Bug: 385674612
Flag: EXEMPT bugfix
Test: atest ShellTaskOrganizerTests
Change-Id: Id07d7df4be90beb9271ba0de772a41814e3bf728

diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 9a0e6ae..7821358e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -171,6 +171,9 @@
     /** @see #setPendingLaunchCookieListener */
     private final ArrayMap<IBinder, TaskListener> mLaunchCookieToListener = new ArrayMap<>();
 
+    /** @see #setPendingTaskListener(int, TaskListener)  */
+    private final ArrayMap<Integer, TaskListener> mPendingTaskToListener = new ArrayMap<>();
+
     // Keeps track of taskId's with visible locusIds. Used to notify any {@link LocusIdListener}s
     // that might be set.
     private final SparseArray<LocusId> mVisibleTasksWithLocusId = new SparseArray<>();
@@ -365,7 +368,7 @@
     }
 
     /**
-     * Adds a listener for a specific task id.
+     * Adds a listener for a specific task id.  This only applies if
      */
     public void addListenerForTaskId(TaskListener listener, int taskId) {
         synchronized (mLock) {
@@ -383,7 +386,12 @@
 
             final TaskAppearedInfo info = mTasks.get(taskId);
             if (info == null) {
-                throw new IllegalArgumentException("addListenerForTaskId unknown taskId=" + taskId);
+                ProtoLog.v(WM_SHELL_TASK_ORG, "Queueing pending listener");
+                // The caller may have received a transition with the task before the organizer
+                // was notified of the task appearing, so set a pending task listener for the
+                // task to be retrieved when the task actually appears
+                mPendingTaskToListener.put(taskId, listener);
+                return;
             }
 
             final TaskListener oldListener = getTaskListener(info.getTaskInfo());
@@ -423,6 +431,14 @@
     public void removeListener(TaskListener listener) {
         synchronized (mLock) {
             ProtoLog.v(WM_SHELL_TASK_ORG, "Remove listener=%s", listener);
+
+            // Remove all occurrences of the pending listener
+            for (int i = mPendingTaskToListener.size() - 1; i >= 0; --i) {
+                if (mPendingTaskToListener.valueAt(i) == listener) {
+                    mPendingTaskToListener.removeAt(i);
+                }
+            }
+
             final int index = mTaskListeners.indexOfValue(listener);
             if (index == -1) {
                 Log.w(TAG, "No registered listener found");
@@ -438,7 +454,7 @@
                 tasks.add(data);
             }
 
-            // Remove listener, there can be the multiple occurrences, so search the whole list.
+            // Remove occurrences of the listener
             for (int i = mTaskListeners.size() - 1; i >= 0; --i) {
                 if (mTaskListeners.valueAt(i) == listener) {
                     mTaskListeners.removeAt(i);
@@ -456,9 +472,11 @@
 
     /**
      * Associated a listener to a pending launch cookie so we can route the task later once it
-     * appears.
+     * appears.  If both this and a pending task-id listener is set, then this will take priority.
      */
     public void setPendingLaunchCookieListener(IBinder cookie, TaskListener listener) {
+        ProtoLog.v(WM_SHELL_TASK_ORG, "setPendingLaunchCookieListener(): cookie=%s listener=%s",
+                cookie, listener);
         synchronized (mLock) {
             mLaunchCookieToListener.put(cookie, listener);
         }
@@ -904,7 +922,7 @@
     }
 
     private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo,
-            boolean removeLaunchCookieIfNeeded) {
+            boolean removePendingIfNeeded) {
 
         final int taskId = runningTaskInfo.taskId;
         TaskListener listener;
@@ -916,16 +934,35 @@
             listener = mLaunchCookieToListener.get(cookie);
             if (listener == null) continue;
 
-            if (removeLaunchCookieIfNeeded) {
+            if (removePendingIfNeeded) {
                 ProtoLog.v(WM_SHELL_TASK_ORG, "Migrating cookie listener to task: taskId=%d",
-                        runningTaskInfo.taskId);
+                        taskId);
                 // Remove the cookie and add the listener.
                 mLaunchCookieToListener.remove(cookie);
+                if (mPendingTaskToListener.containsKey(taskId)
+                        && mPendingTaskToListener.get(taskId) != listener) {
+                    Log.w(TAG, "Conflicting pending task listeners reported for taskId=" + taskId);
+                }
+                mPendingTaskToListener.remove(taskId);
                 mTaskListeners.put(taskId, listener);
             }
             return listener;
         }
 
+        // Next priority goes to the pending task id listener
+        if (mPendingTaskToListener.containsKey(taskId)) {
+            listener = mPendingTaskToListener.get(taskId);
+            if (listener != null) {
+                if (removePendingIfNeeded) {
+                    ProtoLog.v(WM_SHELL_TASK_ORG, "Migrating pending listener to task: taskId=%d",
+                            taskId);
+                    mPendingTaskToListener.remove(taskId);
+                    mTaskListeners.put(taskId, listener);
+                }
+                return listener;
+            }
+        }
+
         // Next priority goes to taskId specific listeners.
         listener = mTaskListeners.get(taskId);
         if (listener != null) return listener;
@@ -1025,13 +1062,21 @@
             }
 
             pw.println();
-            pw.println(innerPrefix + mLaunchCookieToListener.size() + " Launch Cookies");
+            pw.println(innerPrefix + mLaunchCookieToListener.size()
+                    + " Pending launch cookies listeners");
             for (int i = mLaunchCookieToListener.size() - 1; i >= 0; --i) {
                 final IBinder key = mLaunchCookieToListener.keyAt(i);
                 final TaskListener listener = mLaunchCookieToListener.valueAt(i);
                 pw.println(innerPrefix + "#" + i + " cookie=" + key + " listener=" + listener);
             }
 
+            pw.println();
+            pw.println(innerPrefix + mPendingTaskToListener.size() + " Pending task listeners");
+            for (int i = mPendingTaskToListener.size() - 1; i >= 0; --i) {
+                final int taskId = mPendingTaskToListener.keyAt(i);
+                final TaskListener listener = mPendingTaskToListener.valueAt(i);
+                pw.println(innerPrefix + "#" + i + " taskId=" + taskId + " listener=" + listener);
+            }
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
index 2e2f2f0..b151dc3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
@@ -123,8 +123,9 @@
         // Post to keep the lifecycle normal
         // TODO - currently based on type, really it's what the "launch item" is.
         mParentView.post(() -> {
-            ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: calling startActivity, bubble=%s",
-                    getBubbleKey());
+            ProtoLog.d(WM_SHELL_BUBBLES,
+                    "onInitialized: calling startActivity, bubble=%s hasPreparingTransition=%b",
+                    getBubbleKey(), mBubble.getPreparingTransition() != null);
             try {
                 options.setTaskAlwaysOnTop(true /* alwaysOnTop */);
                 options.setPendingIntentBackgroundActivityStartMode(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
index ae443fe..bab1771 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
@@ -690,7 +690,9 @@
             mTaskViewTransitions.prepareOpenAnimation(tv, true /* new */, startT, mFinishT,
                     (ActivityManager.RunningTaskInfo) mTaskInfo, mTaskLeash, mFinishWct);
             // Add the task view task listener manually since we aren't going through
-            // TaskViewTransitions (which normally sets up the listener via a pending launch cookie
+            // TaskViewTransitions (which normally sets up the listener via a pending launch cookie)
+            // Note: In this path, because a new task is being started, the transition may receive
+            // the transition for the task before the organizer does
             mTaskOrganizer.addListenerForTaskId(tv, mTaskInfo.taskId);
 
             if (mFinishWct.isEmpty()) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index deb61d8..e5f53ae 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -297,6 +297,34 @@
     }
 
     @Test
+    public void testAddPendingListenerForTaskId() {
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
+        TrackingTaskListener listener = new TrackingTaskListener();
+
+        // Add the listener first, then report the task to the organizer
+        mOrganizer.addListenerForTaskId(listener, 1);
+        assertFalse(mOrganizer.hasTaskListener(1));
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+
+        // Verify that the listener got notified anyways
+        assertTrue(listener.appeared.contains(task1));
+    }
+
+    @Test
+    public void testRemovePendingListenerForTaskId() {
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
+        TrackingTaskListener listener = new TrackingTaskListener();
+
+        // Add the listener, remove the listener, then report the task to the organizer
+        mOrganizer.addListenerForTaskId(listener, 1);
+        mOrganizer.removeListener(listener);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+
+        // Verify that the pending listener does not get notified
+        assertFalse(listener.appeared.contains(task1));
+    }
+
+    @Test
     public void testAddListenerForTaskId_afterTypeListener() {
         RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         TrackingTaskListener mwListener = new TrackingTaskListener();