blob: 6df313212c76806a4e2e63ae2417b0e14ae52745 [file] [log] [blame]
Pinyao Tingee191b12020-04-29 18:35:39 -07001/*
2 * 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 */
16package com.android.systemui.bubbles
17
Pinyao Tingd4c1ba92020-05-04 20:29:56 -070018import android.annotation.SuppressLint
Pinyao Tingee191b12020-04-29 18:35:39 -070019import android.annotation.UserIdInt
Pinyao Tingd4c1ba92020-05-04 20:29:56 -070020import android.content.pm.LauncherApps
21import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC
22import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
23import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED
24import android.os.UserHandle
Pinyao Ting76ed7ba2020-04-29 18:35:39 -070025import android.util.Log
Pinyao Tingd4c1ba92020-05-04 20:29:56 -070026import com.android.systemui.bubbles.storage.BubbleEntity
Pinyao Tingee191b12020-04-29 18:35:39 -070027import com.android.systemui.bubbles.storage.BubblePersistentRepository
28import com.android.systemui.bubbles.storage.BubbleVolatileRepository
Pinyao Tingee191b12020-04-29 18:35:39 -070029import kotlinx.coroutines.CoroutineScope
30import kotlinx.coroutines.Dispatchers
31import kotlinx.coroutines.Job
32import kotlinx.coroutines.cancelAndJoin
33import kotlinx.coroutines.launch
34import kotlinx.coroutines.yield
Pinyao Tingd4c1ba92020-05-04 20:29:56 -070035
Pinyao Tingee191b12020-04-29 18:35:39 -070036import javax.inject.Inject
37import javax.inject.Singleton
38
39@Singleton
40internal class BubbleDataRepository @Inject constructor(
41 private val volatileRepository: BubbleVolatileRepository,
Pinyao Tingd4c1ba92020-05-04 20:29:56 -070042 private val persistentRepository: BubblePersistentRepository,
43 private val launcherApps: LauncherApps
Pinyao Tingee191b12020-04-29 18:35:39 -070044) {
45
46 private val ioScope = CoroutineScope(Dispatchers.IO)
Pinyao Ting76ed7ba2020-04-29 18:35:39 -070047 private val uiScope = CoroutineScope(Dispatchers.Main)
Pinyao Tingee191b12020-04-29 18:35:39 -070048 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 */
Pinyao Ting76ed7ba2020-04-29 18:35:39 -070054 fun addBubble(@UserIdInt userId: Int, bubble: Bubble) = addBubbles(userId, listOf(bubble))
Pinyao Tingee191b12020-04-29 18:35:39 -070055
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>) {
Pinyao Ting76ed7ba2020-04-29 18:35:39 -070061 if (DEBUG) Log.d(TAG, "adding ${bubbles.size} bubbles")
62 val entities = transform(userId, bubbles).also(volatileRepository::addBubbles)
63 if (entities.isNotEmpty()) persistToDisk()
Pinyao Tingee191b12020-04-29 18:35:39 -070064 }
65
Pinyao Ting76ed7ba2020-04-29 18:35:39 -070066 /**
67 * Removes the bubbles from memory, then persists the snapshot to disk asynchronously.
68 */
Pinyao Tingee191b12020-04-29 18:35:39 -070069 fun removeBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) {
Pinyao Ting76ed7ba2020-04-29 18:35:39 -070070 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
Pinyao Tingd4c1ba92020-05-04 20:29:56 -070075 private fun transform(userId: Int, bubbles: List<Bubble>): List<BubbleEntity> {
Pinyao Ting76ed7ba2020-04-29 18:35:39 -070076 return bubbles.mapNotNull { b ->
Pinyao Ting293b83d2020-05-06 17:10:56 -070077 var shortcutId = b.shortcutInfo?.id
78 if (shortcutId == null) shortcutId = b.entry?.bubbleMetadata?.shortcutId
79 if (shortcutId == null) shortcutId = b.entry?.ranking?.shortcutInfo?.id
80 if (shortcutId == null) return@mapNotNull null
Pinyao Tingd4c1ba92020-05-04 20:29:56 -070081 BubbleEntity(userId, b.packageName, shortcutId)
Pinyao Ting76ed7ba2020-04-29 18:35:39 -070082 }
Pinyao Tingee191b12020-04-29 18:35:39 -070083 }
84
85 /**
86 * Persists the bubbles to disk. When being called multiple times, it waits for first ongoing
87 * write operation to finish then run another write operation exactly once.
88 *
89 * e.g.
90 * Job A started -> blocking I/O
91 * Job B started, cancels A, wait for blocking I/O in A finishes
92 * Job C started, cancels B, wait for job B to finish
93 * Job D started, cancels C, wait for job C to finish
94 * Job A completed
95 * Job B resumes and reaches yield() and is then cancelled
96 * Job C resumes and reaches yield() and is then cancelled
97 * Job D resumes and performs another blocking I/O
98 */
99 private fun persistToDisk() {
100 val prev = job
101 job = ioScope.launch {
102 // if there was an ongoing disk I/O operation, they can be cancelled
103 prev?.cancelAndJoin()
104 // check for cancellation before disk I/O
105 yield()
106 // save to disk
107 persistentRepository.persistsToDisk(volatileRepository.bubbles)
108 }
109 }
Pinyao Ting76ed7ba2020-04-29 18:35:39 -0700110
111 /**
112 * Load bubbles from disk.
113 */
Pinyao Tingd4c1ba92020-05-04 20:29:56 -0700114 @SuppressLint("WrongConstant")
Pinyao Ting76ed7ba2020-04-29 18:35:39 -0700115 fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch {
Pinyao Tingd4c1ba92020-05-04 20:29:56 -0700116 /**
117 * Load BubbleEntity from disk.
118 * e.g.
119 * [
120 * BubbleEntity(0, "com.example.messenger", "id-2"),
121 * BubbleEntity(10, "com.example.chat", "my-id1")
122 * BubbleEntity(0, "com.example.messenger", "id-1")
123 * ]
124 */
125 val entities = persistentRepository.readFromDisk()
126 volatileRepository.addBubbles(entities)
127 /**
128 * Extract userId/packageName from these entities.
129 * e.g.
130 * [
131 * ShortcutKey(0, "com.example.messenger"), ShortcutKey(0, "com.example.chat")
132 * ]
133 */
134 val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet()
135 /**
136 * Retrieve shortcuts with given userId/packageName combination, then construct a mapping
137 * between BubbleEntity and ShortcutInfo.
138 * e.g.
139 * {
140 * BubbleEntity(0, "com.example.messenger", "id-0") ->
141 * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-0"),
142 * BubbleEntity(0, "com.example.messenger", "id-2") ->
143 * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2"),
144 * BubbleEntity(10, "com.example.chat", "id-1") ->
145 * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-1"),
146 * BubbleEntity(10, "com.example.chat", "id-3") ->
147 * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-3")
148 * }
149 */
150 val shortcutMap = shortcutKeys.flatMap { key ->
151 launcherApps.getShortcuts(
152 LauncherApps.ShortcutQuery()
153 .setPackage(key.pkg)
154 .setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId))
155 ?.map { BubbleEntity(key.userId, key.pkg, it.id) to it } ?: emptyList()
156 }.toMap()
157 // For each entity loaded from xml, find the corresponding ShortcutInfo then convert them
158 // into Bubble.
159 val bubbles = entities.mapNotNull { entity -> shortcutMap[entity]?.let { Bubble(it) } }
160 uiScope.launch { cb(bubbles) }
Pinyao Ting76ed7ba2020-04-29 18:35:39 -0700161 }
Pinyao Tingd4c1ba92020-05-04 20:29:56 -0700162
163 private data class ShortcutKey(val userId: Int, val pkg: String)
Pinyao Tingee191b12020-04-29 18:35:39 -0700164}
Pinyao Ting76ed7ba2020-04-29 18:35:39 -0700165
166private const val TAG = "BubbleDataRepository"
167private const val DEBUG = false
Pinyao Tingd4c1ba92020-05-04 20:29:56 -0700168private const val SHORTCUT_QUERY_FLAG =
169 FLAG_MATCH_DYNAMIC or FLAG_MATCH_PINNED_BY_ANY_LAUNCHER or FLAG_MATCH_CACHED