diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index b55d246de65c6..7a27676237a1a 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -129,6 +129,8 @@ android_library { "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 cafa0604d88e6..ad8d57bbf23f2 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, + BubbleDataRepository dataRepository, SysUiState sysUiState, INotificationManager notificationManager) { dumpManager.registerDumpable(TAG, this); @@ -331,6 +334,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mNotifUserManager = notifUserManager; mZenModeController = zenModeController; mFloatingContentCoordinator = floatingContentCoordinator; + mDataRepository = dataRepository; mINotificationManager = notificationManager; mZenModeController.addCallback(new ZenModeController.Callback() { @Override @@ -1018,6 +1022,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // Do removals, if any. ArrayList> removedBubbles = new ArrayList<>(update.removedBubbles); + ArrayList bubblesToBeRemovedFromRepository = new ArrayList<>(); for (Pair removed : removedBubbles) { final Bubble bubble = removed.first; @DismissReason final int reason = removed.second; @@ -1027,6 +1032,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } } + 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // 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 0000000000000..63dd801be7caa --- /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) { + 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) { + 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 72d646e0554d5..e3b630b049f57 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 android.content.Context; 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 @@ public interface BubbleModule { FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, + BubbleDataRepository bubbleDataRepository, SysUiState sysUiState, INotificationManager notifManager) { return new BubbleController( @@ -84,6 +86,7 @@ public interface BubbleModule { 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 0000000000000..b2495c658948f --- /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): 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 0000000000000..3dba268074859 --- /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() + + /** + * Returns a snapshot of all the bubbles. + */ + val bubbles: List + @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) { + 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) { + 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 0000000000000..d0f76077cd51a --- /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 0000000000000..3192f34b69fd3 --- /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) { + 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: + * ``` + * + * ``` + */ +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 58906d7bd2d31..e2f303e87d871 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 @@ public class BubbleControllerTest extends SysuiTestCase { private KeyguardBypassController mKeyguardBypassController; @Mock private FloatingContentCoordinator mFloatingContentCoordinator; + @Mock + private BubbleDataRepository mDataRepository; private SysUiState mSysUiState; private boolean mSysUiStateBubblesExpanded; @@ -275,6 +277,7 @@ public class BubbleControllerTest extends SysuiTestCase { 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 ec1a797533fa4..8a83b84c6b5ee 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 @@ public class NewNotifPipelineBubbleControllerTest extends SysuiTestCase { @Mock private FloatingContentCoordinator mFloatingContentCoordinator; @Mock + private BubbleDataRepository mDataRepository; + @Mock private NotificationShadeWindowView mNotificationShadeWindowView; private SysUiState mSysUiState = new SysUiState(); @@ -250,6 +252,7 @@ public class NewNotifPipelineBubbleControllerTest extends SysuiTestCase { 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 7815ae78823ae..1542b8675ede2 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 @@ public class TestableBubbleController extends BubbleController { 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); } }