summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Shell/res/values-fa/strings.xml2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt67
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/OWNERS1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml12
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt8
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt11
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt190
12 files changed, 285 insertions, 20 deletions
diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml
index 13a2ea2db140..edff47a0be1a 100644
--- a/libs/WindowManager/Shell/res/values-fa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fa/strings.xml
@@ -59,7 +59,7 @@
<string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"خروج از «حالت یک‌دستی»"</string>
<string name="bubbles_settings_button_description" msgid="1301286017420516912">"تنظیمات برای حبابک‌های <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
<string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"سرریز"</string>
- <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"افزودن برگشت به پشته"</string>
+ <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"افزودن برگشتن به پشته"</string>
<string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
<string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g> و <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> مورد بیشتر"</string>
<string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"انتقال به بالا سمت راست"</string>
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 cb03c096f7f1..688023737074 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
@@ -1282,7 +1282,9 @@ public class BubbleController implements ConfigurationChangeListener,
return;
}
mOverflowDataLoadNeeded = false;
- mDataRepository.loadBubbles(mCurrentUserId, (bubbles) -> {
+ List<UserInfo> users = mUserManager.getAliveUsers();
+ List<Integer> userIds = users.stream().map(userInfo -> userInfo.id).toList();
+ mDataRepository.loadBubbles(mCurrentUserId, userIds, (bubbles) -> {
bubbles.forEach(bubble -> {
if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) {
// if the bubble is already active, there's no need to push it to overflow
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
index 942dcd9db54c..896a33449185 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
@@ -17,7 +17,6 @@ package com.android.wm.shell.bubbles
import android.annotation.SuppressLint
import android.annotation.UserIdInt
-import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC
@@ -25,6 +24,8 @@ import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LA
import android.content.pm.UserInfo
import android.os.UserHandle
import android.util.Log
+import android.util.SparseArray
+import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener
import com.android.wm.shell.bubbles.storage.BubbleEntity
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
@@ -38,13 +39,12 @@ import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
-internal class BubbleDataRepository(
- context: Context,
+class BubbleDataRepository(
private val launcherApps: LauncherApps,
- private val mainExecutor: ShellExecutor
+ private val mainExecutor: ShellExecutor,
+ private val persistentRepository: BubblePersistentRepository,
) {
private val volatileRepository = BubbleVolatileRepository(launcherApps)
- private val persistentRepository = BubblePersistentRepository(context)
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var job: Job? = null
@@ -99,6 +99,43 @@ internal class BubbleDataRepository(
if (volatileRepository.sanitizeBubbles(userIds)) persistToDisk()
}
+ /**
+ * Removes all entities that don't have a user in the activeUsers list, if any entities were
+ * removed it persists the new list to disk.
+ */
+ private fun filterForActiveUsersAndPersist(
+ activeUsers: List<Int>,
+ entitiesByUser: SparseArray<List<BubbleEntity>>
+ ): SparseArray<List<BubbleEntity>> {
+ val validEntitiesByUser = SparseArray<List<BubbleEntity>>()
+ var entitiesChanged = false
+ for (i in 0 until entitiesByUser.size()) {
+ val parentUserId = entitiesByUser.keyAt(i)
+ if (activeUsers.contains(parentUserId)) {
+ val validEntities = mutableListOf<BubbleEntity>()
+ // Check if each of the bubbles in the top-level user still has a valid user
+ // as it could belong to a profile and have a different id from the parent.
+ for (entity in entitiesByUser.get(parentUserId)) {
+ if (activeUsers.contains(entity.userId)) {
+ validEntities.add(entity)
+ } else {
+ entitiesChanged = true
+ }
+ }
+ if (validEntities.isNotEmpty()) {
+ validEntitiesByUser.put(parentUserId, validEntities)
+ }
+ } else {
+ entitiesChanged = true
+ }
+ }
+ if (entitiesChanged) {
+ persistToDisk(validEntitiesByUser)
+ return validEntitiesByUser
+ }
+ return entitiesByUser
+ }
+
private fun transform(bubbles: List<Bubble>): List<BubbleEntity> {
return bubbles.mapNotNull { b ->
BubbleEntity(
@@ -130,7 +167,9 @@ internal class BubbleDataRepository(
* Job C resumes and reaches yield() and is then cancelled
* Job D resumes and performs another blocking I/O
*/
- private fun persistToDisk() {
+ private fun persistToDisk(
+ entitiesByUser: SparseArray<List<BubbleEntity>> = volatileRepository.bubbles
+ ) {
val prev = job
job = coroutineScope.launch {
// if there was an ongoing disk I/O operation, they can be cancelled
@@ -138,7 +177,7 @@ internal class BubbleDataRepository(
// check for cancellation before disk I/O
yield()
// save to disk
- persistentRepository.persistsToDisk(volatileRepository.bubbles)
+ persistentRepository.persistsToDisk(entitiesByUser)
}
}
@@ -149,7 +188,12 @@ internal class BubbleDataRepository(
* bubbles.
*/
@SuppressLint("WrongConstant")
- fun loadBubbles(userId: Int, cb: (List<Bubble>) -> Unit) = coroutineScope.launch {
+ @VisibleForTesting
+ fun loadBubbles(
+ userId: Int,
+ currentUsers: List<Int>,
+ cb: (List<Bubble>) -> Unit
+ ) = coroutineScope.launch {
/**
* Load BubbleEntity from disk.
* e.g.
@@ -160,7 +204,12 @@ internal class BubbleDataRepository(
* ]
*/
val entitiesByUser = persistentRepository.readFromDisk()
- val entities = entitiesByUser.get(userId) ?: return@launch
+
+ // Before doing anything, validate that the entities we loaded are valid & have an existing
+ // user.
+ val validEntitiesByUser = filterForActiveUsersAndPersist(currentUsers, entitiesByUser)
+
+ val entities = validEntitiesByUser.get(userId) ?: return@launch
volatileRepository.addBubbles(userId, entities)
/**
* Extract userId/packageName from these entities.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/OWNERS
new file mode 100644
index 000000000000..7af038999797
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/OWNERS
@@ -0,0 +1 @@
+madym@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 03f92aaf9ce9..20c3bd2dccc4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -37,6 +37,7 @@ import com.android.wm.shell.bubbles.BubbleDataRepository;
import com.android.wm.shell.bubbles.BubbleLogger;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.properties.ProdBubbleProperties;
+import com.android.wm.shell.bubbles.storage.BubblePersistentRepository;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -184,7 +185,8 @@ public abstract class WMShellModule {
IWindowManager wmService) {
return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
null /* synchronizer */, floatingContentCoordinator,
- new BubbleDataRepository(context, launcherApps, mainExecutor),
+ new BubbleDataRepository(launcherApps, mainExecutor,
+ new BubblePersistentRepository(context)),
statusBarService, windowManager, windowManagerShellWrapper, userManager,
launcherApps, logger, taskStackListener, organizer, positioner, displayController,
oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index b8407c465741..24aaa9b75ebe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -856,6 +856,9 @@ public class PipTransition extends PipTransitionController {
final int enterAnimationType = mEnterAnimationType;
if (enterAnimationType == ANIM_TYPE_ALPHA) {
startTransaction.setAlpha(leash, 0f);
+ } else {
+ // set alpha to 1, because for multi-activity PiP it will create a new task with alpha 0
+ startTransaction.setAlpha(leash, 1f);
}
startTransaction.apply();
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml
index 4721741611cf..6a87de47def4 100644
--- a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml
@@ -15,6 +15,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.android.wm.shell.flicker">
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
@@ -57,10 +58,11 @@
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
- </application>
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.wm.shell.flicker"
- android:label="WindowManager Shell Flicker Tests">
- </instrumentation>
+ <!-- (b/197936012) Remove startup provider due to test timeout issue -->
+ <provider
+ android:name="androidx.startup.InitializationProvider"
+ android:authorities="${applicationId}.androidx-startup"
+ tools:node="remove" />
+ </application>
</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
index d38bcc290d7b..5c7d1d8df2e8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
@@ -72,7 +72,11 @@ abstract class BaseBubbleScreen(flicker: LegacyFlickerTest) : BaseTest(flicker)
uid,
NotificationManager.BUBBLE_PREFERENCE_NONE
)
- testApp.exit()
+ device.wait(
+ Until.gone(By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)),
+ FIND_OBJECT_TIMEOUT
+ )
+ testApp.exit(wmHelper)
}
extraSpec(this)
@@ -92,7 +96,7 @@ abstract class BaseBubbleScreen(flicker: LegacyFlickerTest) : BaseTest(flicker)
supportedRotations = listOf(Rotation.ROTATION_0)
)
- const val FIND_OBJECT_TIMEOUT = 2000L
+ const val FIND_OBJECT_TIMEOUT = 4000L
const val SYSTEM_UI_PACKAGE = SYSTEMUI_PACKAGE
const val BUBBLE_RES_NAME = "bubble_view"
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
index d02ee4b90e08..3f28ae848d1f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
@@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.bubble
import android.content.Context
import android.graphics.Point
import android.platform.test.annotations.Presubmit
+import android.tools.common.flicker.subject.layers.LayersTraceSubject
import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
import android.tools.device.flicker.legacy.FlickerBuilder
import android.tools.device.flicker.legacy.LegacyFlickerTest
@@ -73,4 +74,14 @@ open class DragToDismissBubbleScreenTest(flicker: LegacyFlickerTest) : BaseBubbl
open fun testAppIsAlwaysVisible() {
flicker.assertLayers { this.isVisible(testApp) }
}
+
+ @Presubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+ flicker.assertLayers {
+ this.visibleLayersShownMoreThanOneConsecutiveEntry(
+ LayersTraceSubject.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS + listOf(testApp)
+ )
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
index c430febae8a6..26aca1830889 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
@@ -81,7 +81,7 @@ class OpenActivityFromBubbleOnLocksreenTest(flicker: LegacyFlickerTest) :
instrumentation.uiAutomation.syncInputTransactions()
val showBubble =
device.wait(
- Until.findObject(By.res("com.android.systemui", "bubble_view")),
+ Until.findObject(By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)),
FIND_OBJECT_TIMEOUT
)
showBubble?.click() ?: error("Bubble notify not found")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
index 43722ae91408..a926bb7d85c3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
@@ -55,6 +55,7 @@ open class SendBubbleNotificationTest(flicker: LegacyFlickerTest) : BaseBubbleSc
FIND_OBJECT_TIMEOUT
)
?: error("No bubbles found")
+ device.waitForIdle()
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt
new file mode 100644
index 000000000000..6d9d62d3ad92
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.bubbles
+
+import android.app.ActivityTaskManager
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.util.SparseArray
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.bubbles.storage.BubbleEntity
+import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
+import com.android.wm.shell.common.ShellExecutor
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+
+class BubbleDataRepositoryTest : ShellTestCase() {
+
+ private val user0BubbleEntities = listOf(
+ BubbleEntity(
+ userId = 0,
+ packageName = "com.example.messenger",
+ shortcutId = "shortcut-1",
+ key = "0k1",
+ desiredHeight = 120,
+ desiredHeightResId = 0,
+ title = null,
+ taskId = 1,
+ locus = null,
+ isDismissable = true
+ ),
+ BubbleEntity(
+ userId = 10,
+ packageName = "com.example.chat",
+ shortcutId = "alice and bob",
+ key = "0k2",
+ desiredHeight = 0,
+ desiredHeightResId = 16537428,
+ title = "title",
+ taskId = 2,
+ locus = null
+ ),
+ BubbleEntity(
+ userId = 0,
+ packageName = "com.example.messenger",
+ shortcutId = "shortcut-2",
+ key = "0k3",
+ desiredHeight = 120,
+ desiredHeightResId = 0,
+ title = null,
+ taskId = ActivityTaskManager.INVALID_TASK_ID,
+ locus = null
+ )
+ )
+
+ private val user1BubbleEntities = listOf(
+ BubbleEntity(
+ userId = 1,
+ packageName = "com.example.messenger",
+ shortcutId = "shortcut-1",
+ key = "1k1",
+ desiredHeight = 120,
+ desiredHeightResId = 0,
+ title = null,
+ taskId = 3,
+ locus = null,
+ isDismissable = true
+ ),
+ BubbleEntity(
+ userId = 12,
+ packageName = "com.example.chat",
+ shortcutId = "alice and bob",
+ key = "1k2",
+ desiredHeight = 0,
+ desiredHeightResId = 16537428,
+ title = "title",
+ taskId = 4,
+ locus = null
+ ),
+ BubbleEntity(
+ userId = 1,
+ packageName = "com.example.messenger",
+ shortcutId = "shortcut-2",
+ key = "1k3",
+ desiredHeight = 120,
+ desiredHeightResId = 0,
+ title = null,
+ taskId = ActivityTaskManager.INVALID_TASK_ID,
+ locus = null
+ ),
+ BubbleEntity(
+ userId = 12,
+ packageName = "com.example.chat",
+ shortcutId = "alice",
+ key = "1k4",
+ desiredHeight = 0,
+ desiredHeightResId = 16537428,
+ title = "title",
+ taskId = 5,
+ locus = null
+ )
+ )
+
+ private val mainExecutor = mock(ShellExecutor::class.java)
+ private val launcherApps = mock(LauncherApps::class.java)
+
+ private val persistedBubbles = SparseArray<List<BubbleEntity>>()
+
+ private lateinit var dataRepository: BubbleDataRepository
+ private lateinit var persistentRepository: BubblePersistentRepository
+
+ @Before
+ fun setup() {
+ persistentRepository = spy(BubblePersistentRepository(mContext))
+ dataRepository = BubbleDataRepository(launcherApps, mainExecutor, persistentRepository)
+
+ // Add the bubbles to the persistent repository
+ persistedBubbles.put(0, user0BubbleEntities)
+ persistedBubbles.put(1, user1BubbleEntities)
+ persistentRepository.persistsToDisk(persistedBubbles)
+ }
+
+ @After
+ fun teardown() {
+ // Clean up any persisted bubbles for the next run
+ persistentRepository.persistsToDisk(SparseArray())
+ }
+
+ @Test
+ fun testLoadBubbles_invalidParent() {
+ val activeUserIds = listOf(10, 1, 12) // Missing user 0 in persistedBubbles
+ dataRepository.loadBubbles(1, activeUserIds) {
+ // Verify that user 0 has been removed from the persisted list
+ val entitiesByUser = persistentRepository.readFromDisk()
+ assertThat(entitiesByUser.get(0)).isNull()
+ }
+ }
+
+ @Test
+ fun testLoadBubbles_invalidChild() {
+ val activeUserIds = listOf(0, 10, 1) // Missing user 1's child user 12
+ dataRepository.loadBubbles(1, activeUserIds) {
+ // Build a list to compare against
+ val user1BubblesWithoutUser12 = mutableListOf<Bubble>()
+ val user1EntitiesWithoutUser12 = mutableListOf<BubbleEntity>()
+ for (entity in user1BubbleEntities) {
+ if (entity.userId != 12) {
+ user1BubblesWithoutUser12.add(entity.toBubble())
+ user1EntitiesWithoutUser12.add(entity)
+ }
+ }
+
+ // Verify that user 12 has been removed from the persisted list
+ val entitiesByUser = persistentRepository.readFromDisk()
+ assertThat(entitiesByUser.get(1)).isEqualTo(user1EntitiesWithoutUser12)
+ }
+ }
+
+ private fun BubbleEntity.toBubble(): Bubble {
+ return Bubble(
+ key,
+ mock(ShortcutInfo::class.java),
+ desiredHeight,
+ desiredHeightResId,
+ title,
+ taskId,
+ locus,
+ isDismissable,
+ mainExecutor,
+ mock(Bubbles.BubbleMetadataFlagListener::class.java)
+ )
+ }
+} \ No newline at end of file