1 /* <lambda>null2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.wm.shell.bubbles 17 18 import android.annotation.SuppressLint 19 import android.annotation.UserIdInt 20 import android.content.pm.LauncherApps 21 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED 22 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC 23 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER 24 import android.content.pm.UserInfo 25 import android.os.UserHandle 26 import android.util.Log 27 import android.util.SparseArray 28 import com.android.internal.annotations.VisibleForTesting 29 import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener 30 import com.android.wm.shell.bubbles.storage.BubbleEntity 31 import com.android.wm.shell.bubbles.storage.BubblePersistentRepository 32 import com.android.wm.shell.bubbles.storage.BubbleVolatileRepository 33 import com.android.wm.shell.common.ShellExecutor 34 import com.android.wm.shell.shared.annotations.ShellBackgroundThread 35 import com.android.wm.shell.shared.annotations.ShellMainThread 36 import java.util.concurrent.Executor 37 import kotlinx.coroutines.CoroutineScope 38 import kotlinx.coroutines.Dispatchers 39 import kotlinx.coroutines.Job 40 import kotlinx.coroutines.SupervisorJob 41 import kotlinx.coroutines.cancelAndJoin 42 import kotlinx.coroutines.launch 43 import kotlinx.coroutines.yield 44 45 class BubbleDataRepository( 46 private val launcherApps: LauncherApps, 47 @ShellMainThread private val mainExecutor: ShellExecutor, 48 @ShellBackgroundThread private val bgExecutor: Executor, 49 private val persistentRepository: BubblePersistentRepository, 50 ) { 51 private val volatileRepository = BubbleVolatileRepository(launcherApps) 52 53 private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) 54 private var job: Job? = null 55 56 // For use in Bubble construction. 57 private lateinit var bubbleMetadataFlagListener: BubbleMetadataFlagListener 58 59 fun setSuppressionChangedListener(listener: BubbleMetadataFlagListener) { 60 bubbleMetadataFlagListener = listener 61 } 62 63 /** 64 * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk 65 * asynchronously. 66 */ 67 fun addBubble(@UserIdInt userId: Int, bubble: Bubble) = addBubbles(userId, listOf(bubble)) 68 69 /** 70 * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk 71 * asynchronously. 72 */ 73 fun addBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) { 74 if (DEBUG) Log.d(TAG, "adding ${bubbles.size} bubbles") 75 val entities = transform(bubbles).also { 76 b -> volatileRepository.addBubbles(userId, b) } 77 if (entities.isNotEmpty()) persistToDisk() 78 } 79 80 /** 81 * Removes the bubbles from memory, then persists the snapshot to disk asynchronously. 82 */ 83 fun removeBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) { 84 if (DEBUG) Log.d(TAG, "removing ${bubbles.size} bubbles") 85 val entities = transform(bubbles).also { 86 b -> volatileRepository.removeBubbles(userId, b) } 87 if (entities.isNotEmpty()) persistToDisk() 88 } 89 90 /** 91 * Removes all the bubbles associated with the provided user from memory. Then persists the 92 * snapshot to disk asynchronously. 93 */ 94 fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentId: Int) { 95 if (volatileRepository.removeBubblesForUser(userId, parentId)) persistToDisk() 96 } 97 98 /** 99 * Remove any bubbles that don't have a user id from the provided list of users. 100 */ 101 fun sanitizeBubbles(users: List<UserInfo>) { 102 val userIds = users.map { u -> u.id } 103 if (volatileRepository.sanitizeBubbles(userIds)) persistToDisk() 104 } 105 106 /** 107 * Removes all entities that don't have a user in the activeUsers list, if any entities were 108 * removed it persists the new list to disk. 109 */ 110 @VisibleForTesting 111 fun filterForActiveUsersAndPersist( 112 activeUsers: List<Int>, 113 entitiesByUser: SparseArray<List<BubbleEntity>> 114 ): SparseArray<List<BubbleEntity>> { 115 val validEntitiesByUser = SparseArray<List<BubbleEntity>>() 116 var entitiesChanged = false 117 for (i in 0 until entitiesByUser.size()) { 118 val parentUserId = entitiesByUser.keyAt(i) 119 if (activeUsers.contains(parentUserId)) { 120 val validEntities = mutableListOf<BubbleEntity>() 121 // Check if each of the bubbles in the top-level user still has a valid user 122 // as it could belong to a profile and have a different id from the parent. 123 for (entity in entitiesByUser.get(parentUserId)) { 124 if (activeUsers.contains(entity.userId)) { 125 validEntities.add(entity) 126 } else { 127 entitiesChanged = true 128 } 129 } 130 if (validEntities.isNotEmpty()) { 131 validEntitiesByUser.put(parentUserId, validEntities) 132 } 133 } else { 134 entitiesChanged = true 135 } 136 } 137 if (entitiesChanged) { 138 persistToDisk(validEntitiesByUser) 139 return validEntitiesByUser 140 } 141 return entitiesByUser 142 } 143 144 private fun transform(bubbles: List<Bubble>): List<BubbleEntity> { 145 return bubbles.mapNotNull { b -> 146 BubbleEntity( 147 b.user.identifier, 148 b.packageName, 149 b.metadataShortcutId ?: return@mapNotNull null, 150 b.key, 151 b.rawDesiredHeight, 152 b.rawDesiredHeightResId, 153 b.title, 154 b.taskId, 155 b.locusId?.id, 156 b.isDismissable 157 ) 158 } 159 } 160 161 /** 162 * Persists the bubbles to disk. When being called multiple times, it waits for first ongoing 163 * write operation to finish then run another write operation exactly once. 164 * 165 * e.g. 166 * Job A started -> blocking I/O 167 * Job B started, cancels A, wait for blocking I/O in A finishes 168 * Job C started, cancels B, wait for job B to finish 169 * Job D started, cancels C, wait for job C to finish 170 * Job A completed 171 * Job B resumes and reaches yield() and is then cancelled 172 * Job C resumes and reaches yield() and is then cancelled 173 * Job D resumes and performs another blocking I/O 174 */ 175 @VisibleForTesting 176 fun persistToDisk( 177 entitiesByUser: SparseArray<List<BubbleEntity>> = volatileRepository.bubbles 178 ) { 179 val prev = job 180 job = coroutineScope.launch { 181 // if there was an ongoing disk I/O operation, they can be cancelled 182 prev?.cancelAndJoin() 183 // check for cancellation before disk I/O 184 yield() 185 // save to disk 186 persistentRepository.persistsToDisk(entitiesByUser) 187 } 188 } 189 190 /** 191 * Load bubbles from disk. 192 * @param cb The callback to be run after the bubbles are loaded. This callback is always made 193 * on the main thread of the hosting process. The callback is only run if there are 194 * bubbles. 195 */ 196 @SuppressLint("WrongConstant") 197 fun loadBubbles( 198 userId: Int, 199 currentUsers: List<Int>, 200 cb: (List<Bubble>) -> Unit 201 ) = coroutineScope.launch { 202 /** 203 * Load BubbleEntity from disk. 204 * e.g. 205 * [ 206 * BubbleEntity(0, "com.example.messenger", "id-2"), 207 * BubbleEntity(10, "com.example.chat", "my-id1") 208 * BubbleEntity(0, "com.example.messenger", "id-1") 209 * ] 210 */ 211 val entitiesByUser = persistentRepository.readFromDisk() 212 213 // Before doing anything, validate that the entities we loaded are valid & have an existing 214 // user. 215 val validEntitiesByUser = filterForActiveUsersAndPersist(currentUsers, entitiesByUser) 216 217 val entities = validEntitiesByUser.get(userId) ?: return@launch 218 volatileRepository.addBubbles(userId, entities) 219 /** 220 * Extract userId/packageName from these entities. 221 * e.g. 222 * [ 223 * ShortcutKey(0, "com.example.messenger"), ShortcutKey(0, "com.example.chat") 224 * ] 225 */ 226 val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet() 227 228 /** 229 * Retrieve shortcuts with given userId/packageName combination, then construct a 230 * mapping from the userId/packageName pair to a list of associated ShortcutInfo. 231 * e.g. 232 * { 233 * ShortcutKey(0, "com.example.messenger") -> [ 234 * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-0"), 235 * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2") 236 * ] 237 * ShortcutKey(10, "com.example.chat") -> [ 238 * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-1"), 239 * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-3") 240 * ] 241 * } 242 */ 243 val shortcutMap = shortcutKeys.flatMap { key -> 244 launcherApps.getShortcuts( 245 LauncherApps.ShortcutQuery() 246 .setPackage(key.pkg) 247 .setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId)) 248 ?: emptyList() 249 }.groupBy { ShortcutKey(it.userId, it.`package`) } 250 // For each entity loaded from xml, find the corresponding ShortcutInfo then convert 251 // them into Bubble. 252 val bubbles = entities.mapNotNull { entity -> 253 shortcutMap[ShortcutKey(entity.userId, entity.packageName)] 254 ?.firstOrNull { shortcutInfo -> entity.shortcutId == shortcutInfo.id } 255 ?.let { shortcutInfo -> 256 Bubble( 257 entity.key, 258 shortcutInfo, 259 entity.desiredHeight, 260 entity.desiredHeightResId, 261 entity.title, 262 entity.taskId, 263 entity.locus, 264 entity.isDismissable, 265 mainExecutor, 266 bgExecutor, 267 bubbleMetadataFlagListener) 268 } 269 } 270 mainExecutor.execute { cb(bubbles) } 271 } 272 } 273 274 data class ShortcutKey(val userId: Int, val pkg: String) 275 276 private const val TAG = "BubbleDataRepository" 277 private const val DEBUG = false 278 private const val SHORTCUT_QUERY_FLAG = 279 FLAG_MATCH_DYNAMIC or FLAG_MATCH_PINNED_BY_ANY_LAUNCHER or FLAG_MATCH_CACHED