Merge "Preload only visible thumbnails and task icons. (Bug 17672056, Bug 18291345)" into lmp-mr1-dev
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 6713944..2561fae 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -776,7 +776,7 @@
          This needs to match the constants in
          policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
     -->
-    <integer name="config_longPressOnHomeBehavior">1</integer>
+    <integer name="config_longPressOnHomeBehavior">0</integer>
 
     <!-- Control the behavior when the user double-taps the home button.
             0 - Nothing
diff --git a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java
index 2bfdb69..b5a6dac 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java
@@ -41,6 +41,7 @@
 import com.android.systemui.RecentsComponent;
 import com.android.systemui.recents.misc.Console;
 import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.model.RecentsTaskLoadPlan;
 import com.android.systemui.recents.model.RecentsTaskLoader;
 import com.android.systemui.recents.model.Task;
 import com.android.systemui.recents.model.TaskGrouping;
@@ -64,6 +65,8 @@
     final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "recents.triggeredFromAltTab";
     final public static String EXTRA_TRIGGERED_FROM_HOME_KEY = "recents.triggeredFromHomeKey";
     final public static String EXTRA_REUSE_TASK_STACK_VIEWS = "recents.reuseTaskStackViews";
+    final public static String EXTRA_NUM_VISIBLE_TASKS = "recents.numVisibleTasks";
+    final public static String EXTRA_NUM_VISIBLE_THUMBNAILS = "recents.numVisibleThumbnails";
 
     final public static String ACTION_START_ENTER_ANIMATION = "action_start_enter_animation";
     final public static String ACTION_TOGGLE_RECENTS_ACTIVITY = "action_toggle_recents_activity";
@@ -76,6 +79,7 @@
     final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity";
 
     static RecentsComponent.Callbacks sRecentsComponentCallbacks;
+    static RecentsTaskLoadPlan sInstanceLoadPlan;
 
     Context mContext;
     LayoutInflater mInflater;
@@ -134,8 +138,15 @@
             }
         }
 
