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