• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.cache;
17 
18 import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon;
19 import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
20 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
21 
22 import android.content.ComponentName;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.pm.ActivityInfo;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.content.res.Resources;
31 import android.database.Cursor;
32 import android.database.sqlite.SQLiteDatabase;
33 import android.database.sqlite.SQLiteException;
34 import android.graphics.Bitmap;
35 import android.graphics.BitmapFactory;
36 import android.graphics.drawable.Drawable;
37 import android.os.Build;
38 import android.os.Handler;
39 import android.os.Looper;
40 import android.os.Process;
41 import android.os.UserHandle;
42 import android.text.TextUtils;
43 import android.util.Log;
44 
45 import com.android.launcher3.icons.BaseIconFactory;
46 import com.android.launcher3.icons.BitmapInfo;
47 import com.android.launcher3.icons.BitmapRenderer;
48 import com.android.launcher3.icons.GraphicsUtils;
49 import com.android.launcher3.util.ComponentKey;
50 import com.android.launcher3.util.SQLiteCacheHelper;
51 
52 import java.util.AbstractMap;
53 import java.util.Collections;
54 import java.util.HashMap;
55 import java.util.HashSet;
56 import java.util.Map;
57 import java.util.Set;
58 import java.util.function.Supplier;
59 
60 import androidx.annotation.NonNull;
61 
62 public abstract class BaseIconCache {
63 
64     private static final String TAG = "BaseIconCache";
65     private static final boolean DEBUG = false;
66 
67     private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
68 
69     // Empty class name is used for storing package default entry.
70     public static final String EMPTY_CLASS_NAME = ".";
71 
72     public static class CacheEntry extends BitmapInfo {
73         public CharSequence title = "";
74         public CharSequence contentDescription = "";
75     }
76 
77     private final HashMap<UserHandle, BitmapInfo> mDefaultIcons = new HashMap<>();
78 
79     protected final Context mContext;
80     protected final PackageManager mPackageManager;
81 
82     private final Map<ComponentKey, CacheEntry> mCache;
83     protected final Handler mWorkerHandler;
84 
85     protected int mIconDpi;
86     protected IconDB mIconDb;
87     protected String mSystemState = "";
88 
89     private final String mDbFileName;
90     private final BitmapFactory.Options mDecodeOptions;
91     private final Looper mBgLooper;
92 
BaseIconCache(Context context, String dbFileName, Looper bgLooper, int iconDpi, int iconPixelSize, boolean inMemoryCache)93     public BaseIconCache(Context context, String dbFileName, Looper bgLooper,
94             int iconDpi, int iconPixelSize, boolean inMemoryCache) {
95         mContext = context;
96         mDbFileName = dbFileName;
97         mPackageManager = context.getPackageManager();
98         mBgLooper = bgLooper;
99         mWorkerHandler = new Handler(mBgLooper);
100 
101         if (inMemoryCache) {
102             mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
103         } else {
104             // Use a dummy cache
105             mCache = new AbstractMap<ComponentKey, CacheEntry>() {
106                 @Override
107                 public Set<Entry<ComponentKey, CacheEntry>> entrySet() {
108                     return Collections.emptySet();
109                 }
110 
111                 @Override
112                 public CacheEntry put(ComponentKey key, CacheEntry value) {
113                     return value;
114                 }
115             };
116         }
117 
118         if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
119             mDecodeOptions = new BitmapFactory.Options();
120             mDecodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE;
121         } else {
122             mDecodeOptions = null;
123         }
124 
125         updateSystemState();
126         mIconDpi = iconDpi;
127         mIconDb = new IconDB(context, dbFileName, iconPixelSize);
128     }
129 
130     /**
131      * Returns the persistable serial number for {@param user}. Subclass should implement proper
132      * caching strategy to avoid making binder call every time.
133      */
getSerialNumberForUser(UserHandle user)134     protected abstract long getSerialNumberForUser(UserHandle user);
135 
136     /**
137      * Return true if the given app is an instant app and should be badged appropriately.
138      */
isInstantApp(ApplicationInfo info)139     protected abstract boolean isInstantApp(ApplicationInfo info);
140 
141     /**
142      * Opens and returns an icon factory. The factory is recycled by the caller.
143      */
getIconFactory()144     protected abstract BaseIconFactory getIconFactory();
145 
updateIconParams(int iconDpi, int iconPixelSize)146     public void updateIconParams(int iconDpi, int iconPixelSize) {
147         mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
148     }
149 
updateIconParamsBg(int iconDpi, int iconPixelSize)150     private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
151         mIconDpi = iconDpi;
152         mDefaultIcons.clear();
153         mIconDb.clear();
154         mIconDb.close();
155         mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
156         mCache.clear();
157     }
158 
getFullResIcon(Resources resources, int iconId)159     private Drawable getFullResIcon(Resources resources, int iconId) {
160         if (resources != null && iconId != 0) {
161             try {
162                 return resources.getDrawableForDensity(iconId, mIconDpi);
163             } catch (Resources.NotFoundException e) { }
164         }
165         return getFullResDefaultActivityIcon(mIconDpi);
166     }
167 
getFullResIcon(String packageName, int iconId)168     public Drawable getFullResIcon(String packageName, int iconId) {
169         try {
170             return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
171         } catch (PackageManager.NameNotFoundException e) { }
172         return getFullResDefaultActivityIcon(mIconDpi);
173     }
174 
getFullResIcon(ActivityInfo info)175     public Drawable getFullResIcon(ActivityInfo info) {
176         try {
177             return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
178                     info.getIconResource());
179         } catch (PackageManager.NameNotFoundException e) { }
180         return getFullResDefaultActivityIcon(mIconDpi);
181     }
182 
makeDefaultIcon(UserHandle user)183     private BitmapInfo makeDefaultIcon(UserHandle user) {
184         try (BaseIconFactory li = getIconFactory()) {
185             return li.makeDefaultIcon(user);
186         }
187     }
188 
189     /**
190      * Remove any records for the supplied ComponentName.
191      */
remove(ComponentName componentName, UserHandle user)192     public synchronized void remove(ComponentName componentName, UserHandle user) {
193         mCache.remove(new ComponentKey(componentName, user));
194     }
195 
196     /**
197      * Remove any records for the supplied package name from memory.
198      */
removeFromMemCacheLocked(String packageName, UserHandle user)199     private void removeFromMemCacheLocked(String packageName, UserHandle user) {
200         HashSet<ComponentKey> forDeletion = new HashSet<>();
201         for (ComponentKey key: mCache.keySet()) {
202             if (key.componentName.getPackageName().equals(packageName)
203                     && key.user.equals(user)) {
204                 forDeletion.add(key);
205             }
206         }
207         for (ComponentKey condemned: forDeletion) {
208             mCache.remove(condemned);
209         }
210     }
211 
212     /**
213      * Removes the entries related to the given package in memory and persistent DB.
214      */
removeIconsForPkg(String packageName, UserHandle user)215     public synchronized void removeIconsForPkg(String packageName, UserHandle user) {
216         removeFromMemCacheLocked(packageName, user);
217         long userSerial = getSerialNumberForUser(user);
218         mIconDb.delete(
219                 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
220                 new String[]{packageName + "/%", Long.toString(userSerial)});
221     }
222 
getUpdateHandler()223     public IconCacheUpdateHandler getUpdateHandler() {
224         updateSystemState();
225         return new IconCacheUpdateHandler(this);
226     }
227 
228     /**
229      * Refreshes the system state definition used to check the validity of the cache. It
230      * incorporates all the properties that can affect the cache like locale and system-version.
231      */
updateSystemState()232     private void updateSystemState() {
233         final String locale =
234                 mContext.getResources().getConfiguration().getLocales().toLanguageTags();
235         mSystemState = locale + "," + Build.VERSION.SDK_INT;
236     }
237 
getIconSystemState(String packageName)238     protected String getIconSystemState(String packageName) {
239         return mSystemState;
240     }
241 
242     /**
243      * Adds an entry into the DB and the in-memory cache.
244      * @param replaceExisting if true, it will recreate the bitmap even if it already exists in
245      *                        the memory. This is useful then the previous bitmap was created using
246      *                        old data.
247      * package private
248      */
addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic, PackageInfo info, long userSerial, boolean replaceExisting)249     protected synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
250             PackageInfo info, long userSerial, boolean replaceExisting) {
251         UserHandle user = cachingLogic.getUser(object);
252         ComponentName componentName = cachingLogic.getComponent(object);
253 
254         final ComponentKey key = new ComponentKey(componentName, user);
255         CacheEntry entry = null;
256         if (!replaceExisting) {
257             entry = mCache.get(key);
258             // We can't reuse the entry if the high-res icon is not present.
259             if (entry == null || entry.icon == null || entry.isLowRes()) {
260                 entry = null;
261             }
262         }
263         if (entry == null) {
264             entry = new CacheEntry();
265             cachingLogic.loadIcon(mContext, object, entry);
266         }
267         entry.title = cachingLogic.getLabel(object);
268         entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
269         mCache.put(key, entry);
270 
271         ContentValues values = newContentValues(entry, entry.title.toString(),
272                 componentName.getPackageName());
273         addIconToDB(values, componentName, info, userSerial);
274     }
275 
276     /**
277      * Updates {@param values} to contain versioning information and adds it to the DB.
278      * @param values {@link ContentValues} containing icon & title
279      */
addIconToDB(ContentValues values, ComponentName key, PackageInfo info, long userSerial)280     private void addIconToDB(ContentValues values, ComponentName key,
281             PackageInfo info, long userSerial) {
282         values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
283         values.put(IconDB.COLUMN_USER, userSerial);
284         values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime);
285         values.put(IconDB.COLUMN_VERSION, info.versionCode);
286         mIconDb.insertOrReplace(values);
287     }
288 
getDefaultIcon(UserHandle user)289     public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
290         if (!mDefaultIcons.containsKey(user)) {
291             mDefaultIcons.put(user, makeDefaultIcon(user));
292         }
293         return mDefaultIcons.get(user);
294     }
295 
isDefaultIcon(Bitmap icon, UserHandle user)296     public boolean isDefaultIcon(Bitmap icon, UserHandle user) {
297         return getDefaultIcon(user).icon == icon;
298     }
299 
300     /**
301      * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
302      * This method is not thread safe, it must be called from a synchronized method.
303      */
cacheLocked( @onNull ComponentName componentName, @NonNull UserHandle user, @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic, boolean usePackageIcon, boolean useLowResIcon)304     protected <T> CacheEntry cacheLocked(
305             @NonNull ComponentName componentName, @NonNull UserHandle user,
306             @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
307             boolean usePackageIcon, boolean useLowResIcon) {
308         return cacheLocked(componentName, user, infoProvider, cachingLogic, usePackageIcon,
309                 useLowResIcon, true);
310     }
311 
cacheLocked( @onNull ComponentName componentName, @NonNull UserHandle user, @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic, boolean usePackageIcon, boolean useLowResIcon, boolean addToMemCache)312     protected <T> CacheEntry cacheLocked(
313             @NonNull ComponentName componentName, @NonNull UserHandle user,
314             @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
315             boolean usePackageIcon, boolean useLowResIcon, boolean addToMemCache) {
316         assertWorkerThread();
317         ComponentKey cacheKey = new ComponentKey(componentName, user);
318         CacheEntry entry = mCache.get(cacheKey);
319         if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
320             entry = new CacheEntry();
321             if (addToMemCache) {
322                 mCache.put(cacheKey, entry);
323             }
324 
325             // Check the DB first.
326             T object = null;
327             boolean providerFetchedOnce = false;
328 
329             if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
330                 object = infoProvider.get();
331                 providerFetchedOnce = true;
332 
333                 if (object != null) {
334                     cachingLogic.loadIcon(mContext, object, entry);
335                 } else {
336                     if (usePackageIcon) {
337                         CacheEntry packageEntry = getEntryForPackageLocked(
338                                 componentName.getPackageName(), user, false);
339                         if (packageEntry != null) {
340                             if (DEBUG) Log.d(TAG, "using package default icon for " +
341                                     componentName.toShortString());
342                             packageEntry.applyTo(entry);
343                             entry.title = packageEntry.title;
344                             entry.contentDescription = packageEntry.contentDescription;
345                         }
346                     }
347                     if (entry.icon == null) {
348                         if (DEBUG) Log.d(TAG, "using default icon for " +
349                                 componentName.toShortString());
350                         getDefaultIcon(user).applyTo(entry);
351                     }
352                 }
353             }
354 
355             if (TextUtils.isEmpty(entry.title)) {
356                 if (object == null && !providerFetchedOnce) {
357                     object = infoProvider.get();
358                     providerFetchedOnce = true;
359                 }
360                 if (object != null) {
361                     entry.title = cachingLogic.getLabel(object);
362                     entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
363                 }
364             }
365         }
366         return entry;
367     }
368 
clear()369     public synchronized void clear() {
370         assertWorkerThread();
371         mIconDb.clear();
372     }
373 
374     /**
375      * Adds a default package entry in the cache. This entry is not persisted and will be removed
376      * when the cache is flushed.
377      */
cachePackageInstallInfo(String packageName, UserHandle user, Bitmap icon, CharSequence title)378     public synchronized void cachePackageInstallInfo(String packageName, UserHandle user,
379             Bitmap icon, CharSequence title) {
380         removeFromMemCacheLocked(packageName, user);
381 
382         ComponentKey cacheKey = getPackageKey(packageName, user);
383         CacheEntry entry = mCache.get(cacheKey);
384 
385         // For icon caching, do not go through DB. Just update the in-memory entry.
386         if (entry == null) {
387             entry = new CacheEntry();
388         }
389         if (!TextUtils.isEmpty(title)) {
390             entry.title = title;
391         }
392         if (icon != null) {
393             BaseIconFactory li = getIconFactory();
394             li.createIconBitmap(icon).applyTo(entry);
395             li.close();
396         }
397         if (!TextUtils.isEmpty(title) && entry.icon != null) {
398             mCache.put(cacheKey, entry);
399         }
400     }
401 
getPackageKey(String packageName, UserHandle user)402     private static ComponentKey getPackageKey(String packageName, UserHandle user) {
403         ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
404         return new ComponentKey(cn, user);
405     }
406 
407     /**
408      * Gets an entry for the package, which can be used as a fallback entry for various components.
409      * This method is not thread safe, it must be called from a synchronized method.
410      */
getEntryForPackageLocked(String packageName, UserHandle user, boolean useLowResIcon)411     protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
412             boolean useLowResIcon) {
413         assertWorkerThread();
414         ComponentKey cacheKey = getPackageKey(packageName, user);
415         CacheEntry entry = mCache.get(cacheKey);
416 
417         if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
418             entry = new CacheEntry();
419             boolean entryUpdated = true;
420 
421             // Check the DB first.
422             if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
423                 try {
424                     int flags = Process.myUserHandle().equals(user) ? 0 :
425                             PackageManager.GET_UNINSTALLED_PACKAGES;
426                     PackageInfo info = mPackageManager.getPackageInfo(packageName, flags);
427                     ApplicationInfo appInfo = info.applicationInfo;
428                     if (appInfo == null) {
429                         throw new NameNotFoundException("ApplicationInfo is null");
430                     }
431 
432                     BaseIconFactory li = getIconFactory();
433                     // Load the full res icon for the application, but if useLowResIcon is set, then
434                     // only keep the low resolution icon instead of the larger full-sized icon
435                     BitmapInfo iconInfo = li.createBadgedIconBitmap(
436                             appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion,
437                             isInstantApp(appInfo));
438                     li.close();
439 
440                     entry.title = appInfo.loadLabel(mPackageManager);
441                     entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
442                     entry.icon = useLowResIcon ? LOW_RES_ICON : iconInfo.icon;
443                     entry.color = iconInfo.color;
444 
445                     // Add the icon in the DB here, since these do not get written during
446                     // package updates.
447                     ContentValues values = newContentValues(
448                             iconInfo, entry.title.toString(), packageName);
449                     addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user));
450 
451                 } catch (NameNotFoundException e) {
452                     if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
453                     entryUpdated = false;
454                 }
455             }
456 
457             // Only add a filled-out entry to the cache
458             if (entryUpdated) {
459                 mCache.put(cacheKey, entry);
460             }
461         }
462         return entry;
463     }
464 
getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes)465     private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
466         Cursor c = null;
467         try {
468             c = mIconDb.query(
469                     lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES,
470                     IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
471                     new String[]{
472                             cacheKey.componentName.flattenToString(),
473                             Long.toString(getSerialNumberForUser(cacheKey.user))});
474             if (c.moveToNext()) {
475                 // Set the alpha to be 255, so that we never have a wrong color
476                 entry.color = setColorAlphaBound(c.getInt(0), 255);
477                 entry.title = c.getString(1);
478                 if (entry.title == null) {
479                     entry.title = "";
480                     entry.contentDescription = "";
481                 } else {
482                     entry.contentDescription = mPackageManager.getUserBadgedLabel(
483                             entry.title, cacheKey.user);
484                 }
485 
486                 if (lowRes) {
487                     entry.icon = LOW_RES_ICON;
488                 } else {
489                     byte[] data = c.getBlob(2);
490                     try {
491                         entry.icon = BitmapFactory.decodeByteArray(data, 0, data.length,
492                                 mDecodeOptions);
493                     } catch (Exception e) { }
494                 }
495                 return true;
496             }
497         } catch (SQLiteException e) {
498             Log.d(TAG, "Error reading icon cache", e);
499         } finally {
500             if (c != null) {
501                 c.close();
502             }
503         }
504         return false;
505     }
506 
507     static final class IconDB extends SQLiteCacheHelper {
508         private final static int RELEASE_VERSION = 26;
509 
510         public final static String TABLE_NAME = "icons";
511         public final static String COLUMN_ROWID = "rowid";
512         public final static String COLUMN_COMPONENT = "componentName";
513         public final static String COLUMN_USER = "profileId";
514         public final static String COLUMN_LAST_UPDATED = "lastUpdated";
515         public final static String COLUMN_VERSION = "version";
516         public final static String COLUMN_ICON = "icon";
517         public final static String COLUMN_ICON_COLOR = "icon_color";
518         public final static String COLUMN_LABEL = "label";
519         public final static String COLUMN_SYSTEM_STATE = "system_state";
520 
521         public final static String[] COLUMNS_HIGH_RES = new String[] {
522                 IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON };
523         public final static String[] COLUMNS_LOW_RES = new String[] {
524                 IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL };
525 
IconDB(Context context, String dbFileName, int iconPixelSize)526         public IconDB(Context context, String dbFileName, int iconPixelSize) {
527             super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME);
528         }
529 
530         @Override
onCreateTable(SQLiteDatabase db)531         protected void onCreateTable(SQLiteDatabase db) {
532             db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
533                     COLUMN_COMPONENT + " TEXT NOT NULL, " +
534                     COLUMN_USER + " INTEGER NOT NULL, " +
535                     COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
536                     COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
537                     COLUMN_ICON + " BLOB, " +
538                     COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, " +
539                     COLUMN_LABEL + " TEXT, " +
540                     COLUMN_SYSTEM_STATE + " TEXT, " +
541                     "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
542                     ");");
543         }
544     }
545 
newContentValues(BitmapInfo bitmapInfo, String label, String packageName)546     private ContentValues newContentValues(BitmapInfo bitmapInfo, String label, String packageName) {
547         ContentValues values = new ContentValues();
548         values.put(IconDB.COLUMN_ICON,
549                 bitmapInfo.isLowRes() ? null : GraphicsUtils.flattenBitmap(bitmapInfo.icon));
550         values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color);
551 
552         values.put(IconDB.COLUMN_LABEL, label);
553         values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName));
554 
555         return values;
556     }
557 
assertWorkerThread()558     private void assertWorkerThread() {
559         if (Looper.myLooper() != mBgLooper) {
560             throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper());
561         }
562     }
563 }
564