Files
Lawnchair/wmshell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
T
2024-11-02 10:55:28 +08:00

275 lines
11 KiB
Kotlin

/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.bubbles
import android.annotation.SuppressLint
import android.annotation.UserIdInt
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
import android.content.pm.UserInfo
import android.os.UserHandle
import android.util.Log
import android.util.SparseArray
import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener
import com.android.wm.shell.bubbles.storage.BubbleEntity
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.bubbles.storage.BubbleVolatileRepository
import com.android.wm.shell.common.ShellExecutor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
class BubbleDataRepository(
private val launcherApps: LauncherApps,
private val mainExecutor: ShellExecutor,
private val persistentRepository: BubblePersistentRepository,
) {
private val volatileRepository = BubbleVolatileRepository(launcherApps)
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var job: Job? = null
// For use in Bubble construction.
private lateinit var bubbleMetadataFlagListener: BubbleMetadataFlagListener
fun setSuppressionChangedListener(listener: BubbleMetadataFlagListener) {
bubbleMetadataFlagListener = listener
}
/**
* Adds the bubble in memory, then persists the snapshot after adding the bubble to disk
* asynchronously.
*/
fun addBubble(@UserIdInt userId: Int, bubble: Bubble) = addBubbles(userId, listOf(bubble))
/**
* Adds the bubble in memory, then persists the snapshot after adding the bubble to disk
* asynchronously.
*/
fun addBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) {
if (DEBUG) Log.d(TAG, "adding ${bubbles.size} bubbles")
val entities = transform(bubbles).also {
b -> volatileRepository.addBubbles(userId, b) }
if (entities.isNotEmpty()) persistToDisk()
}
/**
* Removes the bubbles from memory, then persists the snapshot to disk asynchronously.
*/
fun removeBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) {
if (DEBUG) Log.d(TAG, "removing ${bubbles.size} bubbles")
val entities = transform(bubbles).also {
b -> volatileRepository.removeBubbles(userId, b) }
if (entities.isNotEmpty()) persistToDisk()
}
/**
* Removes all the bubbles associated with the provided user from memory. Then persists the
* snapshot to disk asynchronously.
*/
fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentId: Int) {
if (volatileRepository.removeBubblesForUser(userId, parentId)) persistToDisk()
}
/**
* Remove any bubbles that don't have a user id from the provided list of users.
*/
fun sanitizeBubbles(users: List<UserInfo>) {
val userIds = users.map { u -> u.id }
if (volatileRepository.sanitizeBubbles(userIds)) persistToDisk()
}
/**
* Removes all entities that don't have a user in the activeUsers list, if any entities were
* removed it persists the new list to disk.
*/
@VisibleForTesting
fun filterForActiveUsersAndPersist(
activeUsers: List<Int>,
entitiesByUser: SparseArray<List<BubbleEntity>>
): SparseArray<List<BubbleEntity>> {
val validEntitiesByUser = SparseArray<List<BubbleEntity>>()
var entitiesChanged = false
for (i in 0 until entitiesByUser.size()) {
val parentUserId = entitiesByUser.keyAt(i)
if (activeUsers.contains(parentUserId)) {
val validEntities = mutableListOf<BubbleEntity>()
// Check if each of the bubbles in the top-level user still has a valid user
// as it could belong to a profile and have a different id from the parent.
for (entity in entitiesByUser.get(parentUserId)) {
if (activeUsers.contains(entity.userId)) {
validEntities.add(entity)
} else {
entitiesChanged = true
}
}
if (validEntities.isNotEmpty()) {
validEntitiesByUser.put(parentUserId, validEntities)
}
} else {
entitiesChanged = true
}
}
if (entitiesChanged) {
persistToDisk(validEntitiesByUser)
return validEntitiesByUser
}
return entitiesByUser
}
private fun transform(bubbles: List<Bubble>): List<BubbleEntity> {
return bubbles.mapNotNull { b ->
BubbleEntity(
b.user.identifier,
b.packageName,
b.metadataShortcutId ?: return@mapNotNull null,
b.key,
b.rawDesiredHeight,
b.rawDesiredHeightResId,
b.title,
b.taskId,
b.locusId?.id,
b.isDismissable
)
}
}
/**
* Persists the bubbles to disk. When being called multiple times, it waits for first ongoing
* write operation to finish then run another write operation exactly once.
*
* e.g.
* Job A started -> blocking I/O
* Job B started, cancels A, wait for blocking I/O in A finishes
* Job C started, cancels B, wait for job B to finish
* Job D started, cancels C, wait for job C to finish
* Job A completed
* Job B resumes and reaches yield() and is then cancelled
* Job C resumes and reaches yield() and is then cancelled
* Job D resumes and performs another blocking I/O
*/
@VisibleForTesting
fun persistToDisk(
entitiesByUser: SparseArray<List<BubbleEntity>> = volatileRepository.bubbles
) {
val prev = job
job = coroutineScope.launch {
// if there was an ongoing disk I/O operation, they can be cancelled
prev?.cancelAndJoin()
// check for cancellation before disk I/O
yield()
// save to disk
persistentRepository.persistsToDisk(entitiesByUser)
}
}
/**
* Load bubbles from disk.
* @param cb The callback to be run after the bubbles are loaded. This callback is always made
* on the main thread of the hosting process. The callback is only run if there are
* bubbles.
*/
@SuppressLint("WrongConstant")
fun loadBubbles(
userId: Int,
currentUsers: List<Int>,
cb: (List<Bubble>) -> Unit
) = coroutineScope.launch {
/**
* Load BubbleEntity from disk.
* e.g.
* [
* BubbleEntity(0, "com.example.messenger", "id-2"),
* BubbleEntity(10, "com.example.chat", "my-id1")
* BubbleEntity(0, "com.example.messenger", "id-1")
* ]
*/
val entitiesByUser = persistentRepository.readFromDisk()
// Before doing anything, validate that the entities we loaded are valid & have an existing
// user.
val validEntitiesByUser = filterForActiveUsersAndPersist(currentUsers, entitiesByUser)
val entities = validEntitiesByUser.get(userId) ?: return@launch
volatileRepository.addBubbles(userId, entities)
/**
* Extract userId/packageName from these entities.
* e.g.
* [
* ShortcutKey(0, "com.example.messenger"), ShortcutKey(0, "com.example.chat")
* ]
*/
val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet()
/**
* Retrieve shortcuts with given userId/packageName combination, then construct a
* mapping from the userId/packageName pair to a list of associated ShortcutInfo.
* e.g.
* {
* ShortcutKey(0, "com.example.messenger") -> [
* ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-0"),
* ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2")
* ]
* ShortcutKey(10, "com.example.chat") -> [
* ShortcutInfo(userId=10, pkg="com.example.chat", id="id-1"),
* ShortcutInfo(userId=10, pkg="com.example.chat", id="id-3")
* ]
* }
*/
val shortcutMap = shortcutKeys.flatMap { key ->
launcherApps.getShortcuts(
LauncherApps.ShortcutQuery()
.setPackage(key.pkg)
.setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId))
?: emptyList()
}.groupBy { ShortcutKey(it.userId, it.`package`) }
// For each entity loaded from xml, find the corresponding ShortcutInfo then convert
// them into Bubble.
val bubbles = entities.mapNotNull { entity ->
shortcutMap[ShortcutKey(entity.userId, entity.packageName)]
?.firstOrNull { shortcutInfo -> entity.shortcutId == shortcutInfo.id }
?.let { shortcutInfo ->
Bubble(
entity.key,
shortcutInfo,
entity.desiredHeight,
entity.desiredHeightResId,
entity.title,
entity.taskId,
entity.locus,
entity.isDismissable,
mainExecutor,
bubbleMetadataFlagListener
)
}
}
mainExecutor.execute { cb(bubbles) }
}
}
data class ShortcutKey(val userId: Int, val pkg: String)
private const val TAG = "BubbleDataRepository"
private const val DEBUG = false
private const val SHORTCUT_QUERY_FLAG =
FLAG_MATCH_DYNAMIC or FLAG_MATCH_PINNED_BY_ANY_LAUNCHER or FLAG_MATCH_CACHED