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