Initial changes for recents.

Change-Id: Ide2c202b4a5b25410f0f32bd0a81ccf817ede38f
diff --git a/api/current.txt b/api/current.txt
index 1e7d548..0a0f148 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1295,6 +1295,8 @@
     field public static final int dialog_min_width_minor = 17104900; // 0x1050004
     field public static final int notification_large_icon_height = 17104902; // 0x1050006
     field public static final int notification_large_icon_width = 17104901; // 0x1050005
+    field public static final int recents_thumbnail_height = 17104903; // 0x1050007
+    field public static final int recents_thumbnail_width = 17104904; // 0x1050008
     field public static final int thumbnail_height = 17104897; // 0x1050001
     field public static final int thumbnail_width = 17104898; // 0x1050002
   }
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index c877cd3..7f7616f 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -933,6 +933,16 @@
         }
     }
 
+    /** @hide */
+    public boolean isInHomeStack(int taskId) {
+        try {
+            return ActivityManagerNative.getDefault().isInHomeStack(taskId);
+        } catch (RemoteException e) {
+            // System dead, we will be dead too soon!
+            return false;
+        }
+    }
+
     /**
      * Flag for {@link #moveTaskToFront(int, int)}: also move the "home"
      * activity along with the task, so it is positioned immediately behind
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index f4358e9..c7c81dd 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -654,6 +654,15 @@
             return true;
         }
 
+        case IS_IN_HOME_STACK_TRANSACTION: {
+            data.enforceInterface(IActivityManager.descriptor);
+            int taskId = data.readInt();
+            boolean isInHomeStack = isInHomeStack(taskId);
+            reply.writeNoException();
+            reply.writeInt(isInHomeStack ? 1 : 0);
+            return true;
+        }
+
         case SET_FOCUSED_STACK_TRANSACTION: {
             data.enforceInterface(IActivityManager.descriptor);
             int stackId = data.readInt();
@@ -2833,6 +2842,19 @@
         return info;
     }
     @Override
+    public boolean isInHomeStack(int taskId) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        data.writeInterfaceToken(IActivityManager.descriptor);
+        data.writeInt(taskId);
+        mRemote.transact(IS_IN_HOME_STACK_TRANSACTION, data, reply, 0);
+        reply.readException();
+        boolean isInHomeStack = reply.readInt() > 0;
+        data.recycle();
+        reply.recycle();
+        return isInHomeStack;
+    }
+    @Override
     public void setFocusedStack(int stackId) throws RemoteException
     {
         Parcel data = Parcel.obtain();
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 138eea2..9cfd85a 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -3017,11 +3017,17 @@
                 int h;
                 if (w < 0) {
                     Resources res = r.activity.getResources();
-                    mThumbnailHeight = h =
-                        res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
-
-                    mThumbnailWidth = w =
-                        res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
+                    if (SystemProperties.getBoolean("persist.recents.use_alternate", false)) {
+                        int wId = com.android.internal.R.dimen.recents_thumbnail_width;
+                        int hId = com.android.internal.R.dimen.recents_thumbnail_height;
+                        mThumbnailWidth = w = res.getDimensionPixelSize(wId);
+                        mThumbnailHeight = h = res.getDimensionPixelSize(hId);
+                    } else {
+                        mThumbnailHeight = h =
+                            res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
+                        mThumbnailWidth = w =
+                            res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
+                    }
                 } else {
                     h = mThumbnailHeight;
                 }
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 1943bba..f2cabf4 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -122,6 +122,7 @@
     public void resizeStack(int stackId, Rect bounds) throws RemoteException;
     public List<StackInfo> getAllStackInfos() throws RemoteException;
     public StackInfo getStackInfo(int stackId) throws RemoteException;
+    public boolean isInHomeStack(int taskId) throws RemoteException;
     public void setFocusedStack(int stackId) throws RemoteException;
     public int getTaskForActivity(IBinder token, boolean onlyRoot) throws RemoteException;
     /* oneway */
@@ -717,4 +718,5 @@
     // Start of L transactions
     int GET_TAG_FOR_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+210;
     int START_USER_IN_BACKGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+211;
+    int IS_IN_HOME_STACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+212;
 }
diff --git a/core/res/res/values-sw600dp/dimens.xml b/core/res/res/values-sw600dp/dimens.xml
index d21f9b7..8f83ab2 100644
--- a/core/res/res/values-sw600dp/dimens.xml
+++ b/core/res/res/values-sw600dp/dimens.xml
@@ -22,6 +22,10 @@
     <dimen name="thumbnail_width">200dp</dimen>
     <!-- The height that is used when creating thumbnails of applications. -->
     <dimen name="thumbnail_height">177dp</dimen>
+    <!-- The width that is used when creating thumbnails of applications. -->
+    <dimen name="recents_thumbnail_width">512dp</dimen>
+    <!-- The height that is used when creating thumbnails of applications. -->
+    <dimen name="recents_thumbnail_height">512dp</dimen>
     <!-- The maximum number of action buttons that should be permitted within
          an action bar/action mode. This will be used to determine how many
          showAsAction="ifRoom" items can fit. "always" items can override this. -->
diff --git a/core/res/res/values-sw720dp/dimens.xml b/core/res/res/values-sw720dp/dimens.xml
index ccdb4be..040bb5b 100644
--- a/core/res/res/values-sw720dp/dimens.xml
+++ b/core/res/res/values-sw720dp/dimens.xml
@@ -38,6 +38,10 @@
     <dimen name="thumbnail_width">230dp</dimen>
     <!-- The height that is used when creating thumbnails of applications. -->
     <dimen name="thumbnail_height">135dp</dimen>
+    <!-- The width that is used when creating thumbnails of applications. -->
+    <dimen name="recents_thumbnail_width">512dp</dimen>
+    <!-- The height that is used when creating thumbnails of applications. -->
+    <dimen name="recents_thumbnail_height">512dp</dimen>
 
     <!-- Preference activity, vertical padding for the header list -->
     <dimen name="preference_screen_header_vertical_padding">32dp</dimen>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 8c8c322..8ec2e6f 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -22,6 +22,10 @@
     <dimen name="thumbnail_width">164dp</dimen>
     <!-- The height that is used when creating thumbnails of applications. -->
     <dimen name="thumbnail_height">145dp</dimen>
+    <!-- The width that is used when creating thumbnails of applications. -->
+    <dimen name="recents_thumbnail_width">256dp</dimen>
+    <!-- The height that is used when creating thumbnails of applications. -->
+    <dimen name="recents_thumbnail_height">256dp</dimen>
     <!-- The standard size (both width and height) of an application icon that
          will be displayed in the app launcher and elsewhere. -->
     <dimen name="app_icon_size">48dip</dimen>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index c9d203f..d58e8ad 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2108,6 +2108,9 @@
   <public type="attr" name="requiredForProfile"/>
   <public type="attr" name="pinned" />
 
+  <public type="dimen" name="recents_thumbnail_height" />
+  <public type="dimen" name="recents_thumbnail_width" />
+
   <public type="id" name="shared_element_name" />
 
   <public type="style" name="Widget.Holo.FragmentBreadCrumbs" />
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 8d6fe41..b09cc1d 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -52,6 +52,7 @@
     <uses-permission android:name="android.permission.START_ANY_ACTIVITY" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.GET_TOP_ACTIVITY_INFO" />
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" />
 
     <!-- WindowManager -->
     <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
@@ -140,6 +141,18 @@
             </intent-filter>
         </receiver>
 
+        <!-- Alternate Recents -->
+        <activity android:name=".recents.RecentsActivity"
+                  android:launchMode="singleInstance"
+                  android:excludeFromRecents="true"
+                  android:theme="@style/RecentsTheme">
+            <intent-filter>
+                <action android:name="com.android.systemui.recents.TOGGLE_RECENTS" />
+            </intent-filter>
+        </activity>
+
+        <service android:name=".recents.RecentsService" />
+
         <!-- started from UsbDeviceSettingsManager -->
         <activity android:name=".usb.UsbConfirmActivity"
             android:exported="true"
diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags
index 1ff93d2..48d9722 100644
--- a/packages/SystemUI/proguard.flags
+++ b/packages/SystemUI/proguard.flags
@@ -6,6 +6,11 @@
   public void setGlowAlpha(float);
   public void setGlowScale(float);
 }
+-keep class com.android.systemui.recents.views.TaskIconView {
+	public void setCircularClipRadius(float);
+	public float getCircularClipRadius();
+}
 
 -keep class com.android.systemui.statusbar.phone.PhoneStatusBar
 -keep class com.android.systemui.statusbar.tv.TvStatusBar
+-keep class com.android.systemui.recents.*
\ No newline at end of file
diff --git a/packages/SystemUI/res/anim/recents_from_launcher_enter.xml b/packages/SystemUI/res/anim/recents_from_launcher_enter.xml
new file mode 100644
index 0000000..4bd7e82
--- /dev/null
+++ b/packages/SystemUI/res/anim/recents_from_launcher_enter.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2012, 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.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:shareInterpolator="false"
+     android:zAdjustment="top">
+  <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+         android:fillEnabled="true"
+         android:fillBefore="true" android:fillAfter="true"
+         android:interpolator="@android:interpolator/accelerate_cubic"
+         android:duration="250"/>
+</set>
diff --git a/packages/SystemUI/res/anim/recents_from_launcher_exit.xml b/packages/SystemUI/res/anim/recents_from_launcher_exit.xml
new file mode 100644
index 0000000..becc9d0
--- /dev/null
+++ b/packages/SystemUI/res/anim/recents_from_launcher_exit.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2012, 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.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:shareInterpolator="false"
+     android:zAdjustment="normal">
+  <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+         android:fillEnabled="true"
+         android:fillBefore="true" android:fillAfter="true"
+         android:interpolator="@android:interpolator/decelerate_cubic"
+         android:duration="250"/>
+</set>
diff --git a/packages/SystemUI/res/layout/recents_empty.xml b/packages/SystemUI/res/layout/recents_empty.xml
new file mode 100644
index 0000000..6268628
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_empty.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:textSize="40sp"
+    android:textColor="#ffffffff"
+    android:text="@string/recents_empty_message"
+    android:fontFamily="sans-serif-thin"
+    android:visibility="gone" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 94796af..ce05639 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -507,6 +507,9 @@
     <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_color_space_label">Color correction mode</string>
 
+    <!-- Recents: The empty recents string. [CHAR LIMIT=NONE] -->
+    <string name="recents_empty_message">RECENTS</string>
+
 
     <!-- Glyph to be overlaid atop the battery when the level is extremely low. Do not translate. -->
     <string name="battery_meter_very_low_overlay_symbol">!</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 54f03bd..14af020 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -20,6 +20,13 @@
         <item name="android:windowAnimationStyle">@style/Animation.RecentsActivity</item>
     </style>
 
+    <!-- Alternate Recents theme -->
+    <style name="RecentsTheme" parent="@android:style/Theme.Holo.Wallpaper.NoTitleBar">
+        <item name="android:windowTranslucentStatus">true</item>
+        <item name="android:windowTranslucentNavigation">true</item>
+        <item name="android:windowAnimationStyle">@style/Animation.RecentsActivity</item>
+    </style>
+
     <!-- Animations for a non-full-screen window or activity. -->
     <style name="Animation.RecentsActivity" parent="@android:style/Animation.Activity">
         <item name="android:activityOpenEnterAnimation">@anim/recents_launch_from_launcher_enter</item>
diff --git a/packages/SystemUI/src/com/android/systemui/recent/Recents.java b/packages/SystemUI/src/com/android/systemui/recent/Recents.java
index f5670e1..07c0c78 100644
--- a/packages/SystemUI/src/com/android/systemui/recent/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recent/Recents.java
@@ -16,37 +16,143 @@
 
 package com.android.systemui.recent;
 
+import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Matrix;
 import android.graphics.Paint;
+import android.graphics.Rect;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.Display;
+import android.view.Surface;
+import android.view.SurfaceControl;
 import android.view.View;
-
+import android.view.WindowManager;
 import com.android.systemui.R;
 import com.android.systemui.RecentsComponent;
 import com.android.systemui.SystemUI;
 
+import java.util.List;
+
+
 public class Recents extends SystemUI implements RecentsComponent {
+    /** A handler for messages from the recents implementation */
+    class RecentsMessageHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            if (!mUseAlternateRecents) return;
+            if (msg.what == MSG_UPDATE_FOR_CONFIGURATION) {
+                Resources res = mContext.getResources();
+                float statusBarHeight = res.getDimensionPixelSize(
+                        com.android.internal.R.dimen.status_bar_height);
+                mFirstTaskRect = (Rect) msg.getData().getParcelable("taskRect");
+                mFirstTaskRect.offset(0, (int) statusBarHeight);
+            }
+        }
+    }
+
+    /** A service connection to the recents implementation */
+    class RecentsServiceConnection implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            if (!mUseAlternateRecents) return;
+
+            Log.d(TAG, "[RecentsComponent|ServiceConnection|onServiceConnected] toggleRecents: " +
+                    mToggleRecentsUponServiceBound);
+            mService = new Messenger(service);
+            mServiceIsBound = true;
+
+            // Toggle recents if this service connection was triggered by hitting the recents button
+            if (mToggleRecentsUponServiceBound) {
+                startAlternateRecentsActivity();
+            }
+            mToggleRecentsUponServiceBound = false;
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName className) {
+            if (!mUseAlternateRecents) return;
+
+            Log.d(TAG, "[RecentsComponent|ServiceConnection|onServiceDisconnected]");
+            mService = null;
+            mServiceIsBound = false;
+        }
+    }
+
     private static final String TAG = "Recents";
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
+
+    final static int MSG_UPDATE_FOR_CONFIGURATION = 0;
+    final static int MSG_UPDATE_TASK_THUMBNAIL = 1;
+    final static int MSG_PRELOAD_TASKS = 2;
+    final static int MSG_CANCEL_PRELOAD_TASKS = 3;
+
+    final static String sToggleRecentsAction = "com.android.systemui.recents.TOGGLE_RECENTS";
+    final static String sRecentsPackage = "com.android.systemui";
+    final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity";
+    final static String sRecentsService = "com.android.systemui.recents.RecentsService";
+
+    // Which recents to use
+    boolean mUseAlternateRecents;
+
+    // Recents service binding
+    Messenger mService = null;
+    Messenger mMessenger;
+    boolean mServiceIsBound = false;
+    boolean mToggleRecentsUponServiceBound;
+    RecentsServiceConnection mConnection = new RecentsServiceConnection();
+
+    View mStatusBarView;
+    Rect mFirstTaskRect = new Rect();
+
+    public Recents() {
+        mMessenger = new Messenger(new RecentsMessageHandler());
+    }
 
     @Override
     public void start() {
+        mUseAlternateRecents =
+                SystemProperties.getBoolean("persist.recents.use_alternate", false);
+
         putComponent(RecentsComponent.class, this);
+
+        if (mUseAlternateRecents) {
+            Log.d(TAG, "[RecentsComponent|start]");
+
+            // Try to create a long-running connection to the recents service
+            bindToRecentsService(false);
+        }
     }
 
     @Override
     public void toggleRecents(Display display, int layoutDirection, View statusBarView) {
+        if (mUseAlternateRecents) {
+            // Launch the alternate recents if required
+            toggleAlternateRecents(display, layoutDirection, statusBarView);
+            return;
+        }
+
         if (DEBUG) Log.d(TAG, "toggle recents panel");
         try {
             TaskDescription firstTask = RecentTasksLoader.getInstance(mContext).getFirstTask();
@@ -190,33 +296,227 @@
         }
     }
 
