/* * 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; 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.Canvas; 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.LruCache; import android.util.Pair; import com.android.systemui.recents.model.SpaceNode; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; /** A bitmap load queue */ class TaskResourceLoadQueue { ConcurrentLinkedQueue mQueue = new ConcurrentLinkedQueue(); ConcurrentHashMap mForceLoadSet = new ConcurrentHashMap(); static final Boolean sFalse = new Boolean(false); /** Adds a new task to the load queue */ void addTask(Task t, boolean forceLoad) { Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|addTask]"); if (!mQueue.contains(t)) { mQueue.add(t); } if (forceLoad) { mForceLoadSet.put(t.key, new Boolean(true)); } synchronized(this) { notifyAll(); } } /** * Retrieves the next task from the load queue, as well as whether we want that task to be * force reloaded. */ Pair nextTask() { Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|nextTask]"); Task task = mQueue.poll(); Boolean forceLoadTask = null; if (task != null) { forceLoadTask = mForceLoadSet.remove(task.key); } if (forceLoadTask == null) { forceLoadTask = sFalse; } return new Pair(task, forceLoadTask); } /** Removes a task from the load queue */ void removeTask(Task t) { Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|removeTask]"); mQueue.remove(t); mForceLoadSet.remove(t.key); } /** Clears all the tasks from the load queue */ void clearTasks() { Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|clearTasks]"); mQueue.clear(); mForceLoadSet.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; 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) { mLoadQueue = loadQueue; mApplicationIconCache = applicationIconCache; mThumbnailCache = thumbnailCache; 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) { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|start]"); 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() { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|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) { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|run|" + Thread.currentThread().getId() + "]"); if (mCancelled) { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|cancel|" + Thread.currentThread().getId() + "]"); // 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 { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|waitOnLoadThreadCancelled]"); mLoadThread.wait(); } catch (InterruptedException ie) { ie.printStackTrace(); } } } else { SystemServicesProxy ssp = mSystemServicesProxy; // Load the next item from the queue Pair nextTaskData = mLoadQueue.nextTask(); final Task t = nextTaskData.first; final boolean forceLoadTask = nextTaskData.second; if (t != null) { Drawable loadIcon = mApplicationIconCache.get(t.key); Bitmap loadThumbnail = mThumbnailCache.get(t.key); Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoader|load]", t + " icon: " + loadIcon + " thumbnail: " + loadThumbnail + " forceLoad: " + forceLoadTask); // Load the application icon if (loadIcon == null || forceLoadTask) { ActivityInfo info = ssp.getActivityInfo(t.key.baseIntent.getComponent()); Drawable icon = ssp.getActivityIcon(info); if (!mCancelled) { if (icon != null) { Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoader|loadIcon]", icon); loadIcon = icon; mApplicationIconCache.put(t.key, icon); } } } // Load the thumbnail if (loadThumbnail == null || forceLoadTask) { Bitmap thumbnail = ssp.getTaskThumbnail(t.key.id); if (!mCancelled) { if (thumbnail != null) { Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoader|loadThumbnail]", thumbnail); loadThumbnail = thumbnail; mThumbnailCache.put(t.key, thumbnail); } else { Console.logError(mContext, "Failed to load task top thumbnail for: " + t.key.baseIntent.getComponent().getPackageName()); } } } if (!mCancelled) { // Notify that the task data has changed final Drawable newIcon = loadIcon; final Bitmap newThumbnail = loadThumbnail; mMainThreadHandler.post(new Runnable() { @Override public void run() { t.notifyTaskDataLoaded(newThumbnail, newIcon, forceLoadTask); } }); } } // If there are no other items in the list, then just wait until something is added if (!mCancelled && mLoadQueue.isEmpty()) { synchronized(mLoadQueue) { try { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|waitOnLoadQueue]"); mWaitingOnLoadQueue = true; mLoadQueue.wait(); mWaitingOnLoadQueue = false; } catch (InterruptedException ie) { ie.printStackTrace(); } } } } } } } /** * The drawable cache. By using the Task's key, we can prevent holding onto a reference to the Task * resource data, while keeping the cache data in memory where necessary. */ class DrawableLruCache extends LruCache { public DrawableLruCache(int cacheSize) { super(cacheSize); } @Override protected int sizeOf(Task.TaskKey t, Drawable d) { // The cache size will be measured in kilobytes rather than number of items // NOTE: this isn't actually correct, as the icon may be smaller int maxBytes = (d.getIntrinsicWidth() * d.getIntrinsicHeight() * 4); return maxBytes / 1024; } } /** * The bitmap cache. By using the Task's key, we can prevent holding onto a reference to the Task * resource data, while keeping the cache data in memory where necessary. */ class BitmapLruCache extends LruCache { public BitmapLruCache(int cacheSize) { super(cacheSize); } @Override protected int sizeOf(Task.TaskKey t, Bitmap bitmap) { // The cache size will be measured in kilobytes rather than number of items return bitmap.getAllocationByteCount() / 1024; } } /* Recents task loader * NOTE: We should not hold any references to a Context from a static instance */ public class RecentsTaskLoader { static RecentsTaskLoader sInstance; SystemServicesProxy mSystemServicesProxy; DrawableLruCache mApplicationIconCache; BitmapLruCache mThumbnailCache; TaskResourceLoadQueue mLoadQueue; TaskResourceLoader mLoader; int mMaxThumbnailCacheSize; int mMaxIconCacheSize; BitmapDrawable mDefaultApplicationIcon; Bitmap mDefaultThumbnail; /** 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/8th for the thumbnail cache and 1/32 of the max memory // for icons. int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); mMaxThumbnailCacheSize = maxMemory / 8; mMaxIconCacheSize = mMaxThumbnailCacheSize / 4; int iconCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 : mMaxIconCacheSize; int thumbnailCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 : mMaxThumbnailCacheSize; Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|init]", "thumbnailCache: " + thumbnailCacheSize + " iconCache: " + iconCacheSize); // Initialize the proxy, cache and loaders mSystemServicesProxy = new SystemServicesProxy(context); mLoadQueue = new TaskResourceLoadQueue(); mApplicationIconCache = new DrawableLruCache(iconCacheSize); mThumbnailCache = new BitmapLruCache(thumbnailCacheSize); mLoader = new TaskResourceLoader(mLoadQueue, mApplicationIconCache, mThumbnailCache); // Create the default assets Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); mDefaultThumbnail = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(); c.setBitmap(icon); c.drawColor(0x00000000); c.setBitmap(mDefaultThumbnail); c.drawColor(0x00000000); c.setBitmap(null); mDefaultApplicationIcon = new BitmapDrawable(context.getResources(), icon); Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|defaultBitmaps]", "icon: " + mDefaultApplicationIcon + " thumbnail: " + mDefaultThumbnail, Console.AnsiRed); } /** 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; } /** Reload the set of recent tasks */ SpaceNode reload(Context context, int preloadCount) { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|reload]"); Resources res = context.getResources(); ArrayList tasksToForceLoad = new ArrayList(); TaskStack stack = new TaskStack(context); SpaceNode root = new SpaceNode(context); root.setStack(stack); long t1 = System.currentTimeMillis(); // Get the recent tasks SystemServicesProxy ssp = mSystemServicesProxy; List tasks = ssp.getRecentTasks(25, UserHandle.CURRENT.getIdentifier()); Collections.reverse(tasks); Console.log(Constants.DebugFlags.App.TimeSystemCalls, "[RecentsTaskLoader|getRecentTasks]", "" + (System.currentTimeMillis() - t1) + "ms"); Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|tasks]", "" + tasks.size()); // Remove home/recents tasks Iterator iter = tasks.iterator(); while (iter.hasNext()) { ActivityManager.RecentTaskInfo t = iter.next(); // Skip tasks in the home stack if (ssp.isInHomeStack(t.persistentId)) { iter.remove(); continue; } // Skip tasks from this Recents package if (t.baseIntent.getComponent().getPackageName().equals(context.getPackageName())) { iter.remove(); continue; } } // Add each task to the task stack t1 = System.currentTimeMillis(); int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { ActivityManager.RecentTaskInfo t = tasks.get(i); ActivityInfo info = ssp.getActivityInfo(t.baseIntent.getComponent()); String activityLabel = (t.activityLabel == null ? ssp.getActivityLabel(info) : t.activityLabel.toString()); Bitmap activityIcon = t.activityIcon; boolean isForemostTask = (i == (taskCount - 1)); // Create a new task Task task = new Task(t.persistentId, (t.id > -1), t.baseIntent, activityLabel, activityIcon); // Preload the specified number of apps if (i >= (taskCount - preloadCount)) { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|preloadTask]", "i: " + i + " task: " + t.baseIntent.getComponent().getPackageName()); // Load the icon (if possible and not the foremost task, from the cache) if (!isForemostTask) { task.applicationIcon = mApplicationIconCache.get(task.key); if (task.applicationIcon != null) { // Even though we get things from the cache, we should update them // if they've changed in the bg tasksToForceLoad.add(task); } } if (task.applicationIcon == null) { task.applicationIcon = ssp.getActivityIcon(info); if (task.applicationIcon != null) { mApplicationIconCache.put(task.key, task.applicationIcon); } else { task.applicationIcon = mDefaultApplicationIcon; } } // Load the thumbnail (if possible and not the foremost task, from the cache) if (!isForemostTask) { task.thumbnail = mThumbnailCache.get(task.key); if (task.thumbnail != null) { // Even though we get things from the cache, we should update them if // they've changed in the bg tasksToForceLoad.add(task); } } if (task.thumbnail == null) { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|loadingTaskThumbnail]"); task.thumbnail = ssp.getTaskThumbnail(task.key.id); if (task.thumbnail != null) { mThumbnailCache.put(task.key, task.thumbnail); } else { task.thumbnail = mDefaultThumbnail; } } } // Add the task to the stack Console.log(Constants.DebugFlags.App.TaskDataLoader, " [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName()); stack.addTask(task); } Console.log(Constants.DebugFlags.App.TimeSystemCalls, "[RecentsTaskLoader|getAllTaskTopThumbnail]", "" + (System.currentTimeMillis() - t1) + "ms"); /* // Get all the stacks t1 = System.currentTimeMillis(); List stackInfos = ams.getAllStackInfos(); Console.log(Constants.DebugFlags.App.TimeSystemCalls, "[RecentsTaskLoader|getAllStackInfos]", "" + (System.currentTimeMillis() - t1) + "ms"); Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|stacks]", "" + tasks.size()); for (ActivityManager.StackInfo s : stackInfos) { Console.log(Constants.DebugFlags.App.TaskDataLoader, " [RecentsTaskLoader|stack]", s.toString()); if (stacks.containsKey(s.stackId)) { stacks.get(s.stackId).setRect(s.bounds); } } */ // Start the task loader mLoader.start(context); // Add all the tasks that we are force/re-loading for (Task t : tasksToForceLoad) { mLoadQueue.addTask(t, true); } return root; } /** Acquires the task resource data from the pool. */ public void loadTaskData(Task t) { Drawable applicationIcon = mApplicationIconCache.get(t.key); Bitmap thumbnail = mThumbnailCache.get(t.key); Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|loadTask]", t + " applicationIcon: " + applicationIcon + " thumbnail: " + thumbnail + " thumbnailCacheSize: " + mThumbnailCache.size()); boolean requiresLoad = false; if (applicationIcon == null) { applicationIcon = mDefaultApplicationIcon; requiresLoad = true; } if (thumbnail == null) { thumbnail = mDefaultThumbnail; requiresLoad = true; } if (requiresLoad) { mLoadQueue.addTask(t, false); } t.notifyTaskDataLoaded(thumbnail, applicationIcon, false); } /** Releases the task resource data back into the pool. */ public void unloadTaskData(Task t) { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|unloadTask]", t + " thumbnailCacheSize: " + mThumbnailCache.size()); mLoadQueue.removeTask(t); t.notifyTaskDataUnloaded(mDefaultThumbnail, mDefaultApplicationIcon); } /** Completely removes the resource data from the pool. */ public void deleteTaskData(Task t) { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|deleteTask]", t); mLoadQueue.removeTask(t); mThumbnailCache.remove(t.key); mApplicationIconCache.remove(t.key); t.notifyTaskDataUnloaded(mDefaultThumbnail, mDefaultApplicationIcon); } /** Stops the task loader and clears all pending tasks */ void stopLoader() { Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|stopLoader]"); mLoader.stop(); mLoadQueue.clearTasks(); } void onTrimMemory(int level) { Console.log(Constants.DebugFlags.App.Memory, "[RecentsTaskLoader|onTrimMemory]", Console.trimMemoryLevelToString(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(); break; default: break; } } }