1 /*
2  * Copyright (C) 2022 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.settingslib.applications;
18 
19 import android.graphics.drawable.BitmapDrawable;
20 import android.graphics.drawable.Drawable;
21 import android.os.UserHandle;
22 import android.util.Log;
23 import android.util.LruCache;
24 
25 import androidx.annotation.VisibleForTesting;
26 
27 /**
28  * Cache app icon for management.
29  */
30 public class AppIconCacheManager {
31     private static final String TAG = "AppIconCacheManager";
32     private static final float CACHE_RATIO = 0.1f;
33     @VisibleForTesting
34     static final int MAX_CACHE_SIZE_IN_KB = getMaxCacheInKb();
35     private static final String DELIMITER = ":";
36     private static AppIconCacheManager sAppIconCacheManager;
37     private final LruCache<String, Drawable> mDrawableCache;
38 
AppIconCacheManager()39     private AppIconCacheManager() {
40         mDrawableCache = new LruCache<String, Drawable>(MAX_CACHE_SIZE_IN_KB) {
41             @Override
42             protected int sizeOf(String key, Drawable drawable) {
43                 if (drawable instanceof BitmapDrawable) {
44                     return ((BitmapDrawable) drawable).getBitmap().getByteCount() / 1024;
45                 }
46                 // Rough estimate each pixel will use 4 bytes by default.
47                 return drawable.getIntrinsicHeight() * drawable.getIntrinsicWidth() * 4 / 1024;
48             }
49         };
50     }
51 
52     /**
53      * Get an {@link AppIconCacheManager} instance.
54      */
getInstance()55     public static synchronized AppIconCacheManager getInstance() {
56         if (sAppIconCacheManager == null) {
57             sAppIconCacheManager = new AppIconCacheManager();
58         }
59         return sAppIconCacheManager;
60     }
61 
62     /**
63      * Put app icon to cache
64      *
65      * @param packageName of icon
66      * @param uid         of packageName
67      * @param drawable    app icon
68      */
put(String packageName, int uid, Drawable drawable)69     public void put(String packageName, int uid, Drawable drawable) {
70         final String key = getKey(packageName, uid);
71         if (key == null || drawable == null || drawable.getIntrinsicHeight() < 0
72                 || drawable.getIntrinsicWidth() < 0) {
73             Log.w(TAG, "Invalid key or drawable.");
74             return;
75         }
76         mDrawableCache.put(key, drawable);
77     }
78 
79     /**
80      * Get app icon from cache.
81      *
82      * @param packageName of icon
83      * @param uid         of packageName
84      * @return app icon
85      */
get(String packageName, int uid)86     public Drawable get(String packageName, int uid) {
87         final String key = getKey(packageName, uid);
88         if (key == null) {
89             Log.w(TAG, "Invalid key with package or uid.");
90             return null;
91         }
92         final Drawable cachedDrawable = mDrawableCache.get(key);
93         return cachedDrawable != null ? cachedDrawable.mutate() : null;
94     }
95 
96     /**
97      * Release cache.
98      */
release()99     public static void release() {
100         if (sAppIconCacheManager != null) {
101             sAppIconCacheManager.mDrawableCache.evictAll();
102         }
103     }
104 
getKey(String packageName, int uid)105     private static String getKey(String packageName, int uid) {
106         if (packageName == null || uid < 0) {
107             return null;
108         }
109         return packageName + DELIMITER + UserHandle.getUserId(uid);
110     }
111 
getMaxCacheInKb()112     private static int getMaxCacheInKb() {
113         return Math.round(CACHE_RATIO * Runtime.getRuntime().maxMemory() / 1024);
114     }
115 
116     /**
117      * Clears as much memory as possible.
118      *
119      * @see android.content.ComponentCallbacks2#onTrimMemory(int)
120      */
trimMemory(int level)121     public void trimMemory(int level) {
122         if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
123             // Time to clear everything
124             if (sAppIconCacheManager != null) {
125                 sAppIconCacheManager.mDrawableCache.trimToSize(0);
126             }
127         } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
128                 || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
129             // Tough time but still affordable, clear half of the cache
130             if (sAppIconCacheManager != null) {
131                 final int maxSize = sAppIconCacheManager.mDrawableCache.maxSize();
132                 sAppIconCacheManager.mDrawableCache.trimToSize(maxSize / 2);
133             }
134         }
135     }
136 }
137