+    /** Toggles the alternate recents activity */
+    public void toggleAlternateRecents(Display display, int layoutDirection, View statusBarView) {
+        if (!mUseAlternateRecents) return;
+
+        Log.d(TAG, "[RecentsComponent|toggleRecents] serviceIsBound: " + mServiceIsBound);
+        mStatusBarView = statusBarView;
+        if (!mServiceIsBound) {
+            // Try to create a long-running connection to the recents service before toggling
+            // recents
+            bindToRecentsService(true);
+            return;
+        }
+
+        try {
+            startAlternateRecentsActivity();
+        } catch (ActivityNotFoundException e) {
+            Log.e(TAG, "Failed to launch RecentAppsIntent", e);
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        if (mServiceIsBound) {
+            Resources res = mContext.getResources();
+            int statusBarHeight = res.getDimensionPixelSize(
+                    com.android.internal.R.dimen.status_bar_height);
+            int navBarHeight = res.getDimensionPixelSize(
+                    com.android.internal.R.dimen.navigation_bar_height);
+            Rect rect = new Rect();
+            WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+            wm.getDefaultDisplay().getRectSize(rect);
+
+            // Try and update the recents configuration
+            try {
+                Bundle data = new Bundle();
+                data.putParcelable("windowRect", rect);
+                data.putParcelable("systemInsets", new Rect(0, statusBarHeight, 0, 0));
+                Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0);
+                msg.setData(data);
+                msg.replyTo = mMessenger;
+                mService.send(msg);
+            } catch (RemoteException re) {
+                re.printStackTrace();
+            }
+        }
+    }
+
+    /** Binds to the recents implementation */
+    private void bindToRecentsService(boolean toggleRecentsUponConnection) {
+        if (!mUseAlternateRecents) return;
+
+        mToggleRecentsUponServiceBound = toggleRecentsUponConnection;
+        Intent intent = new Intent();
+        intent.setClassName(sRecentsPackage, sRecentsService);
+        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    /** Loads the first task thumbnail */
+    Bitmap loadFirstTaskThumbnail() {
+        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+        List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1,
+                ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier());
+        for (ActivityManager.RecentTaskInfo t : tasks) {
+            // Skip tasks in the home stack
+            if (am.isInHomeStack(t.persistentId)) {
+                return null;
+            }
+
+            Bitmap thumbnail = am.getTaskTopThumbnail(t.persistentId);
+            return thumbnail;
+        }
+        return null;
+    }
+
+    /** Returns whether there is a first task */
+    boolean hasFirstTask() {
+        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+        List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1,
+                ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier());
+        for (ActivityManager.RecentTaskInfo t : tasks) {
+            // Skip tasks in the home stack
+            if (am.isInHomeStack(t.persistentId)) {
+                continue;
+            }
+
+            return true;
+        }
+        return false;
+    }
+
+    /** Converts from the device rotation to the degree */
+    float getDegreesForRotation(int value) {
+        switch (value) {
+            case Surface.ROTATION_90:
+                return 360f - 90f;
+            case Surface.ROTATION_180:
+                return 360f - 180f;
+            case Surface.ROTATION_270:
+                return 360f - 270f;
+        }
+        return 0f;
+    }
+
+    /** Takes a screenshot of the surface */
+    Bitmap takeScreenshot(Display display) {
+        DisplayMetrics dm = new DisplayMetrics();
+        display.getRealMetrics(dm);
+        float[] dims = {dm.widthPixels, dm.heightPixels};
+        float degrees = getDegreesForRotation(display.getRotation());
+        boolean requiresRotation = (degrees > 0);
+        if (requiresRotation) {
+            // Get the dimensions of the device in its native orientation
+            Matrix m = new Matrix();
+            m.preRotate(-degrees);
+            m.mapPoints(dims);
+            dims[0] = Math.abs(dims[0]);
+            dims[1] = Math.abs(dims[1]);
+        }
+        return SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
+    }
+
+    /** Starts the recents activity */
+    void startAlternateRecentsActivity() {
+        Rect taskRect = mFirstTaskRect;
+        if (taskRect != null && taskRect.width() > 0 && taskRect.height() > 0 && hasFirstTask()) {
+            // Loading from thumbnail
+            Bitmap thumbnail;
+            Bitmap firstThumbnail = loadFirstTaskThumbnail();
+            if (firstThumbnail == null) {
+                // Load the thumbnail from the screenshot
+                WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+                Display display = wm.getDefaultDisplay();
+                Bitmap screenshot = takeScreenshot(display);
+                Resources res = mContext.getResources();
+                int size = Math.min(screenshot.getWidth(), screenshot.getHeight());
+                int statusBarHeight = res.getDimensionPixelSize(
+                        com.android.internal.R.dimen.status_bar_height);
+                thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(),
+                        Bitmap.Config.ARGB_8888);
+                Canvas c = new Canvas(thumbnail);
+                c.drawBitmap(screenshot, new Rect(0, statusBarHeight, size, statusBarHeight + size),
+                        new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null);
+                c.setBitmap(null);
+                // Recycle the old screenshot
+                screenshot.recycle();
+            } else {
+                // Create the thumbnail
+                thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(),
+                        Bitmap.Config.ARGB_8888);
+                int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight());
+                Canvas c = new Canvas(thumbnail);
+                c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size),
+                        new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null);
+                c.setBitmap(null);
+                // Recycle the old thumbnail
+                firstThumbnail.recycle();
+            }
+
+            ActivityOptions opts = ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView,
+                    thumbnail, mFirstTaskRect.left, mFirstTaskRect.top, null);
+            startAlternateRecentsActivity(opts);
+        } else {
+            ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
+                    R.anim.recents_from_launcher_enter,
+                    R.anim.recents_from_launcher_exit);
+            startAlternateRecentsActivity(opts);
+        }
+    }
+
+    /** Starts the recents activity */
+    void startAlternateRecentsActivity(ActivityOptions opts) {
+        Intent intent = new Intent(sToggleRecentsAction);
+        intent.setClassName(sRecentsPackage, sRecentsActivity);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+        if (opts != null) {
+            mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle(
+                    UserHandle.USER_CURRENT));
+        } else {
+            mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
+        }
+    }
+
     @Override
     public void preloadRecentTasksList() {
-        if (DEBUG) Log.d(TAG, "preloading recents");
-        Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT);
-        intent.setClassName("com.android.systemui",
-                "com.android.systemui.recent.RecentsPreloadReceiver");
-        mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
+        if (mUseAlternateRecents) {
+            Log.d(TAG, "[RecentsComponent|preloadRecents]");
+        } else {
+            Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT);
+            intent.setClassName("com.android.systemui",
+                    "com.android.systemui.recent.RecentsPreloadReceiver");
+            mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
 
-        RecentTasksLoader.getInstance(mContext).preloadFirstTask();
+            RecentTasksLoader.getInstance(mContext).preloadFirstTask();
+        }
     }
 
     @Override
     public void cancelPreloadingRecentTasksList() {
-        if (DEBUG) Log.d(TAG, "cancel preloading recents");
-        Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT);
-        intent.setClassName("com.android.systemui",
-                "com.android.systemui.recent.RecentsPreloadReceiver");
-        mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
+        if (mUseAlternateRecents) {
+            Log.d(TAG, "[RecentsComponent|cancelPreload]");
+        } else {
+            Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT);
+            intent.setClassName("com.android.systemui",
+                    "com.android.systemui.recent.RecentsPreloadReceiver");
+            mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
 
