Prevent duplicates from persisted bubbles.

Bug: 157070577
Test: add some bubbles, reboot, verify it doesn't generate duplicates
Change-Id: I95898fa6a1c54e2e57fde5990ba6cfefd1a5c2f0
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 8cfa2d6..15eda06 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -15,7 +15,6 @@
  */
 package com.android.systemui.bubbles;
 
-
 import static android.app.Notification.FLAG_BUBBLE;
 import static android.os.AsyncTask.Status.FINISHED;
 import static android.view.Display.INVALID_DISPLAY;
@@ -106,34 +105,14 @@
     private int mFlags;
 
     /**
-     * Generate a unique identifier for this bubble based on given {@link NotificationEntry}. If
-     * {@link ShortcutInfo} was found in the notification entry, the identifier would be generated
-     * from {@link ShortcutInfo} instead. See also {@link #key(ShortcutInfo)}.
-     */
-    @NonNull
-    public static String key(@NonNull final NotificationEntry entry) {
-        final ShortcutInfo shortcutInfo = entry.getRanking().getShortcutInfo();
-        if (shortcutInfo != null) return key(shortcutInfo);
-        return entry.getKey();
-    }
-
-    /**
-     * Generate a unique identifier for this bubble based on given {@link ShortcutInfo}.
-     * See also {@link #key(NotificationEntry)}.
-     */
-    @NonNull
-    public static String key(@NonNull final ShortcutInfo shortcutInfo) {
-        return shortcutInfo.getUserId() + "|" + shortcutInfo.getPackage() + "|"
-                + shortcutInfo.getId();
-    }
-
-    /**
      * Create a bubble with limited information based on given {@link ShortcutInfo}.
      * Note: Currently this is only being used when the bubble is persisted to disk.
      */
-    Bubble(ShortcutInfo shortcutInfo) {
+    Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo) {
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(shortcutInfo);
         mShortcutInfo = shortcutInfo;
-        mKey = key(shortcutInfo);
+        mKey = key;
         mFlags = 0;
     }
 
@@ -142,7 +121,7 @@
     Bubble(NotificationEntry e,
             BubbleController.NotificationSuppressionChangedListener listener) {
         mEntry = e;
-        mKey = key(e);
+        mKey = e.getKey();
         mLastUpdated = e.getSbn().getPostTime();
         mSuppressionListener = listener;
         mFlags = e.getSbn().getNotification().flags;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 0ec2418..3b58e53 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -606,8 +606,6 @@
 
             mStackView.setUnbubbleConversationCallback(notificationEntry ->
                     onUserChangedBubble(notificationEntry, false /* shouldBubble */));
-            // Lazy load overflow bubbles from disk
-            loadOverflowBubblesFromDisk();
         }
 
         addToWindowManagerMaybe();
@@ -1112,6 +1110,8 @@
 
         @Override
         public void applyUpdate(BubbleData.Update update) {
+            // Lazy load overflow bubbles from disk
+            loadOverflowBubblesFromDisk();
             // Update bubbles in overflow.
             if (mOverflowCallback != null) {
                 mOverflowCallback.run();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
index a0ec7cd..1c5e98b 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
@@ -77,7 +77,7 @@
             var shortcutId = b.shortcutInfo?.id
             if (shortcutId == null) shortcutId = b.entry?.bubbleMetadata?.shortcutId
             if (shortcutId == null) return@mapNotNull null
-            BubbleEntity(userId, b.packageName, shortcutId)
+            BubbleEntity(userId, b.packageName, shortcutId, b.key)
         }
     }
 
@@ -133,17 +133,17 @@
         val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet()
         /**
          * Retrieve shortcuts with given userId/packageName combination, then construct a mapping
-         * between BubbleEntity and ShortcutInfo.
+         * from the userId/packageName pair to a list of associated ShortcutInfo.
          * e.g.
          * {
-         *     BubbleEntity(0, "com.example.messenger", "id-0") ->
+         *     ShortcutKey(0, "com.example.messenger") -> [
          *         ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-0"),
-         *     BubbleEntity(0, "com.example.messenger", "id-2") ->
-         *         ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2"),
-         *     BubbleEntity(10, "com.example.chat", "id-1") ->
+         *         ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2")
+         *     ]
+         *     ShortcutKey(10, "com.example.chat") -> [
          *         ShortcutInfo(userId=10, pkg="com.example.chat", id="id-1"),
-         *     BubbleEntity(10, "com.example.chat", "id-3") ->
          *         ShortcutInfo(userId=10, pkg="com.example.chat", id="id-3")
+         *     ]
          * }
          */
         val shortcutMap = shortcutKeys.flatMap { key ->
@@ -151,11 +151,15 @@
                     LauncherApps.ShortcutQuery()
                             .setPackage(key.pkg)
                             .setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId))
