1 /*
<lambda>null2 * Copyright (C) 2024 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
17 package com.android.launcher3.icons
18
19 import android.content.ComponentName
20 import android.content.pm.ApplicationInfo
21 import android.database.MatrixCursor
22 import android.os.Handler
23 import android.os.Process.myUserHandle
24 import androidx.test.ext.junit.runners.AndroidJUnit4
25 import androidx.test.filters.SmallTest
26 import com.android.launcher3.icons.cache.BaseIconCache
27 import com.android.launcher3.icons.cache.BaseIconCache.IconDB
28 import com.android.launcher3.icons.cache.CachedObject
29 import com.android.launcher3.icons.cache.CachedObjectCachingLogic
30 import com.android.launcher3.icons.cache.IconCacheUpdateHandler
31 import com.android.launcher3.util.RoboApiWrapper
32 import com.google.common.truth.Truth.assertThat
33 import java.util.concurrent.FutureTask
34 import org.junit.After
35 import org.junit.Before
36 import org.junit.Test
37 import org.junit.runner.RunWith
38 import org.mockito.ArgumentCaptor
39 import org.mockito.Captor
40 import org.mockito.Mock
41 import org.mockito.MockitoAnnotations
42 import org.mockito.kotlin.any
43 import org.mockito.kotlin.anyOrNull
44 import org.mockito.kotlin.doAnswer
45 import org.mockito.kotlin.doReturn
46 import org.mockito.kotlin.eq
47 import org.mockito.kotlin.never
48 import org.mockito.kotlin.times
49 import org.mockito.kotlin.verify
50 import org.mockito.kotlin.whenever
51
52 @SmallTest
53 @RunWith(AndroidJUnit4::class)
54 class IconCacheUpdateHandlerTest {
55
56 @Mock private lateinit var iconProvider: IconProvider
57 @Mock private lateinit var baseIconCache: BaseIconCache
58 @Mock private lateinit var cacheDb: IconDB
59 @Mock private lateinit var workerHandler: Handler
60
61 @Captor private lateinit var deleteCaptor: ArgumentCaptor<String>
62
63 private var cursor =
64 MatrixCursor(
65 arrayOf(
66 BaseIconCache.COLUMN_ROWID,
67 BaseIconCache.COLUMN_COMPONENT,
68 BaseIconCache.COLUMN_FRESHNESS_ID,
69 )
70 )
71
72 private lateinit var updateHandlerUnderTest: IconCacheUpdateHandler
73
74 @Before
75 fun setup() {
76 MockitoAnnotations.initMocks(this)
77 doReturn(iconProvider).whenever(baseIconCache).iconProvider
78 doReturn(cursor).whenever(cacheDb).query(any(), any(), any())
79
80 updateHandlerUnderTest = IconCacheUpdateHandler(baseIconCache, cacheDb, workerHandler)
81 }
82
83 @After
84 fun tearDown() {
85 cursor?.close()
86 }
87
88 @Test
89 fun `keeps correct icons irrespective of call order`() {
90 val obj1 = TestCachedObject(1).apply { addToCursor(cursor) }
91 val obj2 = TestCachedObject(2).apply { addToCursor(cursor) }
92
93 updateHandlerUnderTest.updateIcons(obj1)
94 updateHandlerUnderTest.updateIcons(obj2)
95 updateHandlerUnderTest.finish()
96
97 verify(cacheDb, never()).delete(any(), anyOrNull())
98 }
99
100 @Test
101 fun `removes missing entries in single call`() {
102 TestCachedObject(1).addToCursor(cursor)
103 TestCachedObject(2).addToCursor(cursor)
104 TestCachedObject(3).addToCursor(cursor)
105 TestCachedObject(4).addToCursor(cursor)
106 TestCachedObject(5).addToCursor(cursor)
107
108 updateHandlerUnderTest.updateIcons(TestCachedObject(1), TestCachedObject(4))
109 updateHandlerUnderTest.finish()
110
111 verifyItemsDeleted(2, 3, 5)
112 }
113
114 @Test
115 fun `removes missing entries in multiple calls`() {
116 TestCachedObject(1).addToCursor(cursor)
117 TestCachedObject(2).addToCursor(cursor)
118 TestCachedObject(3).addToCursor(cursor)
119 TestCachedObject(4).addToCursor(cursor)
120 TestCachedObject(5).addToCursor(cursor)
121 TestCachedObject(6).addToCursor(cursor)
122
123 updateHandlerUnderTest.updateIcons(TestCachedObject(1), TestCachedObject(2))
124 updateHandlerUnderTest.updateIcons(TestCachedObject(4), TestCachedObject(5))
125 updateHandlerUnderTest.finish()
126
127 verifyItemsDeleted(3, 6)
128 }
129
130 @Test
131 fun `keeps valid app infos`() {
132 val appInfo = ApplicationInfo()
133 doReturn("app-fresh").whenever(iconProvider).getStateForApp(eq(appInfo))
134
135 TestCachedObject(1).addToCursor(cursor)
136 TestCachedObject(2).addToCursor(cursor)
137 cursor.addRow(arrayOf(33, TestCachedObject(1).getPackageKey(), "app-fresh"))
138
139 updateHandlerUnderTest.updateIcons(
140 TestCachedObject(1, appInfo = appInfo),
141 TestCachedObject(2),
142 )
143 updateHandlerUnderTest.finish()
144
145 verify(cacheDb, never()).delete(any(), anyOrNull())
146 }
147
148 @Test
149 fun `deletes stale app infos`() {
150 val appInfo1 = ApplicationInfo()
151 doReturn("app1-fresh").whenever(iconProvider).getStateForApp(eq(appInfo1))
152
153 val appInfo2 = ApplicationInfo()
154 doReturn("app2-fresh").whenever(iconProvider).getStateForApp(eq(appInfo2))
155
156 TestCachedObject(1).addToCursor(cursor)
157 TestCachedObject(2).addToCursor(cursor)
158 cursor.addRow(arrayOf(33, TestCachedObject(1).getPackageKey(), "app1-not-fresh"))
159 cursor.addRow(arrayOf(34, TestCachedObject(2).getPackageKey(), "app2-fresh"))
160
161 updateHandlerUnderTest.updateIcons(
162 TestCachedObject(1, appInfo = appInfo1),
163 TestCachedObject(2, appInfo = appInfo2),
164 )
165 updateHandlerUnderTest.finish()
166
167 verifyItemsDeleted(33)
168 }
169
170 @Test
171 fun `updates stale entries`() {
172 doAnswer { i ->
173 (i.arguments[0] as Runnable).run()
174 true
175 }
176 .whenever(workerHandler)
177 .postAtTime(any(), anyOrNull(), any())
178
179 TestCachedObject(1).addToCursor(cursor)
180 TestCachedObject(2).addToCursor(cursor)
181 TestCachedObject(3).addToCursor(cursor)
182
183 var updatedPackages = mutableSetOf<String>()
184 updateHandlerUnderTest.updateIcons(
185 listOf(
186 TestCachedObject(1, freshnessId = "not-fresh"),
187 TestCachedObject(2, freshnessId = "not-fresh"),
188 TestCachedObject(3),
189 ),
190 CachedObjectCachingLogic,
191 ) { apps, _ ->
192 updatedPackages.addAll(apps)
193 }
194 updateHandlerUnderTest.finish()
195
196 assertThat(updatedPackages)
197 .isEqualTo(
198 mutableSetOf(TestCachedObject(1).cn.packageName, TestCachedObject(2).cn.packageName)
199 )
200 }
201
202 private fun IconCacheUpdateHandler.updateIcons(vararg items: TestCachedObject) {
203 updateIcons(items.toList(), CachedObjectCachingLogic) { _, _ -> }
204 }
205
206 private fun verifyItemsDeleted(vararg rowIds: Long) {
207 verify(cacheDb, times(1)).delete(deleteCaptor.capture(), anyOrNull())
208 val actual =
209 deleteCaptor.value
210 .split('(')
211 ?.get(1)
212 ?.split(')')
213 ?.get(0)
214 ?.split(",")
215 ?.map { it.trim().toLong() }!!
216 .sorted()
217 assertThat(actual).isEqualTo(rowIds.toList().sorted())
218 }
219 }
220
221 /** Utility method to wait for the icon update handler to finish */
waitForUpdateHandlerToFinishnull222 fun IconCache.waitForUpdateHandlerToFinish() {
223 var cacheUpdateInProgress = true
224 while (cacheUpdateInProgress) {
225 val cacheCheck = FutureTask {
226 // Check for pending message on the worker thread itself as some task may be
227 // running currently
228 workerHandler.hasMessages(0, iconUpdateToken)
229 }
230 workerHandler.postDelayed(cacheCheck, 10)
231 RoboApiWrapper.waitForLooperSync(workerHandler.looper)
232 cacheUpdateInProgress = cacheCheck.get()
233 }
234 }
235
236 class TestCachedObject(
237 val rowId: Long,
238 val cn: ComponentName =
239 ComponentName.unflattenFromString("com.android.fake$rowId/.FakeActivity")!!,
240 val freshnessId: String = "fresh-$rowId",
241 val appInfo: ApplicationInfo? = null,
242 ) : CachedObject {
243
getComponentnull244 override fun getComponent() = cn
245
246 override fun getUser() = myUserHandle()
247
248 override fun getLabel(): CharSequence? = null
249
250 override fun getApplicationInfo(): ApplicationInfo? = appInfo
251
252 override fun getFreshnessIdentifier(iconProvider: IconProvider): String? = freshnessId
253
254 fun addToCursor(cursor: MatrixCursor) =
255 cursor.addRow(arrayOf(rowId, cn.flattenToString(), freshnessId))
256
257 fun getPackageKey() =
258 BaseIconCache.getPackageKey(cn.packageName, user).componentName.flattenToString()
259 }
260