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