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