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