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