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.util.Log;
30
31import com.android.systemui.R;
32import com.android.systemui.recents.Constants;
33import com.android.systemui.recents.RecentsConfiguration;
34import com.android.systemui.recents.misc.SystemServicesProxy;
35
36import java.util.Collection;
37import java.util.concurrent.ConcurrentLinkedQueue;
38
39
40/** Handle to an ActivityInfo */
41class ActivityInfoHandle {
42    ActivityInfo info;
43}
44
45/** A bitmap load queue */
46class TaskResourceLoadQueue {
47    ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>();
48
49    /** Adds a new task to the load queue */
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 */
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     */
75    Task nextTask() {
76        return mQueue.poll();
77    }
78
79    /** Removes a task from the load queue */
80    void removeTask(Task t) {
81        mQueue.remove(t);
82    }
83
84    /** Clears all the tasks from the load queue */
85    void clearTasks() {
86        mQueue.clear();
87    }
88
89    /** Returns whether the load queue is empty */
90    boolean isEmpty() {
91        return mQueue.isEmpty();
92    }
93}
94
95/* Task resource loader */
96class 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 */
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 */
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 */
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
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
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 */
265public 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 */
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 */
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 */
327    public static RecentsTaskLoader getInstance() {
328        return sInstance;
329    }
330
331    /** Returns the system services proxy */
332    public SystemServicesProxy getSystemServicesProxy() {
333        return mSystemServicesProxy;
334    }
335
336    /** Returns the activity label using as many cached values as we can. */
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. */
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. */
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. */
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. */
432    public int getApplicationIconCacheSize() {
433        return mMaxIconCacheSize;
434    }
435
436    /** Returns the size of the thumbnail cache. */
437    public int getThumbnailCacheSize() {
438        return mMaxThumbnailCacheSize;
439    }
440
441    /** Creates a new plan for loading the recent tasks. */
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. */
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. */
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. */
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. */
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. */
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 */
501    void stopLoader() {
502        mLoader.stop();
503        mLoadQueue.clearTasks();
504    }
505
506    /** Registers any broadcast receivers. */
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. */
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     */
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