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.content.ComponentCallbacks2;
21 import android.content.Context;
22 import android.content.pm.ActivityInfo;
23 import android.content.res.Resources;
24 import android.graphics.Bitmap;
25 import android.graphics.drawable.BitmapDrawable;
26 import android.graphics.drawable.Drawable;
27 import android.os.Handler;
28 import android.os.HandlerThread;
29 import android.util.Log;
30 
31 import com.android.systemui.R;
32 import com.android.systemui.recents.Constants;
33 import com.android.systemui.recents.RecentsConfiguration;
34 import com.android.systemui.recents.misc.SystemServicesProxy;
35 
36 import java.util.Collection;
37 import java.util.concurrent.ConcurrentLinkedQueue;
38 
39 
40 /** Handle to an ActivityInfo */
41 class ActivityInfoHandle {
42     ActivityInfo info;
43 }
44 
45 /** A bitmap load queue */
46 class TaskResourceLoadQueue {
47     ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>();
48 
49     /** Adds a new task to the load queue */
addTasks(Collection<Task> tasks)50     void addTasks(Collection<Task> tasks) {
51         for (Task t : tasks) {
52             if (!mQueue.contains(t)) {
53                 mQueue.add(t);
54             }
55         }
56         synchronized(this) {
57             notifyAll();
58         }
59     }
60 
61     /** Adds a new task to the load queue */
addTask(Task t)62     void addTask(Task t) {
63         if (!mQueue.contains(t)) {
64             mQueue.add(t);
65         }
66         synchronized(this) {
67             notifyAll();
68         }
69     }
70 
71     /**
72      * Retrieves the next task from the load queue, as well as whether we want that task to be
73      * force reloaded.
74      */
nextTask()75     Task nextTask() {
76         return mQueue.poll();
77     }
78 
79     /** Removes a task from the load queue */
removeTask(Task t)80     void removeTask(Task t) {
81         mQueue.remove(t);
82     }
83 
84     /** Clears all the tasks from the load queue */
clearTasks()85     void clearTasks() {
86         mQueue.clear();
87     }
88 
89     /** Returns whether the load queue is empty */
isEmpty()90     boolean isEmpty() {
91         return mQueue.isEmpty();
92     }
93 }
94 
95 /* Task resource loader */
96 class TaskResourceLoader implements Runnable {
97     static String TAG = "TaskResourceLoader";
98     static boolean DEBUG = false;
99 
100     Context mContext;
101     HandlerThread mLoadThread;
102     Handler mLoadThreadHandler;
103     Handler mMainThreadHandler;
104 
105     SystemServicesProxy mSystemServicesProxy;
106     TaskResourceLoadQueue mLoadQueue;
107     DrawableLruCache mApplicationIconCache;
108     BitmapLruCache mThumbnailCache;
109     Bitmap mDefaultThumbnail;
110     BitmapDrawable mDefaultApplicationIcon;
111 
112     boolean mCancelled;
113     boolean mWaitingOnLoadQueue;
114 
115     /** Constructor, creates a new loading thread that loads task resources in the background */
TaskResourceLoader(TaskResourceLoadQueue loadQueue, DrawableLruCache applicationIconCache, BitmapLruCache thumbnailCache, Bitmap defaultThumbnail, BitmapDrawable defaultApplicationIcon)116     public TaskResourceLoader(TaskResourceLoadQueue loadQueue, DrawableLruCache applicationIconCache,
117                               BitmapLruCache thumbnailCache, Bitmap defaultThumbnail,
118                               BitmapDrawable defaultApplicationIcon) {
119         mLoadQueue = loadQueue;
120         mApplicationIconCache = applicationIconCache;
121         mThumbnailCache = thumbnailCache;
122         mDefaultThumbnail = defaultThumbnail;
123         mDefaultApplicationIcon = defaultApplicationIcon;
124         mMainThreadHandler = new Handler();
125         mLoadThread = new HandlerThread("Recents-TaskResourceLoader",
126                 android.os.Process.THREAD_PRIORITY_BACKGROUND);
127         mLoadThread.start();
128         mLoadThreadHandler = new Handler(mLoadThread.getLooper());
129         mLoadThreadHandler.post(this);
130     }
131 
132     /** Restarts the loader thread */
start(Context context)133     void start(Context context) {
134         mContext = context;
135         mCancelled = false;
136         mSystemServicesProxy = new SystemServicesProxy(context);
137         // Notify the load thread to start loading
138         synchronized(mLoadThread) {
139             mLoadThread.notifyAll();
140         }
141     }
142 
143     /** Requests the loader thread to stop after the current iteration */
stop()144     void stop() {
145         // Mark as cancelled for the thread to pick up
146         mCancelled = true;
147         mSystemServicesProxy = null;
148         // If we are waiting for the load queue for more tasks, then we can just reset the
149         // Context now, since nothing is using it
150         if (mWaitingOnLoadQueue) {
151             mContext = null;
152         }
153     }
154 
155     @Override
run()156     public void run() {
157         while (true) {
158             if (mCancelled) {
159                 // We have to unset the context here, since the background thread may be using it
160                 // when we call stop()
161                 mContext = null;
162                 // If we are cancelled, then wait until we are started again
163                 synchronized(mLoadThread) {
164                     try {
165                         mLoadThread.wait();
166                     } catch (InterruptedException ie) {
167                         ie.printStackTrace();
168                     }
169                 }
170             } else {
171                 RecentsConfiguration config = RecentsConfiguration.getInstance();
172                 SystemServicesProxy ssp = mSystemServicesProxy;
173                 // If we've stopped the loader, then fall through to the above logic to wait on
174                 // the load thread
175                 if (ssp != null) {
176                     // Load the next item from the queue
177                     final Task t = mLoadQueue.nextTask();
178                     if (t != null) {
179                         Drawable cachedIcon = mApplicationIconCache.get(t.key);
180                         Bitmap cachedThumbnail = mThumbnailCache.get(t.key);
181 
182                         // Load the application icon if it is stale or we haven't cached one yet
183                         if (cachedIcon == null) {
184                             cachedIcon = getTaskDescriptionIcon(t.key, t.icon, t.iconFilename, ssp,
185                                     mContext.getResources());
186 
187                             if (cachedIcon == null) {
188                                 ActivityInfo info = ssp.getActivityInfo(
189                                         t.key.baseIntent.getComponent(), t.key.userId);
190                                 if (info != null) {
191                                     if (DEBUG) Log.d(TAG, "Loading icon: " + t.key);
192                                     cachedIcon = ssp.getActivityIcon(info, t.key.userId);
193                                 }
194                             }
195 
196                             if (cachedIcon == null) {
197                                 cachedIcon = mDefaultApplicationIcon;
198                             }
199 
200                             // At this point, even if we can't load the icon, we will set the
201                             // default icon.
202                             mApplicationIconCache.put(t.key, cachedIcon);
203                         }
204                         // Load the thumbnail if it is stale or we haven't cached one yet
205                         if (cachedThumbnail == null) {
206                             if (config.svelteLevel < RecentsConfiguration.SVELTE_DISABLE_LOADING) {
207                                 if (DEBUG) Log.d(TAG, "Loading thumbnail: " + t.key);
208                                 cachedThumbnail = ssp.getTaskThumbnail(t.key.id);
209                             }
210                             if (cachedThumbnail == null) {
211                                 cachedThumbnail = mDefaultThumbnail;
212                             }
213                             // When svelte, we trim the memory to just the visible thumbnails when
214                             // leaving, so don't thrash the cache as the user scrolls (just load
215                             // them from scratch each time)
216                             if (config.svelteLevel < RecentsConfiguration.SVELTE_LIMIT_CACHE) {
217                                 mThumbnailCache.put(t.key, cachedThumbnail);
218                             }
219                         }
220                         if (!mCancelled) {
221                             // Notify that the task data has changed
222                             final Drawable newIcon = cachedIcon;
223                             final Bitmap newThumbnail = cachedThumbnail == mDefaultThumbnail
224                                     ? null : cachedThumbnail;
225                             mMainThreadHandler.post(new Runnable() {
226                                 @Override
227                                 public void run() {
228                                     t.notifyTaskDataLoaded(newThumbnail, newIcon);
229                                 }
230                             });
231                         }
232                     }
233                 }
234 
235                 // If there are no other items in the list, then just wait until something is added
236                 if (!mCancelled && mLoadQueue.isEmpty()) {
237                     synchronized(mLoadQueue) {
238                         try {
239                             mWaitingOnLoadQueue = true;
240                             mLoadQueue.wait();
241                             mWaitingOnLoadQueue = false;
242                         } catch (InterruptedException ie) {
243                             ie.printStackTrace();
244                         }
245                     }
246                 }
247             }
248         }
249     }
250 
getTaskDescriptionIcon(Task.TaskKey taskKey, Bitmap iconBitmap, String iconFilename, SystemServicesProxy ssp, Resources res)251     Drawable getTaskDescriptionIcon(Task.TaskKey taskKey, Bitmap iconBitmap, String iconFilename,
252             SystemServicesProxy ssp, Resources res) {
253         Bitmap tdIcon = iconBitmap != null
254                 ? iconBitmap
255                 : ActivityManager.TaskDescription.loadTaskDescriptionIcon(iconFilename);
256         if (tdIcon != null) {
257             return ssp.getBadgedIcon(new BitmapDrawable(res, tdIcon), taskKey.userId);
258         }
259         return null;
260     }
261 }
262 
263 /* Recents task loader
264  * NOTE: We should not hold any references to a Context from a static instance */
265 public class RecentsTaskLoader {
266     private static final String TAG = "RecentsTaskLoader";
267 
268     static RecentsTaskLoader sInstance;
269     static int INVALID_TASK_ID = -1;
270 
271     SystemServicesProxy mSystemServicesProxy;
272     DrawableLruCache mApplicationIconCache;
273     BitmapLruCache mThumbnailCache;
274     StringLruCache mActivityLabelCache;
275     TaskResourceLoadQueue mLoadQueue;
276     TaskResourceLoader mLoader;
277 
278     RecentsPackageMonitor mPackageMonitor;
279 
280     int mMaxThumbnailCacheSize;
281     int mMaxIconCacheSize;
282     int mNumVisibleTasksLoaded;
283     int mNumVisibleThumbnailsLoaded;
284 
285     BitmapDrawable mDefaultApplicationIcon;
286     Bitmap mDefaultThumbnail;
287 
288     /** Private Constructor */
RecentsTaskLoader(Context context)289     private RecentsTaskLoader(Context context) {
290         mMaxThumbnailCacheSize = context.getResources().getInteger(
291                 R.integer.config_recents_max_thumbnail_count);
292         mMaxIconCacheSize = context.getResources().getInteger(
293                 R.integer.config_recents_max_icon_count);
294         int iconCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 :
295                 mMaxIconCacheSize;
296         int thumbnailCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 :
297                 mMaxThumbnailCacheSize;
298 
299         // Create the default assets
300         Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
301         icon.eraseColor(0x00000000);
302         mDefaultThumbnail = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
303         mDefaultThumbnail.setHasAlpha(false);
304         mDefaultThumbnail.eraseColor(0xFFffffff);
305         mDefaultApplicationIcon = new BitmapDrawable(context.getResources(), icon);
306 
307         // Initialize the proxy, cache and loaders
308         mSystemServicesProxy = new SystemServicesProxy(context);
309         mPackageMonitor = new RecentsPackageMonitor();
310         mLoadQueue = new TaskResourceLoadQueue();
311         mApplicationIconCache = new DrawableLruCache(iconCacheSize);
312         mThumbnailCache = new BitmapLruCache(thumbnailCacheSize);
313         mActivityLabelCache = new StringLruCache(100);
314         mLoader = new TaskResourceLoader(mLoadQueue, mApplicationIconCache, mThumbnailCache,
315                 mDefaultThumbnail, mDefaultApplicationIcon);
316     }
317 
318     /** Initializes the recents task loader */
initialize(Context context)319     public static RecentsTaskLoader initialize(Context context) {
320         if (sInstance == null) {
321             sInstance = new RecentsTaskLoader(context);
322         }
323         return sInstance;
324     }
325 
326     /** Returns the current recents task loader */
getInstance()327     public static RecentsTaskLoader getInstance() {
328         return sInstance;
329     }
330 
331     /** Returns the system services proxy */
getSystemServicesProxy()332     public SystemServicesProxy getSystemServicesProxy() {
333         return mSystemServicesProxy;
334     }
335 
336     /** Returns the activity label using as many cached values as we can. */
getAndUpdateActivityLabel(Task.TaskKey taskKey, ActivityManager.TaskDescription td, SystemServicesProxy ssp, ActivityInfoHandle infoHandle)337     public String getAndUpdateActivityLabel(Task.TaskKey taskKey,
338             ActivityManager.TaskDescription td, SystemServicesProxy ssp,
339             ActivityInfoHandle infoHandle) {
340         // Return the task description label if it exists
341         if (td != null && td.getLabel() != null) {
342             return td.getLabel();
343         }
344         // Return the cached activity label if it exists
345         String label = mActivityLabelCache.getAndInvalidateIfModified(taskKey);
346         if (label != null) {
347             return label;
348         }
349         // All short paths failed, load the label from the activity info and cache it
350         if (infoHandle.info == null) {
351             infoHandle.info = ssp.getActivityInfo(taskKey.baseIntent.getComponent(),
352                     taskKey.userId);
353         }
354         if (infoHandle.info != null) {
355             label = ssp.getActivityLabel(infoHandle.info);
356             mActivityLabelCache.put(taskKey, label);
357         } else {
358             Log.w(TAG, "Missing ActivityInfo for " + taskKey.baseIntent.getComponent()
359                     + " u=" + taskKey.userId);
360         }
361         return label;
362     }
363 
364     /** Returns the activity icon using as many cached values as we can. */
getAndUpdateActivityIcon(Task.TaskKey taskKey, ActivityManager.TaskDescription td, SystemServicesProxy ssp, Resources res, ActivityInfoHandle infoHandle, boolean loadIfNotCached)365     public Drawable getAndUpdateActivityIcon(Task.TaskKey taskKey,
366             ActivityManager.TaskDescription td, SystemServicesProxy ssp,
367             Resources res, ActivityInfoHandle infoHandle, boolean loadIfNotCached) {
368         // Return the cached activity icon if it exists
369         Drawable icon = mApplicationIconCache.getAndInvalidateIfModified(taskKey);
370         if (icon != null) {
371             return icon;
372         }
373 
374         if (loadIfNotCached) {
375             // Return and cache the task description icon if it exists
376             Drawable tdDrawable = mLoader.getTaskDescriptionIcon(taskKey, td.getInMemoryIcon(),
377                     td.getIconFilename(), ssp, res);
378             if (tdDrawable != null) {
379                 mApplicationIconCache.put(taskKey, tdDrawable);
380                 return tdDrawable;
381             }
382 
383             // Load the icon from the activity info and cache it
384             if (infoHandle.info == null) {
385                 infoHandle.info = ssp.getActivityInfo(taskKey.baseIntent.getComponent(),
386                         taskKey.userId);
387             }
388             if (infoHandle.info != null) {
389                 icon = ssp.getActivityIcon(infoHandle.info, taskKey.userId);
390                 if (icon != null) {
391                     mApplicationIconCache.put(taskKey, icon);
392                     return icon;
393                 }
394             }
395         }
396         // We couldn't load any icon
397         return null;
398     }
399 
400     /** Returns the bitmap using as many cached values as we can. */
getAndUpdateThumbnail(Task.TaskKey taskKey, SystemServicesProxy ssp, boolean loadIfNotCached)401     public Bitmap getAndUpdateThumbnail(Task.TaskKey taskKey, SystemServicesProxy ssp,
402             boolean loadIfNotCached) {
403         // Return the cached thumbnail if it exists
404         Bitmap thumbnail = mThumbnailCache.getAndInvalidateIfModified(taskKey);
405         if (thumbnail != null) {
406             return thumbnail;
407         }
408 
409         RecentsConfiguration config = RecentsConfiguration.getInstance();
410         if (config.svelteLevel < RecentsConfiguration.SVELTE_DISABLE_LOADING && loadIfNotCached) {
411             // Load the thumbnail from the system
412             thumbnail = ssp.getTaskThumbnail(taskKey.id);
413             if (thumbnail != null) {
414                 mThumbnailCache.put(taskKey, thumbnail);
415                 return thumbnail;
416             }
417         }
418         // We couldn't load any thumbnail
419         return null;
420     }
421 
422     /** Returns the activity's primary color. */
getActivityPrimaryColor(ActivityManager.TaskDescription td, RecentsConfiguration config)423     public int getActivityPrimaryColor(ActivityManager.TaskDescription td,
424             RecentsConfiguration config) {
425         if (td != null && td.getPrimaryColor() != 0) {
426             return td.getPrimaryColor();
427         }
428         return config.taskBarViewDefaultBackgroundColor;
429     }
430 
431     /** Returns the size of the app icon cache. */
getApplicationIconCacheSize()432     public int getApplicationIconCacheSize() {
433         return mMaxIconCacheSize;
434     }
435 
436     /** Returns the size of the thumbnail cache. */
getThumbnailCacheSize()437     public int getThumbnailCacheSize() {
438         return mMaxThumbnailCacheSize;
439     }
440 
441     /** Creates a new plan for loading the recent tasks. */
createLoadPlan(Context context)442     public RecentsTaskLoadPlan createLoadPlan(Context context) {
443         RecentsConfiguration config = RecentsConfiguration.getInstance();
444         RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(context, config, mSystemServicesProxy);
445         return plan;
446     }
447 
448     /** Preloads recents tasks using the specified plan to store the output. */
preloadTasks(RecentsTaskLoadPlan plan, boolean isTopTaskHome)449     public void preloadTasks(RecentsTaskLoadPlan plan, boolean isTopTaskHome) {
450         plan.preloadPlan(this, isTopTaskHome);
451     }
452 
453     /** Begins loading the heavy task data according to the specified options. */
loadTasks(Context context, RecentsTaskLoadPlan plan, RecentsTaskLoadPlan.Options opts)454     public void loadTasks(Context context, RecentsTaskLoadPlan plan,
455             RecentsTaskLoadPlan.Options opts) {
456         if (opts == null) {
457             throw new RuntimeException("Requires load options");
458         }
459         plan.executePlan(opts, this, mLoadQueue);
460         if (!opts.onlyLoadForCache) {
461             mNumVisibleTasksLoaded = opts.numVisibleTasks;
462             mNumVisibleThumbnailsLoaded = opts.numVisibleTaskThumbnails;
463 
464             // Start the loader
465             mLoader.start(context);
466         }
467     }
468 
469     /** Acquires the task resource data directly from the pool. */
loadTaskData(Task t)470     public void loadTaskData(Task t) {
471         Drawable applicationIcon = mApplicationIconCache.getAndInvalidateIfModified(t.key);
472         Bitmap thumbnail = mThumbnailCache.getAndInvalidateIfModified(t.key);
473 
474         // Grab the thumbnail/icon from the cache, if either don't exist, then trigger a reload and
475         // use the default assets in their place until they load
476         boolean requiresLoad = (applicationIcon == null) || (thumbnail == null);
477         applicationIcon = applicationIcon != null ? applicationIcon : mDefaultApplicationIcon;
478         if (requiresLoad) {
479             mLoadQueue.addTask(t);
480         }
481         t.notifyTaskDataLoaded(thumbnail == mDefaultThumbnail ? null : thumbnail, applicationIcon);
482     }
483 
484     /** Releases the task resource data back into the pool. */
unloadTaskData(Task t)485     public void unloadTaskData(Task t) {
486         mLoadQueue.removeTask(t);
487         t.notifyTaskDataUnloaded(null, mDefaultApplicationIcon);
488     }
489 
490     /** Completely removes the resource data from the pool. */
deleteTaskData(Task t, boolean notifyTaskDataUnloaded)491     public void deleteTaskData(Task t, boolean notifyTaskDataUnloaded) {
492         mLoadQueue.removeTask(t);
493         mThumbnailCache.remove(t.key);
494         mApplicationIconCache.remove(t.key);
495         if (notifyTaskDataUnloaded) {
496             t.notifyTaskDataUnloaded(null, mDefaultApplicationIcon);
497         }
498     }
499 
500     /** Stops the task loader and clears all pending tasks */
stopLoader()501     void stopLoader() {
502         mLoader.stop();
503         mLoadQueue.clearTasks();
504     }
505 
506     /** Registers any broadcast receivers. */
registerReceivers(Context context, RecentsPackageMonitor.PackageCallbacks cb)507     public void registerReceivers(Context context, RecentsPackageMonitor.PackageCallbacks cb) {
508         // Register the broadcast receiver to handle messages related to packages being added/removed
509         mPackageMonitor.register(context, cb);
510     }
511 
512     /** Unregisters any broadcast receivers. */
unregisterReceivers()513     public void unregisterReceivers() {
514         mPackageMonitor.unregister();
515     }
516 
517     /**
518      * Handles signals from the system, trimming memory when requested to prevent us from running
519      * out of memory.
520      */
onTrimMemory(int level)521     public void onTrimMemory(int level) {
522         RecentsConfiguration config = RecentsConfiguration.getInstance();
523         switch (level) {
524             case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
525                 // Stop the loader immediately when the UI is no longer visible
526                 stopLoader();
527                 if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) {
528                     mThumbnailCache.trimToSize(Math.max(mNumVisibleTasksLoaded,
529                             mMaxThumbnailCacheSize / 2));
530                 } else if (config.svelteLevel == RecentsConfiguration.SVELTE_LIMIT_CACHE) {
531                     mThumbnailCache.trimToSize(mNumVisibleThumbnailsLoaded);
532                 } else if (config.svelteLevel >= RecentsConfiguration.SVELTE_DISABLE_CACHE) {
533                     mThumbnailCache.evictAll();
534                 }
535                 mApplicationIconCache.trimToSize(Math.max(mNumVisibleTasksLoaded,
536                         mMaxIconCacheSize / 2));
537                 break;
538             case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
539             case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
540                 // We are leaving recents, so trim the data a bit
541                 mThumbnailCache.trimToSize(Math.max(1, mMaxThumbnailCacheSize / 2));
542                 mApplicationIconCache.trimToSize(Math.max(1, mMaxIconCacheSize / 2));
543                 break;
544             case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
545             case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
546                 // We are going to be low on memory
547                 mThumbnailCache.trimToSize(Math.max(1, mMaxThumbnailCacheSize / 4));
548                 mApplicationIconCache.trimToSize(Math.max(1, mMaxIconCacheSize / 4));
549                 break;
550             case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
551             case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
552                 // We are low on memory, so release everything
553                 mThumbnailCache.evictAll();
554                 mApplicationIconCache.evictAll();
555                 // The cache is small, only clear the label cache when we are critical
556                 mActivityLabelCache.evictAll();
557                 break;
558             default:
559                 break;
560         }
561     }
562 }
563