diff options
Diffstat (limited to 'com/android/systemui/recents/model/RecentsTaskLoader.java')
-rw-r--r-- | com/android/systemui/recents/model/RecentsTaskLoader.java | 663 |
1 files changed, 663 insertions, 0 deletions
diff --git a/com/android/systemui/recents/model/RecentsTaskLoader.java b/com/android/systemui/recents/model/RecentsTaskLoader.java new file mode 100644 index 00000000..1b893862 --- /dev/null +++ b/com/android/systemui/recents/model/RecentsTaskLoader.java @@ -0,0 +1,663 @@ +/* + * 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.ComponentName; +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.Looper; +import android.os.Trace; +import android.util.Log; +import android.util.LruCache; + +import com.android.internal.annotations.GuardedBy; +import com.android.systemui.R; +import com.android.systemui.recents.Recents; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.RecentsDebugFlags; +import com.android.systemui.recents.events.activity.PackagesChangedEvent; +import com.android.systemui.recents.misc.SystemServicesProxy; + +import java.io.PrintWriter; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; + + +/** + * A Task load queue + */ +class TaskResourceLoadQueue { + + ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>(); + + /** 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 BackgroundTaskLoader implements Runnable { + static String TAG = "TaskResourceLoader"; + static boolean DEBUG = false; + + Context mContext; + HandlerThread mLoadThread; + Handler mLoadThreadHandler; + Handler mMainThreadHandler; + + TaskResourceLoadQueue mLoadQueue; + TaskKeyLruCache<Drawable> mIconCache; + BitmapDrawable mDefaultIcon; + + boolean mStarted; + boolean mCancelled; + boolean mWaitingOnLoadQueue; + + private final OnIdleChangedListener mOnIdleChangedListener; + + /** Constructor, creates a new loading thread that loads task resources in the background */ + public BackgroundTaskLoader(TaskResourceLoadQueue loadQueue, + TaskKeyLruCache<Drawable> iconCache, BitmapDrawable defaultIcon, + OnIdleChangedListener onIdleChangedListener) { + mLoadQueue = loadQueue; + mIconCache = iconCache; + mDefaultIcon = defaultIcon; + mMainThreadHandler = new Handler(); + mOnIdleChangedListener = onIdleChangedListener; + mLoadThread = new HandlerThread("Recents-TaskResourceLoader", + android.os.Process.THREAD_PRIORITY_BACKGROUND); + mLoadThread.start(); + mLoadThreadHandler = new Handler(mLoadThread.getLooper()); + } + + /** Restarts the loader thread */ + void start(Context context) { + mContext = context; + mCancelled = false; + if (!mStarted) { + // Start loading on the load thread + mStarted = true; + mLoadThreadHandler.post(this); + } else { + // Notify the load thread to start loading again + 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; + // 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 = Recents.getSystemServices(); + // If we've stopped the loader, then fall through to the above logic to wait on + // the load thread + if (ssp != null) { + processLoadQueueItem(ssp); + } + + // 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; + mMainThreadHandler.post( + () -> mOnIdleChangedListener.onIdleChanged(true)); + mLoadQueue.wait(); + mMainThreadHandler.post( + () -> mOnIdleChangedListener.onIdleChanged(false)); + mWaitingOnLoadQueue = false; + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + } + } + } + + /** + * This needs to be in a separate method to work around an surprising interpreter behavior: + * The register will keep the local reference to cachedThumbnailData even if it falls out of + * scope. Putting it into a method fixes this issue. + */ + private void processLoadQueueItem(SystemServicesProxy ssp) { + // Load the next item from the queue + final Task t = mLoadQueue.nextTask(); + if (t != null) { + Drawable cachedIcon = mIconCache.get(t.key); + + // Load the icon if it is stale or we haven't cached one yet + if (cachedIcon == null) { + cachedIcon = ssp.getBadgedTaskDescriptionIcon(t.taskDescription, + t.key.userId, mContext.getResources()); + + if (cachedIcon == null) { + ActivityInfo info = ssp.getActivityInfo( + t.key.getComponent(), t.key.userId); + if (info != null) { + if (DEBUG) Log.d(TAG, "Loading icon: " + t.key); + cachedIcon = ssp.getBadgedActivityIcon(info, t.key.userId); + } + } + + if (cachedIcon == null) { + cachedIcon = mDefaultIcon; + } + + // At this point, even if we can't load the icon, we will set the + // default icon. + mIconCache.put(t.key, cachedIcon); + } + + if (DEBUG) Log.d(TAG, "Loading thumbnail: " + t.key); + final ThumbnailData thumbnailData = ssp.getTaskThumbnail(t.key.id, + true /* reducedResolution */); + + if (!mCancelled) { + // Notify that the task data has changed + final Drawable finalIcon = cachedIcon; + mMainThreadHandler.post( + () -> t.notifyTaskDataLoaded(thumbnailData, finalIcon)); + } + } + } + + interface OnIdleChangedListener { + void onIdleChanged(boolean idle); + } +} + +/** + * Recents task loader + */ +public class RecentsTaskLoader { + + private static final String TAG = "RecentsTaskLoader"; + private static final boolean DEBUG = false; + + // This activity info LruCache is useful because it can be expensive to retrieve ActivityInfos + // for many tasks, which we use to get the activity labels and icons. Unlike the other caches + // below, this is per-package so we can't invalidate the items in the cache based on the last + // active time. Instead, we rely on the RecentsPackageMonitor to keep us informed whenever a + // package in the cache has been updated, so that we may remove it. + private final LruCache<ComponentName, ActivityInfo> mActivityInfoCache; + private final TaskKeyLruCache<Drawable> mIconCache; + private final TaskKeyLruCache<String> mActivityLabelCache; + private final TaskKeyLruCache<String> mContentDescriptionCache; + private final TaskResourceLoadQueue mLoadQueue; + private final BackgroundTaskLoader mLoader; + private final HighResThumbnailLoader mHighResThumbnailLoader; + @GuardedBy("this") + private final TaskKeyStrongCache<ThumbnailData> mThumbnailCache = new TaskKeyStrongCache<>(); + @GuardedBy("this") + private final TaskKeyStrongCache<ThumbnailData> mTempCache = new TaskKeyStrongCache<>(); + private final int mMaxThumbnailCacheSize; + private final int mMaxIconCacheSize; + private int mNumVisibleTasksLoaded; + + int mDefaultTaskBarBackgroundColor; + int mDefaultTaskViewBackgroundColor; + BitmapDrawable mDefaultIcon; + + private TaskKeyLruCache.EvictionCallback mClearActivityInfoOnEviction = + new TaskKeyLruCache.EvictionCallback() { + @Override + public void onEntryEvicted(Task.TaskKey key) { + if (key != null) { + mActivityInfoCache.remove(key.getComponent()); + } + } + }; + + public RecentsTaskLoader(Context context) { + Resources res = context.getResources(); + mDefaultTaskBarBackgroundColor = + context.getColor(R.color.recents_task_bar_default_background_color); + mDefaultTaskViewBackgroundColor = + context.getColor(R.color.recents_task_view_default_background_color); + mMaxThumbnailCacheSize = res.getInteger(R.integer.config_recents_max_thumbnail_count); + mMaxIconCacheSize = res.getInteger(R.integer.config_recents_max_icon_count); + int iconCacheSize = RecentsDebugFlags.Static.DisableBackgroundCache ? 1 : + mMaxIconCacheSize; + + // Create the default assets + Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8); + icon.eraseColor(0); + mDefaultIcon = new BitmapDrawable(context.getResources(), icon); + + // Initialize the proxy, cache and loaders + int numRecentTasks = ActivityManager.getMaxRecentTasksStatic(); + mHighResThumbnailLoader = new HighResThumbnailLoader(Recents.getSystemServices(), + Looper.getMainLooper(), Recents.getConfiguration().isLowRamDevice); + mLoadQueue = new TaskResourceLoadQueue(); + mIconCache = new TaskKeyLruCache<>(iconCacheSize, mClearActivityInfoOnEviction); + mActivityLabelCache = new TaskKeyLruCache<>(numRecentTasks, mClearActivityInfoOnEviction); + mContentDescriptionCache = new TaskKeyLruCache<>(numRecentTasks, + mClearActivityInfoOnEviction); + mActivityInfoCache = new LruCache(numRecentTasks); + mLoader = new BackgroundTaskLoader(mLoadQueue, mIconCache, mDefaultIcon, + mHighResThumbnailLoader::setTaskLoadQueueIdle); + } + + /** Returns the size of the app icon cache. */ + public int getIconCacheSize() { + return mMaxIconCacheSize; + } + + /** Returns the size of the thumbnail cache. */ + public int getThumbnailCacheSize() { + return mMaxThumbnailCacheSize; + } + + public HighResThumbnailLoader getHighResThumbnailLoader() { + return mHighResThumbnailLoader; + } + + /** Creates a new plan for loading the recent tasks. */ + public RecentsTaskLoadPlan createLoadPlan(Context context) { + RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(context); + return plan; + } + + /** Preloads raw recents tasks using the specified plan to store the output. */ + public synchronized void preloadRawTasks(RecentsTaskLoadPlan plan, + boolean includeFrontMostExcludedTask) { + plan.preloadRawTasks(includeFrontMostExcludedTask); + } + + /** Preloads recents tasks using the specified plan to store the output. */ + public synchronized void preloadTasks(RecentsTaskLoadPlan plan, int runningTaskId, + boolean includeFrontMostExcludedTask) { + try { + Trace.beginSection("preloadPlan"); + plan.preloadPlan(this, runningTaskId, includeFrontMostExcludedTask); + } finally { + Trace.endSection(); + } + } + + /** Begins loading the heavy task data according to the specified options. */ + public synchronized void loadTasks(Context context, RecentsTaskLoadPlan plan, + RecentsTaskLoadPlan.Options opts) { + if (opts == null) { + throw new RuntimeException("Requires load options"); + } + if (opts.onlyLoadForCache && opts.loadThumbnails) { + + // If we are loading for the cache, we'd like to have the real cache only include the + // visible thumbnails. However, we also don't want to reload already cached thumbnails. + // Thus, we copy over the current entries into a second cache, and clear the real cache, + // such that the real cache only contains visible thumbnails. + mTempCache.copyEntries(mThumbnailCache); + mThumbnailCache.evictAll(); + } + plan.executePlan(opts, this); + mTempCache.evictAll(); + if (!opts.onlyLoadForCache) { + mNumVisibleTasksLoaded = opts.numVisibleTasks; + } + } + + /** + * Acquires the task resource data directly from the cache, loading if necessary. + */ + public void loadTaskData(Task t) { + Drawable icon = mIconCache.getAndInvalidateIfModified(t.key); + icon = icon != null ? icon : mDefaultIcon; + mLoadQueue.addTask(t); + t.notifyTaskDataLoaded(t.thumbnail, icon); + } + + /** Releases the task resource data back into the pool. */ + public void unloadTaskData(Task t) { + mLoadQueue.removeTask(t); + t.notifyTaskDataUnloaded(mDefaultIcon); + } + + /** Completely removes the resource data from the pool. */ + public void deleteTaskData(Task t, boolean notifyTaskDataUnloaded) { + mLoadQueue.removeTask(t); + mIconCache.remove(t.key); + mActivityLabelCache.remove(t.key); + mContentDescriptionCache.remove(t.key); + if (notifyTaskDataUnloaded) { + t.notifyTaskDataUnloaded(mDefaultIcon); + } + } + + /** + * Handles signals from the system, trimming memory when requested to prevent us from running + * out of memory. + */ + public synchronized void onTrimMemory(int level) { + switch (level) { + case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: + // Stop the loader immediately when the UI is no longer visible + stopLoader(); + mIconCache.trimToSize(Math.max(mNumVisibleTasksLoaded, + mMaxIconCacheSize / 2)); + break; + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: + case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: + // We are leaving recents, so trim the data a bit + mIconCache.trimToSize(Math.max(1, mMaxIconCacheSize / 2)); + mActivityInfoCache.trimToSize(Math.max(1, + ActivityManager.getMaxRecentTasksStatic() / 2)); + break; + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: + case ComponentCallbacks2.TRIM_MEMORY_MODERATE: + // We are going to be low on memory + mIconCache.trimToSize(Math.max(1, mMaxIconCacheSize / 4)); + mActivityInfoCache.trimToSize(Math.max(1, + ActivityManager.getMaxRecentTasksStatic() / 4)); + break; + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: + case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: + // We are low on memory, so release everything + mIconCache.evictAll(); + mActivityInfoCache.evictAll(); + // The cache is small, only clear the label cache when we are critical + mActivityLabelCache.evictAll(); + mContentDescriptionCache.evictAll(); + mThumbnailCache.evictAll(); + break; + default: + break; + } + } + + /** + * Returns the cached task label if the task key is not expired, updating the cache if it is. + */ + String getAndUpdateActivityTitle(Task.TaskKey taskKey, ActivityManager.TaskDescription td) { + SystemServicesProxy ssp = Recents.getSystemServices(); + + // 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 + ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey); + if (activityInfo != null) { + label = ssp.getBadgedActivityLabel(activityInfo, taskKey.userId); + mActivityLabelCache.put(taskKey, label); + return label; + } + // If the activity info does not exist or fails to load, return an empty label for now, + // but do not cache it + return ""; + } + + /** + * Returns the cached task content description if the task key is not expired, updating the + * cache if it is. + */ + String getAndUpdateContentDescription(Task.TaskKey taskKey, ActivityManager.TaskDescription td, + Resources res) { + SystemServicesProxy ssp = Recents.getSystemServices(); + + // Return the cached content description if it exists + String label = mContentDescriptionCache.getAndInvalidateIfModified(taskKey); + if (label != null) { + return label; + } + + // All short paths failed, load the label from the activity info and cache it + ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey); + if (activityInfo != null) { + label = ssp.getBadgedContentDescription(activityInfo, taskKey.userId, td, res); + if (td == null) { + // Only add to the cache if the task description is null, otherwise, it is possible + // for the task description to change between calls without the last active time + // changing (ie. between preloading and Overview starting) which would lead to stale + // content descriptions + // TODO: Investigate improving this + mContentDescriptionCache.put(taskKey, label); + } + return label; + } + // If the content description does not exist, return an empty label for now, but do not + // cache it + return ""; + } + + /** + * Returns the cached task icon if the task key is not expired, updating the cache if it is. + */ + Drawable getAndUpdateActivityIcon(Task.TaskKey taskKey, ActivityManager.TaskDescription td, + Resources res, boolean loadIfNotCached) { + SystemServicesProxy ssp = Recents.getSystemServices(); + + // Return the cached activity icon if it exists + Drawable icon = mIconCache.getAndInvalidateIfModified(taskKey); + if (icon != null) { + return icon; + } + + if (loadIfNotCached) { + // Return and cache the task description icon if it exists + icon = ssp.getBadgedTaskDescriptionIcon(td, taskKey.userId, res); + if (icon != null) { + mIconCache.put(taskKey, icon); + return icon; + } + + // Load the icon from the activity info and cache it + ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey); + if (activityInfo != null) { + icon = ssp.getBadgedActivityIcon(activityInfo, taskKey.userId); + if (icon != null) { + mIconCache.put(taskKey, icon); + return icon; + } + } + } + // We couldn't load any icon + return null; + } + + /** + * Returns the cached thumbnail if the task key is not expired, updating the cache if it is. + */ + synchronized ThumbnailData getAndUpdateThumbnail(Task.TaskKey taskKey, boolean loadIfNotCached, + boolean storeInCache) { + SystemServicesProxy ssp = Recents.getSystemServices(); + + ThumbnailData cached = mThumbnailCache.getAndInvalidateIfModified(taskKey); + if (cached != null) { + return cached; + } + + cached = mTempCache.getAndInvalidateIfModified(taskKey); + if (cached != null) { + mThumbnailCache.put(taskKey, cached); + return cached; + } + + if (loadIfNotCached) { + RecentsConfiguration config = Recents.getConfiguration(); + if (config.svelteLevel < RecentsConfiguration.SVELTE_DISABLE_LOADING) { + // Load the thumbnail from the system + ThumbnailData thumbnailData = ssp.getTaskThumbnail(taskKey.id, + true /* reducedResolution */); + if (thumbnailData.thumbnail != null) { + if (storeInCache) { + mThumbnailCache.put(taskKey, thumbnailData); + } + return thumbnailData; + } + } + } + + // We couldn't load any thumbnail + return null; + } + + /** + * Returns the task's primary color if possible, defaulting to the default color if there is + * no specified primary color. + */ + int getActivityPrimaryColor(ActivityManager.TaskDescription td) { + if (td != null && td.getPrimaryColor() != 0) { + return td.getPrimaryColor(); + } + return mDefaultTaskBarBackgroundColor; + } + + /** + * Returns the task's background color if possible. + */ + int getActivityBackgroundColor(ActivityManager.TaskDescription td) { + if (td != null && td.getBackgroundColor() != 0) { + return td.getBackgroundColor(); + } + return mDefaultTaskViewBackgroundColor; + } + + /** + * Returns the activity info for the given task key, retrieving one from the system if the + * task key is expired. + */ + ActivityInfo getAndUpdateActivityInfo(Task.TaskKey taskKey) { + SystemServicesProxy ssp = Recents.getSystemServices(); + ComponentName cn = taskKey.getComponent(); + ActivityInfo activityInfo = mActivityInfoCache.get(cn); + if (activityInfo == null) { + activityInfo = ssp.getActivityInfo(cn, taskKey.userId); + if (cn == null || activityInfo == null) { + Log.e(TAG, "Unexpected null component name or activity info: " + cn + ", " + + activityInfo); + return null; + } + mActivityInfoCache.put(cn, activityInfo); + } + return activityInfo; + } + + /** + * Starts loading tasks. + */ + public void startLoader(Context ctx) { + mLoader.start(ctx); + } + + /** + * Stops the task loader and clears all queued, pending task loads. + */ + private void stopLoader() { + mLoader.stop(); + mLoadQueue.clearTasks(); + } + + /**** Event Bus Events ****/ + + public final void onBusEvent(PackagesChangedEvent event) { + // Remove all the cached activity infos for this package. The other caches do not need to + // be pruned at this time, as the TaskKey expiration checks will flush them next time their + // cached contents are requested + Map<ComponentName, ActivityInfo> activityInfoCache = mActivityInfoCache.snapshot(); + for (ComponentName cn : activityInfoCache.keySet()) { + if (cn.getPackageName().equals(event.packageName)) { + if (DEBUG) { + Log.d(TAG, "Removing activity info from cache: " + cn); + } + mActivityInfoCache.remove(cn); + } + } + } + + public synchronized void dump(String prefix, PrintWriter writer) { + String innerPrefix = prefix + " "; + + writer.print(prefix); writer.println(TAG); + writer.print(prefix); writer.println("Icon Cache"); + mIconCache.dump(innerPrefix, writer); + writer.print(prefix); writer.println("Thumbnail Cache"); + mThumbnailCache.dump(innerPrefix, writer); + writer.print(prefix); writer.println("Temp Thumbnail Cache"); + mTempCache.dump(innerPrefix, writer); + } +} |