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