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/Android.bp b/packages/SystemUI/Android.bp
index b55d246..7a27676 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -129,6 +129,8 @@
"androidx.lifecycle_lifecycle-extensions",
"androidx.dynamicanimation_dynamicanimation",
"androidx-constraintlayout_constraintlayout",
+ "kotlinx-coroutines-android",
+ "kotlinx-coroutines-core",
"iconloader_base",
"SystemUI-tags",
"SystemUI-proto",
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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 58906d7..e2f303e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -140,6 +140,8 @@
private KeyguardBypassController mKeyguardBypassController;
@Mock
private FloatingContentCoordinator mFloatingContentCoordinator;
+ @Mock
+ private BubbleDataRepository mDataRepository;
private SysUiState mSysUiState;
private boolean mSysUiStateBubblesExpanded;
@@ -275,6 +277,7 @@
mFeatureFlagsOldPipeline,
mDumpManager,
mFloatingContentCoordinator,
+ mDataRepository,
mSysUiState,
mock(INotificationManager.class));
mBubbleController.setExpandListener(mBubbleExpandListener);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
index ec1a797..8a83b84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
@@ -135,6 +135,8 @@
@Mock
private FloatingContentCoordinator mFloatingContentCoordinator;
@Mock
+ private BubbleDataRepository mDataRepository;
+ @Mock
private NotificationShadeWindowView mNotificationShadeWindowView;
private SysUiState mSysUiState = new SysUiState();
@@ -250,6 +252,7 @@
mFeatureFlagsNewPipeline,
mDumpManager,
mFloatingContentCoordinator,
+ mDataRepository,
mSysUiState,
mock(INotificationManager.class));
mBubbleController.addNotifCallback(mNotifCallback);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java
index 7815ae7..1542b86 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java
@@ -55,14 +55,15 @@
FeatureFlags featureFlags,
DumpManager dumpManager,
FloatingContentCoordinator floatingContentCoordinator,
+ BubbleDataRepository dataRepository,
SysUiState sysUiState,
INotificationManager notificationManager) {
super(context,
notificationShadeWindowController, statusBarStateController, shadeController,
data, Runnable::run, configurationController, interruptionStateProvider,
zenModeController, lockscreenUserManager, groupManager, entryManager,
- notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState,
- notificationManager);
+ notifPipeline, featureFlags, dumpManager, floatingContentCoordinator,
+ dataRepository, sysUiState, notificationManager);
setInflateSynchronously(true);
}
}