-        // When we start, preload the metadata associated with the previous tasks
-        RecentsTaskLoader.getInstance().preload(mContext, RecentsTaskLoader.ALL_TASKS);
+        // When we start, preload the metadata and icons associated with the recent tasks.
+        // We can use a new plan since the caches will be the same.
+        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
+        loader.preloadTasks(plan, true /* isTopTaskHome */);
+        RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
+        launchOpts.numVisibleTasks = loader.getApplicationIconCacheSize();
+        launchOpts.loadThumbnails = false;
+        loader.loadTasks(mContext, plan, launchOpts);
     }
 
     public void onBootCompleted() {
@@ -183,9 +194,11 @@
     }
 
     public void onPreloadRecents() {
-        // When we start, preload the metadata associated with the previous tasks
-        RecentsTaskLoader.getInstance().preload(mContext,
-                Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount);
+        // Preload only the raw task list into a new load plan (which will be consumed by the
+        // RecentsActivity)
+        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+        sInstanceLoadPlan = loader.createLoadPlan(mContext);
+        sInstanceLoadPlan.preloadRawTasks(true);
     }
 
     public void onCancelPreloadingRecents() {
@@ -194,8 +207,10 @@
 
     void showRelativeAffiliatedTask(boolean showNextTask) {
         RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
-        TaskStack stack = loader.getTaskStack(mSystemServicesProxy, mContext.getResources(),
-                -1, -1, RecentsTaskLoader.ALL_TASKS, false, true, null, null);
+        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
+        loader.preloadTasks(plan, true /* isTopTaskHome */);
+        TaskStack stack = plan.getTaskStack();
+
         // Return early if there are no tasks
         if (stack.getTaskCount() == 0) return;
 
@@ -411,11 +426,11 @@
      * Creates the activity options for an app->recents transition.
      */
     ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask,
-            boolean isTopTaskHome) {
+            TaskStack stack, TaskStackView stackView) {
         // Update the destination rect
         Task toTask = new Task();
-        TaskViewTransform toTransform = getThumbnailTransitionTransform(topTask.id, isTopTaskHome,
-                toTask);
+        TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView,
+                topTask.id, toTask);
         if (toTransform != null && toTask.key != null) {
             Rect toTaskRect = toTransform.rect;
             int toHeaderWidth = (int) (mHeaderBar.getMeasuredWidth() * toTransform.scale);
@@ -443,16 +458,8 @@
     }
 
     /** Returns the transition rect for the given task id. */
-    TaskViewTransform getThumbnailTransitionTransform(int runningTaskId, boolean isTopTaskHome,
-                                                      Task runningTaskOut) {
-        // Get the stack of tasks that we are animating into
-        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
-        TaskStack stack = loader.getTaskStack(mSystemServicesProxy, mContext.getResources(),
-                runningTaskId, -1, RecentsTaskLoader.ALL_TASKS, false, isTopTaskHome, null, null);
-        if (stack.getTaskCount() == 0) {
-            return null;
-        }
-
+    TaskViewTransform getThumbnailTransitionTransform(TaskStack stack, TaskStackView stackView,
+            int runningTaskId, Task runningTaskOut) {
         // Find the running task in the TaskStack
         Task task = null;
         ArrayList<Task> tasks = stack.getTasks();
@@ -474,30 +481,42 @@
         }
 
         // Get the transform for the running task
-        mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome);
-        mDummyStackView.getScroller().setStackScrollToInitialState();
-        mTmpTransform = mDummyStackView.getStackAlgorithm().getStackTransform(task,
-                mDummyStackView.getScroller().getStackScroll(), mTmpTransform, null);
+        stackView.getScroller().setStackScrollToInitialState();
+        mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task,
+                stackView.getScroller().getStackScroll(), mTmpTransform, null);
         return mTmpTransform;
     }
 
     /** Starts the recents activity */
     void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, boolean isTopTaskHome) {
-        // If Recents is not the front-most activity and we should animate into it.  If
-        // the activity at the root of the top task stack in the home stack, then we just do a
-        // simple transition.  Otherwise, we animate to the rects defined by the Recents service,
-        // which can differ depending on the number of items in the list.
-        SystemServicesProxy ssp = mSystemServicesProxy;
-        List<ActivityManager.RecentTaskInfo> recentTasks =
-                ssp.getRecentTasks(3, UserHandle.CURRENT.getIdentifier(), isTopTaskHome);
-        boolean useThumbnailTransition = !isTopTaskHome;
-        boolean hasRecentTasks = !recentTasks.isEmpty();
+        if (sInstanceLoadPlan == null) {
+            // Create a new load plan if onPreloadRecents() was never triggered
+            RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+            sInstanceLoadPlan = loader.createLoadPlan(mContext);
+        }
+        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+        loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome);
+        TaskStack stack = sInstanceLoadPlan.getTaskStack();
+
+        // Prepare the dummy stack for the transition
+        mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome);
+        TaskStackViewLayoutAlgorithm.VisibilityReport stackVr =
+                mDummyStackView.computeStackVisibilityReport();
+        boolean hasRecentTasks = stack.getTaskCount() > 0;
+        boolean useThumbnailTransition = !isTopTaskHome && hasRecentTasks;
 
         if (useThumbnailTransition) {
+            // Ensure that we load the running task's icon
+            RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
+            launchOpts.runningTaskId = topTask.id;
+            launchOpts.loadThumbnails = false;
+            loader.loadTasks(mContext, sInstanceLoadPlan, launchOpts);
+
             // Try starting with a thumbnail transition
-            ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, isTopTaskHome);
+            ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, stack,
+                    mDummyStackView);
             if (opts != null) {
-                startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_THUMBNAIL);
+                startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_THUMBNAIL, stackVr);
             } else {
                 // Fall through below to the non-thumbnail transition
                 useThumbnailTransition = false;
@@ -531,11 +550,11 @@
 
                 ActivityOptions opts = getHomeTransitionActivityOptions(fromSearchHome);
                 startAlternateRecentsActivity(topTask, opts,
-                        fromSearchHome ? EXTRA_FROM_SEARCH_HOME : EXTRA_FROM_HOME);
+                        fromSearchHome ? EXTRA_FROM_SEARCH_HOME : EXTRA_FROM_HOME, stackVr);
             } else {
                 // Otherwise we do the normal fade from an unknown source
                 ActivityOptions opts = getUnknownTransitionActivityOptions();
-                startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_HOME);
+                startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_HOME, stackVr);
             }
         }
         mLastToggleTime = System.currentTimeMillis();
@@ -543,7 +562,8 @@
 
     /** Starts the recents activity */
     void startAlternateRecentsActivity(ActivityManager.RunningTaskInfo topTask,
-                                       ActivityOptions opts, String extraFlag) {
+            ActivityOptions opts, String extraFlag,
+            TaskStackViewLayoutAlgorithm.VisibilityReport vr) {
         Intent intent = new Intent(sToggleRecentsAction);
         intent.setClassName(sRecentsPackage, sRecentsActivity);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
@@ -555,6 +575,8 @@
         intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, mTriggeredFromAltTab);
         intent.putExtra(EXTRA_FROM_TASK_ID, (topTask != null) ? topTask.id : -1);
         intent.putExtra(EXTRA_REUSE_TASK_STACK_VIEWS, mCanReuseTaskStackViews);
+        intent.putExtra(EXTRA_NUM_VISIBLE_TASKS, vr.numVisibleTasks);
+        intent.putExtra(EXTRA_NUM_VISIBLE_THUMBNAILS, vr.numVisibleThumbnails);
         if (opts != null) {
             mContext.startActivityAsUser(intent, opts.toBundle(), UserHandle.CURRENT);
         } else {
@@ -575,6 +597,15 @@
         }
     }
 
+    /**
+     * Returns the preloaded load plan and invalidates it.
+     */
+    public static RecentsTaskLoadPlan consumeInstanceLoadPlan() {
+        RecentsTaskLoadPlan plan = sInstanceLoadPlan;
+        sInstanceLoadPlan = null;
+        return plan;
+    }
+
     /**** OnAnimationStartedListener Implementation ****/
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java
index 9b84d2e..4c76af7 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Constants.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java
@@ -64,11 +64,6 @@
             public static String DebugModeVersion = "A";
         }
 
