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