• 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.model
18 
19 import android.appwidget.AppWidgetManager
20 import android.content.ComponentName
21 import android.content.Context
22 import android.os.UserHandle
23 import android.platform.test.rule.AllowedDevices
24 import android.platform.test.rule.DeviceProduct
25 import android.platform.test.rule.LimitDevicesRule
26 import androidx.test.core.app.ApplicationProvider
27 import androidx.test.ext.junit.runners.AndroidJUnit4
28 import com.android.launcher3.AppFilter
29 import com.android.launcher3.DeviceProfile
30 import com.android.launcher3.InvariantDeviceProfile
31 import com.android.launcher3.icons.IconCache
32 import com.android.launcher3.model.data.PackageItemInfo
33 import com.android.launcher3.pm.UserCache
34 import com.android.launcher3.util.ActivityContextWrapper
35 import com.android.launcher3.util.ComponentKey
36 import com.android.launcher3.util.Executors
37 import com.android.launcher3.util.IntSet
38 import com.android.launcher3.util.PackageUserKey
39 import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo
40 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
41 import com.android.launcher3.widget.WidgetSections
42 import com.android.launcher3.widget.WidgetSections.NO_CATEGORY
43 import com.google.common.truth.Truth.assertThat
44 import java.util.concurrent.CountDownLatch
45 import java.util.concurrent.TimeUnit
46 import org.junit.Assert.fail
47 import org.junit.Before
48 import org.junit.Rule
49 import org.junit.Test
50 import org.junit.runner.RunWith
51 import org.mockito.Mock
52 import org.mockito.Mockito.spy
53 import org.mockito.junit.MockitoJUnit
54 import org.mockito.junit.MockitoRule
55 import org.mockito.kotlin.any
56 import org.mockito.kotlin.whenever
57 
58 @AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
59 @RunWith(AndroidJUnit4::class)
60 class WidgetsModelTest {
61     @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
62     @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
63 
64     @Mock private lateinit var appWidgetManager: AppWidgetManager
65     @Mock private lateinit var iconCacheMock: IconCache
66 
67     private lateinit var context: Context
68     private lateinit var idp: InvariantDeviceProfile
69     private lateinit var underTest: WidgetsModel
70 
71     private var widgetSectionCategory: Int = 0
72     private lateinit var appAPackage: String
73 
74     @Before
75     fun setUp() {
76         val appContext: Context = ApplicationProvider.getApplicationContext()
77         idp = InvariantDeviceProfile.INSTANCE[appContext]
78 
79         context =
80             object : ActivityContextWrapper(ApplicationProvider.getApplicationContext()) {
81                 override fun getSystemService(name: String): Any? {
82                     if (name == "appwidget") {
83                         return appWidgetManager
84                     }
85                     return super.getSystemService(name)
86                 }
87 
88                 override fun getDeviceProfile(): DeviceProfile {
89                     return idp.getDeviceProfile(applicationContext).copy(applicationContext)
90                 }
91             }
92 
93         whenever(iconCacheMock.getTitleNoCache(any<LauncherAppWidgetProviderInfo>()))
94             .thenReturn("title")
95 
96         val widgetToCategoryEntry: Map.Entry<ComponentName, IntSet> =
97             WidgetSections.getWidgetsToCategory(context).entries.first()
98         widgetSectionCategory = widgetToCategoryEntry.value.first()
99         val appAWidgetComponent = widgetToCategoryEntry.key
100         appAPackage = appAWidgetComponent.packageName
101 
102         whenever(appWidgetManager.getInstalledProvidersForProfile(any()))
103             .thenReturn(
104                 listOf(
105                     // First widget from widget sections xml
106                     createAppWidgetProviderInfo(appAWidgetComponent),
107                     // A widget that belongs to same package as the widget from widget sections
108                     // xml, but, because it's not mentioned in xml, it would be included in its
109                     // own package section.
110                     createAppWidgetProviderInfo(
111                         ComponentName.createRelative(appAPackage, APP_A_TEST_WIDGET_NAME)
112                     ),
113                     // A widget in different package (none of that app's widgets are in widget
114                     // sections xml)
115                     createAppWidgetProviderInfo(AppBTestWidgetComponent),
116                     // A widget in different app that is meant to be hidden from picker
117                     createAppWidgetProviderInfo(
118                         AppCPinOnlyTestWidgetComponent,
119                         /*hideFromPicker=*/ true,
120                     ),
121                 )
122             )
123 
124         val userCache = spy(UserCache.INSTANCE.get(context))
125         whenever(userCache.userProfiles).thenReturn(listOf(UserHandle.CURRENT))
126 
127         underTest = WidgetsModel(context, idp, iconCacheMock, AppFilter(context))
128     }
129 
130     @Test
131     fun widgetsByPackageForPicker_treatsWidgetSectionsAsSeparatePackageItems() {
132         loadWidgets()
133 
134         val packages: Map<PackageItemInfo, List<WidgetItem>> =
135             underTest.widgetsByPackageItemForPicker
136 
137         // expect 3 package items (no app C as its widget is hidden from picker)
138         // one for the custom section with widget from appA
139         // one for package section for second widget from appA (that wasn't listed in xml)
140         // and one for package section for appB
141         assertThat(packages).hasSize(3)
142 
143         // Each package item when used as a key is distinct (i.e. even if appA is split into custom
144         // package and owner package section, each of them is a distinct key). This ensures that
145         // clicking on a custom widget section doesn't take user to app package section.
146         val distinctPackageUserKeys =
147             packages.map { PackageUserKey.fromPackageItemInfo(it.key) }.distinct()
148         assertThat(distinctPackageUserKeys).hasSize(3)
149 
150         val customSections = packages.filter { it.key.widgetCategory == widgetSectionCategory }
151         assertThat(customSections).hasSize(1)
152         val widgetsInCustomSection = customSections.entries.first().value
153         assertThat(widgetsInCustomSection).hasSize(1)
154 
155         val packageSections = packages.filter { it.key.widgetCategory == NO_CATEGORY }
156         assertThat(packageSections).hasSize(2)
157 
158         // App A's package section
159         val appAPackageSection = packageSections.filter { it.key.packageName == appAPackage }
160         assertThat(appAPackageSection).hasSize(1)
161         val widgetsInAppASection = appAPackageSection.entries.first().value
162         assertThat(widgetsInAppASection).hasSize(1)
163 
164         // App B's package section
165         val appBPackageSection =
166             packageSections.filter { it.key.packageName == AppBTestWidgetComponent.packageName }
167         assertThat(appBPackageSection).hasSize(1)
168         val widgetsInAppBSection = appBPackageSection.entries.first().value
169         assertThat(widgetsInAppBSection).hasSize(1)
170 
171         // No App C's package section - as the only widget hosted by it is hidden in picker
172         val appCPackageSection =
173             packageSections.filter {
174                 it.key.packageName == AppCPinOnlyTestWidgetComponent.packageName
175             }
176         assertThat(appCPackageSection).isEmpty()
177     }
178 
179     @Test
180     fun widgetComponentMap_returnsWidgets() {
181         loadWidgets()
182 
183         val widgetsByComponentKey: Map<ComponentKey, WidgetItem> = underTest.widgetsByComponentKey
184 
185         // Has all widgets including ones not visible in picker
186         assertThat(widgetsByComponentKey).hasSize(4)
187         widgetsByComponentKey.forEach { entry ->
188             assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
189         }
190     }
191 
192     @Test
193     fun widgetComponentMapForPicker_excludesWidgetsHiddenInPicker() {
194         loadWidgets()
195 
196         val widgetsByComponentKey: Map<ComponentKey, WidgetItem> =
197             underTest.widgetsByComponentKeyForPicker
198 
199         // Has all widgets excluding the appC's widget.
200         assertThat(widgetsByComponentKey).hasSize(3)
201         assertThat(
202                 widgetsByComponentKey.filter {
203                     it.key.componentName == AppCPinOnlyTestWidgetComponent
204                 }
205             )
206             .isEmpty()
207         // widgets mapped correctly
208         widgetsByComponentKey.forEach { entry ->
209             assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
210         }
211     }
212 
213     @Test
214     fun widgets_noData_returnsEmpty() {
215         // no loadWidgets()
216 
217         assertThat(underTest.widgetsByComponentKey).isEmpty()
218     }
219 
220     @Test
221     fun getWidgetsByPackageItemForPicker_returnsACopyOfMap() {
222         loadWidgets()
223 
224         val latch = CountDownLatch(1)
225         Executors.MODEL_EXECUTOR.execute {
226             var update = true
227 
228             // each "widgetsByPackageItem" read returns a different copy of the map held internally.
229             // Modifying one shouldn't impact another.
230             for ((_, _) in underTest.widgetsByPackageItemForPicker.entries) {
231                 underTest.widgetsByPackageItemForPicker.clear()
232                 if (update) { // trigger update
233                     update = false
234                     // Similarly, model could update its code independently while a client is
235                     // iterating on the list.
236                     underTest.update(/* packageUser= */ null)
237                 }
238             }
239 
240             latch.countDown()
241         }
242         if (!latch.await(LOAD_WIDGETS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
243             fail("Timed out waiting for test")
244         }
245 
246         // No exception
247     }
248 
249     private fun loadWidgets() {
250         val latch = CountDownLatch(1)
251         Executors.MODEL_EXECUTOR.execute {
252             underTest.update(/* packageUser= */ null)
253             latch.countDown()
254         }
255         if (!latch.await(LOAD_WIDGETS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
256             fail("Timed out waiting widgets to load")
257         }
258     }
259 
260     companion object {
261         // Another widget within app A
262         private const val APP_A_TEST_WIDGET_NAME = "MyProvider"
263 
264         private val AppBTestWidgetComponent: ComponentName =
265             ComponentName.createRelative("com.test.package", "TestProvider")
266 
267         private val AppCPinOnlyTestWidgetComponent: ComponentName =
268             ComponentName.createRelative("com.testC.package", "PinOnlyTestProvider")
269 
270         private const val LOAD_WIDGETS_TIMEOUT_SECONDS = 2L
271     }
272 }
273