1 /*
2  * Copyright (C) 2014 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.systemui.shared.recents.model;
18 
19 import android.app.ActivityManager;
20 import android.content.ComponentCallbacks2;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.pm.ActivityInfo;
24 import android.graphics.drawable.Drawable;
25 import android.os.Looper;
26 import android.os.Trace;
27 import android.util.Log;
28 import android.util.LruCache;
29 
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.Options;
32 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.PreloadOptions;
33 import com.android.systemui.shared.recents.model.Task.TaskKey;
34 import com.android.systemui.shared.recents.model.TaskKeyLruCache.EvictionCallback;
35 import com.android.systemui.shared.system.ActivityManagerWrapper;
36 
37 import java.io.PrintWriter;
38 import java.util.Map;
39 
40 
41 /**
42  * Recents task loader
43  */
44 public class RecentsTaskLoader {
45     private static final String TAG = "RecentsTaskLoader";
46     private static final boolean DEBUG = false;
47 
48     /** Levels of svelte in increasing severity/austerity. */
49     // No svelting.
50     public static final int SVELTE_NONE = 0;
51     // Limit thumbnail cache to number of visible thumbnails when Recents was loaded, disable
52     // caching thumbnails as you scroll.
53     public static final int SVELTE_LIMIT_CACHE = 1;
54     // Disable the thumbnail cache, load thumbnails asynchronously when the activity loads and
55     // evict all thumbnails when hidden.
56     public static final int SVELTE_DISABLE_CACHE = 2;
57     // Disable all thumbnail loading.
58     public static final int SVELTE_DISABLE_LOADING = 3;
59 
60     // This activity info LruCache is useful because it can be expensive to retrieve ActivityInfos
61     // for many tasks, which we use to get the activity labels and icons.  Unlike the other caches
62     // below, this is per-package so we can't invalidate the items in the cache based on the last
63     // active time.  Instead, we rely on the PackageMonitor to keep us informed whenever a
64     // package in the cache has been updated, so that we may remove it.
65     private final LruCache<ComponentName, ActivityInfo> mActivityInfoCache;
66     private final TaskKeyLruCache<Drawable> mIconCache;
67     private final TaskKeyLruCache<String> mActivityLabelCache;
68     private final TaskKeyLruCache<String> mContentDescriptionCache;
69     private final TaskResourceLoadQueue mLoadQueue;
70     private final IconLoader mIconLoader;
71     private final BackgroundTaskLoader mLoader;
72     private final HighResThumbnailLoader mHighResThumbnailLoader;
73     @GuardedBy("this")
74     private final TaskKeyStrongCache<ThumbnailData> mThumbnailCache = new TaskKeyStrongCache<>();
75     @GuardedBy("this")
76     private final TaskKeyStrongCache<ThumbnailData> mTempCache = new TaskKeyStrongCache<>();
77     private final int mMaxThumbnailCacheSize;
78     private final int mMaxIconCacheSize;
79     private int mNumVisibleTasksLoaded;
80     private int mSvelteLevel;
81 
82     private int mDefaultTaskBarBackgroundColor;
83     private int mDefaultTaskViewBackgroundColor;
84 
85     private EvictionCallback mClearActivityInfoOnEviction = new EvictionCallback() {
86         @Override
87         public void onEntryEvicted(TaskKey key) {
88             if (key != null) {
89                 mActivityInfoCache.remove(key.getComponent());
90             }
91         }
92     };
93 
RecentsTaskLoader(Context context, int maxThumbnailCacheSize, int maxIconCacheSize, int svelteLevel)94     public RecentsTaskLoader(Context context, int maxThumbnailCacheSize, int maxIconCacheSize,
95             int svelteLevel) {
96         mMaxThumbnailCacheSize = maxThumbnailCacheSize;
97         mMaxIconCacheSize = maxIconCacheSize;
98         mSvelteLevel = svelteLevel;
99 
100         // Initialize the proxy, cache and loaders
101         int numRecentTasks = ActivityManager.getMaxRecentTasksStatic();
102         mHighResThumbnailLoader = new HighResThumbnailLoader(ActivityManagerWrapper.getInstance(),
103                 Looper.getMainLooper(), ActivityManager.isLowRamDeviceStatic());
104         mLoadQueue = new TaskResourceLoadQueue();
105         mIconCache = new TaskKeyLruCache<>(mMaxIconCacheSize, mClearActivityInfoOnEviction);
106         mActivityLabelCache = new TaskKeyLruCache<>(numRecentTasks, mClearActivityInfoOnEviction);
107         mContentDescriptionCache = new TaskKeyLruCache<>(numRecentTasks,
108                 mClearActivityInfoOnEviction);
109         mActivityInfoCache = new LruCache<>(numRecentTasks);
110 
111         mIconLoader = createNewIconLoader(context, mIconCache, mActivityInfoCache);
112         mLoader = new BackgroundTaskLoader(mLoadQueue, mIconLoader,
113                 mHighResThumbnailLoader::setTaskLoadQueueIdle);
114     }
115 
createNewIconLoader(Context context,TaskKeyLruCache<Drawable> iconCache, LruCache<ComponentName, ActivityInfo> activityInfoCache)116     protected IconLoader createNewIconLoader(Context context,TaskKeyLruCache<Drawable> iconCache,
117             LruCache<ComponentName, ActivityInfo> activityInfoCache) {
118         return new IconLoader.DefaultIconLoader(context, iconCache, activityInfoCache);
119     }
120 
121     /**
122      * Sets the default task bar/view colors if none are provided by the app.
123      */
setDefaultColors(int defaultTaskBarBackgroundColor, int defaultTaskViewBackgroundColor)124     public void setDefaultColors(int defaultTaskBarBackgroundColor,
125             int defaultTaskViewBackgroundColor) {
126         mDefaultTaskBarBackgroundColor = defaultTaskBarBackgroundColor;
127         mDefaultTaskViewBackgroundColor = defaultTaskViewBackgroundColor;
128     }
129 
130     /** Returns the size of the app icon cache. */
getIconCacheSize()131     public int getIconCacheSize() {
132         return mMaxIconCacheSize;
133     }
134 
135     /** Returns the size of the thumbnail cache. */
getThumbnailCacheSize()136     public int getThumbnailCacheSize() {
137         return mMaxThumbnailCacheSize;
138     }
139 
getHighResThumbnailLoader()140     public HighResThumbnailLoader getHighResThumbnailLoader() {
141         return mHighResThumbnailLoader;
142     }
143 
144     /** Preloads recents tasks using the specified plan to store the output. */
preloadTasks(RecentsTaskLoadPlan plan, int runningTaskId)145     public synchronized void preloadTasks(RecentsTaskLoadPlan plan, int runningTaskId) {
146         preloadTasks(plan, runningTaskId, ActivityManagerWrapper.getInstance().getCurrentUserId());
147     }
148 
149     /** Preloads recents tasks using the specified plan to store the output. */
preloadTasks(RecentsTaskLoadPlan plan, int runningTaskId, int currentUserId)150     public synchronized void preloadTasks(RecentsTaskLoadPlan plan, int runningTaskId,
151             int currentUserId) {
152         try {
153             Trace.beginSection("preloadPlan");
154             plan.preloadPlan(new PreloadOptions(), this, runningTaskId, currentUserId);
155         } finally {
156             Trace.endSection();
157         }
158     }
159 
160     /** Begins loading the heavy task data according to the specified options. */
loadTasks(RecentsTaskLoadPlan plan, Options opts)161     public synchronized void loadTasks(RecentsTaskLoadPlan plan, Options opts) {
162         if (opts == null) {
163             throw new RuntimeException("Requires load options");
164         }
165         if (opts.onlyLoadForCache && opts.loadThumbnails) {
166             // If we are loading for the cache, we'd like to have the real cache only include the
167             // visible thumbnails. However, we also don't want to reload already cached thumbnails.
168             // Thus, we copy over the current entries into a second cache, and clear the real cache,
169             // such that the real cache only contains visible thumbnails.
170             mTempCache.copyEntries(mThumbnailCache);
171             mThumbnailCache.evictAll();
172         }
173         plan.executePlan(opts, this);
174         mTempCache.evictAll();
175         if (!opts.onlyLoadForCache) {
176             mNumVisibleTasksLoaded = opts.numVisibleTasks;
177         }
178     }
179 
180     /**
181      * Acquires the task resource data directly from the cache, loading if necessary.
182      */
loadTaskData(Task t)183     public void loadTaskData(Task t) {
184         Drawable icon = mIconCache.getAndInvalidateIfModified(t.key);
185         icon = icon != null ? icon : mIconLoader.getDefaultIcon(t.key.userId);
186         mLoadQueue.addTask(t);
187         t.notifyTaskDataLoaded(t.thumbnail, icon);
188     }
189 
190     /** Releases the task resource data back into the pool. */
unloadTaskData(Task t)191     public void unloadTaskData(Task t) {
192         mLoadQueue.removeTask(t);
193         t.notifyTaskDataUnloaded(mIconLoader.getDefaultIcon(t.key.userId));
194     }
195 
196     /** Completely removes the resource data from the pool. */
deleteTaskData(Task t, boolean notifyTaskDataUnloaded)197     public void deleteTaskData(Task t, boolean notifyTaskDataUnloaded) {
198         mLoadQueue.removeTask(t);
199         mIconCache.remove(t.key);
200         mActivityLabelCache.remove(t.key);
201         mContentDescriptionCache.remove(t.key);
202         if (notifyTaskDataUnloaded) {
203             t.notifyTaskDataUnloaded(mIconLoader.getDefaultIcon(t.key.userId));
204         }
205     }
206 
207     /**
208      * Handles signals from the system, trimming memory when requested to prevent us from running
209      * out of memory.
210      */
onTrimMemory(int level)211     public synchronized void onTrimMemory(int level) {
212         switch (level) {
213             case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
214                 // Stop the loader immediately when the UI is no longer visible
215                 stopLoader();
216                 mIconCache.trimToSize(Math.max(mNumVisibleTasksLoaded,
217                         mMaxIconCacheSize / 2));
218                 break;
219             case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
220             case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
221                 // We are leaving recents, so trim the data a bit
222                 mIconCache.trimToSize(Math.max(1, mMaxIconCacheSize / 2));
223                 mActivityInfoCache.trimToSize(Math.max(1,
224                         ActivityManager.getMaxRecentTasksStatic() / 2));
225                 break;
226             case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
227             case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
228                 // We are going to be low on memory
229                 mIconCache.trimToSize(Math.max(1, mMaxIconCacheSize / 4));
230                 mActivityInfoCache.trimToSize(Math.max(1,
231                         ActivityManager.getMaxRecentTasksStatic() / 4));
232                 break;
233             case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
234             case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
235                 // We are low on memory, so release everything
236                 mIconCache.evictAll();
237                 mActivityInfoCache.evictAll();
238                 // The cache is small, only clear the label cache when we are critical
239                 mActivityLabelCache.evictAll();
240                 mContentDescriptionCache.evictAll();
241                 mThumbnailCache.evictAll();
242                 break;
243             default:
244                 break;
245         }
246     }
247 
onPackageChanged(String packageName)248     public void onPackageChanged(String packageName) {
249         // Remove all the cached activity infos for this package.  The other caches do not need to
250         // be pruned at this time, as the TaskKey expiration checks will flush them next time their
251         // cached contents are requested
252         Map<ComponentName, ActivityInfo> activityInfoCache = mActivityInfoCache.snapshot();
253         for (ComponentName cn : activityInfoCache.keySet()) {
254             if (cn.getPackageName().equals(packageName)) {
255                 if (DEBUG) {
256                     Log.d(TAG, "Removing activity info from cache: " + cn);
257                 }
258                 mActivityInfoCache.remove(cn);
259             }
260         }
261     }
262 
263     /**
264      * Returns the cached task label if the task key is not expired, updating the cache if it is.
265      */
getAndUpdateActivityTitle(TaskKey taskKey, ActivityManager.TaskDescription td)266     String getAndUpdateActivityTitle(TaskKey taskKey, ActivityManager.TaskDescription td) {
267         // Return the task description label if it exists
268         if (td != null && td.getLabel() != null) {
269             return td.getLabel();
270         }
271         // Return the cached activity label if it exists
272         String label = mActivityLabelCache.getAndInvalidateIfModified(taskKey);
273         if (label != null) {
274             return label;
275         }
276         // All short paths failed, load the label from the activity info and cache it
277         ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey);
278         if (activityInfo != null) {
279             label = ActivityManagerWrapper.getInstance().getBadgedActivityLabel(activityInfo,
280                     taskKey.userId);
281             mActivityLabelCache.put(taskKey, label);
282             return label;
283         }
284         // If the activity info does not exist or fails to load, return an empty label for now,
285         // but do not cache it
286         return "";
287     }
288 
289     /**
290      * Returns the cached task content description if the task key is not expired, updating the
291      * cache if it is.
292      */
getAndUpdateContentDescription(TaskKey taskKey, ActivityManager.TaskDescription td)293     String getAndUpdateContentDescription(TaskKey taskKey, ActivityManager.TaskDescription td) {
294         // Return the cached content description if it exists
295         String label = mContentDescriptionCache.getAndInvalidateIfModified(taskKey);
296         if (label != null) {
297             return label;
298         }
299 
300         // All short paths failed, load the label from the activity info and cache it
301         ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey);
302         if (activityInfo != null) {
303             label = ActivityManagerWrapper.getInstance().getBadgedContentDescription(
304                     activityInfo, taskKey.userId, td);
305             if (td == null) {
306                 // Only add to the cache if the task description is null, otherwise, it is possible
307                 // for the task description to change between calls without the last active time
308                 // changing (ie. between preloading and Overview starting) which would lead to stale
309                 // content descriptions
310                 // TODO: Investigate improving this
311                 mContentDescriptionCache.put(taskKey, label);
312             }
313             return label;
314         }
315         // If the content description does not exist, return an empty label for now, but do not
316         // cache it
317         return "";
318     }
319 
320     /**
321      * Returns the cached task icon if the task key is not expired, updating the cache if it is.
322      */
getAndUpdateActivityIcon(TaskKey taskKey, ActivityManager.TaskDescription td, boolean loadIfNotCached)323     Drawable getAndUpdateActivityIcon(TaskKey taskKey, ActivityManager.TaskDescription td,
324             boolean loadIfNotCached) {
325         return mIconLoader.getAndInvalidateIfModified(taskKey, td, loadIfNotCached);
326     }
327 
328     /**
329      * Returns the cached thumbnail if the task key is not expired, updating the cache if it is.
330      */
getAndUpdateThumbnail(TaskKey taskKey, boolean loadIfNotCached, boolean storeInCache)331     synchronized ThumbnailData getAndUpdateThumbnail(TaskKey taskKey, boolean loadIfNotCached,
332             boolean storeInCache) {
333         ThumbnailData cached = mThumbnailCache.getAndInvalidateIfModified(taskKey);
334         if (cached != null) {
335             return cached;
336         }
337 
338         cached = mTempCache.getAndInvalidateIfModified(taskKey);
339         if (cached != null) {
340             mThumbnailCache.put(taskKey, cached);
341             return cached;
342         }
343 
344         if (loadIfNotCached) {
345             if (mSvelteLevel < SVELTE_DISABLE_LOADING) {
346                 // Load the thumbnail from the system
347                 ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance().getTaskThumbnail(
348                         taskKey.id, true /* reducedResolution */);
349                 if (thumbnailData.thumbnail != null) {
350                     if (storeInCache) {
351                         mThumbnailCache.put(taskKey, thumbnailData);
352                     }
353                     return thumbnailData;
354                 }
355             }
356         }
357 
358         // We couldn't load any thumbnail
359         return null;
360     }
361 
362     /**
363      * Returns the task's primary color if possible, defaulting to the default color if there is
364      * no specified primary color.
365      */
getActivityPrimaryColor(ActivityManager.TaskDescription td)366     int getActivityPrimaryColor(ActivityManager.TaskDescription td) {
367         if (td != null && td.getPrimaryColor() != 0) {
368             return td.getPrimaryColor();
369         }
370         return mDefaultTaskBarBackgroundColor;
371     }
372 
373     /**
374      * Returns the task's background color if possible.
375      */
getActivityBackgroundColor(ActivityManager.TaskDescription td)376     int getActivityBackgroundColor(ActivityManager.TaskDescription td) {
377         if (td != null && td.getBackgroundColor() != 0) {
378             return td.getBackgroundColor();
379         }
380         return mDefaultTaskViewBackgroundColor;
381     }
382 
383     /**
384      * Returns the activity info for the given task key, retrieving one from the system if the
385      * task key is expired.
386      */
getAndUpdateActivityInfo(TaskKey taskKey)387     ActivityInfo getAndUpdateActivityInfo(TaskKey taskKey) {
388         return mIconLoader.getAndUpdateActivityInfo(taskKey);
389     }
390 
391     /**
392      * Starts loading tasks.
393      */
startLoader(Context ctx)394     public void startLoader(Context ctx) {
395         mLoader.start(ctx);
396     }
397 
398     /**
399      * Stops the task loader and clears all queued, pending task loads.
400      */
stopLoader()401     private void stopLoader() {
402         mLoader.stop();
403         mLoadQueue.clearTasks();
404     }
405 
dump(String prefix, PrintWriter writer)406     public synchronized void dump(String prefix, PrintWriter writer) {
407         String innerPrefix = prefix + "  ";
408 
409         writer.print(prefix); writer.println(TAG);
410         writer.print(prefix); writer.println("Icon Cache");
411         mIconCache.dump(innerPrefix, writer);
412         writer.print(prefix); writer.println("Thumbnail Cache");
413         mThumbnailCache.dump(innerPrefix, writer);
414         writer.print(prefix); writer.println("Temp Thumbnail Cache");
415         mTempCache.dump(innerPrefix, writer);
416     }
417 }
418