-        RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask();
+            RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask();
+        }
     }
 
     @Override
     public void closeRecents() {
-        if (DEBUG) Log.d(TAG, "closing recents panel");
-        Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT);
-        intent.setPackage("com.android.systemui");
-        mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
+        if (mUseAlternateRecents) {
+            Log.d(TAG, "[RecentsComponent|closeRecents]");
+        } else {
+            Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT);
+            intent.setPackage("com.android.systemui");
+            mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
+
+            RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask();
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Console.java b/packages/SystemUI/src/com/android/systemui/recents/Console.java
new file mode 100644
index 0000000..b3d9ccf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/Console.java
@@ -0,0 +1,96 @@
+/*
+ * 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.content.Context;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.widget.Toast;
+
+public class Console {
+    // Colors
+    public static final String AnsiReset = "\u001B[0m";
+    public static final String AnsiBlack = "\u001B[30m";
+    public static final String AnsiRed = "\u001B[31m";      // SystemUIHandshake
+    public static final String AnsiGreen = "\u001B[32m";    // MeasureAndLayout
+    public static final String AnsiYellow = "\u001B[33m";   // SynchronizeViewsWithModel
+    public static final String AnsiBlue = "\u001B[34m";     // TouchEvents
+    public static final String AnsiPurple = "\u001B[35m";   // Draw
+    public static final String AnsiCyan = "\u001B[36m";     // ClickEvents
+    public static final String AnsiWhite = "\u001B[37m";
+
+    /** Logs a key */
+    public static void log(String key) {
+        Console.log(true, key, "", AnsiReset);
+    }
+
+    /** Logs a conditioned key */
+    public static void log(boolean condition, String key) {
+        if (condition) {
+            Console.log(condition, key, "", AnsiReset);
+        }
+    }
+
+    /** Logs a key in a specific color */
+    public static void log(boolean condition, String key, Object data) {
+        if (condition) {
+            Console.log(condition, key, data, AnsiReset);
+        }
+    }
+
+    /** Logs a key with data in a specific color */
+    public static void log(boolean condition, String key, Object data, String color) {
+        if (condition) {
+            System.out.println(color + key + AnsiReset + " " + data.toString());
+        }
+    }
+
+    /** Logs an error */
+    public static void logError(Context context, String msg) {
+        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+        Log.e("Recents", msg);
+    }
+
+    /** Logs a divider bar */
+    public static void logDivider(boolean condition) {
+        if (condition) {
+            System.out.println("==== [" + System.currentTimeMillis() +
+                    "] ============================================================");
+        }
+    }
+
+    /** Returns the stringified MotionEvent action */
+    public static String motionEventActionToString(int action) {
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                return "Down";
+            case MotionEvent.ACTION_UP:
+                return "Up";
+            case MotionEvent.ACTION_MOVE:
+                return "Move";
+            case MotionEvent.ACTION_CANCEL:
+                return "Cancel";
+            case MotionEvent.ACTION_POINTER_DOWN:
+                return "Pointer Down";
+            case MotionEvent.ACTION_POINTER_UP:
+                return "Pointer Up";
+            default:
+                return "" + action;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java
new file mode 100644
index 0000000..aeae4ab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+/**
+ * Constants
+ * XXX: We are going to move almost all of these into a resource.
+ */
+public class Constants {
+    public static class DebugFlags {
+        // Enable this with any other debug flag to see more info
+        public static final boolean Verbose = false;
+
+        public static class App {
+            public static final boolean EnableTaskFiltering = false;
+            public static final boolean EnableTaskStackClipping = false;
+            public static final boolean EnableBackgroundTaskLoading = true;
+            public static final boolean ForceDisableBackgroundCache = false;
+            public static final boolean TaskDataLoader = false;
+            public static final boolean SystemUIHandshake = false;
+            public static final boolean TimeSystemCalls = false;
+        }
+
+        public static class UI {
+            public static final boolean Draw = false;
+            public static final boolean ClickEvents = false;
+            public static final boolean TouchEvents = false;
+            public static final boolean MeasureAndLayout = false;
+            public static final boolean Clipping = false;
+            public static final boolean HwLayers = true;
+        }
+
+        public static class TaskStack {
+            public static final boolean SynchronizeViewsWithModel = false;
+        }
+
+        public static class ViewPool {
+            public static final boolean PoolCallbacks = false;
+        }
+    }
+
+    public static class Values {
+        public static class Window {
+            public static final float DarkBackgroundDim = 0.5f;
+            public static final float BackgroundDim = 0.35f;
+        }
+
+        public static class RecentsTaskLoader {
+            // XXX: This should be calculated on the first load
+            public static final int PreloadFirstTasksCount = 5;
+            public static final int TaskEntryMultiplier = 1;
+        }
+
+        public static class TaskStackView {
+            public static class Animation {
+                public static final int TaskRemovedReshuffleDuration = 200;
+                public static final int SnapScrollBackDuration = 650;
+                public static final int SwipeDismissDuration = 350;
+                public static final int SwipeSnapBackDuration = 350;
+            }
+
+            // The padding will be applied to the smallest dimension, and then applied to all sides
+            public static final float StackPaddingPct = 0.15f;
+            // The overlap height relative to the task height
+            public static final float StackOverlapPct = 0.65f;
+            // The height of the peek space relative to the stack height
+            public static final float StackPeekHeightPct = 0.1f;
+            // The min scale of the last card in the peek area
+            public static final float StackPeekMinScale = 0.9f;
+            // The number of cards we see in the peek space
+            public static final int StackPeekNumCards = 3;
+        }
+
+        public static class TaskView {
+            public static class Animation {
+                public static final int TaskDataUpdatedFadeDuration = 250;
+                public static final int TaskIconCircularClipInDuration = 225;
+                public static final int TaskIconCircularClipOutDuration = 85;
+            }
+
+            public static final boolean AnimateFrontTaskIconOnEnterRecents = true;
+            public static final boolean AnimateFrontTaskIconOnLeavingRecents = true;
+            public static final boolean AnimateFrontTaskIconOnLeavingUseClip = false;
+            public static final boolean DrawColoredTaskBars = false;
+            public static final boolean UseRoundedCorners = true;
+            public static final float RoundedCornerRadiusDps = 3;
+
+            public static final float TaskBarHeightDps = 54;
+            public static final float TaskIconSizeDps = 60;
+        }
+    }
+
+    // UNMIGRATED CONSTANTS:
+
+    /** Determines whether to layout the stack vertically in landscape mode */
+    public static final boolean LANDSCAPE_LAYOUT_VERTICAL_STACK = true;
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
new file mode 100644
index 0000000..d050847
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -0,0 +1,176 @@
+/*
+ * 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.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import com.android.systemui.recents.model.SpaceNode;
+import com.android.systemui.recents.model.TaskStack;
+import com.android.systemui.recents.views.RecentsView;
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+
+
+/* Activity */
+public class RecentsActivity extends Activity {
+    FrameLayout mContainerView;
+    RecentsView mRecentsView;
+    View mEmptyView;
+    boolean mVisible;
+
+    /** Updates the set of recent tasks */
+    void updateRecentsTasks() {
+        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+        SpaceNode root = loader.reload(this, Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount);
+        ArrayList<TaskStack> stacks = root.getStacks();
+        if (!stacks.isEmpty()) {
+            // XXX: We just replace the root every time for now, we will change this in the future
+            mRecentsView.setBSP(root);
+        }
+
+        // Add the default no-recents layout
+        if (stacks.size() == 1 && stacks.get(0).getTaskCount() == 0) {
+            mEmptyView.setVisibility(View.VISIBLE);
+
+            // Dim the background even more
+            WindowManager.LayoutParams wlp = getWindow().getAttributes();
+            wlp.dimAmount = Constants.Values.Window.DarkBackgroundDim;
+            getWindow().setAttributes(wlp);
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+        } else {
+            mEmptyView.setVisibility(View.GONE);
+        }
+    }
+
+    /** Dismisses recents if we are already visible and the intent is to toggle the recents view */
+    boolean dismissRecentsIfVisible(Intent intent) {
+        if ("com.android.systemui.recents.TOGGLE_RECENTS".equals(intent.getAction())) {
+            if (mVisible) {
+                if (!mRecentsView.launchFirstTask()) {
+                    finish();
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Called with the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Console.logDivider(Constants.DebugFlags.App.SystemUIHandshake);
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onCreate]",
+                getIntent().getAction() + " visible: " + mVisible, Console.AnsiRed);
+
+        // Initialize the loader and the configuration
+        RecentsTaskLoader.initialize(this);
+        RecentsConfiguration.reinitialize(this);
+
+        // Dismiss recents if it is visible and we are toggling
+        if (dismissRecentsIfVisible(getIntent())) return;
+
+        // Set the background dim
+        WindowManager.LayoutParams wlp = getWindow().getAttributes();
+        wlp.dimAmount = Constants.Values.Window.BackgroundDim;
+        getWindow().setAttributes(wlp);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+        // Create the view hierarchy
+        mRecentsView = new RecentsView(this);
+        mRecentsView.setLayoutParams(new FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.MATCH_PARENT,
+                FrameLayout.LayoutParams.MATCH_PARENT));
+
+        // Create the empty view
+        LayoutInflater inflater = LayoutInflater.from(this);
+        mEmptyView = inflater.inflate(R.layout.recents_empty, mContainerView, false);
+
+        mContainerView = new FrameLayout(this);
+        mContainerView.addView(mRecentsView);
+        mContainerView.addView(mEmptyView);
+        setContentView(mContainerView);
+
+        // Update the recent tasks
+        updateRecentsTasks();
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        Console.logDivider(Constants.DebugFlags.App.SystemUIHandshake);
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onNewIntent]",
+                intent.getAction() + " visible: " + mVisible, Console.AnsiRed);
+
+        // Dismiss recents if it is visible and we are toggling
+        if (dismissRecentsIfVisible(intent)) return;
+
+        // Initialize the loader and the configuration
+        RecentsTaskLoader.initialize(this);
+        RecentsConfiguration.reinitialize(this);
+
+        // Update the recent tasks
+        updateRecentsTasks();
+    }
+
+    @Override
+    protected void onStart() {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onStart]", "",
+                Console.AnsiRed);
+        super.onStart();
+        mVisible = true;
+    }
+
+    @Override
+    protected void onResume() {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onResume]", "",
+                Console.AnsiRed);
+        super.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onPause]", "",
+                Console.AnsiRed);
+        super.onPause();
+
+        // Stop the loader when we leave Recents
+        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+        loader.stopLoader();
+    }
+
+    @Override
+    protected void onStop() {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onStop]", "",
+                Console.AnsiRed);
+        super.onStop();
+        mVisible = false;
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (!mRecentsView.unfilterFilteredStacks()) {
+            super.onBackPressed();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java
new file mode 100644
index 0000000..f3881ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java
@@ -0,0 +1,76 @@
+/*
+ * 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.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+
+
+/** A static Recents configuration for the current context
+ * NOTE: We should not hold any references to a Context from a static instance */
+public class RecentsConfiguration {
+    static RecentsConfiguration sInstance;
+
+    DisplayMetrics mDisplayMetrics;
+
+    public boolean layoutVerticalStack;
+    public Rect systemInsets = new Rect();
+
+    /** Private constructor */
+    private RecentsConfiguration() {}
+
+    /** Updates the configuration to the current context */
+    public static RecentsConfiguration reinitialize(Context context) {
+        if (sInstance == null) {
+            sInstance = new RecentsConfiguration();
+        }
+        sInstance.update(context);
+        return sInstance;
+    }
+
+    /** Returns the current recents configuration */
+    public static RecentsConfiguration getInstance() {
+        return sInstance;
+    }
+
+    /** Updates the state, given the specified context */
+    void update(Context context) {
+        mDisplayMetrics = context.getResources().getDisplayMetrics();
+
+        boolean isPortrait = context.getResources().getConfiguration().orientation ==
+                Configuration.ORIENTATION_PORTRAIT;
+        layoutVerticalStack = isPortrait || Constants.LANDSCAPE_LAYOUT_VERTICAL_STACK;
+    }
+
+    public void updateSystemInsets(Rect insets) {
+        systemInsets.set(insets);
+    }
+
+    /** Converts from DPs to PXs */
+    public int pxFromDp(float size) {
+        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                size, mDisplayMetrics));
+    }
+    /** Converts from SPs to PXs */
+    public int pxFromSp(float size) {
+        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
+                size, mDisplayMetrics));
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java
new file mode 100644
index 0000000..522ab0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java
@@ -0,0 +1,103 @@
+/*
+ * 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.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import com.android.systemui.recents.model.TaskStack;
+import com.android.systemui.recents.views.TaskStackView;
+import com.android.systemui.recents.views.TaskViewTransform;
+
+
+/* Service */
+public class RecentsService extends Service {
+    // XXX: This should be getting the message from recents definition
+    final static int MSG_UPDATE_RECENTS_FOR_CONFIGURATION = 0;
+
+    class MessageHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|handleMessage]", msg);
+            if (msg.what == MSG_UPDATE_RECENTS_FOR_CONFIGURATION) {
+                Context context = RecentsService.this;
+                RecentsTaskLoader.initialize(context);
+                RecentsConfiguration.reinitialize(context);
+
+                try {
+                    Bundle data = msg.getData();
+                    Rect windowRect = (Rect) data.getParcelable("windowRect");
+                    Rect systemInsets = (Rect) data.getParcelable("systemInsets");
+                    RecentsConfiguration.getInstance().updateSystemInsets(systemInsets);
+
+                    // Create a dummy task stack & compute the rect for the thumbnail to animate to
+                    TaskStack stack = new TaskStack(context);
+                    TaskStackView tsv = new TaskStackView(context, stack);
+                    tsv.computeRects(windowRect.width(), windowRect.height() - systemInsets.top);
+                    tsv.boundScroll();
+                    TaskViewTransform transform = tsv.getStackTransform(0);
+
+                    data.putParcelable("taskRect", transform.rect);
+                    Message reply = Message.obtain(null, MSG_UPDATE_RECENTS_FOR_CONFIGURATION, 0, 0);
+                    reply.setData(data);
+                    msg.replyTo.send(reply);
+                } catch (RemoteException re) {
+                    re.printStackTrace();
+                }
+            }
+        }
+    }
+
+    Messenger mMessenger = new Messenger(new MessageHandler());
+
+    @Override
+    public void onCreate() {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onCreate]");
+        super.onCreate();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onBind]");
+        return mMessenger.getBinder();
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onUnbind]");
+        return super.onUnbind(intent);
+    }
+
+    @Override
+    public void onRebind(Intent intent) {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onRebind]");
+        super.onRebind(intent);
+    }
+
+    @Override
+    public void onDestroy() {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onDestroy]");
+        super.onDestroy();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java
new file mode 100644
index 0000000..c303ca7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java
@@ -0,0 +1,463 @@
+/*
+ * 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.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+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 com.android.systemui.recents.model.SpaceNode;
+import com.android.systemui.recents.model.Task;
+import com.android.systemui.recents.model.TaskStack;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+
+/** A bitmap load queue */
+class TaskResourceLoadQueue {
+    ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>();
+
+    Task nextTask() {
+        Console.log(Constants.DebugFlags.App.TaskDataLoader, "  [TaskResourceLoadQueue|nextTask]");
+        return mQueue.poll();
+    }
+
+    void addTask(Task t) {
+        Console.log(Constants.DebugFlags.App.TaskDataLoader, "  [TaskResourceLoadQueue|addTask]");
+        if (!mQueue.contains(t)) {
+            mQueue.add(t);
+        }
+        synchronized(this) {
+            notifyAll();
+        }
+    }
+
+    void removeTask(Task t) {
+        Console.log(Constants.DebugFlags.App.TaskDataLoader, "  [TaskResourceLoadQueue|removeTask]");
+        mQueue.remove(t);
+    }
+
+    void clearTasks() {
+        Console.log(Constants.DebugFlags.App.TaskDataLoader, "  [TaskResourceLoadQueue|clearTasks]");
+        mQueue.clear();
+    }
+
+    boolean isEmpty() {
+        return mQueue.isEmpty();
+    }
+}
+
+/* Task resource loader */
+class TaskResourceLoader implements Runnable {
+    Context mContext;
+    HandlerThread mLoadThread;
+    Handler mLoadThreadHandler;
+    Handler mMainThreadHandler;
+
+    TaskResourceLoadQueue mLoadQueue;
+    DrawableLruCache mIconCache;
+    BitmapLruCache mThumbnailCache;
+    boolean mCancelled;
+
+    /** Constructor, creates a new loading thread that loads task resources in the background */
+    public TaskResourceLoader(TaskResourceLoadQueue loadQueue, DrawableLruCache iconCache,
+                              BitmapLruCache thumbnailCache) {
+        mLoadQueue = loadQueue;
+        mIconCache = iconCache;
+        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;
+        // 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;
+    }
+
+    @Override
+    public void run() {
+        while (true) {
+            Console.log(Constants.DebugFlags.App.TaskDataLoader,
+                    "[TaskResourceLoader|run|" + Thread.currentThread().getId() + "]");
+            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 {
+                        Console.log(Constants.DebugFlags.App.TaskDataLoader,
+                                "[TaskResourceLoader|waitOnLoadThreadCancelled]");
+                        mLoadThread.wait();
+                    } catch (InterruptedException ie) {
+                        ie.printStackTrace();
+                    }
+                }
+            } else {
+                // Load the next item from the queue
+                final Task t = mLoadQueue.nextTask();
+                if (t != null) {
+                    try {
+                        Drawable cachedIcon = mIconCache.get(t);
+                        Bitmap cachedThumbnail = mThumbnailCache.get(t);
+                        Console.log(Constants.DebugFlags.App.TaskDataLoader,
+                                "  [TaskResourceLoader|load]",
+                                t + " icon: " + cachedIcon + " thumbnail: " + cachedThumbnail);
+                        // Load the icon
+                        if (cachedIcon == null) {
+                            PackageManager pm = mContext.getPackageManager();
+                            ActivityInfo info = pm.getActivityInfo(t.intent.getComponent(),
+                                    PackageManager.GET_META_DATA);
+                            Drawable icon = info.loadIcon(pm);
+                            if (!mCancelled) {
+                                Console.log(Constants.DebugFlags.App.TaskDataLoader,
+                                        "    [TaskResourceLoader|loadIcon]",
+                                        icon);
+                                t.icon = icon;
+                                mIconCache.put(t, icon);
+                            }
+                        }
+                        // Load the thumbnail
+                        if (cachedThumbnail == null) {
+                            ActivityManager am = (ActivityManager)
+                                    mContext.getSystemService(Context.ACTIVITY_SERVICE);
+                            Bitmap thumbnail = am.getTaskTopThumbnail(t.id);
+                            if (!mCancelled) {
+                                if (thumbnail != null) {
+                                    Console.log(Constants.DebugFlags.App.TaskDataLoader,
+                                            "    [TaskResourceLoader|loadThumbnail]",
+                                            thumbnail);
+                                    t.thumbnail = thumbnail;
+                                    mThumbnailCache.put(t, thumbnail);
+                                } else {
+                                    Console.logError(mContext,
+                                            "Failed to load task top thumbnail for: " +
+                                                    t.intent.getComponent().getPackageName());
+                                }
+                            }
+                        }
+                        if (!mCancelled) {
+                            // Notify that the task data has changed
+                            mMainThreadHandler.post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    t.notifyTaskDataChanged();
+                                }
+                            });
+                        }
+                    } catch (PackageManager.NameNotFoundException ne) {
+                        ne.printStackTrace();
+                    }
+                }
+
+                // 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]");
+                            mLoadQueue.wait();
+                        } catch (InterruptedException ie) {
+                            ie.printStackTrace();
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+/** The drawable cache */
+class DrawableLruCache extends LruCache<Task, Drawable> {
+    public DrawableLruCache(int cacheSize) {
+        super(cacheSize);
+    }
+
+    @Override
+    protected int sizeOf(Task 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 */
+class BitmapLruCache extends LruCache<Task, Bitmap> {
+    public BitmapLruCache(int cacheSize) {
+        super(cacheSize);
+    }
+
+    @Override
+    protected int sizeOf(Task t, Bitmap bitmap) {
+        // The cache size will be measured in kilobytes rather than number of items
+        return bitmap.getByteCount() / 1024;
+    }
+}
+
+/* Recents task loader
+ * NOTE: We should not hold any references to a Context from a static instance */
+public class RecentsTaskLoader {
+    static RecentsTaskLoader sInstance;
+
+    DrawableLruCache mIconCache;
+    BitmapLruCache mThumbnailCache;
+    TaskResourceLoadQueue mLoadQueue;
+    TaskResourceLoader mLoader;
+
+    BitmapDrawable mDefaultIcon;
+    Bitmap mDefaultThumbnail;
+
+    /** Private Constructor */
+    private RecentsTaskLoader(Context context) {
+        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+        int iconCacheSize = Constants.DebugFlags.App.ForceDisableBackgroundCache ? 1 : maxMemory / 16;
+        int thumbnailCacheSize = Constants.DebugFlags.App.ForceDisableBackgroundCache ? 1 : maxMemory / 8;
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake,
+                "[RecentsTaskLoader|init]", "thumbnailCache: " + thumbnailCacheSize +
+                " iconCache: " + iconCacheSize);
+        mLoadQueue = new TaskResourceLoadQueue();
+        mIconCache = new DrawableLruCache(iconCacheSize);
+        mThumbnailCache = new BitmapLruCache(thumbnailCacheSize);
+        mLoader = new TaskResourceLoader(mLoadQueue, mIconCache, 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);
+        mDefaultIcon = new BitmapDrawable(context.getResources(), icon);
+        Console.log(Constants.DebugFlags.App.TaskDataLoader,
+                "[RecentsTaskLoader|defaultBitmaps]",
+                "icon: " + mDefaultIcon + " 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;
+    }
+
+    /** Reload the set of recent tasks */
+    SpaceNode reload(Context context, int preloadCount) {
+        Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsTaskLoader|reload]");
+        TaskStack stack = new TaskStack(context);
+        SpaceNode root = new SpaceNode(context);
+        root.setStack(stack);
+        try {
+            long t1 = System.currentTimeMillis();
+
+            PackageManager pm = context.getPackageManager();
+            ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+            // Get the recent tasks
+            List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(25,
+                    ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier());
+            Collections.reverse(tasks);
+            Console.log(Constants.DebugFlags.App.TimeSystemCalls,
+                    "[RecentsTaskLoader|getRecentTasks]",
+                    "" + (System.currentTimeMillis() - t1) + "ms");
+            Console.log(Constants.DebugFlags.App.SystemUIHandshake,
+                    "[RecentsTaskLoader|tasks]", "" + tasks.size());
+
+            // Remove home/recents tasks
+            Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator();
+            while (iter.hasNext()) {
+                ActivityManager.RecentTaskInfo t = iter.next();
+
+                // Skip tasks in the home stack
+                if (am.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);
+
+                // Load the label, icon and thumbnail
+                ActivityInfo info = pm.getActivityInfo(t.baseIntent.getComponent(),
+                        PackageManager.GET_META_DATA);
+                String title = info.loadLabel(pm).toString();
+                Drawable icon = null;
+                Bitmap thumbnail = null;
+                Task task;
+                if (i >= (taskCount - preloadCount) || !Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
+                    Console.log(Constants.DebugFlags.App.SystemUIHandshake,
+                            "[RecentsTaskLoader|preloadTask]",
+                            "i: " + i + " task: " + t.baseIntent.getComponent().getPackageName());
+                    icon = info.loadIcon(pm);
+                    thumbnail = am.getTaskTopThumbnail(t.id);
+                    for (int j = 0; j < Constants.Values.RecentsTaskLoader.TaskEntryMultiplier; j++) {
+                        Console.log(Constants.DebugFlags.App.SystemUIHandshake,
+                                "  [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName());
+                        task = new Task(t.persistentId, t.baseIntent, title, icon, thumbnail);
+                        if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
+                            if (thumbnail != null) mThumbnailCache.put(task, thumbnail);
+                            if (icon != null) {
+                                mIconCache.put(task, icon);
+                            }
+                        }
+                        stack.addTask(task);
+                    }
+                } else {
+                    for (int j = 0; j < Constants.Values.RecentsTaskLoader.TaskEntryMultiplier; j++) {
+                        Console.log(Constants.DebugFlags.App.SystemUIHandshake,
+                                "  [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName());
+                        task = new Task(t.persistentId, t.baseIntent, title, null, null);
+                        stack.addTask(task);
+                    }
+                }
+
+                /*
+                if (stacks.containsKey(t.stackId)) {
+                    builder = stacks.get(t.stackId);
+                } else {
+                    builder = new TaskStackBuilder();
+                    stacks.put(t.stackId, builder);
+                }
+                */
+            }
+            Console.log(Constants.DebugFlags.App.TimeSystemCalls,
+                    "[RecentsTaskLoader|getAllTaskTopThumbnail]",
+                    "" + (System.currentTimeMillis() - t1) + "ms");
+
+            /*
+            // Get all the stacks
+            t1 = System.currentTimeMillis();
+            List<ActivityManager.StackInfo> stackInfos = ams.getAllStackInfos();
+            Console.log(Constants.DebugFlags.App.TimeSystemCalls, "[RecentsTaskLoader|getAllStackInfos]", "" + (System.currentTimeMillis() - t1) + "ms");
+            Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsTaskLoader|stacks]", "" + tasks.size());
+            for (ActivityManager.StackInfo s : stackInfos) {
+                Console.log(Constants.DebugFlags.App.SystemUIHandshake, "  [RecentsTaskLoader|stack]", s.toString());
+                if (stacks.containsKey(s.stackId)) {
+                    stacks.get(s.stackId).setRect(s.bounds);
+                }
+            }
+            */
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        mLoader.start(context);
+        return root;
+    }
+
+    /** Acquires the task resource data from the pool.
+     * XXX: Move this into Task? */
+    public void loadTaskData(Task t) {
+        if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
+            t.icon = mIconCache.get(t);
+            t.thumbnail = mThumbnailCache.get(t);
+
+            Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|loadTask]",
+                    t + " icon: " + t.icon + " thumbnail: " + t.thumbnail);
+
+            boolean requiresLoad = false;
+            if (t.icon == null) {
+                t.icon = mDefaultIcon;
+                requiresLoad = true;
+            }
+            if (t.thumbnail == null) {
+                t.thumbnail = mDefaultThumbnail;
+                requiresLoad = true;
+            }
+            if (requiresLoad) {
+                mLoadQueue.addTask(t);
+            }
+        }
+    }
+
+    /** Releases the task resource data back into the pool.
+     * XXX: Move this into Task? */
+    public void unloadTaskData(Task t) {
+        if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
+            Console.log(Constants.DebugFlags.App.TaskDataLoader,
+                    "[RecentsTaskLoader|unloadTask]", t);
+            mLoadQueue.removeTask(t);
+            t.icon = mDefaultIcon;
+            t.thumbnail = mDefaultThumbnail;
+        }
+    }
+
+    /** Completely removes the resource data from the pool.
+     * XXX: Move this into Task? */
+    public void deleteTaskData(Task t) {
+        if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
+            Console.log(Constants.DebugFlags.App.TaskDataLoader,
+                    "[RecentsTaskLoader|deleteTask]", t);
+            mLoadQueue.removeTask(t);
+            mThumbnailCache.remove(t);
+            mIconCache.remove(t);
+        }
+        t.icon = mDefaultIcon;
+        t.thumbnail = mDefaultThumbnail;
+    }
+
+    /** Stops the task loader */
+    void stopLoader() {
+        Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|stopLoader]");
+        mLoader.stop();
+        mLoadQueue.clearTasks();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/Utilities.java
new file mode 100644
index 0000000..33e4246
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/Utilities.java
@@ -0,0 +1,39 @@
+/*
+ * 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.graphics.Rect;
+
+/* Common code */
+public class Utilities {
+    public static final Rect tmpRect = new Rect();
+    public static final Rect tmpRect2 = new Rect();
+
+    /** Scales a rect about its centroid */
+    public static void scaleRectAboutCenter(Rect r, float scale) {
+        if (scale != 1.0f) {
+            int cx = r.centerX();
+            int cy = r.centerY();
+            r.offset(-cx, -cy);
+            r.left = (int) (r.left * scale + 0.5f);
+            r.top = (int) (r.top * scale + 0.5f);
+            r.right = (int) (r.right * scale + 0.5f);
+            r.bottom = (int) (r.bottom * scale + 0.5f);
+            r.offset(cx, cy);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java
new file mode 100644
index 0000000..5893abc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java
@@ -0,0 +1,69 @@
+/*
+ * 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.content.Context;
+
+import java.util.ArrayList;
+
+
+/**
+ * The full recents space is partitioned using a BSP into various nodes that define where task
+ * stacks should be placed.
+ */
+public class SpaceNode {
+    Context mContext;
+
+    SpaceNode mStartNode;
+    SpaceNode mEndNode;
+
+    TaskStack mStack;
+
+    public SpaceNode(Context context) {
+        mContext = context;
+    }
+
+    /** Sets the current stack for this space node */
+    public void setStack(TaskStack stack) {
+        mStack = stack;
+    }
+
+    /** Returns the task stack (not null if this is a leaf) */
+    TaskStack getStack() {
+        return mStack;
+    }
+
+    /** Returns whether this is a leaf node */
+    boolean isLeafNode() {
+        return (mStartNode == null) && (mEndNode == null);
+    }
+
+    /** Returns all the descendent task stacks */
+    private void getStacksRec(ArrayList<TaskStack> stacks) {
+        if (isLeafNode()) {
+            stacks.add(mStack);
+        } else {
+            mStartNode.getStacksRec(stacks);
+            mEndNode.getStacksRec(stacks);
+        }
+    }
+    public ArrayList<TaskStack> getStacks() {
+        ArrayList<TaskStack> stacks = new ArrayList<TaskStack>();
+        getStacksRec(stacks);
+        return stacks;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNodeCallbacks.java b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNodeCallbacks.java
new file mode 100644
index 0000000..31b02e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNodeCallbacks.java
@@ -0,0 +1,28 @@
+/*
+ * 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.graphics.Rect;
+
+
+/* BSP node callbacks */
+public interface SpaceNodeCallbacks {
+    /** Notifies when a node is added */
+    public void onSpaceNodeAdded(SpaceNode node);
+    /** Notifies when a node is measured */
+    public void onSpaceNodeMeasured(SpaceNode node, Rect rect);
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
new file mode 100644
index 0000000..9b03c5d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
@@ -0,0 +1,76 @@
+/*
+ * 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.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import com.android.systemui.recents.Constants;
+
+
+/**
+ * A task represents the top most task in the system's task stack.
+ */
+public class Task {
+    public final int id;
+    public final Intent intent;
+    public String title;
+    public Drawable icon;
+    public Bitmap thumbnail;
+
+    TaskCallbacks mCb;
+
+    public Task(int id, Intent intent, String activityTitle, Drawable icon, Bitmap thumbnail) {
+        this.id = id;
+        this.intent = intent;
+        this.title = activityTitle;
+        this.icon = icon;
+        this.thumbnail = thumbnail;
+    }
+
+    /** Set the callbacks */
+    public void setCallbacks(TaskCallbacks cb) {
+        mCb = cb;
+    }
+
+    /** Notifies the callback listeners that this task's data has changed */
+    public void notifyTaskDataChanged() {
+        if (mCb != null) {
+            mCb.onTaskDataChanged(this);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        // If we have multiple task entries for the same task, then we do the simple object
+        // equality check
+        if (Constants.Values.RecentsTaskLoader.TaskEntryMultiplier > 1) {
+            return super.equals(o);
+        }
+
+        // Otherwise, check that the id and intent match (the other fields can be asynchronously
+        // loaded and is unsuitable to testing the identity of this Task)
+        Task t = (Task) o;
+        return (id == t.id) &&
+                (intent.equals(t.intent));
+    }
+
+    @Override
+    public String toString() {
+        return "Task: " + intent.getComponent().getPackageName() + " [" + super.toString() + "]";
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskCallbacks.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskCallbacks.java
new file mode 100644
index 0000000..169f56c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskCallbacks.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+/* Task callbacks */
+public interface TaskCallbacks {
+    /* Notifies when a task's data has been updated */
+    public void onTaskDataChanged(Task task);
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
new file mode 100644
index 0000000..a5aa387
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
@@ -0,0 +1,231 @@
+/*
+ * 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.content.Context;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * An interface for a task filter to query whether a particular task should show in a stack.
+ */
+interface TaskFilter {
+    /** Returns whether the filter accepts the specified task */
+    public boolean acceptTask(Task t, int index);
+}
+
+/**
+ * A list of filtered tasks.
+ */
+class FilteredTaskList {
+    ArrayList<Task> mTasks = new ArrayList<Task>();
+    ArrayList<Task> mFilteredTasks = new ArrayList<Task>();
+    TaskFilter mFilter;
+
+    /** Sets the task filter, saving the current touch state */
+    void setFilter(TaskFilter filter) {
+        mFilter = filter;
+        updateFilteredTasks();
+    }
+
+    /** Removes the task filter and returns the previous touch state */
+    void removeFilter() {
+        mFilter = null;
+        updateFilteredTasks();
+    }
+
+    /** Adds a new task to the task list */
+    void add(Task t) {
+        mTasks.add(t);
+        updateFilteredTasks();
+    }
+
+    /** Sets the list of tasks */
+    void set(List<Task> tasks) {
+        mTasks.clear();
+        mTasks.addAll(tasks);
+        updateFilteredTasks();
+    }
+
+    /** Removes a task from the base list only if it is in the filtered list */
+    boolean remove(Task t) {
+        if (mFilteredTasks.contains(t)) {
+            boolean removed = mTasks.remove(t);
+            updateFilteredTasks();
+            return removed;
+        }
+        return false;
+    }
+
+    /** Returns the index of this task in the list of filtered tasks */
+    int indexOf(Task t) {
+        return mFilteredTasks.indexOf(t);
+    }
+
+    /** Returns the size of the list of filtered tasks */
+    int size() {
+        return mFilteredTasks.size();
+    }
+
+    /** Returns whether the filtered list contains this task */
+    boolean contains(Task t) {
+        return mFilteredTasks.contains(t);
+    }
+
+    /** Updates the list of filtered tasks whenever the base task list changes */
+    private void updateFilteredTasks() {
+        mFilteredTasks.clear();
+        if (mFilter != null) {
+            int taskCount = mTasks.size();
+            for (int i = 0; i < taskCount; i++) {
+                Task t = mTasks.get(i);
+                if (mFilter.acceptTask(t, i)) {
+                    mFilteredTasks.add(t);
+                }
+            }
+        } else {
+            mFilteredTasks.addAll(mTasks);
+        }
+    }
+
+    /** Returns whether this task list is filtered */
+    boolean hasFilter() {
+        return (mFilter != null);
+    }
+
+    /** Returns the list of filtered tasks */
+    ArrayList<Task> getTasks() {
+        return mFilteredTasks;
+    }
+}
+
+/**
+ * The task stack contains a list of multiple tasks.
+ */
+public class TaskStack {
+    Context mContext;
+
+    FilteredTaskList mTaskList = new FilteredTaskList();
+    TaskStackCallbacks mCb;
+
+    public TaskStack(Context context) {
+        mContext = context;
+    }
+
+    /** Sets the callbacks for this task stack */
+    public void setCallbacks(TaskStackCallbacks cb) {
+        mCb = cb;
+    }
+
+    /** Adds a new task */
+    public void addTask(Task t) {
+        mTaskList.add(t);
+        if (mCb != null) {
+            mCb.onStackTaskAdded(this, t);
+        }
+    }
+
+    /** Removes a task */
+    public void removeTask(Task t) {
+        if (mTaskList.contains(t)) {
+            mTaskList.remove(t);
+            if (mCb != null) {
+                mCb.onStackTaskRemoved(this, t);
+            }
+        }
+    }
+
+    /** Sets a few tasks in one go */
+    public void setTasks(List<Task> tasks) {
+        int taskCount = mTaskList.getTasks().size();
+        for (int i = 0; i < taskCount; i++) {
+            Task t = mTaskList.getTasks().get(i);
+            if (mCb != null) {
+                mCb.onStackTaskRemoved(this, t);
+            }
+        }
+        mTaskList.set(tasks);
+        for (Task t : tasks) {
+            if (mCb != null) {
+                mCb.onStackTaskAdded(this, t);
+            }
+        }
+    }
+
+    /** Gets the tasks */
+    public ArrayList<Task> getTasks() {
+        return mTaskList.getTasks();
+    }
+
+    /** Gets the number of tasks */
+    public int getTaskCount() {
+        return mTaskList.size();
+    }
+
+    /** Returns the index of this task in this current task stack */
+    public int indexOfTask(Task t) {
+        return mTaskList.indexOf(t);
+    }
+
+    /** Tests whether a task is in this current task stack */
+    public boolean containsTask(Task t) {
+        return mTaskList.contains(t);
+    }
+
+    /** Filters the stack into tasks similar to the one specified */
+    public void filterTasks(Task t) {
+        // Set the task list filter
+        // XXX: This is a dummy filter that currently just accepts every other task.
+        mTaskList.setFilter(new TaskFilter() {
+            @Override
+            public boolean acceptTask(Task t, int i) {
+                if (i % 2 == 0) {
+                    return true;
+                }
+                return false;
+            }
+        });
+        if (mCb != null) {
+            mCb.onStackFiltered(this);
+        }
+    }
+
+    /** Unfilters the current stack */
+    public void unfilterTasks() {
+        // Unset the filter, then update the virtual scroll
+        mTaskList.removeFilter();
+        if (mCb != null) {
+            mCb.onStackUnfiltered(this);
+        }
+    }
+
+    /** Returns whether tasks are currently filtered */
+    public boolean hasFilteredTasks() {
+        return mTaskList.hasFilter();
+    }
+
+    @Override
+    public String toString() {
+        String str = "Tasks:\n";
+        for (Task t : mTaskList.getTasks()) {
+            str += "  " + t.toString() + "\n";
+        }
+        return str;
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStackCallbacks.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStackCallbacks.java
new file mode 100644
index 0000000..4bec655
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStackCallbacks.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/* Task stack callbacks */
+public interface TaskStackCallbacks {
+    /* Notifies when a task has been added to the stack */
+    public void onStackTaskAdded(TaskStack stack, Task t);
+    /* Notifies when a task has been removed from the stack */
+    public void onStackTaskRemoved(TaskStack stack, Task t);
+    /** Notifies when the stack was filtered */
+    public void onStackFiltered(TaskStack stack);
+    /** Notifies when the stack was un-filtered */
+    public void onStackUnfiltered(TaskStack stack);
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
new file mode 100644
index 0000000..c92041c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -0,0 +1,228 @@
+/*
+ * 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.views;
+
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.UserHandle;
+import android.view.View;
+import android.widget.FrameLayout;
+import com.android.systemui.recents.Console;
+import com.android.systemui.recents.Constants;
+import com.android.systemui.recents.RecentsConfiguration;
+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;
+
+
+/**
+ * This view is the the top level layout that contains TaskStacks (which are laid out according
+ * to their SpaceNode bounds.
+ */
+public class RecentsView extends FrameLayout implements TaskStackViewCallbacks {
+    // The space partitioning root of this container
+    SpaceNode mBSP;
+
+    public RecentsView(Context context) {
+        super(context);
+        setWillNotDraw(false);
+    }
+
+    /** Set/get the bsp root node */
+    public void setBSP(SpaceNode n) {
+        mBSP = n;
+
+        // XXX: We shouldn't be recereating new stacks every time, but for now, that is OK
+        // Add all the stacks for this partition
+        removeAllViews();
+        ArrayList<TaskStack> stacks = mBSP.getStacks();
+        for (TaskStack stack : stacks) {
+            TaskStackView stackView = new TaskStackView(getContext(), stack);
+            stackView.setCallbacks(this);
+            addView(stackView);
+        }
+    }
+
+    /** Launches the first task from the first stack if possible */
+    public boolean launchFirstTask() {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            TaskStackView stackView = (TaskStackView) getChildAt(i);
+            TaskStack stack = stackView.mStack;
+            ArrayList<Task> tasks = stack.getTasks();
+            if (!tasks.isEmpty()) {
+                Task task = tasks.get(tasks.size() - 1);
+                TaskView tv = null;
+                if (stackView.getChildCount() > 0) {
+                    TaskView stv = (TaskView) stackView.getChildAt(stackView.getChildCount() - 1);
+                    if (stv.getTask() == task) {
+                        tv = stv;
+                    }
+                }
+                onTaskLaunched(stackView, tv, stack, task);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|measure]", "width: " + width + " height: " + height, Console.AnsiGreen);
+
+        // We measure our stack views sans the status bar.  It will handle the nav bar itself.
+        RecentsConfiguration config = RecentsConfiguration.getInstance();
+        int childHeight = height - config.systemInsets.top;
+
+        // Measure each child
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                child.measure(widthMeasureSpec,
+                        MeasureSpec.makeMeasureSpec(childHeight, heightMode));
+            }
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|layout]", new Rect(left, top, right, bottom) + " changed: " + changed, Console.AnsiGreen);
+        // We offset our stack views by the status bar height.  It will handle the nav bar itself.
+        RecentsConfiguration config = RecentsConfiguration.getInstance();
+        top += config.systemInsets.top;
+
+        // Layout each child
+        // XXX: Based on the space node for that task view
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final int width = child.getMeasuredWidth();
+                final int height = child.getMeasuredHeight();
+                child.layout(left, top, left + width, top + height);
+            }
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        Console.log(Constants.DebugFlags.UI.Draw, "[RecentsView|dispatchDraw]", "", Console.AnsiPurple);
+        super.dispatchDraw(canvas);
+    }
+
+    @Override
+    protected boolean fitSystemWindows(Rect insets) {
+        Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|fitSystemWindows]", "insets: " + insets, Console.AnsiGreen);
+
+        // Update the configuration with the latest system insets and trigger a relayout
+        RecentsConfiguration config = RecentsConfiguration.getInstance();
+        config.updateSystemInsets(insets);
+        requestLayout();
+
+        return true;
+    }
+
+    /** Unfilters any filtered stacks */
+    public boolean unfilterFilteredStacks() {
+        if (mBSP != null) {
+            // Check if there are any filtered stacks and unfilter them before we back out of Recents
+            boolean stacksUnfiltered = false;
+            ArrayList<TaskStack> stacks = mBSP.getStacks();
+            for (TaskStack stack : stacks) {
+                if (stack.hasFilteredTasks()) {
+                    stack.unfilterTasks();
+                    stacksUnfiltered = true;
+                }
+            }
+            return stacksUnfiltered;
+        }
+        return false;
+    }
+
+    /**** View.OnClickListener Implementation ****/
+
+    @Override
+    public void onTaskLaunched(final TaskStackView stackView, final TaskView tv,
+                               final TaskStack stack, final Task task) {
+        final Runnable launchRunnable = new Runnable() {
+            @Override
+            public void run() {
+                TaskViewTransform transform;
+                View sourceView = tv;
+                int offsetX = 0;
+                int offsetY = 0;
+                if (tv == null) {
+                    // Launch the activity
+                    sourceView = stackView;
+                    transform = stackView.getStackTransform(stack.indexOfTask(task));
+                    offsetX = transform.rect.left;
+                    offsetY = transform.rect.top;
+                } else {
+                    transform = stackView.getStackTransform(stack.indexOfTask(task));
+                }
+
+                // Compute the thumbnail to scale up from
+                ActivityOptions opts = null;
+                int thumbnailWidth = transform.rect.width();
+                int thumbnailHeight = transform.rect.height();
+                if (task.thumbnail != null && thumbnailWidth > 0 && thumbnailHeight > 0 &&
+                        task.thumbnail.getWidth() > 0 && task.thumbnail.getHeight() > 0) {
+                    // Resize the thumbnail to the size of the view that we are animating from
+                    Bitmap b = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight,
+                            Bitmap.Config.ARGB_8888);
+                    Canvas c = new Canvas(b);
+                    c.drawBitmap(task.thumbnail,
+                            new Rect(0, 0, task.thumbnail.getWidth(), task.thumbnail.getHeight()),
+                            new Rect(0, 0, thumbnailWidth, thumbnailHeight), null);
+                    c.setBitmap(null);
+                    opts = ActivityOptions.makeThumbnailScaleUpAnimation(sourceView,
+                            b, offsetX, offsetY);
+                }
+
+                // Launch the activity with the desired animation
+                Intent i = new Intent(task.intent);
+                i.setFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
+                        | Intent.FLAG_ACTIVITY_TASK_ON_HOME
+                        | Intent.FLAG_ACTIVITY_NEW_TASK);
+                if (opts != null) {
+                    getContext().startActivityAsUser(i, opts.toBundle(), UserHandle.CURRENT);
+                } else {
+                    getContext().startActivityAsUser(i, UserHandle.CURRENT);
+                }
+            }
+        };
+
+        // Launch the app right away if there is no task view, otherwise, animate the icon out first
+        if (tv == null || !Constants.Values.TaskView.AnimateFrontTaskIconOnLeavingRecents) {
+            launchRunnable.run();
+        } else {
+            tv.animateOnLeavingRecents(launchRunnable);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java
new file mode 100644
index 0000000..fe661bc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java
@@ -0,0 +1,389 @@
+/*
+ * 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.views;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+
+/**
+ * This class facilitates swipe to dismiss. It defines an interface to be implemented by the
+ * by the class hosting the views that need to swiped, and, using this interface, handles touch
+ * events and translates / fades / animates the view as it is dismissed.
+ */
+public class SwipeHelper {
+    static final String TAG = "SwipeHelper";
+    private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
+    private static final boolean CONSTRAIN_SWIPE = true;
+    private static final boolean FADE_OUT_DURING_SWIPE = true;
+    private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
+
+    public static final int X = 0;
+    public static final int Y = 1;
+
+    private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
+
+    private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
+    private int DEFAULT_ESCAPE_ANIMATION_DURATION = 75; // ms
+    private int MAX_ESCAPE_ANIMATION_DURATION = 150; // ms
+    private int MAX_DISMISS_VELOCITY = 2000; // dp/sec
+    private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
+
+    public static float ALPHA_FADE_START = 0.15f; // fraction of thumbnail width
+                                                 // where fade starts
+    static final float ALPHA_FADE_END = 0.65f; // fraction of thumbnail width
+                                              // beyond which alpha->0
+    private float mMinAlpha = 0f;
+
+    private float mPagingTouchSlop;
+    Callback mCallback;
+    private int mSwipeDirection;
+    private VelocityTracker mVelocityTracker;
+
+    private float mInitialTouchPos;
+    private boolean mDragging;
+
+    private View mCurrView;
+    private boolean mCanCurrViewBeDimissed;
+    private float mDensityScale;
+
+    public boolean mAllowSwipeTowardsStart = true;
+    public boolean mAllowSwipeTowardsEnd = true;
+    private boolean mRtl;
+
+    public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
+            float pagingTouchSlop) {
+        mCallback = callback;
+        mSwipeDirection = swipeDirection;
+        mVelocityTracker = VelocityTracker.obtain();
+        mDensityScale = densityScale;
+        mPagingTouchSlop = pagingTouchSlop;
+    }
+
+    public void setDensityScale(float densityScale) {
+        mDensityScale = densityScale;
+    }
+
+    public void setPagingTouchSlop(float pagingTouchSlop) {
+        mPagingTouchSlop = pagingTouchSlop;
+    }
+
+    public void cancelOngoingDrag() {
+        if (mDragging) {
+            if (mCurrView != null) {
+                mCallback.onDragCancelled(mCurrView);
+                setTranslation(mCurrView, 0);
+                mCallback.onSnapBackCompleted(mCurrView);
+                mCurrView = null;
+            }
+            mDragging = false;
+        }
+    }
+
+    public void resetTranslation(View v) {
+        setTranslation(v, 0);
+    }
+
+    private float getPos(MotionEvent ev) {
+        return mSwipeDirection == X ? ev.getX() : ev.getY();
+    }
+
+    private float getTranslation(View v) {
+        return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
+    }
+
+    private float getVelocity(VelocityTracker vt) {
+        return mSwipeDirection == X ? vt.getXVelocity() :
+                vt.getYVelocity();
+    }
+
+    private ObjectAnimator createTranslationAnimation(View v, float newPos) {
+        ObjectAnimator anim = ObjectAnimator.ofFloat(v,
+                mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos);
+        return anim;
+    }
+
+    private float getPerpendicularVelocity(VelocityTracker vt) {
+        return mSwipeDirection == X ? vt.getYVelocity() :
+                vt.getXVelocity();
+    }
+
+    private void setTranslation(View v, float translate) {
+        if (mSwipeDirection == X) {
+            v.setTranslationX(translate);
+        } else {
+            v.setTranslationY(translate);
+        }
+    }
+
+    private float getSize(View v) {
+        final DisplayMetrics dm = v.getContext().getResources().getDisplayMetrics();
+        return mSwipeDirection == X ? dm.widthPixels : dm.heightPixels;
+    }
+
+    public void setMinAlpha(float minAlpha) {
+        mMinAlpha = minAlpha;
+    }
+
+    float getAlphaForOffset(View view) {
+        float viewSize = getSize(view);
+        final float fadeSize = ALPHA_FADE_END * viewSize;
+        float result = 1.0f;
+        float pos = getTranslation(view);
+        if (pos >= viewSize * ALPHA_FADE_START) {
+            result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
+        } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
+            result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
+        }
+        result = Math.min(result, 1.0f);
+        result = Math.max(result, 0f);
+        return Math.max(mMinAlpha, result);
+    }
+
+    /**
+     * Determines whether the given view has RTL layout.
+     */
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+    public static boolean isLayoutRtl(View view) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection();
+        } else {
+            return false;
+        }
+    }
+
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        final int action = ev.getAction();
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mDragging = false;
+                mCurrView = mCallback.getChildAtPosition(ev);
+                mVelocityTracker.clear();
+                if (mCurrView != null) {
+                    mRtl = isLayoutRtl(mCurrView);
+                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
+                    mVelocityTracker.addMovement(ev);
+                    mInitialTouchPos = getPos(ev);
+                } else {
+                    mCanCurrViewBeDimissed = false;
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (mCurrView != null) {
+                    mVelocityTracker.addMovement(ev);
+                    float pos = getPos(ev);
+                    float delta = pos - mInitialTouchPos;
+                    if (Math.abs(delta) > mPagingTouchSlop) {
+                        mCallback.onBeginDrag(mCurrView);
+                        mDragging = true;
+                        mInitialTouchPos = getPos(ev) - getTranslation(mCurrView);
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mDragging = false;
+                mCurrView = null;
+                break;
+        }
+        return mDragging;
+    }
+
+    /**
+     * @param view The view to be dismissed
+     * @param velocity The desired pixels/second speed at which the view should move
+     */
+    private void dismissChild(final View view, float velocity) {
+        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
+        float newPos;
+        if (velocity < 0
+                || (velocity == 0 && getTranslation(view) < 0)
+                // if we use the Menu to dismiss an item in landscape, animate up
+                || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) {
+            newPos = -getSize(view);
+        } else {
+            newPos = getSize(view);
+        }
+        int duration = MAX_ESCAPE_ANIMATION_DURATION;
+        if (velocity != 0) {
+            duration = Math.min(duration,
+                                (int) (Math.abs(newPos - getTranslation(view)) *
+                                        1000f / Math.abs(velocity)));
+        } else {
+            duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
+        }
+
+        ValueAnimator anim = createTranslationAnimation(view, newPos);
+        anim.setInterpolator(sLinearInterpolator);
+        anim.setDuration(duration);
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mCallback.onChildDismissed(view);
+                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
+                    view.setAlpha(1.f);
+                }
+            }
+        });
+        anim.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
+                    view.setAlpha(getAlphaForOffset(view));
+                }
+            }
+        });
+        anim.start();
+    }
+
+    private void snapChild(final View view, float velocity) {
+        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
+        ValueAnimator anim = createTranslationAnimation(view, 0);
+        int duration = SNAP_ANIM_LEN;
+        anim.setDuration(duration);
+        anim.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
+                    view.setAlpha(getAlphaForOffset(view));
+                }
+            }
+        });
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
+                    view.setAlpha(1.0f);
+                }
+                mCallback.onSnapBackCompleted(view);
+            }
+        });
+        anim.start();
+    }
+
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!mDragging) {
+            if (!onInterceptTouchEvent(ev)) {
+                return mCanCurrViewBeDimissed;
+            }
+        }
+
+        mVelocityTracker.addMovement(ev);
+        final int action = ev.getAction();
+        switch (action) {
+            case MotionEvent.ACTION_OUTSIDE:
+            case MotionEvent.ACTION_MOVE:
+                if (mCurrView != null) {
+                    float delta = getPos(ev) - mInitialTouchPos;
+                    setSwipeAmount(delta);
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                if (mCurrView != null) {
+                    endSwipe(mVelocityTracker);
+                }
+                break;
+        }
+        return true;
+    }
+
+    private void setSwipeAmount(float amount) {
+        // don't let items that can't be dismissed be dragged more than
+        // maxScrollDistance
+        if (CONSTRAIN_SWIPE
+                && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) {
+            float size = getSize(mCurrView);
+            float maxScrollDistance = 0.15f * size;
+            if (Math.abs(amount) >= size) {
+                amount = amount > 0 ? maxScrollDistance : -maxScrollDistance;
+            } else {
+                amount = maxScrollDistance * (float) Math.sin((amount/size)*(Math.PI/2));
+            }
+        }
+        setTranslation(mCurrView, amount);
+        if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
+            float alpha = getAlphaForOffset(mCurrView);
+            mCurrView.setAlpha(alpha);
+        }
+    }
+
+    private boolean isValidSwipeDirection(float amount) {
+        if (mSwipeDirection == X) {
+            if (mRtl) {
+                return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart;
+            } else {
+                return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd;
+            }
+        }
+
+        // Vertical swipes are always valid.
+        return true;
+    }
+
+    private void endSwipe(VelocityTracker velocityTracker) {
+        float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
+        velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
+        float velocity = getVelocity(velocityTracker);
+        float perpendicularVelocity = getPerpendicularVelocity(velocityTracker);
+        float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
+        float translation = getTranslation(mCurrView);
+        // Decide whether to dismiss the current view
+        boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
+                Math.abs(translation) > 0.6 * getSize(mCurrView);
+        boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
+                (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
+                (velocity > 0) == (translation > 0);
+
+        boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
+                && isValidSwipeDirection(translation)
+                && (childSwipedFastEnough || childSwipedFarEnough);
+
+        if (dismissChild) {
+            // flingadingy
+            dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
+        } else {
+            // snappity
+            mCallback.onDragCancelled(mCurrView);
+            snapChild(mCurrView, velocity);
+        }
+    }
+
+    public interface Callback {
+        View getChildAtPosition(MotionEvent ev);
+
+        boolean canChildBeDismissed(View v);
+
+        void onBeginDrag(View v);
+
+        void onChildDismissed(View v);
+
+        void onSnapBackCompleted(View v);
+
+        void onDragCancelled(View v);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
new file mode 100644
index 0000000..9dd6c0b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -0,0 +1,1075 @@
+/*
+ * 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.views;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+import android.widget.FrameLayout;
+import android.widget.OverScroller;
+import android.widget.Toast;
+import com.android.systemui.recents.Console;
+import com.android.systemui.recents.Constants;
+import com.android.systemui.recents.RecentsConfiguration;
+import com.android.systemui.recents.RecentsTaskLoader;
+import com.android.systemui.recents.Utilities;
+import com.android.systemui.recents.model.Task;
+import com.android.systemui.recents.model.TaskStack;
+import com.android.systemui.recents.model.TaskStackCallbacks;
+
+import java.util.ArrayList;
+
+/** The TaskView callbacks */
+interface TaskStackViewCallbacks {
+    public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t);
+}
+
+/* The visual representation of a task stack view */
+public class TaskStackView extends FrameLayout implements TaskStackCallbacks, TaskViewCallbacks,
+        ViewPoolConsumer<TaskView, Task>, View.OnClickListener {
+    TaskStack mStack;
+    TaskStackViewTouchHandler mTouchHandler;
+    TaskStackViewCallbacks mCb;
+    ViewPool<TaskView, Task> mViewPool;
+
+    // The various rects that define the stack view
+    Rect mRect = new Rect();
+    Rect mStackRect = new Rect();
+    Rect mStackRectSansPeek = new Rect();
+    Rect mTaskRect = new Rect();
+
+    // The virtual stack scroll that we use for the card layout
+    int mStackScroll;
+    int mMinScroll;
+    int mMaxScroll;
+    OverScroller mScroller;
+    ObjectAnimator mScrollAnimator;
+
+    // Optimizations
+    int mHwLayersRefCount;
+    int mStackViewsAnimationDuration;
+    boolean mStackViewsDirty = true;
+    boolean mAwaitingFirstLayout = true;
+
+    public TaskStackView(Context context, TaskStack stack) {
+        super(context);
+        mStack = stack;
+        mStack.setCallbacks(this);
+        mScroller = new OverScroller(context);
+        mTouchHandler = new TaskStackViewTouchHandler(context, this);
+        mViewPool = new ViewPool<TaskView, Task>(context, this);
+    }
+
+    /** Sets the callbacks */
+    void setCallbacks(TaskStackViewCallbacks cb) {
+        mCb = cb;
+    }
+
+    /** Requests that the views be synchronized with the model */
+    void requestSynchronizeStackViewsWithModel() {
+        requestSynchronizeStackViewsWithModel(0);
+    }
+    void requestSynchronizeStackViewsWithModel(int duration) {
+        Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel,
+                "[TaskStackView|requestSynchronize]", "", Console.AnsiYellow);
+        if (!mStackViewsDirty) {
+            invalidate();
+        }
+        if (mAwaitingFirstLayout) {
+            // Skip the animation if we are awaiting first layout
+            mStackViewsAnimationDuration = 0;
+        } else {
+            mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration);
+        }
+        mStackViewsDirty = true;
+    }
+
+    // XXX: Optimization: Use a mapping of Task -> View
+    private TaskView getChildViewForTask(Task t) {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            TaskView tv = (TaskView) getChildAt(i);
+            if (tv.getTask() == t) {
+                return tv;
+            }
+        }
+        return null;
+    }
+
+    /** Update/get the transform */
+    public TaskViewTransform getStackTransform(int indexInStack) {
+        TaskViewTransform transform = new TaskViewTransform();
+
+        // Map the items to an continuous position relative to the current scroll
+        int numPeekCards = Constants.Values.TaskStackView.StackPeekNumCards;
+        float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height();
+        float peekHeight = Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height();
+        float t = ((indexInStack * overlapHeight) - getStackScroll()) / overlapHeight;
+        float boundedT = Math.max(t, -(numPeekCards + 1));
+
+        // Set the scale relative to its position
+        float minScale = Constants.Values.TaskStackView.StackPeekMinScale;
+        float scaleRange = 1f - minScale;
+        float scaleInc = scaleRange / numPeekCards;
+        float scale = Math.max(minScale, Math.min(1f, 1f + (boundedT * scaleInc)));
+        float scaleYOffset = ((1f - scale) * mTaskRect.height()) / 2;
+        transform.scale = scale;
+
+        // Set the translation
+        if (boundedT < 0f) {
+            transform.translationY = (int) ((Math.max(-numPeekCards, boundedT) /
+                    numPeekCards) * peekHeight - scaleYOffset);
+        } else {
+            transform.translationY = (int) (boundedT * overlapHeight - scaleYOffset);
+        }
+
+        // Update the rect and visibility
+        transform.rect.set(mTaskRect);
+        if (t < -(numPeekCards + 1)) {
+            transform.visible = false;
+        } else {
+            transform.rect.offset(0, transform.translationY);
+            Utilities.scaleRectAboutCenter(transform.rect, scale);
+            transform.visible = Rect.intersects(mRect, transform.rect);
+        }
+        transform.t = t;
+        return transform;
+    }
+
+    /** Synchronizes the views with the model */
+    void synchronizeStackViewsWithModel() {
+        Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel,
+                "[TaskStackView|synchronizeViewsWithModel]",
+                "mStackViewsDirty: " + mStackViewsDirty, Console.AnsiYellow);
+        if (mStackViewsDirty) {
+
+            // XXX: Optimization: Use binary search to find the visible range
+            // XXX: Optimize to not call getStackTransform() so many times
+            // XXX: Consider using TaskViewTransform pool to prevent allocations
+            // XXX: Iterate children views, update transforms and remove all that are not visible
+            //      For all remaining tasks, update transforms and if visible add the view
+
+            // Update the visible state of all the tasks
+            ArrayList<Task> tasks = mStack.getTasks();
+            int taskCount = tasks.size();
+            for (int i = 0; i < taskCount; i++) {
+                Task task = tasks.get(i);
+                TaskViewTransform transform = getStackTransform(i);
+                TaskView tv = getChildViewForTask(task);
+
+                if (transform.visible) {
+                    if (tv == null) {
+                        tv = mViewPool.pickUpViewFromPool(task, task);
+                        // When we are picking up a new view from the view pool, prepare it for any
+                        // following animation by putting it in a reasonable place
+                        if (mStackViewsAnimationDuration > 0 && i != 0) {
+                            // XXX: We have to animate when filtering, etc. Maybe we should have a
+                            //      runnable that ensures that tasks are animated in a special way
+                            //      when they are entering the scene?
+                            int fromIndex = (transform.t < 0) ? (i - 1) : (i + 1);
+                            tv.updateViewPropertiesFromTask(null, getStackTransform(fromIndex), 0);
+                        }
+                    }
+                } else {
+                    if (tv != null) {
+                        mViewPool.returnViewToPool(tv);
+                    }
+                }
+            }
+
+            // Update all the current view children
+            // NOTE: We have to iterate in reverse where because we are removing views directly
+            int childCount = getChildCount();
+            for (int i = childCount - 1; i >= 0; i--) {
+                TaskView tv = (TaskView) getChildAt(i);
+                Task task = tv.getTask();
+                TaskViewTransform transform = getStackTransform(mStack.indexOfTask(task));
+                if (!transform.visible) {
+                    mViewPool.returnViewToPool(tv);
+                } else {
+                    tv.updateViewPropertiesFromTask(null, transform, mStackViewsAnimationDuration);
+                }
+            }
+
+            Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel,
+                    "  [TaskStackView|viewChildren]", "" + getChildCount());
+
+            mStackViewsAnimationDuration = 0;
+            mStackViewsDirty = false;
+        }
+    }
+
+    /** Sets the current stack scroll */
+    public void setStackScroll(int value) {
+        mStackScroll = value;
+        requestSynchronizeStackViewsWithModel();
+    }
+
+    /** Gets the current stack scroll */
+    public int getStackScroll() {
+        return mStackScroll;
+    }
+
+    /** Animates the stack scroll into bounds */
+    ObjectAnimator animateBoundScroll(int duration) {
+        int curScroll = getStackScroll();
+        int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
+        if (newScroll != curScroll) {
+            // Enable hw layers on the stack
+            addHwLayersRefCount();
+
+            // Abort any current animations
+            mScroller.abortAnimation();
+            if (mScrollAnimator != null) {
+                mScrollAnimator.cancel();
+                mScrollAnimator.removeAllListeners();
+            }
+
+            // Start a new scroll animation
+            mScrollAnimator = ObjectAnimator.ofInt(this, "stackScroll", curScroll, newScroll);
+            mScrollAnimator.setDuration(duration);
+            mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    setStackScroll((Integer) animation.getAnimatedValue());
+                }
+            });
+            mScrollAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    // Disable hw layers on the stack
+                    decHwLayersRefCount();
+                }
+            });
+            mScrollAnimator.start();
+        }
+        return mScrollAnimator;
+    }
+
+    /** Aborts any current stack scrolls */
+    void abortBoundScrollAnimation() {
+        if (mScrollAnimator != null) {
+            mScrollAnimator.cancel();
+        }
+    }
+
+    /** Bounds the current scroll if necessary */
+    public boolean boundScroll() {
+        int curScroll = getStackScroll();
+        int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
+        if (newScroll != curScroll) {
+            setStackScroll(newScroll);
+            return true;
+        }
+        return false;
+    }
+
+    /** Returns whether the current scroll is out of bounds */
+    boolean isScrollOutOfBounds() {
+        return (getStackScroll() < 0) || (getStackScroll() > mMaxScroll);
+    }
+
+    /** Updates the min and max virtual scroll bounds */
+    void updateMinMaxScroll(boolean boundScrollToNewMinMax) {
+        // Compute the min and max scroll values
+        int numTasks = Math.max(1, mStack.getTaskCount());
+        int taskHeight = mTaskRect.height();
+        int stackHeight = mStackRectSansPeek.height();
+        int maxScrollHeight = taskHeight + (int) ((numTasks - 1) *
+                Constants.Values.TaskStackView.StackOverlapPct * taskHeight);
+        mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight;
+        mMaxScroll = maxScrollHeight - stackHeight;
+
+        // Debug logging
+        if (Constants.DebugFlags.UI.MeasureAndLayout) {
+            Console.log("  [TaskStack|minScroll] " + mMinScroll);
+            Console.log("  [TaskStack|maxScroll] " + mMaxScroll);
+        }
+
+        if (boundScrollToNewMinMax) {
+            boundScroll();
+        }
+    }
+
+    /** Enables the hw layers and increments the hw layer requirement ref count */
+    void addHwLayersRefCount() {
+        Console.log(Constants.DebugFlags.UI.HwLayers,
+                "[TaskStackView|addHwLayersRefCount] refCount: " +
+                        mHwLayersRefCount + "->" + (mHwLayersRefCount + 1));
+        if (mHwLayersRefCount == 0) {
+            // Enable hw layers on each of the children
+            int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                TaskView tv = (TaskView) getChildAt(i);
+                tv.enableHwLayers();
+            }
+        }
+        mHwLayersRefCount++;
+    }
+
+    /** Decrements the hw layer requirement ref count and disables the hw layers when we don't
+        need them anymore. */
+    void decHwLayersRefCount() {
+        Console.log(Constants.DebugFlags.UI.HwLayers,
+                "[TaskStackView|decHwLayersRefCount] refCount: " +
+                        mHwLayersRefCount + "->" + (mHwLayersRefCount - 1));
+        mHwLayersRefCount--;
+        if (mHwLayersRefCount == 0) {
+            // Disable hw layers on each of the children
+            int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                TaskView tv = (TaskView) getChildAt(i);
+                tv.disableHwLayers();
+            }
+        } else if (mHwLayersRefCount < 0) {
+            throw new RuntimeException("Invalid hw layers ref count");
+        }
+    }
+
+    @Override
+    public void computeScroll() {
+        if (mScroller.computeScrollOffset()) {
+            setStackScroll(mScroller.getCurrY());
+            invalidate();
+
+            // If we just finished scrolling, then disable the hw layers
+            if (mScroller.isFinished()) {
+                decHwLayersRefCount();
+            }
+        }
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return mTouchHandler.onInterceptTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        return mTouchHandler.onTouchEvent(ev);
+    }
+
+    @Override
+    public void dispatchDraw(Canvas canvas) {
+        Console.log(Constants.DebugFlags.UI.Draw, "[TaskStackView|dispatchDraw]", "",
+                Console.AnsiPurple);
+        synchronizeStackViewsWithModel();
+        super.dispatchDraw(canvas);
+    }
+
+    @Override
+    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        if (Constants.DebugFlags.App.EnableTaskStackClipping) {
+            TaskView tv = (TaskView) child;
+            TaskView nextTv = null;
+            int curIndex = indexOfChild(tv);
+            if (curIndex < (getChildCount() - 1)) {
+                // Clip against the next view (if we aren't animating its alpha)
+                nextTv = (TaskView) getChildAt(curIndex + 1);
+                if (nextTv.getAlpha() == 1f) {
+                    Rect curRect = tv.getClippingRect(Utilities.tmpRect, false);
+                    Rect nextRect = nextTv.getClippingRect(Utilities.tmpRect2, true);
+                    RecentsConfiguration config = RecentsConfiguration.getInstance();
+                    // The hit rects are relative to the task view, which needs to be offset by the
+                    // system bar height
+                    curRect.offset(0, config.systemInsets.top);
+                    nextRect.offset(0, config.systemInsets.top);
+                    // Compute the clip region
+                    Region clipRegion = new Region();
+                    clipRegion.op(curRect, Region.Op.UNION);
+                    clipRegion.op(nextRect, Region.Op.DIFFERENCE);
+                    // Clip the canvas
+                    int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
+                    canvas.clipRegion(clipRegion);
+                    boolean invalidate = super.drawChild(canvas, child, drawingTime);
+                    canvas.restoreToCount(saveCount);
+                    return invalidate;
+                }
+            }
+        }
+        return super.drawChild(canvas, child, drawingTime);
+    }
+
+    /** Computes the stack and task rects */
+    public void computeRects(int width, int height) {
+        // Note: We let the stack view be the full height because we want the cards to go under the
+        //       navigation bar if possible.  However, the stack rects which we use to calculate
+        //       max scroll, etc. need to take the nav bar into account
+
+        // Compute the stack rects
+        RecentsConfiguration config = RecentsConfiguration.getInstance();
+        mRect.set(0, 0, width, height);
+        mStackRect.set(mRect);
+        mStackRect.bottom -= config.systemInsets.bottom;
+
+        int smallestDimension = Math.min(width, height);
+        int padding = (int) (Constants.Values.TaskStackView.StackPaddingPct * smallestDimension / 2f);
+        mStackRect.inset(padding, padding);
+        mStackRectSansPeek.set(mStackRect);
+        mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height();
+
+        // Compute the task rect
+        if (RecentsConfiguration.getInstance().layoutVerticalStack) {
+            int minHeight = (int) (mStackRect.height() -
+                    (Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height()));
+            int size = Math.min(minHeight, Math.min(mStackRect.width(), mStackRect.height()));
+            int centerX = mStackRect.centerX();
+            mTaskRect.set(centerX - size / 2, mStackRectSansPeek.top,
+                    centerX + size / 2, mStackRectSansPeek.top + size);
+        } else {
+            int size = Math.min(mStackRect.width(), mStackRect.height());
+            int centerY = mStackRect.centerY();
+            mTaskRect.set(mStackRectSansPeek.top, centerY - size / 2,
+                    mStackRectSansPeek.top + size, centerY + size / 2);
+        }
+
+        // Update the scroll bounds
+        updateMinMaxScroll(false);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+        Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|measure]",
+                "width: " + width + " height: " + height +
+                " awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen);
+
+        // Compute our stack/task rects
+        computeRects(width, height);
+
+        // Debug logging
+        if (Constants.DebugFlags.UI.MeasureAndLayout) {
+            Console.log("  [TaskStack|fullRect] " + mRect);
+            Console.log("  [TaskStack|stackRect] " + mStackRect);
+            Console.log("  [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
+            Console.log("  [TaskStack|taskRect] " + mTaskRect);
+        }
+
+        // If this is the first layout, then scroll to the front of the stack and synchronize the
+        // stack views immediately
+        if (mAwaitingFirstLayout) {
+            setStackScroll(mMaxScroll);
+            requestSynchronizeStackViewsWithModel();
+            synchronizeStackViewsWithModel();
+
+            // Animate the icon of the first task view
+            if (Constants.Values.TaskView.AnimateFrontTaskIconOnEnterRecents) {
+                TaskView tv = (TaskView) getChildAt(getChildCount() - 1);
+                if (tv != null) {
+                    tv.animateOnEnterRecents();
+                }
+            }
+        }
+
+        // Measure each of the children
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            TaskView t = (TaskView) getChildAt(i);
+            t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY));
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|layout]",
+                "" + new Rect(left, top, right, bottom), Console.AnsiGreen);
+
+        // Debug logging
+        if (Constants.DebugFlags.UI.MeasureAndLayout) {
+            Console.log("  [TaskStack|fullRect] " + mRect);
+            Console.log("  [TaskStack|stackRect] " + mStackRect);
+            Console.log("  [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
+            Console.log("  [TaskStack|taskRect] " + mTaskRect);
+        }
+
+        // Layout each of the children
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            TaskView t = (TaskView) getChildAt(i);
+            t.layout(mTaskRect.left, mStackRectSansPeek.top,
+                    mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height());
+        }
+
+        if (!mAwaitingFirstLayout) {
+            requestSynchronizeStackViewsWithModel();
+        } else {
+            mAwaitingFirstLayout = false;
+        }
+    }
+
+    @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+        requestSynchronizeStackViewsWithModel();
+    }
+
+    public boolean isTransformedTouchPointInView(float x, float y, View child) {
+        return isTransformedTouchPointInView(x, y, child, null);
+    }
+
+    /**** TaskStackCallbacks Implementation ****/
+
+    @Override
+    public void onStackTaskAdded(TaskStack stack, Task t) {
+        requestSynchronizeStackViewsWithModel();
+    }
+
+    @Override
+    public void onStackTaskRemoved(TaskStack stack, Task t) {
+        // Remove the view associated with this task, we can't rely on updateTransforms
+        // to work here because the task is no longer in the list
+        int childCount = getChildCount();
+        for (int i = childCount - 1; i >= 0; i--) {
+            TaskView tv = (TaskView) getChildAt(i);
+            if (tv.getTask() == t) {
+                mViewPool.returnViewToPool(tv);
+                break;
+            }
+        }
+
+        updateMinMaxScroll(true);
+        requestSynchronizeStackViewsWithModel(Constants.Values.TaskStackView.Animation.TaskRemovedReshuffleDuration);
+    }
+
+    @Override
+    public void onStackFiltered(TaskStack stack) {
+        requestSynchronizeStackViewsWithModel();
+    }
+
+    @Override
+    public void onStackUnfiltered(TaskStack stack) {
+        requestSynchronizeStackViewsWithModel();
+    }
+
+    /**** ViewPoolConsumer Implementation ****/
+
+    @Override
+    public TaskView createView(Context context) {
+        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]");
+        return new TaskView(context);
+    }
+
+    @Override
+    public void prepareViewToEnterPool(TaskView tv) {
+        Task task = tv.getTask();
+        tv.resetViewProperties();
+        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]",
+                tv.getTask() + " tv: " + tv);
+
+        // Report that this tasks's data is no longer being used
+        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+        loader.unloadTaskData(task);
+        tv.unbindFromTask();
+
+        // Detach the view from the hierarchy
+        detachViewFromParent(tv);
+
+        // Disable hw layers on this view
+        tv.disableHwLayers();
+    }
+
+    @Override
+    public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) {
+        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]",
+                "isNewView: " + isNewView);
+
+        // Setup and attach the view to the window
+        Task task = prepareData;
+        // We try and rebind the task (this MUST be done before the task filled)
+        tv.bindToTask(task, this);
+        // Request that this tasks's data be filled
+        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+        loader.loadTaskData(task);
+        tv.syncToTask();
+
+        // Find the index where this task should be placed in the children
+        int insertIndex = -1;
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            Task tvTask = ((TaskView) getChildAt(i)).getTask();
+            if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) {
+                insertIndex = i;
+                break;
+            }
+        }
+
+        // Add/attach the view to the hierarchy
+        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "  [TaskStackView|insertIndex]",
+                "" + insertIndex);
+        if (isNewView) {
+            addView(tv, insertIndex);
+            tv.setOnClickListener(this);
+        } else {
+            attachViewToParent(tv, insertIndex, tv.getLayoutParams());
+        }
+
+        // Enable hw layers on this view if hw layers are enabled on the stack
+        if (mHwLayersRefCount > 0) {
+            tv.enableHwLayers();
+        }
+    }
+
+    @Override
+    public boolean hasPreferredData(TaskView tv, Task preferredData) {
+        return (tv.getTask() == preferredData);
+    }
+
+    /**** TaskViewCallbacks Implementation ****/
+
+    @Override
+    public void onTaskIconClicked(TaskView tv) {
+        Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Icon]",
+                tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(),
+                Console.AnsiCyan);
+        if (Constants.DebugFlags.App.EnableTaskFiltering) {
+            if (mStack.hasFilteredTasks()) {
+                mStack.unfilterTasks();
+            } else {
+                mStack.filterTasks(tv.getTask());
+            }
+        } else {
+            Toast.makeText(getContext(), "Task Filtering TBD", Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    /**** View.OnClickListener Implementation ****/
+
+    @Override
+    public void onClick(View v) {
+        TaskView tv = (TaskView) v;
+        Task task = tv.getTask();
+        Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]",
+                task + " cb: " + mCb);
+
+        if (mCb != null) {
+            mCb.onTaskLaunched(this, tv, mStack, task);
+        }
+    }
+}
+
+/* Handles touch events */
+class TaskStackViewTouchHandler {
+    static int INACTIVE_POINTER_ID = -1;
+
+    TaskStackView mSv;
+    VelocityTracker mVelocityTracker;
+
+    boolean mIsScrolling;
+    boolean mIsSwiping;
+
+    int mInitialMotionX, mInitialMotionY;
+    int mLastMotionX, mLastMotionY;
+    int mActivePointerId = INACTIVE_POINTER_ID;
+    TaskView mActiveTaskView = null;
+
+    int mTotalScrollMotion;
+    int mMinimumVelocity;
+    int mMaximumVelocity;
+    // The scroll touch slop is used to calculate when we start scrolling
+    int mScrollTouchSlop;
+    // The swipe touch slop is used to calculate when we start swiping left/right, this takes
+    // precendence over the scroll touch slop in case the user makes a gesture that starts scrolling
+    // but is intended to be a swipe
+    int mSwipeTouchSlop;
+    // After a certain amount of scrolling, we should start ignoring checks for swiping
+    int mMaxScrollMotionToRejectSwipe;
+
+    public TaskStackViewTouchHandler(Context context, TaskStackView sv) {
+        ViewConfiguration configuration = ViewConfiguration.get(context);
+        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mScrollTouchSlop = configuration.getScaledTouchSlop();
+        mSwipeTouchSlop = 2 * mScrollTouchSlop;
+        mMaxScrollMotionToRejectSwipe = 4 * mScrollTouchSlop;
+        mSv = sv;
+    }
+
+    /** Velocity tracker helpers */
+    void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+    void initVelocityTrackerIfNotExists() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+    }
+    void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    /** Returns the view at the specified coordinates */
+    TaskView findViewAtPoint(int x, int y) {
+        int childCount = mSv.getChildCount();
+        for (int i = childCount - 1; i >= 0; i--) {
+            TaskView tv = (TaskView) mSv.getChildAt(i);
+            if (tv.getVisibility() == View.VISIBLE) {
+                if (mSv.isTransformedTouchPointInView(x, y, tv)) {
+                    return tv;
+                }
+            }
+        }
+        return null;
+    }
+
+    /** Touch preprocessing for handling below */
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        Console.log(Constants.DebugFlags.UI.TouchEvents,
+                "[TaskStackViewTouchHandler|interceptTouchEvent]",
+                Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
+
+        boolean hasChildren = (mSv.getChildCount() > 0);
+        if (!hasChildren) {
+            return false;
+        }
+
+        boolean wasScrolling = !mSv.mScroller.isFinished() ||
+                (mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning());
+        int action = ev.getAction();
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                // Save the touch down info
+                mInitialMotionX = mLastMotionX = (int) ev.getX();
+                mInitialMotionY = mLastMotionY = (int) ev.getY();
+                mActivePointerId = ev.getPointerId(0);
+                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
+                // Stop the current scroll if it is still flinging
+                mSv.mScroller.abortAnimation();
+                mSv.abortBoundScrollAnimation();
+                // Initialize the velocity tracker
+                initOrResetVelocityTracker();
+                mVelocityTracker.addMovement(ev);
+                // Check if the scroller is finished yet
+                mIsScrolling = !mSv.mScroller.isFinished();
+                mIsSwiping = false;
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                if (mActivePointerId == INACTIVE_POINTER_ID) break;
+
+                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                int y = (int) ev.getY(activePointerIndex);
+                int x = (int) ev.getX(activePointerIndex);
+                if (mActiveTaskView != null &&
+                        mTotalScrollMotion < mMaxScrollMotionToRejectSwipe &&
+                        Math.abs(x - mInitialMotionX) > Math.abs(y - mInitialMotionY) &&
+                        Math.abs(x - mInitialMotionX) > mSwipeTouchSlop) {
+                    // Start swiping and stop scrolling
+                    mIsScrolling = false;
+                    mIsSwiping = true;
+                    System.out.println("SWIPING: " + mActiveTaskView);
+                    // Initialize the velocity tracker if necessary
+                    initOrResetVelocityTracker();
+                    mVelocityTracker.addMovement(ev);
+                    // Disallow parents from intercepting touch events
+                    final ViewParent parent = mSv.getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                    // Enable HW layers
+                    mSv.addHwLayersRefCount();
+                } else if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
+                    // Save the touch move info
+                    mIsScrolling = true;
+                    // Initialize the velocity tracker if necessary
+                    initVelocityTrackerIfNotExists();
+                    mVelocityTracker.addMovement(ev);
+                    // Disallow parents from intercepting touch events
+                    final ViewParent parent = mSv.getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                    // Enable HW layers
+                    mSv.addHwLayersRefCount();
+                }
+
+                mLastMotionX = x;
+                mLastMotionY = y;
+                break;
+            }
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP: {
+                // Animate the scroll back if we've cancelled
+                mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration);
+                // Reset the drag state and the velocity tracker
+                mIsScrolling = false;
+                mIsSwiping = false;
+                mActivePointerId = INACTIVE_POINTER_ID;
+                mActiveTaskView = null;
+                mTotalScrollMotion = 0;
+                recycleVelocityTracker();
+                break;
+            }
+        }
+
+        return wasScrolling || mIsScrolling || mIsSwiping;
+    }
+
+    /** Handles touch events once we have intercepted them */
+    public boolean onTouchEvent(MotionEvent ev) {
+        Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel,
+                "[TaskStackViewTouchHandler|touchEvent]",
+                Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
+
+        // Short circuit if we have no children
+        boolean hasChildren = (mSv.getChildCount() > 0);
+        if (!hasChildren) {
+            return false;
+        }
+
+        // Update the velocity tracker
+        initVelocityTrackerIfNotExists();
+        mVelocityTracker.addMovement(ev);
+
+        int action = ev.getAction();
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                // Save the touch down info
+                mInitialMotionX = mLastMotionX = (int) ev.getX();
+                mInitialMotionY = mLastMotionY = (int) ev.getY();
+                mActivePointerId = ev.getPointerId(0);
+                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
+                // Stop the current scroll if it is still flinging
+                mSv.mScroller.abortAnimation();
+                mSv.abortBoundScrollAnimation();
+                // Initialize the velocity tracker
+                initOrResetVelocityTracker();
+                mVelocityTracker.addMovement(ev);
+                // XXX: Set mIsScrolling or mIsSwiping?
+                // Disallow parents from intercepting touch events
+                final ViewParent parent = mSv.getParent();
+                if (parent != null) {
+                    parent.requestDisallowInterceptTouchEvent(true);
+                }
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                if (mActivePointerId == INACTIVE_POINTER_ID) break;
+
+                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                int x = (int) ev.getX(activePointerIndex);
+                int y = (int) ev.getY(activePointerIndex);
+                int deltaY = mLastMotionY - y;
+                int deltaX = x - mLastMotionX;
+                if (!mIsSwiping) {
+                    if (mActiveTaskView != null &&
+                            mTotalScrollMotion < mMaxScrollMotionToRejectSwipe &&
+                            Math.abs(x - mInitialMotionX) > Math.abs(y - mInitialMotionY) &&
+                            Math.abs(x - mInitialMotionX) > mSwipeTouchSlop) {
+                        mIsScrolling = false;
+                        mIsSwiping = true;
+                        System.out.println("SWIPING: " + mActiveTaskView);
+                        // Initialize the velocity tracker if necessary
+                        initOrResetVelocityTracker();
+                        mVelocityTracker.addMovement(ev);
+                        // Disallow parents from intercepting touch events
+                        final ViewParent parent = mSv.getParent();
+                        if (parent != null) {
+                            parent.requestDisallowInterceptTouchEvent(true);
+                        }
+                        // Enable HW layers
+                        mSv.addHwLayersRefCount();
+                    }
+                }
+                if (!mIsSwiping && !mIsScrolling) {
+                    if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
+                        mIsScrolling = true;
+                        // Initialize the velocity tracker
+                        initOrResetVelocityTracker();
+                        mVelocityTracker.addMovement(ev);
+                        // Disallow parents from intercepting touch events
+                        final ViewParent parent = mSv.getParent();
+                        if (parent != null) {
+                            parent.requestDisallowInterceptTouchEvent(true);
+                        }
+                        // Enable HW layers
+                        mSv.addHwLayersRefCount();
+                    }
+                }
+                if (mIsScrolling) {
+                    mSv.setStackScroll(mSv.getStackScroll() + deltaY);
+                    if (mSv.isScrollOutOfBounds()) {
+                        mVelocityTracker.clear();
+                    }
+                } else if (mIsSwiping) {
+                    mActiveTaskView.setTranslationX(mActiveTaskView.getTranslationX() + deltaX);
+                }
+                mLastMotionX = x;
+                mLastMotionY = y;
+                mTotalScrollMotion += Math.abs(deltaY);
+                break;
+            }
+            case MotionEvent.ACTION_UP: {
+                if (mIsScrolling || mIsSwiping) {
+                    final TaskView activeTv = mActiveTaskView;
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+
+                    if (mIsSwiping) {
+                        int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
+                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+                            // Fling to dismiss
+                            int newScrollX = (int) (Math.signum(initialVelocity) *
+                                    activeTv.getMeasuredWidth());
+                            int duration = Math.min(Constants.Values.TaskStackView.Animation.SwipeDismissDuration,
+                                    (int) (Math.abs(newScrollX - activeTv.getScrollX()) *
+                                            1000f / Math.abs(initialVelocity)));
+                            activeTv.animate()
+                                    .translationX(newScrollX)
+                                    .alpha(0f)
+                                    .setDuration(duration)
+                                    .setListener(new AnimatorListenerAdapter() {
+                                        @Override
+                                        public void onAnimationEnd(Animator animation) {
+                                            Task task = activeTv.getTask();
+                                            Activity activity = (Activity) mSv.getContext();
+
+                                            // We have to disable the listener to ensure that we
+                                            // don't hit this again
+                                            activeTv.animate().setListener(null);
+
+                                            // Remove the task from the view
+                                            mSv.mStack.removeTask(task);
+
+                                            // Remove any stored data from the loader
+                                            RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
+                                            loader.deleteTaskData(task);
+
+                                            // Remove the task from activity manager
+                                            final ActivityManager am = (ActivityManager)
+                                                activity.getSystemService(Context.ACTIVITY_SERVICE);
+                                            if (am != null) {
+                                                am.removeTask(activeTv.getTask().id,
+                                                        ActivityManager.REMOVE_TASK_KILL_PROCESS);
+                                            }
+
+                                            // If there are no remaining tasks, then just close the activity
+                                            if (mSv.mStack.getTaskCount() == 0) {
+                                                activity.finish();
+                                            }
+
+                                            // Disable HW layers
+                                            mSv.decHwLayersRefCount();
+                                        }
+                                    })
+                                    .start();
+                            // Enable HW layers
+                            mSv.addHwLayersRefCount();
+                        } else {
+                            // Animate it back into place
+                            // XXX: Make this animation a function of the velocity OR distance
+                            int duration = Constants.Values.TaskStackView.Animation.SwipeSnapBackDuration;
+                            activeTv.animate()
+                                    .translationX(0)
+                                    .setDuration(duration)
+                                    .setListener(new AnimatorListenerAdapter() {
+                                        @Override
+                                        public void onAnimationEnd(Animator animation) {
+                                            // Disable HW layers
+                                            mSv.decHwLayersRefCount();
+                                        }
+                                    })
+                                    .start();
+                            // Enable HW layers
+                            mSv.addHwLayersRefCount();
+                        }
+                    } else {
+                        int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
+                        if ((Math.abs(velocity) > mMinimumVelocity)) {
+                            Console.log(Constants.DebugFlags.UI.TouchEvents,
+                                "[TaskStackViewTouchHandler|fling]",
+                                "scroll: " + mSv.getStackScroll() + " velocity: " + velocity,
+                                    Console.AnsiGreen);
+                            // Enable HW layers on the stack
+                            mSv.addHwLayersRefCount();
+                            // Fling scroll
+                            mSv.mScroller.fling(0, mSv.getStackScroll(),
+                                    0, -velocity,
+                                    0, 0,
+                                    mSv.mMinScroll, mSv.mMaxScroll,
+                                    0, 0);
+                            // Invalidate to kick off computeScroll
+                            mSv.invalidate();
+                        } else if (mSv.isScrollOutOfBounds()) {
+                            // Animate the scroll back into bounds
+                            // XXX: Make this animation a function of the velocity OR distance
+                            mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration);
+                        }
+                    }
+                }
+
+                mActivePointerId = INACTIVE_POINTER_ID;
+                mIsScrolling = false;
+                mIsSwiping = false;
+                mTotalScrollMotion = 0;
+                recycleVelocityTracker();
+                // Disable HW layers
+                mSv.decHwLayersRefCount();
+                break;
+            }
+            case MotionEvent.ACTION_CANCEL: {
+                if (mIsScrolling || mIsSwiping) {
+                    if (mIsSwiping) {
+                        // Animate it back into place
+                        // XXX: Make this animation a function of the velocity OR distance
+                        int duration = Constants.Values.TaskStackView.Animation.SwipeSnapBackDuration;
+                        mActiveTaskView.animate()
+                                .translationX(0)
+                                .setDuration(duration)
+                                .start();
+                    } else {
+                        // Animate the scroll back into bounds
+                        // XXX: Make this animation a function of the velocity OR distance
+                        mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration);
+                    }
+                }
+
+                mActivePointerId = INACTIVE_POINTER_ID;
+                mIsScrolling = false;
+                mIsSwiping = false;
+                mTotalScrollMotion = 0;
+                recycleVelocityTracker();
+                // Disable HW layers
+                mSv.decHwLayersRefCount();
+                break;
+            }
+        }
+        return true;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
new file mode 100644
index 0000000..b1d0d13
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -0,0 +1,386 @@
+/*
+ * 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.views;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.view.Gravity;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import com.android.systemui.recents.Console;
+import com.android.systemui.recents.Constants;
+import com.android.systemui.recents.RecentsConfiguration;
+import com.android.systemui.recents.model.Task;
+import com.android.systemui.recents.model.TaskCallbacks;
+
+/** The TaskView callbacks */
+interface TaskViewCallbacks {
+    public void onTaskIconClicked(TaskView tv);
+    // public void onTaskViewReboundToTask(TaskView tv, Task t);
+}
+
+/** The task thumbnail view */
+class TaskThumbnailView extends ImageView {
+    Task mTask;
+    int mBarColor;
+
+    Path mRoundedRectClipPath = new Path();
+
+    public TaskThumbnailView(Context context) {
+        super(context);
+        setScaleType(ScaleType.FIT_XY);
+    }
+
+    /** Binds the thumbnail view to the task */
+    void rebindToTask(Task t, boolean animate) {
+        mTask = t;
+        if (t.thumbnail != null) {
+            // Update the bar color
+            if (Constants.Values.TaskView.DrawColoredTaskBars) {
+                int[] colors = {0xFFCC0C39, 0xFFE6781E, 0xFFC8CF02, 0xFF1693A7};
+                mBarColor = colors[mTask.intent.getComponent().getPackageName().length() % colors.length];
+            }
+
+            setImageBitmap(t.thumbnail);
+            if (animate) {
+                setAlpha(0f);
+                animate().alpha(1f)
+                        .setDuration(Constants.Values.TaskView.Animation.TaskDataUpdatedFadeDuration)
+                        .start();
+            }
+        }
+    }
+
+    /** Unbinds the thumbnail view from the task */
+    void unbindFromTask() {
+        mTask = null;
+        setImageDrawable(null);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        // Update the rounded rect clip path
+        RecentsConfiguration config = RecentsConfiguration.getInstance();
+        float radius = config.pxFromDp(Constants.Values.TaskView.RoundedCornerRadiusDps);
+        mRoundedRectClipPath.reset();
+        mRoundedRectClipPath.addRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()),
+                radius, radius, Path.Direction.CW);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (Constants.Values.TaskView.UseRoundedCorners) {
+            canvas.clipPath(mRoundedRectClipPath);
+        }
+
+        super.onDraw(canvas);
+
+        if (Constants.Values.TaskView.DrawColoredTaskBars) {
+            RecentsConfiguration config = RecentsConfiguration.getInstance();
+            int taskBarHeight = config.pxFromDp(Constants.Values.TaskView.TaskBarHeightDps);
+            // XXX: If we actually use this, this should be pulled out into a TextView that we
+            // inflate
+
+            // Draw the task bar
+            Rect r = new Rect();
+            Paint p = new Paint();
+            p.setAntiAlias(true);
+            p.setSubpixelText(true);
+            p.setColor(mBarColor);
+            p.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
+            canvas.drawRect(0, 0, getMeasuredWidth(), taskBarHeight, p);
+            p.setColor(0xFFffffff);
+            p.setTextSize(68);
+            p.getTextBounds("X", 0, 1, r);
+            int offset = (int) (taskBarHeight - r.height()) / 2;
+            canvas.drawText(mTask.title, offset, offset + r.height(), p);
+        }
+    }
+}
+
+/* The task icon view */
+class TaskIconView extends ImageView {
+    Task mTask;
+
+    Path mClipPath = new Path();
+    float mClipRadius;
+    Point mClipOrigin = new Point();
+    ObjectAnimator mCircularClipAnimator;
+
+    public TaskIconView(Context context) {
+        super(context);
+        mClipPath = new Path();
+        mClipRadius = 1f;
+    }
+
+    /** Binds the icon view to the task */
+    void rebindToTask(Task t, boolean animate) {
+        mTask = t;
+        if (t.icon != null) {
+            setImageDrawable(t.icon);
+            if (animate) {
+                setAlpha(0f);
+                animate().alpha(1f)
+                        .setDuration(Constants.Values.TaskView.Animation.TaskDataUpdatedFadeDuration)
+                        .start();
+            }
+        }
+    }
+
+    /** Unbinds the icon view from the task */
+    void unbindFromTask() {
+        mTask = null;
+        setImageDrawable(null);
+    }
+
+    /** Sets the circular clip radius on the icon */
+    public void setCircularClipRadius(float r) {
+        Console.log(Constants.DebugFlags.UI.Clipping, "[TaskView|setCircularClip]", "" + r);
+        mClipRadius = r;
+        invalidate();
+    }
+
+    /** Gets the circular clip radius on the icon */
+    public float getCircularClipRadius() {
+        return mClipRadius;
+    }
+
+    /** Animates the circular clip radius on the icon */
+    void animateCircularClip(boolean brNotTl, float newRadius, int duration, int startDelay,
+                             TimeInterpolator interpolator,
+                             AnimatorListenerAdapter listener) {
+        if (mCircularClipAnimator != null) {
+            mCircularClipAnimator.cancel();
+            mCircularClipAnimator.removeAllListeners();
+        }
+        if (brNotTl) {
+            mClipOrigin.set(0, 0);
+        } else {
+            mClipOrigin.set(getMeasuredWidth(), getMeasuredHeight());
+        }
+        mCircularClipAnimator = ObjectAnimator.ofFloat(this, "circularClipRadius", newRadius);
+        mCircularClipAnimator.setStartDelay(startDelay);
+        mCircularClipAnimator.setDuration(duration);
+        mCircularClipAnimator.setInterpolator(interpolator);
+        if (listener != null) {
+            mCircularClipAnimator.addListener(listener);
+        }
+        mCircularClipAnimator.start();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
+        int width = getMeasuredWidth();
+        int height = getMeasuredHeight();
+        int maxSize = (int) Math.ceil(Math.sqrt(width * width + height * height));
+        mClipPath.reset();
+        mClipPath.addCircle(mClipOrigin.x, mClipOrigin.y, mClipRadius * maxSize, Path.Direction.CW);
+        canvas.clipPath(mClipPath);
+        super.onDraw(canvas);
+        canvas.restoreToCount(saveCount);
+    }
+}
+
+/* A task view */
+public class TaskView extends FrameLayout implements View.OnClickListener, TaskCallbacks {
+    Task mTask;
+    TaskThumbnailView mThumbnailView;
+    TaskIconView mIconView;
+    TaskViewCallbacks mCb;
+
+    public TaskView(Context context) {
+        super(context);
+        mThumbnailView = new TaskThumbnailView(context);
+        mIconView = new TaskIconView(context);
+        mIconView.setOnClickListener(this);
+        addView(mThumbnailView);
+        addView(mIconView);
+
+        RecentsConfiguration config = RecentsConfiguration.getInstance();
+        int barHeight = config.pxFromDp(Constants.Values.TaskView.TaskBarHeightDps);
+        int iconSize = config.pxFromDp(Constants.Values.TaskView.TaskIconSizeDps);
+        int offset = barHeight - (iconSize / 2);
+
+        // XXX: Lets keep the icon in the corner for the time being
+        offset = iconSize / 4;
+
+        /*
+        ((LayoutParams) mThumbnailView.getLayoutParams()).leftMargin = barHeight / 2;
+        ((LayoutParams) mThumbnailView.getLayoutParams()).rightMargin = barHeight / 2;
+        ((LayoutParams) mThumbnailView.getLayoutParams()).bottomMargin = barHeight;
+        */
+        ((LayoutParams) mIconView.getLayoutParams()).gravity = Gravity.END;
+        ((LayoutParams) mIconView.getLayoutParams()).width = iconSize;
+        ((LayoutParams) mIconView.getLayoutParams()).height = iconSize;
+        ((LayoutParams) mIconView.getLayoutParams()).topMargin = offset;
+        ((LayoutParams) mIconView.getLayoutParams()).rightMargin = offset;
+    }
+
+    /** Set the task and callback */
+    void bindToTask(Task t, TaskViewCallbacks cb) {
+        mTask = t;
+        mTask.setCallbacks(this);
+        mCb = cb;
+    }
+
+    /** Actually synchronizes the model data into the views */
+    void syncToTask() {
+        mThumbnailView.rebindToTask(mTask, false);
+        mIconView.rebindToTask(mTask, false);
+    }
+
+    /** Unset the task and callback */
+    void unbindFromTask() {
+        mTask.setCallbacks(null);
+        mThumbnailView.unbindFromTask();
+        mIconView.unbindFromTask();
+    }
+
+    /** Gets the task */
+    Task getTask() {
+        return mTask;
+    }
+
+    /** Synchronizes this view's properties with the task's transform */
+    void updateViewPropertiesFromTask(TaskViewTransform animateFromTransform,
+                                      TaskViewTransform transform, int duration) {
+        if (duration > 0) {
+            if (animateFromTransform != null) {
+                setTranslationY(animateFromTransform.translationY);
+                setScaleX(animateFromTransform.scale);
+                setScaleY(animateFromTransform.scale);
+            }
+            animate().translationY(transform.translationY)
+                    .scaleX(transform.scale)
+                    .scaleY(transform.scale)
+                    .setDuration(duration)
+                    .setInterpolator(new AccelerateDecelerateInterpolator())
+                    .start();
+        } else {
+            setTranslationY(transform.translationY);
+            setScaleX(transform.scale);
+            setScaleY(transform.scale);
+        }
+    }
+
+    /** Resets this view's properties */
+    void resetViewProperties() {
+        setTranslationX(0f);
+        setTranslationY(0f);
+        setScaleX(1f);
+        setScaleY(1f);
+        setAlpha(1f);
+    }
+
+    /** Animates this task view as it enters recents */
+    public void animateOnEnterRecents() {
+        mIconView.setCircularClipRadius(0f);
+        mIconView.animateCircularClip(true, 1f,
+            Constants.Values.TaskView.Animation.TaskIconCircularClipInDuration,
+            300, new AccelerateInterpolator(), null);
+    }
+
+    /** Animates this task view as it exits recents */
+    public void animateOnLeavingRecents(final Runnable r) {
+        if (Constants.Values.TaskView.AnimateFrontTaskIconOnLeavingUseClip) {
+            mIconView.animateCircularClip(false, 0f,
+                Constants.Values.TaskView.Animation.TaskIconCircularClipOutDuration, 0,
+                new DecelerateInterpolator(),
+                new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        r.run();
+                    }
+                });
+        } else {
+            mIconView.animate()
+                .alpha(0f)
+                .setDuration(Constants.Values.TaskView.Animation.TaskIconCircularClipOutDuration)
+                .setInterpolator(new DecelerateInterpolator())
+                .setListener(
+                    new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            r.run();
+                        }
+                    })
+                .start();
+        }
+    }
+
+    /** Returns the rect we want to clip (it may not be the full rect) */
+    Rect getClippingRect(Rect outRect, boolean accountForRoundedRects) {
+        getHitRect(outRect);
+        // XXX: We should get the hit rect of the thumbnail view and intersect, but this is faster
+        outRect.right = outRect.left + mThumbnailView.getRight();
+        outRect.bottom = outRect.top + mThumbnailView.getBottom();
+        // We need to shrink the next rect by the rounded corners since those are draw on
+        // top of the current view
+        if (accountForRoundedRects) {
+            RecentsConfiguration config = RecentsConfiguration.getInstance();
+            float radius = config.pxFromDp(Constants.Values.TaskView.RoundedCornerRadiusDps);
+            outRect.inset((int) radius, (int) radius);
+        }
+        return outRect;
+    }
+
+    /** Enable the hw layers on this task view */
+    void enableHwLayers() {
+        Console.log(Constants.DebugFlags.UI.HwLayers, "[TaskView|enableHwLayers]");
+        mThumbnailView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+    }
+
+    /** Disable the hw layers on this task view */
+    void disableHwLayers() {
+        Console.log(Constants.DebugFlags.UI.HwLayers, "[TaskView|disableHwLayers]");
+        mThumbnailView.setLayerType(View.LAYER_TYPE_NONE, null);
+    }
+
+    @Override
+    public void onTaskDataChanged(Task task) {
+        Console.log(Constants.DebugFlags.App.EnableBackgroundTaskLoading,
+                "[TaskView|onTaskDataChanged]", task);
+
+        // Only update this task view if the changed task is the same as the task for this view
+        if (mTask == task) {
+            mThumbnailView.rebindToTask(mTask, true);
+            mIconView.rebindToTask(mTask, true);
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        mCb.onTaskIconClicked(this);
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
new file mode 100644
index 0000000..66c52a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
@@ -0,0 +1,35 @@
+/*
+ * 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.views;
+
+import android.graphics.Rect;
+
+
+/* The transform state for a task view */
+public class TaskViewTransform {
+    public int translationY = 0;
+    public float scale = 1f;
+    public boolean visible = true;
+    public Rect rect = new Rect();
+    float t;
+
+    @Override
+    public String toString() {
+        return "TaskViewTransform y: " + translationY + " scale: " + scale +
+                " visible: " + visible + " rect: " + rect;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java
new file mode 100644
index 0000000..f7d7095
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java
@@ -0,0 +1,69 @@
+/*
+ * 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.views;
+
+import android.content.Context;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+
+
+/* A view pool to manage more views than we can visibly handle */
+public class ViewPool<V, T> {
+    Context mContext;
+    ViewPoolConsumer<V, T> mViewCreator;
+    LinkedList<V> mPool = new LinkedList<V>();
+
+    /** Initializes the pool with a fixed predetermined pool size */
+    public ViewPool(Context context, ViewPoolConsumer<V, T> viewCreator) {
+        mContext = context;
+        mViewCreator = viewCreator;
+    }
+
+    /** Returns a view into the pool */
+    void returnViewToPool(V v) {
+        mViewCreator.prepareViewToEnterPool(v);
+        mPool.push(v);
+    }
+
+    /** Gets a view from the pool and prepares it */
+    V pickUpViewFromPool(T preferredData, T prepareData) {
+        V v = null;
+        boolean isNewView = false;
+        if (mPool.isEmpty()) {
+            v = mViewCreator.createView(mContext);
+            isNewView = true;
+        } else {
+            // Try and find a preferred view
+            Iterator<V> iter = mPool.iterator();
+            while (iter.hasNext()) {
+                V vpv = iter.next();
+                if (mViewCreator.hasPreferredData(vpv, preferredData)) {
+                    v = vpv;
+                    iter.remove();
+                    break;
+                }
+            }
+            // Otherwise, just grab the first view
+            if (v == null) {
+                v = mPool.pop();
+            }
+        }
+        mViewCreator.prepareViewToLeavePool(v, prepareData, isNewView);
+        return v;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewPoolConsumer.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPoolConsumer.java
new file mode 100644
index 0000000..50f45bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPoolConsumer.java
@@ -0,0 +1,28 @@
+/*
+ * 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.views;
+
+import android.content.Context;
+
+
+/* An interface to the consumer of a view pool */
+public interface ViewPoolConsumer<V, T> {
+    public V createView(Context context);
+    public void prepareViewToEnterPool(V v);
+    public void prepareViewToLeavePool(V v, T prepareData, boolean isNewView);
+    public boolean hasPreferredData(V v, T preferredData);
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 864211d..128f636 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -7245,6 +7245,24 @@
     }
 
     @Override
+    public boolean isInHomeStack(int taskId) {
+        enforceCallingPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS,
+                "getStackInfo()");
+        long ident = Binder.clearCallingIdentity();
+        try {
+            synchronized (this) {
+                TaskRecord tr = recentTaskForIdLocked(taskId);
+                if (tr != null) {
+                    return tr.stack.isHomeStack();
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+        return false;
+    }
+
+    @Override
     public int getTaskForActivity(IBinder token, boolean onlyRoot) {
         synchronized(this) {
             return ActivityRecord.getTaskForActivityLocked(token, onlyRoot);
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index f3ccdd6..087ad83c 100755
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -71,6 +71,7 @@
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.util.EventLog;
@@ -715,10 +716,17 @@
         int w = mThumbnailWidth;
         int h = mThumbnailHeight;
         if (w < 0) {
-            mThumbnailWidth = w =
-                res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
-            mThumbnailHeight = h =
-                res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
+            if (SystemProperties.getBoolean("persist.recents.use_alternate", false)) {
+                mThumbnailWidth = w =
+                   res.getDimensionPixelSize(com.android.internal.R.dimen.recents_thumbnail_width);
+                mThumbnailHeight = h =
+                   res.getDimensionPixelSize(com.android.internal.R.dimen.recents_thumbnail_height);
+            } else {
+                mThumbnailWidth = w =
+                    res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
+                mThumbnailHeight = h =
+                    res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
+            }
         }
 
         if (w > 0) {