| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.recents.model; |
| |
| import android.app.ActivityManager; |
| import android.content.ComponentCallbacks2; |
| import android.content.Context; |
| import android.content.pm.ActivityInfo; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.UserHandle; |
| import android.util.Log; |
| |
| import com.android.systemui.recents.Constants; |
| import com.android.systemui.recents.RecentsConfiguration; |
| import com.android.systemui.recents.misc.SystemServicesProxy; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.concurrent.ConcurrentLinkedQueue; |
| |
| |
| /** Handle to an ActivityInfo */ |
| class ActivityInfoHandle { |
| ActivityInfo info; |
| } |
| |
| /** A bitmap load queue */ |
| class TaskResourceLoadQueue { |
| ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>(); |
| |
| /** Adds a new task to the load queue */ |
| void addTasks(Collection<Task> tasks) { |
| for (Task t : tasks) { |
| if (!mQueue.contains(t)) { |
| mQueue.add(t); |
| } |
| } |
| synchronized(this) { |
| notifyAll(); |
| } |
| } |
| |
| /** Adds a new task to the load queue */ |
| void addTask(Task t) { |
| if (!mQueue.contains(t)) { |
| mQueue.add(t); |
| } |
| synchronized(this) { |
| notifyAll(); |
| } |
| } |
| |
| /** |
| * Retrieves the next task from the load queue, as well as whether we want that task to be |
| * force reloaded. |
| */ |
| Task nextTask() { |
| return mQueue.poll(); |
| } |
| |
| /** Removes a task from the load queue */ |
| void removeTask(Task t) { |
| mQueue.remove(t); |
| } |
| |
| /** Clears all the tasks from the load queue */ |
| void clearTasks() { |
| mQueue.clear(); |
| } |
| |
| /** Returns whether the load queue is empty */ |
| boolean isEmpty() { |
| return mQueue.isEmpty(); |
| } |
| } |
| |
| /* Task resource loader */ |
| class TaskResourceLoader implements Runnable { |
| Context mContext; |
| HandlerThread mLoadThread; |
| Handler mLoadThreadHandler; |
| Handler mMainThreadHandler; |
| |
| SystemServicesProxy mSystemServicesProxy; |
| TaskResourceLoadQueue mLoadQueue; |
| DrawableLruCache mApplicationIconCache; |
| BitmapLruCache mThumbnailCache; |
| Bitmap mDefaultThumbnail; |
| BitmapDrawable mDefaultApplicationIcon; |
| |
| boolean mCancelled; |
| boolean mWaitingOnLoadQueue; |
| |
| /** Constructor, creates a new loading thread that loads task resources in the background */ |
| public TaskResourceLoader(TaskResourceLoadQueue loadQueue, DrawableLruCache applicationIconCache, |
| BitmapLruCache thumbnailCache, Bitmap defaultThumbnail, |
| BitmapDrawable defaultApplicationIcon) { |
| mLoadQueue = loadQueue; |
| mApplicationIconCache = applicationIconCache; |
| mThumbnailCache = thumbnailCache; |
| mDefaultThumbnail = defaultThumbnail; |
| mDefaultApplicationIcon = defaultApplicationIcon; |
| mMainThreadHandler = new Handler(); |
| mLoadThread = new HandlerThread("Recents-TaskResourceLoader"); |
| mLoadThread.setPriority(Thread.NORM_PRIORITY - 1); |
| mLoadThread.start(); |
| mLoadThreadHandler = new Handler(mLoadThread.getLooper()); |
| mLoadThreadHandler.post(this); |
| } |
| |
| /** Restarts the loader thread */ |
| void start(Context context) { |
| mContext = context; |
| mCancelled = false; |
| mSystemServicesProxy = new SystemServicesProxy(context); |
| // Notify the load thread to start loading |
| synchronized(mLoadThread) { |
| mLoadThread.notifyAll(); |
| } |
| } |
| |
| /** Requests the loader thread to stop after the current iteration */ |
| void stop() { |
| // Mark as cancelled for the thread to pick up |
| mCancelled = true; |
| mSystemServicesProxy = null; |
| // If we are waiting for the load queue for more tasks, then we can just reset the |
| // Context now, since nothing is using it |
| if (mWaitingOnLoadQueue) { |
| mContext = null; |
| } |
| } |
| |
| @Override |
| public void run() { |
| while (true) { |
| if (mCancelled) { |
| // We have to unset the context here, since the background thread may be using it |
| // when we call stop() |
| mContext = null; |
| // If we are cancelled, then wait until we are started again |
| synchronized(mLoadThread) { |
| try { |
| mLoadThread.wait(); |
| } catch (InterruptedException ie) { |
| ie.printStackTrace(); |
| } |
| } |
| } else { |
| SystemServicesProxy ssp = mSystemServicesProxy; |
| |
| // Load the next item from the queue |
| final Task t = mLoadQueue.nextTask(); |
| if (t != null) { |
| Drawable cachedIcon = mApplicationIconCache.get(t.key); |
| Bitmap cachedThumbnail = mThumbnailCache.get(t.key); |
| // Load the application icon if it is stale or we haven't cached one yet |
| if (cachedIcon == null) { |
| ActivityInfo info = ssp.getActivityInfo(t.key.baseIntent.getComponent(), |
| t.key.userId); |
| if (info != null) { |
| cachedIcon = ssp.getActivityIcon(info, t.key.userId); |
| } |
| if (cachedIcon == null) { |
| cachedIcon = mDefaultApplicationIcon; |
| } |
| // At this point, even if we can't load the icon, we will set the default |
| // icon. |
| mApplicationIconCache.put(t.key, cachedIcon); |
| } |
| // Load the thumbnail if it is stale or we haven't cached one yet |
| if (cachedThumbnail == null) { |
| cachedThumbnail = ssp.getTaskThumbnail(t.key.id); |
| if (cachedThumbnail != null) { |
| cachedThumbnail.setHasAlpha(false); |
| } else { |
| cachedThumbnail = mDefaultThumbnail; |
| } |
| mThumbnailCache.put(t.key, cachedThumbnail); |
| } |
| if (!mCancelled) { |
| // Notify that the task data has changed |
| final Drawable newIcon = cachedIcon; |
| final Bitmap newThumbnail = cachedThumbnail; |
| mMainThreadHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| t.notifyTaskDataLoaded(newThumbnail, newIcon); |
| } |
| }); |
| } |
| } |
| |
| // If there are no other items in the list, then just wait until something is added |
| if (!mCancelled && mLoadQueue.isEmpty()) { |
| synchronized(mLoadQueue) { |
| try { |
| mWaitingOnLoadQueue = true; |
| mLoadQueue.wait(); |
| mWaitingOnLoadQueue = false; |
| } catch (InterruptedException ie) { |
| ie.printStackTrace(); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /* Recents task loader |
| * NOTE: We should not hold any references to a Context from a static instance */ |
| public class RecentsTaskLoader { |
| private static final String TAG = "RecentsTaskLoader"; |
| |
| static RecentsTaskLoader sInstance; |
| |
| SystemServicesProxy mSystemServicesProxy; |
| DrawableLruCache mApplicationIconCache; |
| BitmapLruCache mThumbnailCache; |
| StringLruCache mActivityLabelCache; |
| TaskResourceLoadQueue mLoadQueue; |
| TaskResourceLoader mLoader; |
| |
| RecentsPackageMonitor mPackageMonitor; |
| |
| int mMaxThumbnailCacheSize; |
| int mMaxIconCacheSize; |
| |
| BitmapDrawable mDefaultApplicationIcon; |
| Bitmap mDefaultThumbnail; |
| Bitmap mLoadingThumbnail; |
| |
| /** Private Constructor */ |
| private RecentsTaskLoader(Context context) { |
| // Calculate the cache sizes, we just use a reasonable number here similar to those |
| // suggested in the Android docs, 1/6th for the thumbnail cache and 1/30 of the max memory |
| // for icons. |
| int maxMemory = (int) Runtime.getRuntime().maxMemory(); |
| mMaxThumbnailCacheSize = maxMemory / 6; |
| mMaxIconCacheSize = mMaxThumbnailCacheSize / 5; |
| int iconCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 : |
| mMaxIconCacheSize; |
| int thumbnailCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 : |
| mMaxThumbnailCacheSize; |
| |
| // Create the default assets |
| Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); |
| icon.eraseColor(0x00000000); |
| mDefaultThumbnail = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); |
| mDefaultThumbnail.setHasAlpha(false); |
| mDefaultThumbnail.eraseColor(0xFFffffff); |
| mLoadingThumbnail = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); |
| mLoadingThumbnail.setHasAlpha(false); |
| mLoadingThumbnail.eraseColor(0xFFffffff); |
| mDefaultApplicationIcon = new BitmapDrawable(context.getResources(), icon); |
| |
| // Initialize the proxy, cache and loaders |
| mSystemServicesProxy = new SystemServicesProxy(context); |
| mPackageMonitor = new RecentsPackageMonitor(); |
| mLoadQueue = new TaskResourceLoadQueue(); |
| mApplicationIconCache = new DrawableLruCache(iconCacheSize); |
| mThumbnailCache = new BitmapLruCache(thumbnailCacheSize); |
| mActivityLabelCache = new StringLruCache(100); |
| mLoader = new TaskResourceLoader(mLoadQueue, mApplicationIconCache, mThumbnailCache, |
| mDefaultThumbnail, mDefaultApplicationIcon); |
| } |
| |
| /** Initializes the recents task loader */ |
| public static RecentsTaskLoader initialize(Context context) { |
| if (sInstance == null) { |
| sInstance = new RecentsTaskLoader(context); |
| } |
| return sInstance; |
| } |
| |
| /** Returns the current recents task loader */ |
| public static RecentsTaskLoader getInstance() { |
| return sInstance; |
| } |
| |
| /** Returns the system services proxy */ |
| public SystemServicesProxy getSystemServicesProxy() { |
| return mSystemServicesProxy; |
| } |
| |
| /** Gets the list of recent tasks, ordered from back to front. */ |
| private static List<ActivityManager.RecentTaskInfo> getRecentTasks(SystemServicesProxy ssp) { |
| RecentsConfiguration config = RecentsConfiguration.getInstance(); |
| List<ActivityManager.RecentTaskInfo> tasks = |
| ssp.getRecentTasks(config.maxNumTasksToLoad, |
| UserHandle.CURRENT.getIdentifier()); |
| Collections.reverse(tasks); |
| return tasks; |
| } |
| |
| /** Returns the activity icon using as many cached values as we can. */ |
| public Drawable getAndUpdateActivityIcon(Task.TaskKey taskKey, |
| ActivityManager.TaskDescription td, SystemServicesProxy ssp, |
| Resources res, ActivityInfoHandle infoHandle, boolean preloadTask) { |
| // Return the cached activity icon if it exists |
| Drawable icon = mApplicationIconCache.getAndInvalidateIfModified(taskKey); |
| if (icon != null) { |
| return icon; |
| } |
| // Return the task description icon if it exists |
| if (td != null && td.getIcon() != null) { |
| icon = ssp.getBadgedIcon(new BitmapDrawable(res, td.getIcon()), taskKey.userId); |
| mApplicationIconCache.put(taskKey, icon); |
| return icon; |
| } |
| // If we are preloading this task, continue to load the activity icon |
| if (preloadTask) { |
| // All short paths failed, load the icon from the activity info and cache it |
| if (infoHandle.info == null) { |
| infoHandle.info = ssp.getActivityInfo(taskKey.baseIntent.getComponent(), |
| taskKey.userId); |
| } |
| if (infoHandle.info != null) { |
| icon = ssp.getActivityIcon(infoHandle.info, taskKey.userId); |
| if (icon != null) { |
| mApplicationIconCache.put(taskKey, icon); |
| return icon; |
| } |
| } |
| } |
| // If we couldn't load any icon, return null |
| return null; |
| } |
| |
| /** Returns the activity label using as many cached values as we can. */ |
| public String getAndUpdateActivityLabel(Task.TaskKey taskKey, |
| ActivityManager.TaskDescription td, SystemServicesProxy ssp, |
| ActivityInfoHandle infoHandle) { |
| // Return the task description label if it exists |
| if (td != null && td.getLabel() != null) { |
| return td.getLabel(); |
| } |
| // Return the cached activity label if it exists |
| String label = mActivityLabelCache.getAndInvalidateIfModified(taskKey); |
| if (label != null) { |
| return label; |
| } |
| // All short paths failed, load the label from the activity info and cache it |
| if (infoHandle.info == null) { |
| infoHandle.info = ssp.getActivityInfo(taskKey.baseIntent.getComponent(), |
| taskKey.userId); |
| } |
| if (infoHandle.info != null) { |
| label = ssp.getActivityLabel(infoHandle.info); |
| mActivityLabelCache.put(taskKey, label); |
| } else { |
| Log.w(TAG, "Missing ActivityInfo for " + taskKey.baseIntent.getComponent() |
| + " u=" + taskKey.userId); |
| } |
| return label; |
| } |
| |
| /** Returns the activity's primary color. */ |
| public int getActivityPrimaryColor(ActivityManager.TaskDescription td, |
| RecentsConfiguration config) { |
| if (td != null && td.getPrimaryColor() != 0) { |
| return td.getPrimaryColor(); |
| } |
| return config.taskBarViewDefaultBackgroundColor; |
| } |
| |
| /** Reload the set of recent tasks */ |
| public SpaceNode reload(Context context, int preloadCount) { |
| ArrayList<Task.TaskKey> taskKeys = new ArrayList<Task.TaskKey>(); |
| ArrayList<Task> tasksToLoad = new ArrayList<Task>(); |
| TaskStack stack = getTaskStack(mSystemServicesProxy, context.getResources(), |
| -1, preloadCount, true, taskKeys, tasksToLoad); |
| SpaceNode root = new SpaceNode(); |
| root.setStack(stack); |
| |
| // Start the task loader and add all the tasks we need to load |
| mLoader.start(context); |
| mLoadQueue.addTasks(tasksToLoad); |
| |
| // Update the package monitor with the list of packages to listen for |
| mPackageMonitor.setTasks(taskKeys); |
| |
| return root; |
| } |
| |
| /** Creates a lightweight stack of the current recent tasks, without thumbnails and icons. */ |
| public TaskStack getTaskStack(SystemServicesProxy ssp, Resources res, |
| int preloadTaskId, int preloadTaskCount, |
| boolean loadTaskThumbnails, List<Task.TaskKey> taskKeysOut, |
| List<Task> tasksToLoadOut) { |
| RecentsConfiguration config = RecentsConfiguration.getInstance(); |
| List<ActivityManager.RecentTaskInfo> tasks = getRecentTasks(ssp); |
| HashMap<Task.ComponentNameKey, ActivityInfoHandle> activityInfoCache = |
| new HashMap<Task.ComponentNameKey, ActivityInfoHandle>(); |
| ArrayList<Task> tasksToAdd = new ArrayList<Task>(); |
| TaskStack stack = new TaskStack(); |
| |
| int taskCount = tasks.size(); |
| for (int i = 0; i < taskCount; i++) { |
| ActivityManager.RecentTaskInfo t = tasks.get(i); |
| |
| // Compose the task key |
| Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.baseIntent, t.userId, |
| t.firstActiveTime, t.lastActiveTime); |
| |
| // Get an existing activity info handle if possible |
| Task.ComponentNameKey cnKey = taskKey.getComponentNameKey(); |
| ActivityInfoHandle infoHandle; |
| boolean hasCachedActivityInfo = false; |
| if (activityInfoCache.containsKey(cnKey)) { |
| infoHandle = activityInfoCache.get(cnKey); |
| hasCachedActivityInfo = true; |
| } else { |
| infoHandle = new ActivityInfoHandle(); |
| } |
| |
| // Determine whether to preload this task |
| boolean preloadTask = false; |
| if (preloadTaskId > 0) { |
| preloadTask = (t.id == preloadTaskId); |
| } else if (preloadTaskCount > 0) { |
| preloadTask = (i >= (taskCount - preloadTaskCount)); |
| } |
| |
| // Load the label, icon, and color |
| String activityLabel = getAndUpdateActivityLabel(taskKey, t.taskDescription, |
| ssp, infoHandle); |
| Drawable activityIcon = getAndUpdateActivityIcon(taskKey, t.taskDescription, |
| ssp, res, infoHandle, preloadTask); |
| int activityColor = getActivityPrimaryColor(t.taskDescription, config); |
| |
| // Update the activity info cache |
| if (!hasCachedActivityInfo && infoHandle.info != null) { |
| activityInfoCache.put(cnKey, infoHandle); |
| } |
| |
| // Add the task to the stack |
| Task task = new Task(taskKey, (t.id > -1), t.affiliatedTaskId, t.affiliatedTaskColor, |
| activityLabel, activityIcon, activityColor, (i == (taskCount - 1)), |
| config.lockToAppEnabled); |
| |
| if (preloadTask && loadTaskThumbnails) { |
| // Load the thumbnail from the cache if possible |
| task.thumbnail = mThumbnailCache.getAndInvalidateIfModified(taskKey); |
| if (task.thumbnail == null) { |
| // Load the thumbnail from the system |
| task.thumbnail = ssp.getTaskThumbnail(taskKey.id); |
| if (task.thumbnail != null) { |
| task.thumbnail.setHasAlpha(false); |
| mThumbnailCache.put(taskKey, task.thumbnail); |
| } |
| } |
| if (task.thumbnail == null && tasksToLoadOut != null) { |
| // Either the task has changed since the last active time, or it was not |
| // previously cached, so try and load the task anew. |
| tasksToLoadOut.add(task); |
| } |
| } |
| |
| // Add to the list of task keys |
| if (taskKeysOut != null) { |
| taskKeysOut.add(taskKey); |
| } |
| // Add the task to the stack |
| tasksToAdd.add(task); |
| } |
| stack.setTasks(tasksToAdd); |
| stack.createAffiliatedGroupings(config); |
| return stack; |
| } |
| |
| /** Acquires the task resource data directly from the pool. */ |
| public void loadTaskData(Task t) { |
| Drawable applicationIcon = mApplicationIconCache.getAndInvalidateIfModified(t.key); |
| Bitmap thumbnail = mThumbnailCache.getAndInvalidateIfModified(t.key); |
| |
| // Grab the thumbnail/icon from the cache, if either don't exist, then trigger a reload and |
| // use the default assets in their place until they load |
| boolean requiresLoad = (applicationIcon == null) || (thumbnail == null); |
| applicationIcon = applicationIcon != null ? applicationIcon : mDefaultApplicationIcon; |
| thumbnail = thumbnail != null ? thumbnail : mDefaultThumbnail; |
| if (requiresLoad) { |
| mLoadQueue.addTask(t); |
| } |
| t.notifyTaskDataLoaded(thumbnail, applicationIcon); |
| } |
| |
| /** Releases the task resource data back into the pool. */ |
| public void unloadTaskData(Task t) { |
| mLoadQueue.removeTask(t); |
| t.notifyTaskDataUnloaded(mDefaultThumbnail, mDefaultApplicationIcon); |
| } |
| |
| /** Completely removes the resource data from the pool. */ |
| public void deleteTaskData(Task t, boolean notifyTaskDataUnloaded) { |
| mLoadQueue.removeTask(t); |
| mThumbnailCache.remove(t.key); |
| mApplicationIconCache.remove(t.key); |
| if (notifyTaskDataUnloaded) { |
| t.notifyTaskDataUnloaded(mDefaultThumbnail, mDefaultApplicationIcon); |
| } |
| } |
| |
| /** Stops the task loader and clears all pending tasks */ |
| void stopLoader() { |
| mLoader.stop(); |
| mLoadQueue.clearTasks(); |
| } |
| |
| /** Registers any broadcast receivers. */ |
| public void registerReceivers(Context context, RecentsPackageMonitor.PackageCallbacks cb) { |
| // Register the broadcast receiver to handle messages related to packages being added/removed |
| mPackageMonitor.register(context, cb); |
| } |
| |
| /** Unregisters any broadcast receivers. */ |
| public void unregisterReceivers() { |
| mPackageMonitor.unregister(); |
| } |
| |
| /** |
| * Handles signals from the system, trimming memory when requested to prevent us from running |
| * out of memory. |
| */ |
| public void onTrimMemory(int level) { |
| switch (level) { |
| case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: |
| // Stop the loader immediately when the UI is no longer visible |
| stopLoader(); |
| break; |
| case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: |
| case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: |
| // We are leaving recents, so trim the data a bit |
| mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 2); |
| mApplicationIconCache.trimToSize(mMaxIconCacheSize / 2); |
| break; |
| case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: |
| case ComponentCallbacks2.TRIM_MEMORY_MODERATE: |
| // We are going to be low on memory |
| mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 4); |
| mApplicationIconCache.trimToSize(mMaxIconCacheSize / 4); |
| break; |
| case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: |
| case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: |
| // We are low on memory, so release everything |
| mThumbnailCache.evictAll(); |
| mApplicationIconCache.evictAll(); |
| // The cache is small, only clear the label cache when we are critical |
| mActivityLabelCache.evictAll(); |
| break; |
| default: |
| break; |
| } |
| } |
| } |