-        public static class RecentsTaskLoader {
-            // XXX: This should be calculated on the first load
-            public static final int PreloadFirstTasksCount = 6;
-        }
-
         public static class TaskStackView {
             public static final int TaskStackOverscrollRange = 150;
             public static final int FilterStartDelay = 25;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index de95ae8..9a7fec4 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -27,10 +27,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
-import android.content.res.Configuration;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.UserHandle;
 import android.util.Pair;
 import android.view.KeyEvent;
@@ -43,6 +40,7 @@
 import com.android.systemui.recents.misc.ReferenceCountedTrigger;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.misc.Utilities;
+import com.android.systemui.recents.model.RecentsTaskLoadPlan;
 import com.android.systemui.recents.model.RecentsTaskLoader;
 import com.android.systemui.recents.model.SpaceNode;
 import com.android.systemui.recents.model.Task;
@@ -168,9 +166,10 @@
             if (action.equals(Intent.ACTION_SCREEN_OFF)) {
                 // When the screen turns off, dismiss Recents to Home
                 dismissRecentsToHome(false);
-                // Start preloading some tasks in the background
-                RecentsTaskLoader.getInstance().preload(RecentsActivity.this,
-                        Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount);
+                // Preload the metadata for all tasks in the background
+                RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+                RecentsTaskLoadPlan plan = loader.createLoadPlan(context);
+                loader.preloadTasks(plan, true /* isTopTaskHome */);
             } else if (action.equals(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED)) {
                 // When the search activity changes, update the Search widget
                 refreshSearchWidget();
@@ -193,6 +192,10 @@
         // Update the configuration based on the launch intent
         boolean fromSearchHome = launchIntent.getBooleanExtra(
                 AlternateRecentsComponent.EXTRA_FROM_SEARCH_HOME, false);
+        int numVisibleTasks = launchIntent.getIntExtra(
+                AlternateRecentsComponent.EXTRA_NUM_VISIBLE_TASKS, 0);
+        int numVisibleThumbnails = launchIntent.getIntExtra(
+                AlternateRecentsComponent.EXTRA_NUM_VISIBLE_THUMBNAILS, 0);
         mConfig.launchedFromHome = fromSearchHome || launchIntent.getBooleanExtra(
                 AlternateRecentsComponent.EXTRA_FROM_HOME, false);
         mConfig.launchedFromAppWithThumbnail = launchIntent.getBooleanExtra(
@@ -204,16 +207,29 @@
         mConfig.launchedReuseTaskStackViews = launchIntent.getBooleanExtra(
                 AlternateRecentsComponent.EXTRA_REUSE_TASK_STACK_VIEWS, false);
 
-        // Load all the tasks
+        // If AlternateRecentsComponent has preloaded a load plan, then use that to prevent
+        // reconstructing the task stack
         RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
-        SpaceNode root = loader.reload(this,
-                Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount,
-                mConfig.launchedFromHome);
-        ArrayList<TaskStack> stacks = root.getStacks();
-        if (!stacks.isEmpty()) {
-            mRecentsView.setTaskStacks(root.getStacks());
+        RecentsTaskLoadPlan plan = AlternateRecentsComponent.consumeInstanceLoadPlan();
+        if (plan == null) {
+            plan = loader.createLoadPlan(this);
+            loader.preloadTasks(plan, mConfig.launchedFromHome);
         }
-        mConfig.launchedWithNoRecentTasks = !root.hasTasks();
+
+        // Start loading tasks according to the load plan
+        RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options();
+        loadOpts.runningTaskId = mConfig.launchedToTaskId;
+        loadOpts.numVisibleTasks = numVisibleTasks;
+        loadOpts.numVisibleTaskThumbnails = numVisibleThumbnails;
+        loader.loadTasks(this, plan, loadOpts);
+
+        SpaceNode root = plan.getSpaceNode();
+        ArrayList<TaskStack> stacks = root.getStacks();
+        boolean hasTasks = root.hasTasks();
+        if (hasTasks) {
+            mRecentsView.setTaskStacks(stacks);
+        }
+        mConfig.launchedWithNoRecentTasks = !hasTasks;
 
         // Create the home intent runnable
         Intent homeIntent = new Intent(Intent.ACTION_MAIN, null);
@@ -442,6 +458,8 @@
 
     /** Inflates the debug overlay if debug mode is enabled. */
     void inflateDebugOverlay() {
+        if (!Constants.DebugFlags.App.EnableDebugMode) return;
+
         if (mConfig.debugModeEnabled && mDebugOverlay == null) {
             // Inflate the overlay and seek bars
             mDebugOverlay = (DebugOverlayView) mDebugOverlayStub.inflate();
@@ -600,13 +618,17 @@
                 settings.edit().remove(Constants.Values.App.Key_DebugModeEnabled).apply();
                 mConfig.debugModeEnabled = false;
                 inflateDebugOverlay();
-                mDebugOverlay.disable();
+                if (mDebugOverlay != null) {
+                    mDebugOverlay.disable();
+                }
             } else {
                 // Enable the debug mode
                 settings.edit().putBoolean(Constants.Values.App.Key_DebugModeEnabled, true).apply();
                 mConfig.debugModeEnabled = true;
                 inflateDebugOverlay();
-                mDebugOverlay.enable();
+                if (mDebugOverlay != null) {
+                    mDebugOverlay.enable();
+                }
             }
             Toast.makeText(this, "Debug mode (" + Constants.Values.App.DebugModeVersion + ") " +
                 (mConfig.debugModeEnabled ? "Enabled" : "Disabled") + ", please restart Recents now",
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
index 51b3fb5..9a4bd08 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -244,6 +244,7 @@
 
         Bitmap thumbnail = SystemServicesProxy.getThumbnail(mAm, taskId);
         if (thumbnail != null) {
+            thumbnail.setHasAlpha(false);
             // We use a dumb heuristic for now, if the thumbnail is purely transparent in the top
             // left pixel, then assume the whole thumbnail is transparent. Generally, proper
             // screenshots are always composed onto a bitmap that has no alpha.
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
new file mode 100644
index 0000000..41251c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
@@ -0,0 +1,224 @@
+/*
+ * 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.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.util.Log;
+import com.android.systemui.recents.RecentsConfiguration;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+
+/**
+ * This class stores the loading state as it goes through multiple stages of loading:
+ *   - preloadRawTasks() will load the raw set of recents tasks from the system
+ *   - preloadPlan() will construct a new task stack with all metadata and only icons and thumbnails
+ *     that are currently in the cache
+ *   - executePlan() will actually load and fill in the icons and thumbnails according to the load
+ *     options specified, such that we can transition into the Recents activity seamlessly
+ */
+public class RecentsTaskLoadPlan {
+    static String TAG = "RecentsTaskLoadPlan";
+    static boolean DEBUG = false;
+
+    /** The set of conditions to load tasks. */
+    public static class Options {
+        public int runningTaskId = -1;
+        public boolean loadIcons = true;
+        public boolean loadThumbnails = true;
+        public int numVisibleTasks = 0;
+        public int numVisibleTaskThumbnails = 0;
+    }
+
+    Context mContext;
+    RecentsConfiguration mConfig;
+    SystemServicesProxy mSystemServicesProxy;
+
+    List<ActivityManager.RecentTaskInfo> mRawTasks;
+    TaskStack mStack;
+    HashMap<Task.ComponentNameKey, ActivityInfoHandle> mActivityInfoCache =
+            new HashMap<Task.ComponentNameKey, ActivityInfoHandle>();
+
+    /** Package level ctor */
+    RecentsTaskLoadPlan(Context context, RecentsConfiguration config, SystemServicesProxy ssp) {
+        mContext = context;
+        mConfig = config;
+        mSystemServicesProxy = ssp;
+    }
+
+    /**
+     * An optimization to preload the raw list of tasks.
+     */
+    public synchronized void preloadRawTasks(boolean isTopTaskHome) {
+        mRawTasks = mSystemServicesProxy.getRecentTasks(mConfig.maxNumTasksToLoad,
+                UserHandle.CURRENT.getIdentifier(), isTopTaskHome);
+        Collections.reverse(mRawTasks);
+
+        if (DEBUG) Log.d(TAG, "preloadRawTasks, tasks: " + mRawTasks.size());
+    }
+
+    /**
+     * Preloads the list of recent tasks from the system.  After this call, the TaskStack will
+     * have a list of all the recent tasks with their metadata, not including icons or
+     * thumbnails which were not cached and have to be loaded.
+     */
+    synchronized void preloadPlan(RecentsTaskLoader loader, boolean isTopTaskHome) {
+        if (DEBUG) Log.d(TAG, "preloadPlan");
+
+        mActivityInfoCache.clear();
+        mStack = new TaskStack();
+
+        Resources res = mContext.getResources();
+        ArrayList<Task> loadedTasks = new ArrayList<Task>();
+        if (mRawTasks == null) {
+            preloadRawTasks(isTopTaskHome);
+        }
+        int taskCount = mRawTasks.size();
+        for (int i = 0; i < taskCount; i++) {
+            ActivityManager.RecentTaskInfo t = mRawTasks.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 hadCachedActivityInfo = false;
+            if (mActivityInfoCache.containsKey(cnKey)) {
+                infoHandle = mActivityInfoCache.get(cnKey);
+                hadCachedActivityInfo = true;
+            } else {
+                infoHandle = new ActivityInfoHandle();
+            }
+
+            // Load the label, icon, and color
+            String activityLabel = loader.getAndUpdateActivityLabel(taskKey, t.taskDescription,
+                    mSystemServicesProxy, infoHandle);
+            Drawable activityIcon = loader.getAndUpdateActivityIcon(taskKey, t.taskDescription,
+                    mSystemServicesProxy, res, infoHandle, false);
+            int activityColor = loader.getActivityPrimaryColor(t.taskDescription, mConfig);
+
+            // Update the activity info cache
+            if (!hadCachedActivityInfo && infoHandle.info != null) {
+                mActivityInfoCache.put(cnKey, infoHandle);
+            }
+
+            Bitmap icon = t.taskDescription != null
+                    ? t.taskDescription.getInMemoryIcon()
+                    : null;
+            String iconFilename = t.taskDescription != null
+                    ? t.taskDescription.getIconFilename()
+                    : null;
+
+            // Add the task to the stack
+            Task task = new Task(taskKey, (t.id != RecentsTaskLoader.INVALID_TASK_ID),
+                    t.affiliatedTaskId, t.affiliatedTaskColor, activityLabel, activityIcon,
+                    activityColor, (i == (taskCount - 1)), mConfig.lockToAppEnabled, icon,
+                    iconFilename);
+            task.thumbnail = loader.getAndUpdateThumbnail(taskKey, mSystemServicesProxy, false);
+            loadedTasks.add(task);
+        }
+        mStack.setTasks(loadedTasks);
+        mStack.createAffiliatedGroupings(mConfig);
+
+        // Assertion
+        if (mStack.getTaskCount() != mRawTasks.size()) {
+            throw new RuntimeException("Loading failed");
+        }
+    }
+
+    /**
+     * Called to apply the actual loading based on the specified conditions.
+     */
+    synchronized void executePlan(Options opts, RecentsTaskLoader loader) {
+        if (DEBUG) Log.d(TAG, "executePlan, # tasks: " + opts.numVisibleTasks +
+                ", # thumbnails: " + opts.numVisibleTaskThumbnails +
+                ", running task id: " + opts.runningTaskId);
+
+        Resources res = mContext.getResources();
+
+        // Iterate through each of the tasks and load them according to the load conditions.
+        ArrayList<Task> tasks = mStack.getTasks();
+        int taskCount = tasks.size();
+        for (int i = 0; i < taskCount; i++) {
+            ActivityManager.RecentTaskInfo t = mRawTasks.get(i);
+            Task task = tasks.get(i);
+            Task.TaskKey taskKey = task.key;
+
+            // Get an existing activity info handle if possible
+            Task.ComponentNameKey cnKey = taskKey.getComponentNameKey();
+            ActivityInfoHandle infoHandle;
+            boolean hadCachedActivityInfo = false;
+            if (mActivityInfoCache.containsKey(cnKey)) {
+                infoHandle = mActivityInfoCache.get(cnKey);
+                hadCachedActivityInfo = true;
+            } else {
+                infoHandle = new ActivityInfoHandle();
+            }
+
+            boolean isRunningTask = (task.key.id == opts.runningTaskId);
+            boolean isVisibleTask = i >= (taskCount - opts.numVisibleTasks);
+            boolean isVisibleThumbnail = i >= (taskCount - opts.numVisibleTaskThumbnails);
+
+            if (opts.loadIcons && (isRunningTask || isVisibleTask)) {
+                if (task.activityIcon == null) {
+                    if (DEBUG) Log.d(TAG, "\tLoading icon: " + taskKey);
+                    task.activityIcon = loader.getAndUpdateActivityIcon(taskKey, t.taskDescription,
+                            mSystemServicesProxy, res, infoHandle, true);
+                }
+            }
+            if (opts.loadThumbnails && (isRunningTask || isVisibleThumbnail)) {
+                if (task.thumbnail == null) {
+                    if (DEBUG) Log.d(TAG, "\tLoading thumbnail: " + taskKey);
+                    task.thumbnail = loader.getAndUpdateThumbnail(taskKey, mSystemServicesProxy,
+                            true);
+                }
+            }
+
+            // Update the activity info cache
+            if (!hadCachedActivityInfo && infoHandle.info != null) {
+                mActivityInfoCache.put(cnKey, infoHandle);
+            }
+        }
+    }
+
+    /**
+     * Composes and returns a TaskStack from the preloaded list of recent tasks.
+     */
+    public TaskStack getTaskStack() {
+        return mStack;
+    }
+
+    /**
+     * Composes and returns a SpaceNode from the preloaded list of recent tasks.
+     */
+    public SpaceNode getSpaceNode() {
+        SpaceNode node = new SpaceNode();
+        node.setStack(mStack);
+        return node;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
index 390507f..6fd738d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
@@ -26,7 +26,6 @@
 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.R;
@@ -34,11 +33,7 @@
 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;
 
 
@@ -205,9 +200,7 @@
                     // 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 {
+                        if (cachedThumbnail == null) {
                             cachedThumbnail = mDefaultThumbnail;
                         }
                         mThumbnailCache.put(t.key, cachedThumbnail);
@@ -260,7 +253,7 @@
     private static final String TAG = "RecentsTaskLoader";
 
     static RecentsTaskLoader sInstance;
-    public static final int ALL_TASKS = -1;
+    static int INVALID_TASK_ID = -1;
 
     SystemServicesProxy mSystemServicesProxy;
     DrawableLruCache mApplicationIconCache;
@@ -273,6 +266,7 @@
 
     int mMaxThumbnailCacheSize;
     int mMaxIconCacheSize;
+    int mNumVisibleTasksLoaded;
 
     BitmapDrawable mDefaultApplicationIcon;
     Bitmap mDefaultThumbnail;
@@ -325,55 +319,6 @@
         return mSystemServicesProxy;
     }
 
-    /** Gets the list of recent tasks, ordered from back to front. */
-    private static List<ActivityManager.RecentTaskInfo> getRecentTasks(SystemServicesProxy ssp,
-            int numTasksToLoad, boolean isTopTaskHome) {
-        List<ActivityManager.RecentTaskInfo> tasks =
-                ssp.getRecentTasks(numTasksToLoad, UserHandle.CURRENT.getIdentifier(),
-                        isTopTaskHome);
-        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;
-        }
-
-        // If we are preloading this task, continue to load the task description icon or the
-        // activity icon
-        if (preloadTask) {
-
-            // Return and cache the task description icon if it exists
-            Drawable tdDrawable = mLoader.getTaskDescriptionIcon(taskKey, td.getInMemoryIcon(),
-                    td.getIconFilename(), ssp, res);
-            if (tdDrawable != null) {
-                mApplicationIconCache.put(taskKey, tdDrawable);
-                return tdDrawable;
-            }
-
-            // 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,
@@ -402,6 +347,63 @@
         return label;
     }
 
+    /** 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 loadIfNotCached) {
+        // Return the cached activity icon if it exists
+        Drawable icon = mApplicationIconCache.getAndInvalidateIfModified(taskKey);
+        if (icon != null) {
+            return icon;
+        }
+
+        if (loadIfNotCached) {
+            // Return and cache the task description icon if it exists
+            Drawable tdDrawable = mLoader.getTaskDescriptionIcon(taskKey, td.getInMemoryIcon(),
+                    td.getIconFilename(), ssp, res);
+            if (tdDrawable != null) {
+                mApplicationIconCache.put(taskKey, tdDrawable);
+                return tdDrawable;
+            }
+
+            // 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;
+                }
+            }
+        }
+        // We couldn't load any icon
+        return null;
+    }
+
+    /** Returns the bitmap using as many cached values as we can. */
+    public Bitmap getAndUpdateThumbnail(Task.TaskKey taskKey, SystemServicesProxy ssp,
+            boolean loadIfNotCached) {
+        // Return the cached thumbnail if it exists
+        Bitmap thumbnail = mThumbnailCache.getAndInvalidateIfModified(taskKey);
+        if (thumbnail != null) {
+            return thumbnail;
+        }
+
+        if (loadIfNotCached) {
+            // Load the thumbnail from the system
+            thumbnail = ssp.getTaskThumbnail(taskKey.id);
+            if (thumbnail != null) {
+                mThumbnailCache.put(taskKey, thumbnail);
+                return thumbnail;
+            }
+        }
+        // We couldn't load any thumbnail
+        return null;
+    }
+
     /** Returns the activity's primary color. */
     public int getActivityPrimaryColor(ActivityManager.TaskDescription td,
             RecentsConfiguration config) {
@@ -411,127 +413,33 @@
         return config.taskBarViewDefaultBackgroundColor;
     }
 
-    /** Reload the set of recent tasks */
-    public SpaceNode reload(Context context, int preloadCount, boolean isTopTaskHome) {
-        ArrayList<Task.TaskKey> taskKeys = new ArrayList<Task.TaskKey>();
-        ArrayList<Task> tasksToLoad = new ArrayList<Task>();
-        TaskStack stack = getTaskStack(mSystemServicesProxy, context.getResources(),
-                -1, preloadCount, RecentsTaskLoader.ALL_TASKS, true, isTopTaskHome, taskKeys,
-                tasksToLoad);
-        SpaceNode root = new SpaceNode();
-        root.setStack(stack);
-
-        // Start the task loader and add all the tasks we need to load
-        mLoadQueue.addTasks(tasksToLoad);
-        mLoader.start(context);
-
-        return root;
+    /** Returns the size of the app icon cache. */
+    public int getApplicationIconCacheSize() {
+        return mMaxIconCacheSize;
     }
 
-    /** Preloads the set of recent tasks (not including thumbnails). */
-    public void preload(Context context, int numTasksToPreload) {
-        ArrayList<Task> tasksToLoad = new ArrayList<Task>();
-        getTaskStack(mSystemServicesProxy, context.getResources(),
-                -1, -1, numTasksToPreload, true, true, null, tasksToLoad);
-
-        // Start the task loader and add all the tasks we need to load
-        mLoadQueue.addTasks(tasksToLoad);
-        mLoader.start(context);
-    }
-
-    /** Creates a lightweight stack of the current recent tasks, without thumbnails and icons. */
-    public synchronized TaskStack getTaskStack(SystemServicesProxy ssp, Resources res,
-            int preloadTaskId, int preloadTaskCount, int loadTaskCount,
-            boolean loadTaskThumbnails, boolean isTopTaskHome,
-            List<Task.TaskKey> taskKeysOut, List<Task> tasksToLoadOut) {
+    public RecentsTaskLoadPlan createLoadPlan(Context context) {
         RecentsConfiguration config = RecentsConfiguration.getInstance();
-        List<ActivityManager.RecentTaskInfo> tasks = getRecentTasks(ssp,
-                (loadTaskCount == ALL_TASKS ? config.maxNumTasksToLoad : loadTaskCount),
-                isTopTaskHome);
-        HashMap<Task.ComponentNameKey, ActivityInfoHandle> activityInfoCache =
-                new HashMap<Task.ComponentNameKey, ActivityInfoHandle>();
-        ArrayList<Task> tasksToAdd = new ArrayList<Task>();
-        TaskStack stack = new TaskStack();
+        RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(context, config, mSystemServicesProxy);
+        return plan;
+    }
 
-        int taskCount = tasks.size();
-        for (int i = 0; i < taskCount; i++) {
-            ActivityManager.RecentTaskInfo t = tasks.get(i);
+    public void preloadTasks(RecentsTaskLoadPlan plan, boolean isTopTaskHome) {
+        plan.preloadPlan(this, isTopTaskHome);
+    }
 
-            // 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);
-            }
-
-            Bitmap icon = t.taskDescription != null
-                    ? t.taskDescription.getInMemoryIcon()
-                    : null;
-            String iconFilename = t.taskDescription != null
-                    ? t.taskDescription.getIconFilename()
-                    : null;
-
-            // 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, icon, iconFilename);
-
-            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);
+    public void loadTasks(Context context, RecentsTaskLoadPlan plan,
+            RecentsTaskLoadPlan.Options opts) {
+        if (opts == null) {
+            throw new RuntimeException("Requires load options");
         }
-        stack.setTasks(tasksToAdd);
-        stack.createAffiliatedGroupings(config);
-        return stack;
+        plan.executePlan(opts, this);
+        if (opts.numVisibleTasks > 0) {
+            mNumVisibleTasksLoaded = opts.numVisibleTasks;
+        }
+
+        // Start the loader
+        mLoader.start(context);
     }
 
     /** Acquires the task resource data directly from the pool. */
@@ -591,24 +499,22 @@
             case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
                 // Stop the loader immediately when the UI is no longer visible
                 stopLoader();
-                mThumbnailCache.trimToSize(Math.max(
-                        Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount,
+                mThumbnailCache.trimToSize(Math.max(mNumVisibleTasksLoaded,
                         mMaxThumbnailCacheSize / 2));
-                mApplicationIconCache.trimToSize(Math.max(
-                        Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount,
+                mApplicationIconCache.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
-                mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 2);
-                mApplicationIconCache.trimToSize(mMaxIconCacheSize / 2);
+                mThumbnailCache.trimToSize(Math.max(1, mMaxThumbnailCacheSize / 2));
+                mApplicationIconCache.trimToSize(Math.max(1, 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);
+                mThumbnailCache.trimToSize(Math.max(1, mMaxThumbnailCacheSize / 4));
+                mApplicationIconCache.trimToSize(Math.max(1, mMaxIconCacheSize / 4));
                 break;
             case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
             case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index 6093584..77a050a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -216,6 +216,9 @@
 
     /** Requests all task stacks to start their exit-recents animation */
     public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
+        // We have to increment/decrement the post animation trigger in case there are no children
+        // to ensure that it runs
+        ctx.postAnimationTrigger.increment();
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
             View child = getChildAt(i);
@@ -224,6 +227,7 @@
                 stackView.startExitToHomeAnimation(ctx);
             }
         }
+        ctx.postAnimationTrigger.decrement();
 
         // Notify of the exit animation
         mCb.onExitToHomeAnimationTriggered();
@@ -503,7 +507,7 @@
 
         // Launch the app right away if there is no task view, otherwise, animate the icon out first
         if (tv == null) {
-            post(launchRunnable);
+            launchRunnable.run();
         } else {
             if (!task.group.isFrontMostTask(task)) {
                 // For affiliated tasks that are behind other tasks, we must animate the front cards
@@ -512,7 +516,7 @@
             } else {
                 // Otherwise, we can start the task transition immediately
                 stackView.startLaunchTaskAnimation(tv, null, lockToTask);
-                postDelayed(launchRunnable, 17);
+                launchRunnable.run();
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index bef4cd1..c3077b0 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -624,6 +624,14 @@
     }
 
     /**
+     * Computes the maximum number of visible tasks and thumbnails.  Requires that
+     * updateMinMaxScrollForStack() is called first.
+     */
+    public TaskStackViewLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() {
+        return mLayoutAlgorithm.computeStackVisibilityReport(mStack.getTasks());
+    }
+
+    /**
      * This is called with the full window width and height to allow stack view children to
      * perform the full screen transition down.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java
index c549d2b..5767e18 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java
@@ -35,6 +35,18 @@
     // These are all going to change
     static final float StackPeekMinScale = 0.8f; // The min scale of the last card in the peek area
 
+    // A report of the visibility state of the stack
+    public class VisibilityReport {
+        public int numVisibleTasks;
+        public int numVisibleThumbnails;
+
+        /** Package level ctor */
+        VisibilityReport(int tasks, int thumbnails) {
+            numVisibleTasks = tasks;
+            numVisibleThumbnails = thumbnails;
+        }
+    }
+
     RecentsConfiguration mConfig;
 
     // The various rects that define the stack view
@@ -117,7 +129,8 @@
         float pTaskHeightOffset = pAtBottomOfStackRect -
                 screenYToCurveProgress(mStackVisibleRect.bottom - taskHeight);
         float pNavBarOffset = pAtBottomOfStackRect -
-                screenYToCurveProgress(mStackVisibleRect.bottom - (mStackVisibleRect.bottom - mStackRect.bottom));
+                screenYToCurveProgress(mStackVisibleRect.bottom - (mStackVisibleRect.bottom -
+                        mStackRect.bottom));
 
         // Update the task offsets
         float pAtBackMostCardTop = 0.5f;
@@ -130,8 +143,8 @@
 
             if (i < (taskCount - 1)) {
                 // Increment the peek height
-                float pPeek = task.group.isFrontMostTask(task) ? pBetweenAffiliateOffset :
-                    pWithinAffiliateOffset;
+                float pPeek = task.group.isFrontMostTask(task) ?
+                        pBetweenAffiliateOffset : pWithinAffiliateOffset;
                 pAtSecondFrontMostCardTop = pAtFrontMostCardTop;
                 pAtFrontMostCardTop += pPeek;
             }
@@ -153,19 +166,72 @@
         mInitialScrollP = Math.max(0, mInitialScrollP);
     }
 
+    /**
+     * Computes the maximum number of visible tasks and thumbnails.  Requires that
+     * computeMinMaxScroll() is called first.
+     */
+    public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) {
+        if (tasks.size() <= 1) {
+            return new VisibilityReport(1, 1);
+        }
+
+        // Walk backwards in the task stack and count the number of tasks and visible thumbnails
+        int taskHeight = mTaskRect.height();
+        int numVisibleTasks = 1;
+        int numVisibleThumbnails = 1;
+        float progress = mTaskProgressMap.get(tasks.get(tasks.size() - 1).key) - mInitialScrollP;
+        int prevScreenY = curveProgressToScreenY(progress);
+        for (int i = tasks.size() - 2; i >= 0; i--) {
+            Task task = tasks.get(i);
+            progress = mTaskProgressMap.get(task.key) - mInitialScrollP;
+            if (progress < 0) {
+                break;
+            }
+            boolean isFrontMostTaskInGroup = task.group.isFrontMostTask(task);
+            if (isFrontMostTaskInGroup) {
+                float scaleAtP = curveProgressToScale(progress);
+                int scaleYOffsetAtP = (int) (((1f - scaleAtP) * taskHeight) / 2);
+                int screenY = curveProgressToScreenY(progress) + scaleYOffsetAtP;
+                boolean hasVisibleThumbnail = (prevScreenY - screenY) > mConfig.taskBarHeight;
+                if (hasVisibleThumbnail) {
+                    numVisibleThumbnails++;
+                    numVisibleTasks++;
+                    prevScreenY = screenY;
+                } else {
+                    // Once we hit the next front most task that does not have a visible thumbnail,
+                    // walk through remaining visible set
+                    for (int j = i; j >= 0; j--) {
+                        numVisibleTasks++;
+                        progress = mTaskProgressMap.get(tasks.get(j).key) - mInitialScrollP;
+                        if (progress < 0) {
+                            break;
+                        }
+                    }
+                    break;
+                }
+            } else if (!isFrontMostTaskInGroup) {
+                // Affiliated task, no thumbnail
+                numVisibleTasks++;
+            }
+        }
+        return new VisibilityReport(numVisibleTasks, numVisibleThumbnails);
+    }
+
     /** Update/get the transform */
-    public TaskViewTransform getStackTransform(Task task, float stackScroll, TaskViewTransform transformOut,
-            TaskViewTransform prevTransform) {
+    public TaskViewTransform getStackTransform(Task task, float stackScroll,
+            TaskViewTransform transformOut, TaskViewTransform prevTransform) {
         // Return early if we have an invalid index
         if (task == null || !mTaskProgressMap.containsKey(task.key)) {
             transformOut.reset();
             return transformOut;
         }
-        return getStackTransform(mTaskProgressMap.get(task.key), stackScroll, transformOut, prevTransform);
+        return getStackTransform(mTaskProgressMap.get(task.key), stackScroll, transformOut,
+                prevTransform);
     }
 
     /** Update/get the transform */
-    public TaskViewTransform getStackTransform(float taskProgress, float stackScroll, TaskViewTransform transformOut, TaskViewTransform prevTransform) {
+    public TaskViewTransform getStackTransform(float taskProgress, float stackScroll,
+            TaskViewTransform transformOut, TaskViewTransform prevTransform) {
         float pTaskRelative = taskProgress - stackScroll;
         float pBounded = Math.max(0, Math.min(pTaskRelative, 1f));
         // If the task top is outside of the bounds below the screen, then immediately reset it