Persists bubbles to disk (part 1)

Bubbles are now write-through an xml file on disk upon addition/update,
asynchronously.

Bug: 149713060
Test: manually verify when new bubble pops up it is write through xml
Change-Id: I5d9f58cd0858ca96d5ec05c72aad4ae3a18c0aa0
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index cafa060..ad8d57b 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -153,6 +153,7 @@
     private final NotificationGroupManager mNotificationGroupManager;
     private final ShadeController mShadeController;
     private final FloatingContentCoordinator mFloatingContentCoordinator;
+    private final BubbleDataRepository mDataRepository;
 
     private BubbleData mBubbleData;
     private ScrimView mBubbleScrim;
@@ -294,13 +295,14 @@
             FeatureFlags featureFlags,
             DumpManager dumpManager,
             FloatingContentCoordinator floatingContentCoordinator,
+            BubbleDataRepository dataRepository,
             SysUiState sysUiState,
             INotificationManager notificationManager) {
         this(context, notificationShadeWindowController, statusBarStateController, shadeController,
                 data, null /* synchronizer */, configurationController, interruptionStateProvider,
                 zenModeController, notifUserManager, groupManager, entryManager,
-                notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState,
-                notificationManager);
+                notifPipeline, featureFlags, dumpManager, floatingContentCoordinator,
+                dataRepository, sysUiState, notificationManager);
     }
 
     /**
@@ -322,6 +324,7 @@
             FeatureFlags featureFlags,
             DumpManager dumpManager,
             FloatingContentCoordinator floatingContentCoordinator,
+            BubbleDataRepository dataRepository,
             SysUiState sysUiState,
             INotificationManager notificationManager) {
         dumpManager.registerDumpable(TAG, this);
@@ -331,6 +334,7 @@
         mNotifUserManager = notifUserManager;
         mZenModeController = zenModeController;
         mFloatingContentCoordinator = floatingContentCoordinator;
+        mDataRepository = dataRepository;
         mINotificationManager = notificationManager;
         mZenModeController.addCallback(new ZenModeController.Callback() {
             @Override
@@ -1018,6 +1022,7 @@
             // Do removals, if any.
             ArrayList<Pair<Bubble, Integer>> removedBubbles =
                     new ArrayList<>(update.removedBubbles);
+            ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>();
             for (Pair<Bubble, Integer> removed : removedBubbles) {
                 final Bubble bubble = removed.first;
                 @DismissReason final int reason = removed.second;
@@ -1027,6 +1032,9 @@
                 if (reason == DISMISS_USER_CHANGED) {
                     continue;
                 }
+                if (reason == DISMISS_NOTIF_CANCEL) {
+                    bubblesToBeRemovedFromRepository.add(bubble);
+                }
                 if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
                     if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())
                         && (!bubble.showInShade()
@@ -1056,9 +1064,12 @@
                     }
                 }
             }
+            mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository);
 
             if (update.addedBubble != null) {
+                mDataRepository.addBubble(mCurrentUserId, update.addedBubble);
                 mStackView.addBubble(update.addedBubble);
+
             }
 
             if (update.updatedBubble != null) {
@@ -1068,6 +1079,7 @@
             // At this point, the correct bubbles are inflated in the stack.
             // Make sure the order in bubble data is reflected in bubble row.
             if (update.orderChanged) {
+                mDataRepository.addBubbles(mCurrentUserId, update.bubbles);
                 mStackView.updateBubbleOrder(update.bubbles);
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
new file mode 100644
index 0000000..63dd801
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 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.systemui.bubbles
+
+import android.annotation.UserIdInt
+import com.android.systemui.bubbles.storage.BubblePersistentRepository
+import com.android.systemui.bubbles.storage.BubbleVolatileRepository
+import com.android.systemui.bubbles.storage.BubbleXmlEntity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.yield
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+internal class BubbleDataRepository @Inject constructor(
+    private val volatileRepository: BubbleVolatileRepository,
+    private val persistentRepository: BubblePersistentRepository
+) {
+
+    private val ioScope = CoroutineScope(Dispatchers.IO)
+    private var job: Job? = null
+
+    /**
+     * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk
+     * asynchronously.
+     */
+    fun addBubble(@UserIdInt userId: Int, bubble: Bubble) {
+        volatileRepository.addBubble(
+            BubbleXmlEntity(userId, bubble.packageName, bubble.shortcutInfo?.id ?: return))
+        persistToDisk()
+    }
+
+    /**
+     * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk
+     * asynchronously.
+     */
+    fun addBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) {
+        volatileRepository.addBubbles(bubbles.mapNotNull {
+            val shortcutId = it.shortcutInfo?.id ?: return@mapNotNull null
+            BubbleXmlEntity(userId, it.packageName, shortcutId)
+        })
+        persistToDisk()
+    }
+
+    fun removeBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) {
+        volatileRepository.removeBubbles(bubbles.mapNotNull {
+            val shortcutId = it.shortcutInfo?.id ?: return@mapNotNull null
+            BubbleXmlEntity(userId, it.packageName, shortcutId)
+        })
+        persistToDisk()
+    }
+
+    /**
+     * Persists the bubbles to disk. When being called multiple times, it waits for first ongoing
+     * write operation to finish then run another write operation exactly once.
+     *
+     * e.g.
+     * Job A started -> blocking I/O
+     * Job B started, cancels A, wait for blocking I/O in A finishes
+     * Job C started, cancels B, wait for job B to finish
+     * Job D started, cancels C, wait for job C to finish
+     * Job A completed
+     * Job B resumes and reaches yield() and is then cancelled
+     * Job C resumes and reaches yield() and is then cancelled
+     * Job D resumes and performs another blocking I/O
+     */
+    private fun persistToDisk() {
+        val prev = job
+        job = ioScope.launch {
+            // if there was an ongoing disk I/O operation, they can be cancelled
+            prev?.cancelAndJoin()
+            // check for cancellation before disk I/O
+            yield()
+            // save to disk
+            persistentRepository.persistsToDisk(volatileRepository.bubbles)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java
index 72d646e..e3b630b 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java
@@ -21,6 +21,7 @@
 
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.bubbles.BubbleData;
+import com.android.systemui.bubbles.BubbleDataRepository;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -65,6 +66,7 @@
             FeatureFlags featureFlags,
             DumpManager dumpManager,
             FloatingContentCoordinator floatingContentCoordinator,
+            BubbleDataRepository bubbleDataRepository,
             SysUiState sysUiState,
             INotificationManager notifManager) {
         return new BubbleController(
@@ -84,6 +86,7 @@
                 featureFlags,
                 dumpManager,
                 floatingContentCoordinator,
+                bubbleDataRepository,
                 sysUiState,
                 notifManager);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt
new file mode 100644
index 0000000..b2495c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 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.systemui.bubbles.storage
+
+import android.content.Context
+import android.util.AtomicFile
+import android.util.Log
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class BubblePersistentRepository @Inject constructor(
+    context: Context
+) {
+
+    private val bubbleFile: AtomicFile = AtomicFile(File(context.filesDir,
+            "overflow_bubbles.xml"), "overflow-bubbles")
+
+    fun persistsToDisk(bubbles: List<BubbleXmlEntity>): Boolean {
+        synchronized(bubbleFile) {
+            val stream: FileOutputStream = try { bubbleFile.startWrite() } catch (e: IOException) {
+                Log.e(TAG, "Failed to save bubble file", e)
+                return false
+            }
+            try {
+                writeXml(stream, bubbles)
+                bubbleFile.finishWrite(stream)
+                return true
+            } catch (e: Exception) {
+                Log.e(TAG, "Failed to save bubble file, restoring backup", e)
+                bubbleFile.failWrite(stream)
+            }
+        }
+        return false
+    }
+}
+
+private const val TAG = "BubblePersistentRepository"
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt
new file mode 100644
index 0000000..3dba268
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 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.systemui.bubbles.storage
+
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private const val CAPACITY = 16
+
+/**
+ * BubbleVolatileRepository holds the most updated snapshot of list of bubbles for in-memory
+ * manipulation.
+ */
+@Singleton
+class BubbleVolatileRepository @Inject constructor() {
+    /**
+     * An ordered set of bubbles based on their natural ordering.
+     */
+    private val entities = mutableSetOf<BubbleXmlEntity>()
+
+    /**
+     * Returns a snapshot of all the bubbles.
+     */
+    val bubbles: List<BubbleXmlEntity>
+        @Synchronized
+        get() = entities.toList()
+
+    /**
+     * Add the bubble to memory and perform a de-duplication. In case the bubble already exists,
+     * the bubble will be moved to the last.
+     */
+    fun addBubble(bubble: BubbleXmlEntity) = addBubbles(listOf(bubble))
+
+    /**
+     * Add the bubbles to memory and perform a de-duplication. In case a bubble already exists,
+     * it will be moved to the last.
+     */
+    @Synchronized
+    fun addBubbles(bubbles: List<BubbleXmlEntity>) {
+        if (bubbles.isEmpty()) return
+        bubbles.forEach { entities.remove(it) }
+        if (entities.size + bubbles.size >= CAPACITY) {
+            entities.drop(entities.size + bubbles.size - CAPACITY)
+        }
+        entities.addAll(bubbles.reversed())
+    }
+
+    @Synchronized
+    fun removeBubbles(bubbles: List<BubbleXmlEntity>) {
+        bubbles.forEach { entities.remove(it) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlEntity.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlEntity.kt
new file mode 100644
index 0000000..d0f7607
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlEntity.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2020 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.systemui.bubbles.storage
+
+import android.annotation.UserIdInt
+
+data class BubbleXmlEntity(
+    @UserIdInt val userId: Int,
+    val packageName: String,
+    val shortcutId: 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
new file mode 100644
index 0000000..3192f34b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 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.systemui.bubbles.storage
+
+import com.android.internal.util.FastXmlSerializer
+import org.xmlpull.v1.XmlSerializer
+import java.io.IOException
+import java.io.OutputStream
+import java.nio.charset.StandardCharsets
+
+private const val TAG_BUBBLES = "bs"
+private const val TAG_BUBBLE = "bb"
+private const val ATTR_USER_ID = "uid"
+private const val ATTR_PACKAGE = "pkg"
+private const val ATTR_SHORTCUT_ID = "sid"
+
+/**
+ * Writes the bubbles in xml format into given output stream.
+ */
+@Throws(IOException::class)
+fun writeXml(stream: OutputStream, bubbles: List<BubbleXmlEntity>) {
+    val serializer: XmlSerializer = FastXmlSerializer()
+    serializer.setOutput(stream, StandardCharsets.UTF_8.name())
+    serializer.startDocument(null, true)
+    serializer.startTag(null, TAG_BUBBLES)
+    bubbles.forEach { b -> writeXmlEntry(serializer, b) }
+    serializer.endTag(null, TAG_BUBBLES)
+    serializer.endDocument()
+}
+
+/**
+ * Creates a xml entry for given bubble in following format:
+ * ```
+ * <bb uid="0" pkg="com.example.messenger" sid="my-shortcut" />
+ * ```
+ */
+private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleXmlEntity) {
+    try {
+        serializer.startTag(null, TAG_BUBBLE)
+        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.endTag(null, TAG_BUBBLE)
+    } catch (e: IOException) {
+        throw RuntimeException(e)
+    }
+}
\ No newline at end of file