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.wm.shell.bubbles.storage
17 
18 import android.annotation.UserIdInt
19 import android.content.pm.LauncherApps
20 import android.os.UserHandle
21 import android.util.SparseArray
22 import com.android.internal.annotations.VisibleForTesting
23 import com.android.wm.shell.bubbles.ShortcutKey
24 
25 private const val CAPACITY = 16
26 
27 /**
28  * BubbleVolatileRepository holds the most updated snapshot of list of bubbles for in-memory
29  * manipulation.
30  */
31 class BubbleVolatileRepository(private val launcherApps: LauncherApps) {
32 
33     /**
34      * Set of bubbles per user. Each set of bubbles is ordered by recency.
35      */
36     private var entitiesByUser = SparseArray<MutableList<BubbleEntity>>()
37 
38     /**
39      * The capacity of the cache.
40      */
41     @VisibleForTesting
42     var capacity = CAPACITY
43 
44     /**
45      * Returns a snapshot of all the bubbles, a map of the userId to bubble list.
46      */
47     val bubbles: SparseArray<List<BubbleEntity>>
48         @Synchronized
49         get() {
50             val map = SparseArray<List<BubbleEntity>>()
51             for (i in 0 until entitiesByUser.size()) {
52                 val k = entitiesByUser.keyAt(i)
53                 val v = entitiesByUser.valueAt(i)
54                 map.put(k, v.toList())
55             }
56             return map
57         }
58 
59     /**
60      * Returns the entity list of the provided user's bubbles or creates one if it doesn't exist.
61      */
62     @Synchronized
63     fun getEntities(userId: Int): MutableList<BubbleEntity> {
64         val entities = entitiesByUser.get(userId)
65         return when (entities) {
66             null -> mutableListOf<BubbleEntity>().also {
67                 entitiesByUser.put(userId, it)
68             }
69             else -> entities
70         }
71     }
72 
73     /**
74      * Add the bubbles to memory and perform a de-duplication. In case a bubble already exists,
75      * it will be moved to the last.
76      */
77     @Synchronized
78     fun addBubbles(userId: Int, bubbles: List<BubbleEntity>) {
79         if (bubbles.isEmpty()) return
80         // Get the list for this user
81         var entities = getEntities(userId)
82         // Verify the size of given bubbles is within capacity, otherwise trim down to capacity
83         val bubblesInRange = bubbles.takeLast(capacity)
84         // To ensure natural ordering of the bubbles, removes bubbles which already exist
85         val uniqueBubbles = bubblesInRange.filterNot { b: BubbleEntity ->
86             entities.removeIf { e: BubbleEntity -> b.key == e.key } }
87         val overflowCount = entities.size + bubblesInRange.size - capacity
88         if (overflowCount > 0) {
89             // Uncache ShortcutInfo of bubbles that will be removed due to capacity
90             uncache(entities.take(overflowCount))
91             entities = entities.drop(overflowCount).toMutableList()
92         }
93         entities.addAll(bubblesInRange)
94         entitiesByUser.put(userId, entities)
95         cache(uniqueBubbles)
96     }
97 
98     @Synchronized
99     fun removeBubbles(@UserIdInt userId: Int, bubbles: List<BubbleEntity>) =
100             uncache(bubbles.filter { b: BubbleEntity ->
101                 getEntities(userId).removeIf { e: BubbleEntity -> b.key == e.key } })
102 
103     /**
104      * Removes all the bubbles associated with the provided userId.
105      * @return whether bubbles were removed or not.
106      */
107     @Synchronized
108     fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentUserId: Int): Boolean {
109         if (parentUserId != -1) {
110             return removeBubblesForUserWithParent(userId, parentUserId)
111         } else {
112             val entities = entitiesByUser.get(userId)
113             entitiesByUser.remove(userId)
114             return entities != null
115         }
116     }
117 
118     /**
119      * Removes all the bubbles associated with the provided userId when that userId is part of
120      * a profile (e.g. managed account).
121      *
122      * @return whether bubbles were removed or not.
123      */
124     @Synchronized
125     private fun removeBubblesForUserWithParent(
126         @UserIdInt userId: Int,
127         @UserIdInt parentUserId: Int
128     ): Boolean {
129         if (entitiesByUser.get(parentUserId) != null) {
130             return entitiesByUser.get(parentUserId).removeIf {
131                 b: BubbleEntity -> b.userId == userId }
132         }
133         return false
134     }
135 
136     /**
137      * Goes through all the persisted bubbles and removes them if the user is not in the active
138      * list of users.
139      *
140      * @return whether the list of bubbles changed or not (i.e. was a removal made).
141      */
142     @Synchronized
143     fun sanitizeBubbles(activeUsers: List<Int>): Boolean {
144         for (i in 0 until entitiesByUser.size()) {
145             // First check if the user is a parent / top-level user
146             val parentUserId = entitiesByUser.keyAt(i)
147             if (!activeUsers.contains(parentUserId)) {
148                 entitiesByUser.remove(parentUserId)
149                 return true
150             } else if (entitiesByUser.get(parentUserId) != null) {
151                 // Then check if each of the bubbles in the top-level user, still has a valid user
152                 // as it could belong to a profile and have a different id from the parent.
153                 return entitiesByUser.get(parentUserId).removeIf { b: BubbleEntity ->
154                     !activeUsers.contains(b.userId)
155                 }
156             }
157         }
158         return false
159     }
160 
161     private fun cache(bubbles: List<BubbleEntity>) {
162         bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) ->
163             launcherApps.cacheShortcuts(key.pkg, bubbles.map { it.shortcutId },
164                     UserHandle.of(key.userId), LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS)
165         }
166     }
167 
168     private fun uncache(bubbles: List<BubbleEntity>) {
169         bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) ->
170             launcherApps.uncacheShortcuts(key.pkg, bubbles.map { it.shortcutId },
171                     UserHandle.of(key.userId), LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS)
172         }
173     }
174 }
175