• 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.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