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.Context 21 import android.content.pm.LauncherApps 22 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED 23 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC 24 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER 25 import android.content.pm.UserInfo 26 import android.os.UserHandle 27 import android.util.Log 28 import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener 29 import com.android.wm.shell.bubbles.storage.BubbleEntity 30 import com.android.wm.shell.bubbles.storage.BubblePersistentRepository 31 import com.android.wm.shell.bubbles.storage.BubbleVolatileRepository 32 import com.android.wm.shell.common.ShellExecutor 33 import kotlinx.coroutines.CoroutineScope 34 import kotlinx.coroutines.Dispatchers 35 import kotlinx.coroutines.Job 36 import kotlinx.coroutines.cancelAndJoin 37 import kotlinx.coroutines.launch 38 import kotlinx.coroutines.yield 39 40 internal class BubbleDataRepository( 41 context: Context, 42 private val launcherApps: LauncherApps, 43 private val mainExecutor: ShellExecutor 44 ) { 45 private val volatileRepository = BubbleVolatileRepository(launcherApps) 46 private val persistentRepository = BubblePersistentRepository(context) 47 48 private val ioScope = CoroutineScope(Dispatchers.IO) 49 private var job: Job? = null 50 51 // For use in Bubble construction. 52 private lateinit var bubbleMetadataFlagListener: BubbleMetadataFlagListener 53 54 fun setSuppressionChangedListener(listener: BubbleMetadataFlagListener) { 55 bubbleMetadataFlagListener = listener 56 } 57 58 /** 59 * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk 60 * asynchronously. 61 */ 62 fun addBubble(@UserIdInt userId: Int, bubble: Bubble) = addBubbles(userId, listOf(bubble)) 63 64 /** 65 * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk 66 * asynchronously. 67 */ 68 fun addBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) { 69 if (DEBUG) Log.d(TAG, "adding ${bubbles.size} bubbles") 70 val entities = transform(bubbles).also { 71 b -> volatileRepository.addBubbles(userId, b) } 72 if (entities.isNotEmpty()) persistToDisk() 73 } 74 75 /** 76 * Removes the bubbles from memory, then persists the snapshot to disk asynchronously. 77 */ 78 fun removeBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) { 79 if (DEBUG) Log.d(TAG, "removing ${bubbles.size} bubbles") 80 val entities = transform(bubbles).also { 81 b -> volatileRepository.removeBubbles(userId, b) } 82 if (entities.isNotEmpty()) persistToDisk() 83 } 84 85 /** 86 * Removes all the bubbles associated with the provided user from memory. Then persists the 87 * snapshot to disk asynchronously. 88 */ 89 fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentId: Int) { 90 if (volatileRepository.removeBubblesForUser(userId, parentId)) persistToDisk() 91 } 92 93 /** 94 * Remove any bubbles that don't have a user id from the provided list of users. 95 */ 96 fun sanitizeBubbles(users: List<UserInfo>) { 97 val userIds = users.map { u -> u.id } 98 if (volatileRepository.sanitizeBubbles(userIds)) persistToDisk() 99 } 100 101 private fun transform(bubbles: List<Bubble>): List<BubbleEntity> { 102 return bubbles.mapNotNull { b -> 103 BubbleEntity( 104 b.user.identifier, 105 b.packageName, 106 b.metadataShortcutId ?: return@mapNotNull null, 107 b.key, 108 b.rawDesiredHeight, 109 b.rawDesiredHeightResId, 110 b.title, 111 b.taskId, 112 b.locusId?.id, 113 b.isDismissable 114 ) 115 } 116 } 117 118 /** 119 * Persists the bubbles to disk. When being called multiple times, it waits for first ongoing 120 * write operation to finish then run another write operation exactly once. 121 * 122 * e.g. 123 * Job A started -> blocking I/O 124 * Job B started, cancels A, wait for blocking I/O in A finishes 125 * Job C started, cancels B, wait for job B to finish 126 * Job D started, cancels C, wait for job C to finish 127 * Job A completed 128 * Job B resumes and reaches yield() and is then cancelled 129 * Job C resumes and reaches yield() and is then cancelled 130 * Job D resumes and performs another blocking I/O 131 */ 132 private fun persistToDisk() { 133 val prev = job 134 job = ioScope.launch { 135 // if there was an ongoing disk I/O operation, they can be cancelled 136 prev?.cancelAndJoin() 137 // check for cancellation before disk I/O 138 yield() 139 // save to disk 140 persistentRepository.persistsToDisk(volatileRepository.bubbles) 141 } 142 } 143 144 /** 145 * Load bubbles from disk. 146 * @param cb The callback to be run after the bubbles are loaded. This callback is always made 147 * on the main thread of the hosting process. The callback is only run if there are 148 * bubbles. 149 */ 150 @SuppressLint("WrongConstant") 151 fun loadBubbles(userId: Int, cb: (List<Bubble>) -> Unit) = ioScope.launch { 152 /** 153 * Load BubbleEntity from disk. 154 * e.g. 155 * [ 156 * BubbleEntity(0, "com.example.messenger", "id-2"), 157 * BubbleEntity(10, "com.example.chat", "my-id1") 158 * BubbleEntity(0, "com.example.messenger", "id-1") 159 * ] 160 */ 161 val entitiesByUser = persistentRepository.readFromDisk() 162 val entities = entitiesByUser.get(userId) ?: return@launch 163 volatileRepository.addBubbles(userId, entities) 164 /** 165 * Extract userId/packageName from these entities. 166 * e.g. 167 * [ 168 * ShortcutKey(0, "com.example.messenger"), ShortcutKey(0, "com.example.chat") 169 * ] 170 */ 171 val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet() 172 173 /** 174 * Retrieve shortcuts with given userId/packageName combination, then construct a 175 * mapping from the userId/packageName pair to a list of associated ShortcutInfo. 176 * e.g. 177 * { 178 * ShortcutKey(0, "com.example.messenger") -> [ 179 * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-0"), 180 * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2") 181 * ] 182 * ShortcutKey(10, "com.example.chat") -> [ 183 * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-1"), 184 * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-3") 185 * ] 186 * } 187 */ 188 val shortcutMap = shortcutKeys.flatMap { key -> 189 launcherApps.getShortcuts( 190 LauncherApps.ShortcutQuery() 191 .setPackage(key.pkg) 192 .setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId)) 193 ?: emptyList() 194 }.groupBy { ShortcutKey(it.userId, it.`package`) } 195 // For each entity loaded from xml, find the corresponding ShortcutInfo then convert 196 // them into Bubble. 197 val bubbles = entities.mapNotNull { entity -> 198 shortcutMap[ShortcutKey(entity.userId, entity.packageName)] 199 ?.firstOrNull { shortcutInfo -> entity.shortcutId == shortcutInfo.id } 200 ?.let { shortcutInfo -> 201 Bubble( 202 entity.key, 203 shortcutInfo, 204 entity.desiredHeight, 205 entity.desiredHeightResId, 206 entity.title, 207 entity.taskId, 208 entity.locus, 209 entity.isDismissable, 210 mainExecutor, 211 bubbleMetadataFlagListener 212 ) 213 } 214 } 215 mainExecutor.execute { cb(bubbles) } 216 } 217 } 218 219 data class ShortcutKey(val userId: Int, val pkg: String) 220 221 private const val TAG = "BubbleDataRepository" 222 private const val DEBUG = false 223 private const val SHORTCUT_QUERY_FLAG = 224 FLAG_MATCH_DYNAMIC or FLAG_MATCH_PINNED_BY_ANY_LAUNCHER or FLAG_MATCH_CACHED