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 8c8a56a773f7c..dfd878f632838 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 @@ -368,6 +368,13 @@ public class BubbleController { return; } } + for (Bubble b : mBubbleData.getOverflowBubbles()) { + if (task.taskId == b.getTaskId()) { + promoteBubbleFromOverflow(b); + mBubbleData.setExpanded(true); + return; + } + } } }); @@ -815,7 +822,35 @@ public class BubbleController { setIsBubble(bubble, true /* isBubble */); } - @VisibleForTesting + /** + * Expands and selects the provided bubble as long as it already exists in the stack or the + * overflow. + * + * This is currently only used when opening a bubble via clicking on a conversation widget. + */ + public void expandStackAndSelectBubble(Bubble b) { + if (b == null) { + return; + } + if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) { + // already in the stack + mBubbleData.setSelectedBubble(b); + mBubbleData.setExpanded(true); + } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) { + // promote it out of the overflow + promoteBubbleFromOverflow(b); + } + } + + /** + * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble + * exists for this entry, and it is able to bubble, a new bubble will be created. + * + * This is the method to use when opening a bubble via a notification or in a state where + * the device might not be unlocked. + * + * @param entry the entry to use for the bubble. + */ public void expandStackAndSelectBubble(BubbleEntry entry) { if (mIsStatusBarShade) { mNotifEntryToExpandOnShadeUnlock = null; @@ -1382,6 +1417,21 @@ public class BubbleController { }); } + @Override + public void expandStackAndSelectBubble(Bubble bubble) { + mMainExecutor.execute(() -> { + BubbleController.this.expandStackAndSelectBubble(bubble); + }); + } + + @Override + @Nullable + public Bubble getBubbleWithShortcutId(String shortcutId) { + return mMainExecutor.executeBlockingForResult(() -> { + return BubbleController.this.mBubbleData.getAnyBubbleWithShortcutId(shortcutId); + }, Bubble.class); + } + @Override public void onTaskbarChanged(Bundle b) { mMainExecutor.execute(() -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 69a741c674dbe..6f5cfd114688e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -26,6 +26,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.LocusId; import android.content.pm.ShortcutInfo; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -874,6 +875,34 @@ public class BubbleData { return b; } + /** @return any bubble (in the stack or the overflow) that matches the provided shortcutId. */ + @Nullable + Bubble getAnyBubbleWithShortcutId(String shortcutId) { + if (TextUtils.isEmpty(shortcutId)) { + return null; + } + for (int i = 0; i < mBubbles.size(); i++) { + Bubble bubble = mBubbles.get(i); + String bubbleShortcutId = bubble.getShortcutInfo() != null + ? bubble.getShortcutInfo().getId() + : bubble.getMetadataShortcutId(); + if (shortcutId.equals(bubbleShortcutId)) { + return bubble; + } + } + + for (int i = 0; i < mOverflowBubbles.size(); i++) { + Bubble bubble = mOverflowBubbles.get(i); + String bubbleShortcutId = bubble.getShortcutInfo() != null + ? bubble.getShortcutInfo().getId() + : bubble.getMetadataShortcutId(); + if (shortcutId.equals(bubbleShortcutId)) { + return bubble; + } + } + return null; + } + @VisibleForTesting(visibility = PRIVATE) @Nullable public Bubble getBubbleInStackWithKey(String key) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index a93ce01dfc7bc..c73b5eebc5c27 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -118,6 +118,19 @@ public interface Bubbles { */ void expandStackAndSelectBubble(BubbleEntry entry); + /** + * Request the stack expand if needed, then select the specified Bubble as current. + * + * @param bubble the bubble to be selected + */ + void expandStackAndSelectBubble(Bubble bubble); + + /** + * @return a bubble that matches the provided shortcutId, if one exists. + */ + @Nullable + Bubble getBubbleWithShortcutId(String shortcutId); + /** Called for any taskbar changes. */ void onTaskbarChanged(Bundle b); diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java index c416b5edb264e..b031637e4016b 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java @@ -35,9 +35,11 @@ import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.people.PeopleSpaceUtils; +import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.wmshell.BubblesManager; +import com.android.wm.shell.bubbles.Bubble; import java.util.Optional; @@ -53,14 +55,35 @@ public class LaunchConversationActivity extends Activity { private final UserManager mUserManager; private boolean mIsForTesting; private IStatusBarService mIStatusBarService; + private CommandQueue mCommandQueue; + private Bubble mBubble; + private NotificationEntry mEntryToBubble; @Inject public LaunchConversationActivity(NotificationEntryManager notificationEntryManager, - Optional bubblesManagerOptional, UserManager userManager) { + Optional bubblesManagerOptional, UserManager userManager, + CommandQueue commandQueue) { super(); mNotificationEntryManager = notificationEntryManager; mBubblesManagerOptional = bubblesManagerOptional; mUserManager = userManager; + mCommandQueue = commandQueue; + mCommandQueue.addCallback(new CommandQueue.Callbacks() { + // (b/190833924) Wait for the app transition to finish before showing the bubble, + // opening the bubble while the transition is happening can mess with the placement + // of the bubble's surface. + @Override + public void appTransitionFinished(int displayId) { + if (mBubblesManagerOptional.isPresent()) { + if (mBubble != null) { + mBubblesManagerOptional.get().expandStackAndSelectBubble(mBubble); + } else if (mEntryToBubble != null) { + mBubblesManagerOptional.get().expandStackAndSelectBubble(mEntryToBubble); + } + } + mCommandQueue.removeCallback(this); + } + }); } @Override @@ -95,14 +118,28 @@ public class LaunchConversationActivity extends Activity { return; } - NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( - notificationKey); - if (entry != null && entry.canBubble() && mBubblesManagerOptional.isPresent()) { - if (DEBUG) Log.d(TAG, "Open bubble for conversation"); - mBubblesManagerOptional.get().expandStackAndSelectBubble(entry); - // Just opt-out and don't cancel the notification for bubbles. - finish(); - return; + // We can potentially bubble without a notification, so rather than rely on + // notificationKey here (which could be null if there's no notification or if the + // bubble is suppressing the notification), so we'll use the shortcutId for lookups. + // This misses one specific case: a bubble that was never opened & still has a + // visible notification, but the bubble was dismissed & aged out of the overflow. + // So it wouldn't exist in the stack or overflow to be looked up BUT the notif entry + // would still exist & be bubbleable. So if we don't get a bubble from the + // shortcutId, fallback to notificationKey if it exists. + if (mBubblesManagerOptional.isPresent()) { + mBubble = mBubblesManagerOptional.get().getBubbleWithShortcutId(tileId); + NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( + notificationKey); + if (mBubble != null || (entry != null && entry.canBubble())) { + mEntryToBubble = entry; + if (DEBUG) { + Log.d(TAG, + "Opening bubble: " + mBubble + ", entry: " + mEntryToBubble); + } + // Just opt-out and don't cancel the notification for bubbles. + finish(); + return; + } } if (mIStatusBarService == null) { diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java index 10c4a55ad2409..5441bd4c958dc 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -84,6 +84,7 @@ import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ZenModeController; +import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleEntry; import com.android.wm.shell.bubbles.Bubbles; @@ -657,6 +658,22 @@ public class BubblesManager implements Dumpable { mBubbles.expandStackAndSelectBubble(notifToBubbleEntry(entry)); } + /** + * Request the stack expand if needed, then select the specified Bubble as current. + * + * @param bubble the bubble to be selected + */ + public void expandStackAndSelectBubble(Bubble bubble) { + mBubbles.expandStackAndSelectBubble(bubble); + } + + /** + * @return a bubble that matches the provided shortcutId, if one exists. + */ + public Bubble getBubbleWithShortcutId(String shortcutId) { + return mBubbles.getBubbleWithShortcutId(shortcutId); + } + /** See {@link NotifCallback}. */ public void addNotifCallback(NotifCallback callback) { mCallbacks.add(callback); diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java index ccb40e1161153..5f4d90b3666f8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java @@ -16,11 +16,14 @@ package com.android.systemui.people.widget; +import static android.view.Display.DEFAULT_DISPLAY; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -39,9 +42,11 @@ import androidx.test.filters.SmallTest; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.wmshell.BubblesManager; +import com.android.wm.shell.bubbles.Bubble; import org.junit.Before; import org.junit.Test; @@ -87,6 +92,8 @@ public class LaunchConversationActivityTest extends SysuiTestCase { @Mock private UserManager mUserManager; + private CommandQueue mCommandQueue; + @Captor private ArgumentCaptor mNotificationVisibilityCaptor; @@ -95,8 +102,9 @@ public class LaunchConversationActivityTest extends SysuiTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mCommandQueue = new CommandQueue(mContext); mActivity = new LaunchConversationActivity(mNotificationEntryManager, - Optional.of(mBubblesManager), mUserManager); + Optional.of(mBubblesManager), mUserManager, mCommandQueue); mActivity.setIsForTesting(true, mIStatusBarService); mIntent = new Intent(); mIntent.putExtra(PeopleSpaceWidgetProvider.EXTRA_TILE_ID, "tile ID"); @@ -159,9 +167,13 @@ public class LaunchConversationActivityTest extends SysuiTestCase { mActivity.setIntent(mIntent); mActivity.onCreate(new Bundle()); + assertThat(mActivity.isFinishing()).isTrue(); + mCommandQueue.appTransitionFinished(DEFAULT_DISPLAY); + verify(mIStatusBarService, times(1)).onNotificationClear(any(), anyInt(), any(), anyInt(), anyInt(), mNotificationVisibilityCaptor.capture()); - verify(mBubblesManager, never()).expandStackAndSelectBubble(any()); + verify(mBubblesManager, never()).expandStackAndSelectBubble(any(Bubble.class)); + verify(mBubblesManager, never()).expandStackAndSelectBubble(any(NotificationEntry.class)); NotificationVisibility nv = mNotificationVisibilityCaptor.getValue(); assertThat(nv.count).isEqualTo(NOTIF_COUNT); @@ -175,6 +187,9 @@ public class LaunchConversationActivityTest extends SysuiTestCase { mActivity.setIntent(mIntent); mActivity.onCreate(new Bundle()); + assertThat(mActivity.isFinishing()).isTrue(); + mCommandQueue.appTransitionFinished(DEFAULT_DISPLAY); + // Don't clear the notification for bubbles. verify(mIStatusBarService, never()).onNotificationClear(any(), anyInt(), any(), anyInt(), anyInt(), any()); @@ -190,8 +205,27 @@ public class LaunchConversationActivityTest extends SysuiTestCase { mActivity.onCreate(new Bundle()); assertThat(mActivity.isFinishing()).isTrue(); + mCommandQueue.appTransitionFinished(DEFAULT_DISPLAY); + verify(mIStatusBarService, never()).onNotificationClear(any(), anyInt(), any(), anyInt(), anyInt(), any()); - verify(mBubblesManager, never()).expandStackAndSelectBubble(any()); + verify(mBubblesManager, never()).expandStackAndSelectBubble(any(Bubble.class)); + verify(mBubblesManager, never()).expandStackAndSelectBubble(any(NotificationEntry.class)); + } + + @Test + public void testBubbleWithNoNotifOpensBubble() throws Exception { + Bubble bubble = mock(Bubble.class); + when(mBubblesManager.getBubbleWithShortcutId(any())).thenReturn(bubble); + + mIntent.putExtra(PeopleSpaceWidgetProvider.EXTRA_NOTIFICATION_KEY, + EMPTY_STRING); + mActivity.setIntent(mIntent); + mActivity.onCreate(new Bundle()); + + assertThat(mActivity.isFinishing()).isTrue(); + mCommandQueue.appTransitionFinished(DEFAULT_DISPLAY); + + verify(mBubblesManager, times(1)).expandStackAndSelectBubble(eq(bubble)); } }