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