-                    ?.map { BubbleEntity(key.userId, key.pkg, it.id) to it } ?: emptyList()
-        }.toMap()
+                    ?: emptyList()
+        }.groupBy { ShortcutKey(it.userId, it.`package`) }
         // For each entity loaded from xml, find the corresponding ShortcutInfo then convert them
         // into Bubble.
-        val bubbles = entities.mapNotNull { entity -> shortcutMap[entity]?.let { Bubble(it) } }
+        val bubbles = entities.mapNotNull { entity ->
+            shortcutMap[ShortcutKey(entity.userId, entity.packageName)]
+                    ?.first { shortcutInfo -> entity.shortcutId == shortcutInfo.id }
+                    ?.let { shortcutInfo -> Bubble(entity.key, shortcutInfo) }
+        }
         uiScope.launch { cb(bubbles) }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt
index 4690a8e..4348261 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt
@@ -20,5 +20,6 @@
 data class BubbleEntity(
     @UserIdInt val userId: Int,
     val packageName: String,
-    val shortcutId: String
+    val shortcutId: String,
+    val key: String
 )
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
index 821b64c..1df9f72 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
@@ -30,6 +30,7 @@
 private const val ATTR_USER_ID = "uid"
 private const val ATTR_PACKAGE = "pkg"
 private const val ATTR_SHORTCUT_ID = "sid"
+private const val ATTR_KEY = "key"
 
 /**
  * Writes the bubbles in xml format into given output stream.
@@ -48,7 +49,7 @@
 /**
  * Creates a xml entry for given bubble in following format:
  * ```
- * <bb uid="0" pkg="com.example.messenger" sid="my-shortcut" />
+ * <bb uid="0" pkg="com.example.messenger" sid="my-shortcut" key="my-key" />
  * ```
  */
 private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleEntity) {
@@ -57,6 +58,7 @@
         serializer.attribute(null, ATTR_USER_ID, bubble.userId.toString())
         serializer.attribute(null, ATTR_PACKAGE, bubble.packageName)
         serializer.attribute(null, ATTR_SHORTCUT_ID, bubble.shortcutId)
+        serializer.attribute(null, ATTR_KEY, bubble.key)
         serializer.endTag(null, TAG_BUBBLE)
     } catch (e: IOException) {
         throw RuntimeException(e)
@@ -83,7 +85,8 @@
     return BubbleEntity(
             parser.getAttributeWithName(ATTR_USER_ID)?.toInt() ?: return null,
             parser.getAttributeWithName(ATTR_PACKAGE) ?: return null,
-            parser.getAttributeWithName(ATTR_SHORTCUT_ID) ?: return null
+            parser.getAttributeWithName(ATTR_SHORTCUT_ID) ?: return null,
+            parser.getAttributeWithName(ATTR_KEY) ?: return null
     )
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt
index d49d021..f468192 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt
@@ -29,9 +29,9 @@
 class BubblePersistentRepositoryTest : SysuiTestCase() {
 
     private val bubbles = listOf(
-            BubbleEntity(0, "com.example.messenger", "shortcut-1"),
-            BubbleEntity(10, "com.example.chat", "alice and bob"),
-            BubbleEntity(0, "com.example.messenger", "shortcut-2")
+            BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1"),
+            BubbleEntity(10, "com.example.chat", "alice and bob", "key-2"),
+            BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3")
     )
     private lateinit var repository: BubblePersistentRepository
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt
index 7acc937..ee48846 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt
@@ -28,9 +28,9 @@
 @RunWith(AndroidTestingRunner::class)
 class BubbleVolatileRepositoryTest : SysuiTestCase() {
 
-    private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1")
-    private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob")
-    private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2")
+    private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1")
+    private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob", "k2")
+    private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3")
     private val bubbles = listOf(bubble1, bubble2, bubble3)
 
     private lateinit var repository: BubbleVolatileRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt
index ef4580c..79701ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt
@@ -31,17 +31,17 @@
 class BubbleXmlHelperTest : SysuiTestCase() {
 
     private val bubbles = listOf(
-        BubbleEntity(0, "com.example.messenger", "shortcut-1"),
-        BubbleEntity(10, "com.example.chat", "alice and bob"),
-        BubbleEntity(0, "com.example.messenger", "shortcut-2")
+        BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1"),
+        BubbleEntity(10, "com.example.chat", "alice and bob", "k2"),
+        BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3")
     )
 
     @Test
     fun testWriteXml() {
         val expectedEntries = """
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" />
-            <bb uid="10" pkg="com.example.chat" sid="alice and bob" />
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" />
+            <bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" />
         """.trimIndent()
         ByteArrayOutputStream().use {
             writeXml(it, bubbles)
@@ -56,9 +56,9 @@
         val src = """
             <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
             <bs>
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" />
-            <bb uid="10" pkg="com.example.chat" sid="alice and bob" />
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" />
+            <bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" />
             </bs>
         """.trimIndent()
         val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))