• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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