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