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.widget.picker.model.data
18 
19 import android.content.ComponentName
20 import android.content.Context
21 import android.os.UserHandle
22 import android.platform.test.rule.AllowedDevices
23 import android.platform.test.rule.DeviceProduct
24 import android.platform.test.rule.LimitDevicesRule
25 import androidx.test.core.app.ApplicationProvider
26 import androidx.test.ext.junit.runners.AndroidJUnit4
27 import com.android.launcher3.InvariantDeviceProfile
28 import com.android.launcher3.LauncherAppState
29 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
30 import com.android.launcher3.icons.IconCache
31 import com.android.launcher3.icons.cache.CachedObject
32 import com.android.launcher3.model.WidgetItem
33 import com.android.launcher3.model.data.ItemInfo
34 import com.android.launcher3.model.data.PackageItemInfo
35 import com.android.launcher3.util.ActivityContextWrapper
36 import com.android.launcher3.util.PackageUserKey
37 import com.android.launcher3.util.WidgetUtils
38 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
39 import com.android.launcher3.widget.PendingAddWidgetInfo
40 import com.android.launcher3.widget.model.WidgetsListBaseEntry
41 import com.android.launcher3.widget.model.WidgetsListContentEntry
42 import com.android.launcher3.widget.model.WidgetsListHeaderEntry
43 import com.android.launcher3.widget.picker.WidgetRecommendationCategory
44 import com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY
45 import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser
46 import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser
47 import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withRecommendedWidgets
48 import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withWidgets
49 import com.google.common.truth.Truth.assertThat
50 import org.junit.Before
51 import org.junit.Rule
52 import org.junit.Test
53 import org.junit.runner.RunWith
54 import org.mockito.Mock
55 import org.mockito.invocation.InvocationOnMock
56 import org.mockito.junit.MockitoJUnit
57 import org.mockito.junit.MockitoRule
58 import org.mockito.kotlin.any
59 import org.mockito.kotlin.doAnswer
60 
61 // Tests for code / classes in WidgetPickerData file.
62 
63 @RunWith(AndroidJUnit4::class)
64 @AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
65 class WidgetPickerDataTest {
66     @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
67     @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
68 
69     @Mock private lateinit var iconCache: IconCache
70 
71     private lateinit var userHandle: UserHandle
72     private lateinit var context: Context
73     private lateinit var testInvariantProfile: InvariantDeviceProfile
74 
75     private lateinit var app1PackageItemInfo: PackageItemInfo
76     private lateinit var app2PackageItemInfo: PackageItemInfo
77 
78     private lateinit var app1WidgetItem1: WidgetItem
79     private lateinit var app1WidgetItem2: WidgetItem
80     private lateinit var app2WidgetItem1: WidgetItem
81 
82     @Before
83     fun setUp() {
84         userHandle = UserHandle.CURRENT
85         context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
86         testInvariantProfile = LauncherAppState.getIDP(context)
87 
88         doAnswer { invocation: InvocationOnMock ->
89                 val componentWithLabel = invocation.getArgument<Any>(0) as CachedObject
90                 componentWithLabel.getComponent().shortClassName
91             }
92             .`when`(iconCache)
93             .getTitleNoCache(any<CachedObject>())
94 
95         app1PackageItemInfo = packageItemInfoWithTitle(APP_1_PACKAGE_NAME, APP_1_PACKAGE_TITLE)
96         app2PackageItemInfo = packageItemInfoWithTitle(APP_2_PACKAGE_NAME, APP_2_PACKAGE_TITLE)
97 
98         app1WidgetItem1 = createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_1_CLASS_NAME)
99         app1WidgetItem2 = createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_2_CLASS_NAME)
100         app2WidgetItem1 = createWidgetItem(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME)
101     }
102 
103     @Test
104     fun withWidgets_returnsACopyWithProvidedWidgets() {
105         // only app two
106         val widgetPickerData = WidgetPickerData(allWidgets = appTwoWidgetsListBaseEntries())
107 
108         // update: only app 1 and default list set
109         val newAllWidgets: List<WidgetsListBaseEntry> =
110             appOneWidgetsListBaseEntries(includeWidgetTwo = true)
111         val newDefaultWidgets: List<WidgetsListBaseEntry> =
112             appOneWidgetsListBaseEntries(includeWidgetTwo = false)
113 
114         val newWidgetData = widgetPickerData.withWidgets(newAllWidgets, newDefaultWidgets)
115 
116         assertThat(newWidgetData.allWidgets).containsExactlyElementsIn(newAllWidgets)
117         assertThat(newWidgetData.defaultWidgets).containsExactlyElementsIn(newDefaultWidgets)
118     }
119 
120     @Test
121     fun withWidgets_noExplicitDefaults_unsetsOld() {
122         // only app two
123         val widgetPickerData =
124             WidgetPickerData(
125                 allWidgets = appTwoWidgetsListBaseEntries(),
126                 defaultWidgets = appTwoWidgetsListBaseEntries(),
127             )
128 
129         val newWidgetData =
130             widgetPickerData.withWidgets(allWidgets = appOneWidgetsListBaseEntries())
131 
132         assertThat(newWidgetData.allWidgets)
133             .containsExactlyElementsIn(appOneWidgetsListBaseEntries())
134         assertThat(newWidgetData.defaultWidgets).isEmpty() // previous values cleared.
135     }
136 
137     @Test
138     fun withRecommendedWidgets_returnsACopyWithProvidedRecommendedWidgets() {
139         val widgetPickerData =
140             WidgetPickerData(
141                 allWidgets =
142                     buildList {
143                         addAll(appOneWidgetsListBaseEntries())
144                         addAll(appTwoWidgetsListBaseEntries())
145                     },
146                 defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
147             )
148         val recommendations: List<ItemInfo> =
149             listOf(
150                 PendingAddWidgetInfo(
151                     app1WidgetItem1.widgetInfo,
152                     CONTAINER_WIDGETS_PREDICTION,
153                     CATEGORY_1,
154                 ),
155                 PendingAddWidgetInfo(
156                     app2WidgetItem1.widgetInfo,
157                     CONTAINER_WIDGETS_PREDICTION,
158                     CATEGORY_2,
159                 ),
160             )
161 
162         val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
163 
164         assertThat(updatedData.recommendations.keys).containsExactly(CATEGORY_1, CATEGORY_2)
165         assertThat(updatedData.recommendations[CATEGORY_1]).containsExactly(app1WidgetItem1)
166         assertThat(updatedData.recommendations[CATEGORY_2]).containsExactly(app2WidgetItem1)
167     }
168 
169     @Test
170     fun withRecommendedWidgets_noCategory_usesDefault() {
171         val widgetPickerData =
172             WidgetPickerData(
173                 allWidgets =
174                     buildList {
175                         addAll(appOneWidgetsListBaseEntries())
176                         addAll(appTwoWidgetsListBaseEntries())
177                     },
178                 defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
179             )
180         val recommendations: List<ItemInfo> =
181             listOf(
182                 PendingAddWidgetInfo(app1WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
183                 PendingAddWidgetInfo(app2WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
184             )
185 
186         val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
187 
188         assertThat(updatedData.recommendations.keys)
189             .containsExactly(DEFAULT_WIDGET_RECOMMENDATION_CATEGORY)
190         assertThat(updatedData.recommendations[DEFAULT_WIDGET_RECOMMENDATION_CATEGORY])
191             .containsExactly(app1WidgetItem1, app2WidgetItem1)
192     }
193 
194     @Test
195     fun withRecommendedWidgets_emptyRecommendations_clearsOld() {
196         val widgetPickerData =
197             WidgetPickerData(
198                 allWidgets =
199                     buildList {
200                         addAll(appOneWidgetsListBaseEntries())
201                         addAll(appTwoWidgetsListBaseEntries())
202                     },
203                 defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
204                 recommendations = mapOf(CATEGORY_1 to listOf(app1WidgetItem1)),
205             )
206 
207         val updatedData = widgetPickerData.withRecommendedWidgets(listOf())
208 
209         assertThat(updatedData.recommendations).isEmpty()
210     }
211 
212     @Test
213     fun withRecommendedWidgets_widgetNotInAllWidgets_filteredOut() {
214         val widgetPickerData =
215             WidgetPickerData(
216                 allWidgets =
217                     buildList {
218                         addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false))
219                         addAll(appTwoWidgetsListBaseEntries())
220                     },
221                 defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
222             )
223 
224         val recommendations: List<ItemInfo> =
225             listOf(
226                 PendingAddWidgetInfo(app1WidgetItem2.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
227                 PendingAddWidgetInfo(app2WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
228             )
229         val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
230 
231         assertThat(updatedData.recommendations).hasSize(1)
232         // no app1widget2
233         assertThat(updatedData.recommendations.values.first()).containsExactly(app2WidgetItem1)
234     }
235 
236     @Test
237     fun findContentEntryForPackageUser_returnsCorrectEntry() {
238         val widgetPickerData =
239             WidgetPickerData(
240                 allWidgets =
241                     buildList {
242                         addAll(appOneWidgetsListBaseEntries())
243                         addAll(appTwoWidgetsListBaseEntries())
244                     },
245                 defaultWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) },
246             )
247         val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
248 
249         val contentEntry = findContentEntryForPackageUser(widgetPickerData, app1PackageUserKey)
250 
251         assertThat(contentEntry).isNotNull()
252         assertThat(contentEntry?.mPkgItem).isEqualTo(app1PackageItemInfo)
253         assertThat(contentEntry?.mWidgets).hasSize(2)
254     }
255 
256     @Test
257     fun findContentEntryForPackageUser_fromDefaults_returnsEntryFromDefaultWidgets() {
258         val widgetPickerData =
259             WidgetPickerData(
260                 allWidgets =
261                     buildList {
262                         addAll(appOneWidgetsListBaseEntries())
263                         addAll(appTwoWidgetsListBaseEntries())
264                     },
265                 defaultWidgets =
266                     buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) },
267             )
268         val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
269 
270         val contentEntry =
271             findContentEntryForPackageUser(
272                 widgetPickerData = widgetPickerData,
273                 packageUserKey = app1PackageUserKey,
274                 fromDefaultWidgets = true,
275             )
276 
277         assertThat(contentEntry).isNotNull()
278         assertThat(contentEntry?.mPkgItem).isEqualTo(app1PackageItemInfo)
279         // only one widget (since default widgets had only one widget for app A
280         assertThat(contentEntry?.mWidgets).hasSize(1)
281     }
282 
283     @Test
284     fun findContentEntryForPackageUser_noMatch_returnsNull() {
285         val app2PackageUserKey = PackageUserKey.fromPackageItemInfo(app2PackageItemInfo)
286         val widgetPickerData =
287             WidgetPickerData(allWidgets = buildList { addAll(appOneWidgetsListBaseEntries()) })
288 
289         val contentEntry = findContentEntryForPackageUser(widgetPickerData, app2PackageUserKey)
290 
291         assertThat(contentEntry).isNull()
292     }
293 
294     @Test
295     fun findAllWidgetsForPackageUser_returnsListOfWidgets() {
296         val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
297         val widgetPickerData =
298             WidgetPickerData(
299                 allWidgets =
300                     buildList {
301                         addAll(appOneWidgetsListBaseEntries())
302                         addAll(appTwoWidgetsListBaseEntries())
303                     },
304                 defaultWidgets =
305                     buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) },
306             )
307 
308         val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
309 
310         // both widgets returned irrespective of default widgets list
311         assertThat(widgets).hasSize(2)
312     }
313 
314     @Test
315     fun findAllWidgetsForPackageUser_noMatch_returnsEmptyList() {
316         val widgetPickerData =
317             WidgetPickerData(allWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) })
318         val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
319 
320         val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
321 
322         assertThat(widgets).isEmpty()
323     }
324 
325     private fun packageItemInfoWithTitle(packageName: String, title: String): PackageItemInfo {
326         val packageItemInfo = PackageItemInfo(packageName, userHandle)
327         packageItemInfo.title = title
328         return packageItemInfo
329     }
330 
331     private fun createWidgetItem(packageName: String, widgetProviderName: String): WidgetItem {
332         val providerInfo =
333             WidgetUtils.createAppWidgetProviderInfo(
334                 ComponentName.createRelative(packageName, widgetProviderName)
335             )
336         val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
337         return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
338     }
339 
340     private fun appTwoWidgetsListBaseEntries(): List<WidgetsListBaseEntry> = buildList {
341         val widgets = listOf(app2WidgetItem1)
342         add(WidgetsListHeaderEntry.create(app2PackageItemInfo, APP_2_SECTION_NAME, widgets))
343         add(WidgetsListContentEntry(app2PackageItemInfo, APP_2_SECTION_NAME, widgets))
344     }
345 
346     private fun appOneWidgetsListBaseEntries(
347         includeWidgetTwo: Boolean = true
348     ): List<WidgetsListBaseEntry> = buildList {
349         val widgets =
350             if (includeWidgetTwo) {
351                 listOf(app1WidgetItem1, app1WidgetItem2)
352             } else {
353                 listOf(app1WidgetItem1)
354             }
355 
356         add(WidgetsListHeaderEntry.create(app1PackageItemInfo, APP_1_SECTION_NAME, widgets))
357         add(WidgetsListContentEntry(app1PackageItemInfo, APP_1_SECTION_NAME, widgets))
358     }
359 
360     companion object {
361         private const val APP_1_PACKAGE_NAME = "com.example.app1"
362         private const val APP_1_PACKAGE_TITLE = "App1"
363         private const val APP_1_SECTION_NAME = "A" // for fast popup
364         private const val APP_1_PROVIDER_1_CLASS_NAME = "app1Provider1"
365         private const val APP_1_PROVIDER_2_CLASS_NAME = "app1Provider2"
366 
367         private const val APP_2_PACKAGE_NAME = "com.example.app2"
368         private const val APP_2_PACKAGE_TITLE = "SomeApp2"
369         private const val APP_2_SECTION_NAME = "S" // for fast popup
370         private const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1"
371 
372         private val CATEGORY_1 =
373             WidgetRecommendationCategory(/* categoryTitleRes= */ 0, /* order= */ 0)
374         private val CATEGORY_2 =
375             WidgetRecommendationCategory(/* categoryTitleRes= */ 1, /* order= */ 1)
376     }
377 }
378