1 /* 2 * Copyright (C) 2023 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 package com.android.launcher3.icons; 17 18 import static android.os.Process.myUserHandle; 19 20 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 21 22 import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE; 23 import static com.android.launcher3.icons.IconCacheUpdateHandlerTestKt.waitForUpdateHandlerToFinish; 24 import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG; 25 import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent; 26 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 27 import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY; 28 import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2; 29 import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; 30 import static com.android.launcher3.util.TestUtil.runOnExecutorSync; 31 32 import static org.junit.Assert.assertEquals; 33 import static org.junit.Assert.assertFalse; 34 import static org.junit.Assert.assertNotNull; 35 import static org.junit.Assert.assertNull; 36 import static org.junit.Assert.assertTrue; 37 import static org.mockito.Mockito.doReturn; 38 import static org.mockito.Mockito.spy; 39 40 import android.content.ComponentName; 41 import android.content.Context; 42 import android.content.ContextWrapper; 43 import android.content.Intent; 44 import android.content.pm.LauncherActivityInfo; 45 import android.content.pm.LauncherApps; 46 import android.content.pm.ShortcutInfo; 47 import android.content.pm.ShortcutInfo.Builder; 48 import android.graphics.Bitmap; 49 import android.graphics.Bitmap.Config; 50 import android.graphics.drawable.Icon; 51 import android.os.PersistableBundle; 52 import android.os.UserHandle; 53 import android.text.TextUtils; 54 55 import androidx.annotation.Nullable; 56 import androidx.test.ext.junit.runners.AndroidJUnit4; 57 import androidx.test.filters.SmallTest; 58 59 import com.android.launcher3.LauncherAppState; 60 import com.android.launcher3.icons.cache.CachingLogic; 61 import com.android.launcher3.icons.cache.IconCacheUpdateHandler; 62 import com.android.launcher3.icons.cache.LauncherActivityCachingLogic; 63 import com.android.launcher3.model.data.AppInfo; 64 import com.android.launcher3.model.data.ItemInfoWithIcon; 65 import com.android.launcher3.model.data.PackageItemInfo; 66 import com.android.launcher3.model.data.WorkspaceItemInfo; 67 import com.android.launcher3.settings.SettingsActivity; 68 import com.android.launcher3.shortcuts.ShortcutKey; 69 import com.android.launcher3.util.ApplicationInfoWrapper; 70 import com.android.launcher3.util.ComponentKey; 71 import com.android.launcher3.util.PackageUserKey; 72 import com.android.launcher3.util.SandboxApplication; 73 74 import com.google.common.truth.Truth; 75 76 import org.junit.After; 77 import org.junit.Before; 78 import org.junit.Rule; 79 import org.junit.Test; 80 import org.junit.runner.RunWith; 81 82 import java.util.Collections; 83 import java.util.HashSet; 84 import java.util.Set; 85 86 @SmallTest 87 @RunWith(AndroidJUnit4.class) 88 public class IconCacheTest { 89 90 @Rule public SandboxApplication mContext = new SandboxApplication(); 91 92 private IconCache mIconCache; 93 94 private ComponentName mMyComponent; 95 96 @Before setup()97 public void setup() { 98 mMyComponent = new ComponentName(mContext, SettingsActivity.class); 99 100 // In memory icon cache 101 mIconCache = LauncherAppState.getInstance(mContext).getIconCache(); 102 } 103 104 @After tearDown()105 public void tearDown() { 106 mIconCache.close(); 107 } 108 109 @Test getShortcutInfoBadge_nullComponent_overrideAllowed()110 public void getShortcutInfoBadge_nullComponent_overrideAllowed() throws Exception { 111 String overridePackage = "com.android.settings"; 112 ItemInfoWithIcon item = getBadgingInfo(mContext, null, overridePackage); 113 assertTrue(item instanceof PackageItemInfo); 114 assertEquals(((PackageItemInfo) item).packageName, overridePackage); 115 } 116 117 @Test getShortcutInfoBadge_withComponent_overrideAllowed()118 public void getShortcutInfoBadge_withComponent_overrideAllowed() throws Exception { 119 String overridePackage = "com.android.settings"; 120 ItemInfoWithIcon item = getBadgingInfo(mContext, mMyComponent, overridePackage); 121 assertTrue(item instanceof PackageItemInfo); 122 assertEquals(((PackageItemInfo) item).packageName, overridePackage); 123 } 124 125 @Test getShortcutInfoBadge_nullComponent()126 public void getShortcutInfoBadge_nullComponent() throws Exception { 127 ItemInfoWithIcon item = getBadgingInfo(mContext, null, null); 128 assertTrue(item instanceof PackageItemInfo); 129 assertEquals(((PackageItemInfo) item).packageName, mContext.getPackageName()); 130 } 131 132 @Test getShortcutInfoBadge_withComponent()133 public void getShortcutInfoBadge_withComponent() throws Exception { 134 ItemInfoWithIcon item = getBadgingInfo(mContext, mMyComponent, null); 135 assertTrue(item instanceof AppInfo); 136 assertEquals(((AppInfo) item).componentName, mMyComponent); 137 } 138 139 @Test getShortcutInfoBadge_overrideNotAllowed()140 public void getShortcutInfoBadge_overrideNotAllowed() throws Exception { 141 String overridePackage = "com.android.settings"; 142 String otherPackage = mContext.getPackageName() + ".does.not.exist"; 143 Context otherContext = new ContextWrapper(mContext) { 144 @Override 145 public String getPackageName() { 146 return otherPackage; 147 } 148 }; 149 ItemInfoWithIcon item = getBadgingInfo(otherContext, null, overridePackage); 150 assertTrue(item instanceof PackageItemInfo); 151 // Badge is set to the original package, and not the override package 152 assertEquals(((PackageItemInfo) item).packageName, otherPackage); 153 } 154 155 @Test launcherActivityInfo_cached_in_memory()156 public void launcherActivityInfo_cached_in_memory() { 157 ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); 158 UserHandle user = myUserHandle(); 159 ComponentKey cacheKey = new ComponentKey(cn, user); 160 161 LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class) 162 .resolveActivity(makeLaunchIntent(cn), user); 163 assertNotNull(lai); 164 165 WorkspaceItemInfo info = new WorkspaceItemInfo(); 166 info.intent = makeLaunchIntent(cn); 167 runOnExecutorSync(MODEL_EXECUTOR, 168 () -> mIconCache.getTitleAndIcon(info, lai, DEFAULT_LOOKUP_FLAG)); 169 assertNotNull(info.bitmap); 170 assertFalse(info.bitmap.isLowRes()); 171 172 // Verify that icon is in memory cache 173 runOnExecutorSync(MODEL_EXECUTOR, 174 () -> assertNotNull(mIconCache.getInMemoryEntryLocked(cacheKey))); 175 176 // Schedule async update and wait for it to complete 177 Set<PackageUserKey> updates = 178 executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE); 179 180 // Verify that the icon was not updated and is still in memory cache 181 Truth.assertThat(updates).isEmpty(); 182 runOnExecutorSync(MODEL_EXECUTOR, 183 () -> assertNotNull(mIconCache.getInMemoryEntryLocked(cacheKey))); 184 } 185 186 @Test shortcutInfo_not_cached_in_memory()187 public void shortcutInfo_not_cached_in_memory() { 188 CacheableShortcutInfo si = mockShortcutInfo(0); 189 ShortcutKey cacheKey = ShortcutKey.fromInfo(si.getShortcutInfo()); 190 191 WorkspaceItemInfo info = new WorkspaceItemInfo(); 192 runOnExecutorSync(MODEL_EXECUTOR, () -> mIconCache.getShortcutIcon(info, si)); 193 assertNotNull(info.bitmap); 194 assertFalse(info.bitmap.isLowRes()); 195 196 // Verify that icon is in memory cache 197 runOnExecutorSync(MODEL_EXECUTOR, 198 () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey))); 199 200 Set<PackageUserKey> updates = 201 executeIconUpdate(si, CacheableShortcutCachingLogic.INSTANCE); 202 // Verify that the icon was not updated and is still in memory cache 203 Truth.assertThat(updates).isEmpty(); 204 runOnExecutorSync(MODEL_EXECUTOR, 205 () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey))); 206 207 // Now update the shortcut with a newer version 208 updates = executeIconUpdate( 209 mockShortcutInfo(System.currentTimeMillis() + 2000), 210 CacheableShortcutCachingLogic.INSTANCE); 211 212 // Verify that icon was updated but it is still not in mem-cache 213 Truth.assertThat(updates).containsExactly( 214 new PackageUserKey(cacheKey.getPackageName(), cacheKey.user)); 215 runOnExecutorSync(MODEL_EXECUTOR, 216 () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey))); 217 } 218 219 @Test item_kept_in_db_if_nothing_changes()220 public void item_kept_in_db_if_nothing_changes() { 221 ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); 222 UserHandle user = myUserHandle(); 223 224 LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class) 225 .resolveActivity(makeLaunchIntent(cn), user); 226 assertNotNull(lai); 227 228 // Since this is a new update, there should not be any update 229 Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); 230 assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); 231 232 // Another update should not cause any changes 233 Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); 234 assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); 235 } 236 237 @Test item_updated_in_db_if_appInfo_changes()238 public void item_updated_in_db_if_appInfo_changes() { 239 ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); 240 UserHandle user = myUserHandle(); 241 242 LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class) 243 .resolveActivity(makeLaunchIntent(cn), user); 244 assertNotNull(lai); 245 246 // Since this is a new update, there should not be any update 247 Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); 248 assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); 249 250 // Another update should trigger an update 251 lai.getApplicationInfo().sourceDir = "some-random-source-dir"; 252 Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)) 253 .containsExactly(new PackageUserKey(TEST_PACKAGE, user)); 254 assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); 255 } 256 257 @Test item_removed_in_db_if_item_removed()258 public void item_removed_in_db_if_item_removed() { 259 ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); 260 UserHandle user = myUserHandle(); 261 262 LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class) 263 .resolveActivity(makeLaunchIntent(cn), user); 264 assertNotNull(lai); 265 266 // Since this is a new update, there should not be any update 267 Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); 268 assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); 269 270 // Another update should trigger an update 271 ComponentName cn2 = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY2); 272 LauncherActivityInfo lai2 = mContext.getSystemService(LauncherApps.class) 273 .resolveActivity(makeLaunchIntent(cn2), user); 274 275 Truth.assertThat(executeIconUpdate(lai2, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); 276 assertFalse(mIconCache.isItemInDb(new ComponentKey(cn, user))); 277 assertTrue(mIconCache.isItemInDb(new ComponentKey(cn2, user))); 278 } 279 280 /** 281 * Executes the icon update for the provided entry and returns the updated packages 282 */ executeIconUpdate(T object, CachingLogic<T> cachingLogic)283 private <T> Set<PackageUserKey> executeIconUpdate(T object, CachingLogic<T> cachingLogic) { 284 HashSet<PackageUserKey> updates = new HashSet<>(); 285 286 runOnExecutorSync(MODEL_EXECUTOR, () -> { 287 IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler(); 288 updateHandler.updateIcons( 289 Collections.singletonList(object), 290 cachingLogic, 291 (a, b) -> a.forEach(p -> updates.add(new PackageUserKey(p, b)))); 292 updateHandler.finish(); 293 }); 294 waitForUpdateHandlerToFinish(mIconCache); 295 return updates; 296 } 297 mockShortcutInfo(long updateTime)298 private CacheableShortcutInfo mockShortcutInfo(long updateTime) { 299 ShortcutInfo info = new ShortcutInfo.Builder( 300 getInstrumentation().getContext(), "test-shortcut") 301 .setIntent(new Intent(Intent.ACTION_VIEW)) 302 .setShortLabel("Test") 303 .setIcon(Icon.createWithBitmap(Bitmap.createBitmap(200, 200, Config.ARGB_8888))) 304 .build(); 305 ShortcutInfo spied = spy(info); 306 doReturn(updateTime).when(spied).getLastChangedTimestamp(); 307 return new CacheableShortcutInfo(spied, 308 new ApplicationInfoWrapper(getInstrumentation().getContext().getApplicationInfo())); 309 } 310 getBadgingInfo(Context context, @Nullable ComponentName cn, @Nullable String badgeOverride)311 private ItemInfoWithIcon getBadgingInfo(Context context, 312 @Nullable ComponentName cn, @Nullable String badgeOverride) throws Exception { 313 Builder builder = new Builder(context, "test-shortcut") 314 .setIntent(new Intent(Intent.ACTION_VIEW)) 315 .setShortLabel("Test"); 316 if (cn != null) { 317 builder.setActivity(cn); 318 } 319 if (!TextUtils.isEmpty(badgeOverride)) { 320 PersistableBundle extras = new PersistableBundle(); 321 extras.putString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, badgeOverride); 322 builder.setExtras(extras); 323 } 324 ShortcutInfo info = builder.build(); 325 return MODEL_EXECUTOR.submit(() -> mIconCache.getShortcutInfoBadgeItem(info)).get(); 326 } 327 } 328