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