• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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