Merge "Persists bubbles to disk (part 3)" into rvc-dev am: 8e65e07631
Change-Id: I97a6606d5d7afe0ef428b613d9533300275f2be5
This commit is contained in:
@@ -102,6 +102,13 @@ class Bubble implements BubbleViewProvider {
|
|||||||
return user.getIdentifier() + "|" + entry.getSbn().getPackageName();
|
return user.getIdentifier() + "|" + entry.getSbn().getPackageName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Decouple Bubble from NotificationEntry and transform ShortcutInfo into Bubble
|
||||||
|
Bubble(ShortcutInfo shortcutInfo) {
|
||||||
|
mShortcutInfo = shortcutInfo;
|
||||||
|
mKey = shortcutInfo.getId();
|
||||||
|
mGroupId = shortcutInfo.getId();
|
||||||
|
}
|
||||||
|
|
||||||
/** Used in tests when no UI is required. */
|
/** Used in tests when no UI is required. */
|
||||||
@VisibleForTesting(visibility = PRIVATE)
|
@VisibleForTesting(visibility = PRIVATE)
|
||||||
Bubble(NotificationEntry e,
|
Bubble(NotificationEntry e,
|
||||||
|
|||||||
@@ -15,24 +15,32 @@
|
|||||||
*/
|
*/
|
||||||
package com.android.systemui.bubbles
|
package com.android.systemui.bubbles
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.UserIdInt
|
import android.annotation.UserIdInt
|
||||||
|
import android.content.pm.LauncherApps
|
||||||
|
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.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED
|
||||||
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.android.systemui.bubbles.storage.BubbleEntity
|
||||||
import com.android.systemui.bubbles.storage.BubblePersistentRepository
|
import com.android.systemui.bubbles.storage.BubblePersistentRepository
|
||||||
import com.android.systemui.bubbles.storage.BubbleVolatileRepository
|
import com.android.systemui.bubbles.storage.BubbleVolatileRepository
|
||||||
import com.android.systemui.bubbles.storage.BubbleXmlEntity
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class BubbleDataRepository @Inject constructor(
|
internal class BubbleDataRepository @Inject constructor(
|
||||||
private val volatileRepository: BubbleVolatileRepository,
|
private val volatileRepository: BubbleVolatileRepository,
|
||||||
private val persistentRepository: BubblePersistentRepository
|
private val persistentRepository: BubblePersistentRepository,
|
||||||
|
private val launcherApps: LauncherApps
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||||
@@ -64,10 +72,10 @@ internal class BubbleDataRepository @Inject constructor(
|
|||||||
if (entities.isNotEmpty()) persistToDisk()
|
if (entities.isNotEmpty()) persistToDisk()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun transform(userId: Int, bubbles: List<Bubble>): List<BubbleXmlEntity> {
|
private fun transform(userId: Int, bubbles: List<Bubble>): List<BubbleEntity> {
|
||||||
return bubbles.mapNotNull { b ->
|
return bubbles.mapNotNull { b ->
|
||||||
val shortcutId = b.shortcutInfo?.id ?: return@mapNotNull null
|
val shortcutId = b.shortcutInfo?.id ?: return@mapNotNull null
|
||||||
BubbleXmlEntity(userId, b.packageName, shortcutId)
|
BubbleEntity(userId, b.packageName, shortcutId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,15 +108,60 @@ internal class BubbleDataRepository @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Load bubbles from disk.
|
* Load bubbles from disk.
|
||||||
*/
|
*/
|
||||||
|
// TODO: call this method from BubbleController and update UI
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch {
|
fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch {
|
||||||
val bubbleXmlEntities = persistentRepository.readFromDisk()
|
/**
|
||||||
volatileRepository.addBubbles(bubbleXmlEntities)
|
* Load BubbleEntity from disk.
|
||||||
uiScope.launch {
|
* e.g.
|
||||||
// TODO: transform bubbleXmlEntities into bubbles
|
* [
|
||||||
// cb(bubbles)
|
* BubbleEntity(0, "com.example.messenger", "id-2"),
|
||||||
}
|
* BubbleEntity(10, "com.example.chat", "my-id1")
|
||||||
|
* BubbleEntity(0, "com.example.messenger", "id-1")
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
val entities = persistentRepository.readFromDisk()
|
||||||
|
volatileRepository.addBubbles(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
|
||||||
|
* between BubbleEntity and ShortcutInfo.
|
||||||
|
* e.g.
|
||||||
|
* {
|
||||||
|
* BubbleEntity(0, "com.example.messenger", "id-0") ->
|
||||||
|
* ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-0"),
|
||||||
|
* BubbleEntity(0, "com.example.messenger", "id-2") ->
|
||||||
|
* ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2"),
|
||||||
|
* BubbleEntity(10, "com.example.chat", "id-1") ->
|
||||||
|
* ShortcutInfo(userId=10, pkg="com.example.chat", id="id-1"),
|
||||||
|
* BubbleEntity(10, "com.example.chat", "id-3") ->
|
||||||
|
* 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))
|
||||||
|
?.map { BubbleEntity(key.userId, key.pkg, it.id) to it } ?: emptyList()
|
||||||
|
}.toMap()
|
||||||
|
// For each entity loaded from xml, find the corresponding ShortcutInfo then convert them
|
||||||
|
// into Bubble.
|
||||||
|
val bubbles = entities.mapNotNull { entity -> shortcutMap[entity]?.let { Bubble(it) } }
|
||||||
|
uiScope.launch { cb(bubbles) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class ShortcutKey(val userId: Int, val pkg: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val TAG = "BubbleDataRepository"
|
private const val TAG = "BubbleDataRepository"
|
||||||
private const val DEBUG = false
|
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
|
||||||
@@ -17,7 +17,7 @@ package com.android.systemui.bubbles.storage
|
|||||||
|
|
||||||
import android.annotation.UserIdInt
|
import android.annotation.UserIdInt
|
||||||
|
|
||||||
data class BubbleXmlEntity(
|
data class BubbleEntity(
|
||||||
@UserIdInt val userId: Int,
|
@UserIdInt val userId: Int,
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val shortcutId: String
|
val shortcutId: String
|
||||||
@@ -32,7 +32,7 @@ class BubblePersistentRepository @Inject constructor(
|
|||||||
private val bubbleFile: AtomicFile = AtomicFile(File(context.filesDir,
|
private val bubbleFile: AtomicFile = AtomicFile(File(context.filesDir,
|
||||||
"overflow_bubbles.xml"), "overflow-bubbles")
|
"overflow_bubbles.xml"), "overflow-bubbles")
|
||||||
|
|
||||||
fun persistsToDisk(bubbles: List<BubbleXmlEntity>): Boolean {
|
fun persistsToDisk(bubbles: List<BubbleEntity>): Boolean {
|
||||||
if (DEBUG) Log.d(TAG, "persisting ${bubbles.size} bubbles")
|
if (DEBUG) Log.d(TAG, "persisting ${bubbles.size} bubbles")
|
||||||
synchronized(bubbleFile) {
|
synchronized(bubbleFile) {
|
||||||
val stream: FileOutputStream = try { bubbleFile.startWrite() } catch (e: IOException) {
|
val stream: FileOutputStream = try { bubbleFile.startWrite() } catch (e: IOException) {
|
||||||
@@ -52,7 +52,7 @@ class BubblePersistentRepository @Inject constructor(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readFromDisk(): List<BubbleXmlEntity> {
|
fun readFromDisk(): List<BubbleEntity> {
|
||||||
synchronized(bubbleFile) {
|
synchronized(bubbleFile) {
|
||||||
try { return bubbleFile.openRead().use(::readXml) } catch (e: Throwable) {
|
try { return bubbleFile.openRead().use(::readXml) } catch (e: Throwable) {
|
||||||
Log.e(TAG, "Failed to open bubble file", e)
|
Log.e(TAG, "Failed to open bubble file", e)
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ class BubbleVolatileRepository @Inject constructor() {
|
|||||||
/**
|
/**
|
||||||
* An ordered set of bubbles based on their natural ordering.
|
* An ordered set of bubbles based on their natural ordering.
|
||||||
*/
|
*/
|
||||||
private val entities = mutableSetOf<BubbleXmlEntity>()
|
private val entities = mutableSetOf<BubbleEntity>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a snapshot of all the bubbles.
|
* Returns a snapshot of all the bubbles.
|
||||||
*/
|
*/
|
||||||
val bubbles: List<BubbleXmlEntity>
|
val bubbles: List<BubbleEntity>
|
||||||
@Synchronized
|
@Synchronized
|
||||||
get() = entities.toList()
|
get() = entities.toList()
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ class BubbleVolatileRepository @Inject constructor() {
|
|||||||
* it will be moved to the last.
|
* it will be moved to the last.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun addBubbles(bubbles: List<BubbleXmlEntity>) {
|
fun addBubbles(bubbles: List<BubbleEntity>) {
|
||||||
if (bubbles.isEmpty()) return
|
if (bubbles.isEmpty()) return
|
||||||
bubbles.forEach { entities.remove(it) }
|
bubbles.forEach { entities.remove(it) }
|
||||||
if (entities.size + bubbles.size >= CAPACITY) {
|
if (entities.size + bubbles.size >= CAPACITY) {
|
||||||
@@ -53,7 +53,7 @@ class BubbleVolatileRepository @Inject constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun removeBubbles(bubbles: List<BubbleXmlEntity>) {
|
fun removeBubbles(bubbles: List<BubbleEntity>) {
|
||||||
bubbles.forEach { entities.remove(it) }
|
bubbles.forEach { entities.remove(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ private const val ATTR_SHORTCUT_ID = "sid"
|
|||||||
* Writes the bubbles in xml format into given output stream.
|
* Writes the bubbles in xml format into given output stream.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun writeXml(stream: OutputStream, bubbles: List<BubbleXmlEntity>) {
|
fun writeXml(stream: OutputStream, bubbles: List<BubbleEntity>) {
|
||||||
val serializer: XmlSerializer = FastXmlSerializer()
|
val serializer: XmlSerializer = FastXmlSerializer()
|
||||||
serializer.setOutput(stream, StandardCharsets.UTF_8.name())
|
serializer.setOutput(stream, StandardCharsets.UTF_8.name())
|
||||||
serializer.startDocument(null, true)
|
serializer.startDocument(null, true)
|
||||||
@@ -51,7 +51,7 @@ fun writeXml(stream: OutputStream, bubbles: List<BubbleXmlEntity>) {
|
|||||||
* <bb uid="0" pkg="com.example.messenger" sid="my-shortcut" />
|
* <bb uid="0" pkg="com.example.messenger" sid="my-shortcut" />
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleXmlEntity) {
|
private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleEntity) {
|
||||||
try {
|
try {
|
||||||
serializer.startTag(null, TAG_BUBBLE)
|
serializer.startTag(null, TAG_BUBBLE)
|
||||||
serializer.attribute(null, ATTR_USER_ID, bubble.userId.toString())
|
serializer.attribute(null, ATTR_USER_ID, bubble.userId.toString())
|
||||||
@@ -66,8 +66,8 @@ private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleXmlEntity) {
|
|||||||
/**
|
/**
|
||||||
* Reads the bubbles from xml file.
|
* Reads the bubbles from xml file.
|
||||||
*/
|
*/
|
||||||
fun readXml(stream: InputStream): List<BubbleXmlEntity> {
|
fun readXml(stream: InputStream): List<BubbleEntity> {
|
||||||
val bubbles = mutableListOf<BubbleXmlEntity>()
|
val bubbles = mutableListOf<BubbleEntity>()
|
||||||
val parser: XmlPullParser = Xml.newPullParser()
|
val parser: XmlPullParser = Xml.newPullParser()
|
||||||
parser.setInput(stream, StandardCharsets.UTF_8.name())
|
parser.setInput(stream, StandardCharsets.UTF_8.name())
|
||||||
XmlUtils.beginDocument(parser, TAG_BUBBLES)
|
XmlUtils.beginDocument(parser, TAG_BUBBLES)
|
||||||
@@ -78,9 +78,9 @@ fun readXml(stream: InputStream): List<BubbleXmlEntity> {
|
|||||||
return bubbles
|
return bubbles
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readXmlEntry(parser: XmlPullParser): BubbleXmlEntity? {
|
private fun readXmlEntry(parser: XmlPullParser): BubbleEntity? {
|
||||||
while (parser.eventType != XmlPullParser.START_TAG) { parser.next() }
|
while (parser.eventType != XmlPullParser.START_TAG) { parser.next() }
|
||||||
return BubbleXmlEntity(
|
return BubbleEntity(
|
||||||
parser.getAttributeWithName(ATTR_USER_ID)?.toInt() ?: return null,
|
parser.getAttributeWithName(ATTR_USER_ID)?.toInt() ?: return null,
|
||||||
parser.getAttributeWithName(ATTR_PACKAGE) ?: return null,
|
parser.getAttributeWithName(ATTR_PACKAGE) ?: return null,
|
||||||
parser.getAttributeWithName(ATTR_SHORTCUT_ID) ?: return null
|
parser.getAttributeWithName(ATTR_SHORTCUT_ID) ?: return null
|
||||||
|
|||||||
Reference in New Issue
Block a user