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