Merge "Implementing app-centric Shelf."
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 580f721..eeae20f 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -1108,7 +1108,7 @@
      * of {@link #RECENT_WITH_EXCLUDED} and {@link #RECENT_IGNORE_UNAVAILABLE}.
      *
      * @return Returns a list of RecentTaskInfo records describing each of
-     * the recent tasks.
+     * the recent tasks. Most recently activated tasks go first.
      *
      * @hide
      */
diff --git a/packages/SystemUI/res/drawable-anydpi/nav_app_divider.xml b/packages/SystemUI/res/drawable-anydpi/nav_app_divider.xml
deleted file mode 100644
index b2d988e..0000000
--- a/packages/SystemUI/res/drawable-anydpi/nav_app_divider.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-Copyright (C) 2015 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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="1dp"
-        android:height="24dp"
-        android:viewportWidth="1"
-        android:viewportHeight="24">
-    <path
-        android:pathData="M0,0 L1,0 L1,24 L0,24 z"
-        android:fillColor="#AAFFFFFF"/>
-</vector>
diff --git a/packages/SystemUI/res/layout/navigation_bar_with_apps.xml b/packages/SystemUI/res/layout/navigation_bar_with_apps.xml
index 01c239e..ac95b5e 100644
--- a/packages/SystemUI/res/layout/navigation_bar_with_apps.xml
+++ b/packages/SystemUI/res/layout/navigation_bar_with_apps.xml
@@ -82,21 +82,6 @@
                     android:layout_width="wrap_content"
                     android:layout_height="match_parent"
                     />
-
-                <ImageView android:id="@+id/app_divider"
-                    android:focusable="false"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center"
-                    android:layout_marginLeft="10dp"
-                    android:layout_marginRight="10dp"
-                    android:src="@drawable/nav_app_divider"
-                    />
-
-                <com.android.systemui.statusbar.phone.NavigationBarRecents
-                    android:layout_width="wrap_content"
-                    android:layout_height="match_parent"
-                    />
             </LinearLayout>
 
             <FrameLayout
@@ -248,21 +233,6 @@
                     android:layout_width="wrap_content"
                     android:layout_height="match_parent"
                     />
-
-                <ImageView android:id="@+id/app_divider"
-                    android:focusable="false"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center"
-                    android:layout_marginLeft="10dp"
-                    android:layout_marginRight="10dp"
-                    android:src="@drawable/nav_app_divider"
-                    />
-
-            <com.android.systemui.statusbar.phone.NavigationBarRecents
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                />
             </LinearLayout>
 
             <FrameLayout
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppButtonData.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppButtonData.java
new file mode 100644
index 0000000..2c6987c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppButtonData.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 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.statusbar.phone;
+
+import android.app.ActivityManager.RecentTaskInfo;
+
+import java.util.ArrayList;
+
+/**
+ * Data associated with an app button.
+ */
+class AppButtonData {
+    public final AppInfo appInfo;
+    public boolean pinned;
+    // Recent tasks for this app, sorted by lastActiveTime, descending.
+    public ArrayList<RecentTaskInfo> tasks;
+
+    public AppButtonData(AppInfo appInfo, boolean pinned) {
+        this.appInfo = appInfo;
+        this.pinned = pinned;
+    }
+
+    /**
+     * Returns true if the button contains no useful information and should be removed.
+     */
+    public boolean isEmpty() {
+        return !pinned && (tasks == null || tasks.isEmpty());
+    }
+
+    public void addTask(RecentTaskInfo task) {
+        if (tasks == null) {
+            tasks = new ArrayList<RecentTaskInfo>();
+        }
+        tasks.add(task);
+    }
+
+    public void clearTasks() {
+        if (tasks != null) {
+            tasks.clear();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppInfo.java
index e34c821..8f0b532 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppInfo.java
@@ -39,4 +39,16 @@
     public UserHandle getUser() {
         return mUser;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final AppInfo other = (AppInfo) obj;
+        return mComponentName.equals(other.mComponentName) && mUser.equals(other.mUser);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/GetActivityIconTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/GetActivityIconTask.java
index f46d1a6..d2bec7c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/GetActivityIconTask.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/GetActivityIconTask.java
@@ -20,6 +20,12 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.RemoteException;
@@ -30,7 +36,7 @@
  * Retrieves the icon for an activity and sets it as the Drawable on an ImageView. The ImageView
  * is hidden if the activity isn't recognized or if there is no icon.
  */
-class GetActivityIconTask extends AsyncTask<AppInfo, Void, Drawable> {
+class GetActivityIconTask extends AsyncTask<AppButtonData, Void, Drawable> {
     private final static String TAG = "GetActivityIconTask";
 
     private final PackageManager mPackageManager;
@@ -44,11 +50,12 @@
     }
 
     @Override
-    protected Drawable doInBackground(AppInfo... params) {
+    protected Drawable doInBackground(AppButtonData... params) {
         if (params.length != 1) {
             throw new IllegalArgumentException("Expected one parameter");
         }
-        AppInfo appInfo = params[0];
+        AppButtonData buttonData = params[0];
+        AppInfo appInfo = buttonData.appInfo;
         try {
             IPackageManager mPM = AppGlobals.getPackageManager();
             ActivityInfo ai = mPM.getActivityInfo(
@@ -62,7 +69,37 @@
             }
 
             Drawable unbadgedIcon = ai.loadIcon(mPackageManager);
-            return mPackageManager.getUserBadgedIcon(unbadgedIcon, appInfo.getUser());
+            Drawable badgedIcon =
+                    mPackageManager.getUserBadgedIcon(unbadgedIcon, appInfo.getUser());
+
+            if (NavigationBarApps.DEBUG) {
+                // Draw pinned indicator and number of running tasks.
+                Bitmap bitmap = Bitmap.createBitmap(
+                        badgedIcon.getIntrinsicWidth(),
+                        badgedIcon.getIntrinsicHeight(),
+                        Bitmap.Config.ARGB_8888);
+                Canvas canvas = new Canvas(bitmap);
+                badgedIcon.setBounds(
+                        0, 0, badgedIcon.getIntrinsicWidth(), badgedIcon.getIntrinsicHeight());
+                badgedIcon.draw(canvas);
+                Paint paint = new Paint();
+                paint.setStyle(Paint.Style.FILL);
+                if (buttonData.pinned) {
+                    paint.setColor(Color.WHITE);
+                    canvas.drawCircle(10, 10, 10, paint);
+                }
+                if (buttonData.tasks != null && buttonData.tasks.size() > 0) {
+                    paint.setColor(Color.BLACK);
+                    canvas.drawCircle(60, 30, 30, paint);
+                    paint.setColor(Color.WHITE);
+                    paint.setTextSize(50);
+                    paint.setTypeface(Typeface.create("sans-serif", Typeface.BOLD));
+                    canvas.drawText(Integer.toString(buttonData.tasks.size()), 50, 50, paint);
+                }
+                badgedIcon = new BitmapDrawable(null, bitmap);
+            }
+
+            return  badgedIcon;
         } catch (RemoteException e) {
             Slog.w(TAG, "Icon not found for " + appInfo, e);
             return null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java
index 5c01f01..1c9b04f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java
@@ -19,7 +19,11 @@
 import android.animation.LayoutTransition;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.ActivityManager.RecentTaskInfo;
+import android.app.ActivityManagerNative;
 import android.app.ActivityOptions;
+import android.app.IActivityManager;
+import android.app.ITaskStackListener;
 import android.content.BroadcastReceiver;
 import android.content.ClipData;
 import android.content.ClipDescription;
@@ -29,8 +33,10 @@
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.AttributeSet;
@@ -51,12 +57,12 @@
 
 /**
  * Container for application icons that appear in the navigation bar. Their appearance is similar
- * to the launcher hotseat. Clicking an icon launches the associated activity. A long click will
- * trigger a drag to allow the icons to be reordered. As an icon is dragged the other icons shift
- * to make space for it to be dropped. These layout changes are animated.
+ * to the launcher hotseat. Clicking an icon launches or activates the associated activity. A long
+ * click will trigger a drag to allow the icons to be reordered. As an icon is dragged the other
+ * icons shift to make space for it to be dropped. These layout changes are animated.
  */
 class NavigationBarApps extends LinearLayout {
-    private final static boolean DEBUG = false;
+    public final static boolean DEBUG = false;
     private final static String TAG = "NavigationBarApps";
 
     /**
@@ -97,16 +103,9 @@
         }
     };
 
-    public static NavigationBarAppsModel getModel(Context context) {
-        if (sAppsModel == null) {
-            sAppsModel = new NavigationBarAppsModel(context);
-        }
-        return sAppsModel;
-    }
-
     public NavigationBarApps(Context context, AttributeSet attrs) {
         super(context, attrs);
-        getModel(context);
+        sAppsModel = new NavigationBarAppsModel(context);
         mPackageManager = context.getPackageManager();
         mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
         mLayoutInflater = LayoutInflater.from(context);
@@ -124,19 +123,27 @@
         transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
         transition.setStagger(LayoutTransition.CHANGE_DISAPPEARING, 0);
         setLayoutTransition(transition);
+
+        TaskStackListener taskStackListener = new TaskStackListener();
+        IActivityManager iam = ActivityManagerNative.getDefault();
+        try {
+            iam.registerTaskStackListener(taskStackListener);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "registerTaskStackListener failed", e);
+        }
     }
 
     // Monitor that catches events like "app uninstalled".
     private class AppPackageMonitor extends PackageMonitor {
         @Override
         public void onPackageRemoved(String packageName, int uid) {
-            postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId()));
+            postUnpinIfUnlauncheable(packageName, new UserHandle(getChangingUserId()));
             super.onPackageRemoved(packageName, uid);
         }
 
         @Override
         public void onPackageModified(String packageName) {
-            postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId()));
+            postUnpinIfUnlauncheable(packageName, new UserHandle(getChangingUserId()));
             super.onPackageModified(packageName);
         }
 
@@ -146,7 +153,7 @@
                 UserHandle user = new UserHandle(getChangingUserId());
 
                 for (String packageName : packages) {
-                    postRemoveIfUnlauncheable(packageName, user);
+                    postUnpinIfUnlauncheable(packageName, user);
                 }
             }
             super.onPackagesAvailable(packages);
@@ -158,31 +165,36 @@
                 UserHandle user = new UserHandle(getChangingUserId());
 
                 for (String packageName : packages) {
-                    postRemoveIfUnlauncheable(packageName, user);
+                    postUnpinIfUnlauncheable(packageName, user);
                 }
             }
             super.onPackagesUnavailable(packages);
         }
     }
 
-    private void postRemoveIfUnlauncheable(final String packageName, final UserHandle user) {
+    private void postUnpinIfUnlauncheable(final String packageName, final UserHandle user) {
         // This method doesn't necessarily get called in the main thread. Redirect the call into
         // the main thread.
         post(new Runnable() {
             @Override
             public void run() {
                 if (!isAttachedToWindow()) return;
-                removeIfUnlauncheable(packageName, user);
+                unpinIfUnlauncheable(packageName, user);
             }
         });
     }
 
-    private void removeIfUnlauncheable(String packageName, UserHandle user) {
-        // Remove icons for all apps that match a package that perhaps became unlauncheable.
+    private void unpinIfUnlauncheable(String packageName, UserHandle user) {
+        // Unpin icons for all apps that match a package that perhaps became unlauncheable.
+        boolean appsWereUnpinned = false;
         for(int i = getChildCount() - 1; i >= 0; --i) {
             View child = getChildAt(i);
-            AppInfo appInfo = (AppInfo)child.getTag();
-            if (appInfo == null) continue;  // Skip the drag placeholder.
+            AppButtonData appButtonData = (AppButtonData)child.getTag();
+            if (appButtonData == null) continue;  // Skip the drag placeholder.
+
+            if (!appButtonData.pinned) continue;
+
+            AppInfo appInfo = appButtonData.appInfo;
             if (!appInfo.getUser().equals(user)) continue;
 
             ComponentName appComponentName = appInfo.getComponentName();
@@ -192,10 +204,15 @@
                 continue;
             }
 
-            removeViewAt(i);
+            appButtonData.pinned = false;
+            appsWereUnpinned = true;
+
+            if (appButtonData.isEmpty()) {
+                removeViewAt(i);
+            }
         }
-        if (getChildCount() != sAppsModel.getApps().size()) {
-            saveApps();
+        if (appsWereUnpinned) {
+            savePinnedApps();
         }
     }
 
@@ -215,7 +232,8 @@
         parent.setLayoutTransition(transition);
 
         sAppsModel.setCurrentUser(ActivityManager.getCurrentUser());
-        recreateAppButtons();
+        recreatePinnedAppButtons();
+        updateRecentApps();
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_USER_SWITCHED);
@@ -232,11 +250,23 @@
         mAppPackageMonitor.unregister();
     }
 
+    private void addAppButton(AppButtonData appButtonData) {
+        ImageView button = createAppButton(appButtonData);
+        addView(button);
+
+        AppInfo app = appButtonData.appInfo;
+        CharSequence appLabel = getAppLabel(mPackageManager, app.getComponentName());
+        button.setContentDescription(appLabel);
+
+        // Load the icon asynchronously.
+        new GetActivityIconTask(mPackageManager, button).execute(appButtonData);
+    }
+
     /**
      * Creates an ImageView icon for each pinned app. Removes any existing icons. May be called
      * to synchronize the current view with the shared data mode.
      */
-    public void recreateAppButtons() {
+    private void recreatePinnedAppButtons() {
         // Remove any existing icon buttons.
         removeAllViews();
 
@@ -244,54 +274,46 @@
         int appCount = apps.size();
         for (int i = 0; i < appCount; i++) {
             AppInfo app = apps.get(i);
-            ImageView button = createAppButton(app);
-            addView(button);
-
-            CharSequence appLabel = getAppLabel(mPackageManager, app.getComponentName());
-            button.setContentDescription(appLabel);
-
-            // Load the icon asynchronously.
-            new GetActivityIconTask(mPackageManager, button).execute(app);
+            addAppButton(new AppButtonData(app, true /* pinned */));
         }
     }
 
     /**
-     * Saves apps stored in app icons into the data model.
+     * Saves pinned apps stored in app icons into the data model.
      */
-    private void saveApps() {
+    private void savePinnedApps() {
         List<AppInfo> apps = new ArrayList<AppInfo>();
         int childCount = getChildCount();
         for (int i = 0; i != childCount; ++i) {
             View child = getChildAt(i);
-            AppInfo appInfo = (AppInfo)child.getTag();
-            if (appInfo == null) continue;  // Skip the drag placeholder.
-            apps.add(appInfo);
+            AppButtonData appButtonData = (AppButtonData)child.getTag();
+            if (appButtonData == null) continue;  // Skip the drag placeholder.
+            if(!appButtonData.pinned) continue;
+            apps.add(appButtonData.appInfo);
         }
         sAppsModel.setApps(apps);
     }
 
     /**
-     * Creates a new ImageView for a launcher activity, inflated from
-     * R.layout.navigation_bar_app_item.
+     * Creates a new ImageView for an app, inflated from R.layout.navigation_bar_app_item.
      */
-    private ImageView createAppButton(AppInfo appInfo) {
+    private ImageView createAppButton(AppButtonData appButtonData) {
         ImageView button = (ImageView) mLayoutInflater.inflate(
                 R.layout.navigation_bar_app_item, this, false /* attachToRoot */);
         button.setOnClickListener(new AppClickListener());
         // TODO: Ripple effect. Use either KeyButtonRipple or the default ripple background.
         button.setOnLongClickListener(new AppLongClickListener());
         button.setOnDragListener(new AppIconDragListener());
-        button.setTag(appInfo);
+        button.setTag(appButtonData);
         return button;
     }
 
-    // Not shared with NavigationBarRecents because the data model is specific to pinned apps.
     private class AppLongClickListener implements View.OnLongClickListener {
         @Override
         public boolean onLongClick(View v) {
             mDragView = (ImageView) v;
-            AppInfo app = (AppInfo) v.getTag();
-            startAppDrag(mDragView, app);
+            AppButtonData appButtonData = (AppButtonData) v.getTag();
+            startAppDrag(mDragView, appButtonData.appInfo);
             return true;
         }
     }
@@ -443,30 +465,30 @@
             return true;
         }
 
+        boolean dragResult = true;
         AppInfo appInfo = getAppFromDragEvent(event);
         if (appInfo == null) {
             // This wasn't a valid drop. Clean up the placeholder.
             removePlaceholderDragViewIfNeeded();
-            return false;
+            dragResult = false;
+        } else if (mDragView.getTag() == null) {
+            // This is a drag that adds a new app. Convert the placeholder to a real icon.
+            updateApp(mDragView, new AppButtonData(appInfo, true /* pinned */));
         }
-
-        // If this was an existing app being dragged then end the drag.
-        if (mDragView.getTag() != null) {
-            endDrag();
-            return true;
-        }
-
-        // The drop had valid data. Convert the placeholder to a real icon.
-        updateApp(mDragView, appInfo);
         endDrag();
-        return true;
+        return dragResult;
     }
 
     /** Cleans up at the end of a drag. */
     private void endDrag() {
+        // An earlier drag event might have canceled the drag. If so, there is nothing to do.
+        if (mDragView == null) return;
+
         mDragView.setVisibility(View.VISIBLE);
         mDragView = null;
-        saveApps();
+        savePinnedApps();
+        // Add recent tasks to the info of the potentially added app.
+        updateRecentApps();
     }
 
     /** Returns an app info from a DragEvent, or null if the data wasn't valid. */
@@ -506,9 +528,9 @@
     }
 
     /** Updates the app at a given view index. */
-    private void updateApp(ImageView button, AppInfo appInfo) {
-        button.setTag(appInfo);
-        new GetActivityIconTask(mPackageManager, button).execute(appInfo);
+    private void updateApp(ImageView button, AppButtonData appButtonData) {
+        button.setTag(appButtonData);
+        new GetActivityIconTask(mPackageManager, button).execute(appButtonData);
     }
 
     /** Removes the empty placeholder view. */
@@ -518,7 +540,6 @@
             return;
         }
         removeView(mDragView);
-        endDrag();
     }
 
     /** Cleans up at the end of the drag. */
@@ -526,6 +547,7 @@
         if (DEBUG) Slog.d(TAG, "onDragEnded");
         // If the icon wasn't already dropped into the app list then remove the placeholder.
         removePlaceholderDragViewIfNeeded();
+        endDrag();
         return true;
     }
 
@@ -561,9 +583,7 @@
      * A click listener that launches an activity.
      */
     private class AppClickListener implements View.OnClickListener {
-        @Override
-        public void onClick(View v) {
-            AppInfo appInfo = (AppInfo)v.getTag();
+        private void launchApp(AppInfo appInfo, View anchor) {
             Intent launchIntent = sAppsModel.buildAppLaunchIntent(appInfo);
             if (launchIntent == null) {
                 Toast.makeText(
@@ -576,32 +596,226 @@
             // already open in a visible window. In that case we should move the task to front
             // with minimal animation, perhaps using ActivityManager.moveTaskToFront().
             Rect sourceBounds = new Rect();
-            v.getBoundsOnScreen(sourceBounds);
+            anchor.getBoundsOnScreen(sourceBounds);
             ActivityOptions opts =
-                    ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
+                    ActivityOptions.makeScaleUpAnimation(
+                            anchor, 0, 0, anchor.getWidth(), anchor.getHeight());
             Bundle optsBundle = opts.toBundle();
             launchIntent.setSourceBounds(sourceBounds);
 
             mContext.startActivityAsUser(launchIntent, optsBundle, appInfo.getUser());
         }
+
+        private void activateLatestTask(List<RecentTaskInfo> tasks) {
+            // 'tasks' is guaranteed to be non-empty.
+            int latestTaskPersistentId = tasks.get(0).persistentId;
+            // Launch or bring the activity to front.
+            IActivityManager manager = ActivityManagerNative.getDefault();
+            try {
+                manager.startActivityFromRecents(latestTaskPersistentId, null /* options */);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Exception when activating a recent task", e);
+            } catch (IllegalArgumentException e) {
+                Slog.e(TAG, "Exception when activating a recent task", e);
+            }
+        }
+
+        @Override
+        public void onClick(View v) {
+            AppButtonData appButtonData = (AppButtonData)v.getTag();
+
+            if (appButtonData.tasks == null || appButtonData.tasks.size() == 0) {
+                launchApp(appButtonData.appInfo, v);
+            } else {
+                activateLatestTask(appButtonData.tasks);
+            }
+        }
     }
 
     private void onUserSwitched(int currentUserId) {
         sAppsModel.setCurrentUser(currentUserId);
-        recreateAppButtons();
+        recreatePinnedAppButtons();
     }
 
     private void onManagedProfileRemoved(UserHandle removedProfile) {
+        // Unpin apps from the removed profile.
+        boolean itemsWereUnpinned = false;
         for(int i = getChildCount() - 1; i >= 0; --i) {
             View view = getChildAt(i);
-            AppInfo appInfo = (AppInfo)view.getTag();
-            if (appInfo == null) return;  // Skip the drag placeholder.
-            if (!appInfo.getUser().equals(removedProfile)) continue;
+            AppButtonData appButtonData = (AppButtonData)view.getTag();
+            if (appButtonData == null) return;  // Skip the drag placeholder.
+            if (!appButtonData.pinned) continue;
+            if (!appButtonData.appInfo.getUser().equals(removedProfile)) continue;
 
-            removeViewAt(i);
+            appButtonData.pinned = false;
+            itemsWereUnpinned = true;
+            if (appButtonData.isEmpty()) {
+                removeViewAt(i);
+            }
         }
-        if (getChildCount() != sAppsModel.getApps().size()) {
-            saveApps();
+        if (itemsWereUnpinned) {
+            savePinnedApps();
+        }
+    }
+
+    /**
+     * Returns app data for a button that matches the provided app info, if it exists, or null
+     * otherwise.
+     */
+    private AppButtonData findAppButtonData(AppInfo appInfo) {
+        int size = getChildCount();
+        for (int i = 0; i < size; ++i) {
+            View view = getChildAt(i);
+            AppButtonData appButtonData = (AppButtonData)view.getTag();
+            if (appButtonData == null) continue;  // Skip the drag placeholder.
+            if (appButtonData.appInfo.equals(appInfo)) {
+                return appButtonData;
+            }
+        }
+        return null;
+    }
+
+    private void updateTasks(List<RecentTaskInfo> tasks) {
+        // Remove tasks from all app buttons.
+        for (int i = getChildCount() - 1; i >= 0; --i) {
+            View view = getChildAt(i);
+            AppButtonData appButtonData = (AppButtonData)view.getTag();
+            if (appButtonData == null) return;  // Skip the drag placeholder.
+            appButtonData.clearTasks();
+        }
+
+        // Re-add tasks to app buttons, adding new buttons if needed.
+        int size = tasks.size();
+        for (int i = 0; i != size; ++i) {
+            RecentTaskInfo task = tasks.get(i);
+            AppInfo taskAppInfo = taskToAppInfo(task);
+            if (taskAppInfo == null) continue;
+            AppButtonData appButtonData = findAppButtonData(taskAppInfo);
+            if (appButtonData == null) {
+                appButtonData = new AppButtonData(taskAppInfo, false);
+                addAppButton(appButtonData);
+            }
+            appButtonData.addTask(task);
+        }
+
+        // Remove unpinned apps that now have no tasks.
+        for (int i = getChildCount() - 1; i >= 0; --i) {
+            View view = getChildAt(i);
+            AppButtonData appButtonData = (AppButtonData)view.getTag();
+            if (appButtonData == null) return;  // Skip the drag placeholder.
+            if (appButtonData.isEmpty()) {
+                removeViewAt(i);
+            }
+        }
+
+        if (DEBUG) {
+            for (int i = getChildCount() - 1; i >= 0; --i) {
+                View view = getChildAt(i);
+                AppButtonData appButtonData = (AppButtonData)view.getTag();
+                if (appButtonData == null) return;  // Skip the drag placeholder.
+                new GetActivityIconTask(mPackageManager, (ImageView )view).execute(appButtonData);
+
+            }
+        }
+    }
+
+    private void updateRecentApps() {
+        ActivityManager activityManager =
+                (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+        // TODO: Should this be getRunningTasks?
+        List<RecentTaskInfo> recentTasks = activityManager.getRecentTasksForUser(
+                ActivityManager.getMaxAppRecentsLimitStatic(),
+                ActivityManager.RECENT_IGNORE_HOME_STACK_TASKS |
+                        ActivityManager.RECENT_IGNORE_UNAVAILABLE |
+                        ActivityManager.RECENT_INCLUDE_PROFILES,
+                UserHandle.USER_CURRENT);
+        if (DEBUG) Slog.d(TAG, "Got recents " + recentTasks.size());
+        updateTasks(recentTasks);
+    }
+
+    private static ComponentName getActivityForTask(RecentTaskInfo task) {
+        // If the task was started from an alias, return the actual activity component that was
+        // initially started.
+        if (task.origActivity != null) {
+            return task.origActivity;
+        }
+        // Prefer the first activity of the task.
+        if (task.baseActivity != null) {
+            return task.baseActivity;
+        }
+        // Then goes the activity that started the task.
+        if (task.realActivity != null) {
+            return task.realActivity;
+        }
+        // This should not happen, but fall back to the base intent's activity component name.
+        return task.baseIntent.getComponent();
+    }
+
+    private ComponentName getLaunchComponentForPackage(String packageName, int userId) {
+        // This code is based on ApplicationPackageManager.getLaunchIntentForPackage.
+        PackageManager packageManager = mContext.getPackageManager();
+
+        // First see if the package has an INFO activity; the existence of
+        // such an activity is implied to be the desired front-door for the
+        // overall package (such as if it has multiple launcher entries).
+        Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
+        intentToResolve.addCategory(Intent.CATEGORY_INFO);
+        intentToResolve.setPackage(packageName);
+        List<ResolveInfo> ris = packageManager.queryIntentActivitiesAsUser(
+                intentToResolve, 0, userId);
+
+        // Otherwise, try to find a main launcher activity.
+        if (ris == null || ris.size() <= 0) {
+            // reuse the intent instance
+            intentToResolve.removeCategory(Intent.CATEGORY_INFO);
+            intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
+            intentToResolve.setPackage(packageName);
+            ris = packageManager.queryIntentActivitiesAsUser(intentToResolve, 0, userId);
+        }
+        if (ris == null || ris.size() <= 0) {
+            Slog.e(TAG, "Failed to build intent for " + packageName);
+            return null;
+        }
+        return new ComponentName(ris.get(0).activityInfo.packageName,
+                ris.get(0).activityInfo.name);
+    }
+
+    private AppInfo taskToAppInfo(RecentTaskInfo task) {
+        ComponentName componentName = getActivityForTask(task);
+        UserHandle taskUser = new UserHandle(task.userId);
+        AppInfo appInfo = new AppInfo(componentName, taskUser);
+
+        if (sAppsModel.buildAppLaunchIntent(appInfo) == null) {
+            // If task's activity is not launcheable, fall back to a launch component of the
+            // task's package.
+            ComponentName component = getLaunchComponentForPackage(
+                    componentName.getPackageName(), task.userId);
+
+            if (component == null) {
+                return null;
+            }
+
+            appInfo = new AppInfo(component, taskUser);
+        }
+
+        return appInfo;
+    }
+
+    /**
+     * A listener that updates the app buttons whenever the recents task stack changes.
+     */
+    private class TaskStackListener extends ITaskStackListener.Stub {
+        @Override
+        public void onTaskStackChanged() throws RemoteException {
+            // Post the message back to the UI thread.
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    if (isAttachedToWindow()) {
+                        updateRecentApps();
+                    }
+                }
+            });
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarRecents.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarRecents.java
deleted file mode 100644
index 7ff56ba..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarRecents.java
+++ /dev/null
@@ -1,294 +0,0 @@
-/*
- * Copyright (C) 2015 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.statusbar.phone;
-
-import android.app.ActivityManager;
-import android.app.ActivityManager.RecentTaskInfo;
-import android.app.ActivityManagerNative;
-import android.app.IActivityManager;
-import android.app.ITaskStackListener;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.util.AttributeSet;
-import android.util.Slog;
-import android.util.SparseBooleanArray;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import com.android.systemui.R;
-
-import java.util.List;
-
-/**
- * Recent task icons appearing in the navigation bar. Touching an icon brings the activity to the
- * front. The tag for each icon's View contains the RecentTaskInfo.
- */
-class NavigationBarRecents extends LinearLayout {
-    private final static boolean DEBUG = false;
-    private final static String TAG = "NavigationBarRecents";
-
-    // Maximum number of icons to show.
-    // TODO: Implement an overflow UI so the shelf can display an unlimited number of recents.
-    private final static int MAX_RECENTS = 10;
-
-    private final ActivityManager mActivityManager;
-    private final PackageManager mPackageManager;
-    private final LayoutInflater mLayoutInflater;
-    // All icons share the same long-click listener.
-    private final AppLongClickListener mAppLongClickListener;
-    private final TaskStackListenerImpl mTaskStackListener;
-
-    public NavigationBarRecents(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        mActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-        mPackageManager = getContext().getPackageManager();
-        mLayoutInflater = LayoutInflater.from(context);
-        mAppLongClickListener = new AppLongClickListener(context);
-
-        // Listen for task stack changes and refresh when they happen. Update notifications happen
-        // on an IPC thread, so use Handler to handle the message on the main thread.
-        // TODO: This has too much latency. It only adds the icon when app launch is completed
-        // and the launch animation is done playing. This class should add the icon immediately
-        // when the launch starts.
-        Handler handler = new Handler();
-        mTaskStackListener = new TaskStackListenerImpl(handler);
-        IActivityManager iam = ActivityManagerNative.getDefault();
-        try {
-            iam.registerTaskStackListener(mTaskStackListener);
-        } catch (RemoteException e) {
-            e.printStackTrace();
-        }
-    }
-
-    private void updateRecentApps() {
-        // TODO: Should this be getRunningTasks?
-        // TODO: Query other UserHandles?
-        List<RecentTaskInfo> recentTasks = mActivityManager.getRecentTasksForUser(
-                MAX_RECENTS,
-                ActivityManager.RECENT_IGNORE_HOME_STACK_TASKS |
-                ActivityManager.RECENT_IGNORE_UNAVAILABLE |
-                ActivityManager.RECENT_INCLUDE_PROFILES,
-                UserHandle.USER_CURRENT);
-        if (DEBUG) Slog.d(TAG, "Got recents " + recentTasks.size());
-        removeMissingRecents(recentTasks);
-        addNewRecents(recentTasks);
-    }
-
-    // Removes any icons that disappeared from recents.
-    private void removeMissingRecents(List<RecentTaskInfo> recentTasks) {
-        // Build a set of the new task ids.
-        SparseBooleanArray newTaskIds = new SparseBooleanArray();
-        for (RecentTaskInfo task : recentTasks) {
-            newTaskIds.put(task.persistentId, true);
-        }
-
-        // Iterate through the currently displayed tasks. If they no longer exist in recents,
-        // remove them.
-        int i = 0;
-        while (i < getChildCount()) {
-            RecentTaskInfo currentTask = (RecentTaskInfo) getChildAt(i).getTag();
-            if (!newTaskIds.get(currentTask.persistentId)) {
-                if (DEBUG) Slog.d(TAG, "Removing " + currentTask.baseIntent);
-                removeViewAt(i);
-            } else {
-                i++;
-            }
-        }
-    }
-
-    // Adds new tasks at the end of the icon list.
-    private void addNewRecents(List<RecentTaskInfo> recentTasks) {
-        // Build a set of the current task ids.
-        SparseBooleanArray currentTaskIds = new SparseBooleanArray();
-        for (int i = 0; i < getChildCount(); i++) {
-            RecentTaskInfo task = (RecentTaskInfo) getChildAt(i).getTag();
-            currentTaskIds.put(task.persistentId, true);
-        }
-
-        // Add tasks that don't currently exist to the end of the view.
-        for (RecentTaskInfo task : recentTasks) {
-            // Don't overflow the list.
-            if (getChildCount() >= MAX_RECENTS) {
-                return;
-            }
-            // Don't add tasks that are already being shown.
-            if (currentTaskIds.get(task.persistentId)) {
-                continue;
-            }
-            addRecentAppButton(task);
-        }
-    }
-
-    // Adds an icon at the end of the shelf.
-    private void addRecentAppButton(RecentTaskInfo task) {
-        if (DEBUG) Slog.d(TAG, "Adding " + task.baseIntent);
-
-        // Add an icon for the task.
-        ImageView button = (ImageView) mLayoutInflater.inflate(
-                R.layout.navigation_bar_app_item, this, false /* attachToRoot */);
-        button.setOnLongClickListener(mAppLongClickListener);
-        addView(button);
-
-        ComponentName activityName = getActivityForTask(task);
-        CharSequence appLabel = NavigationBarApps.getAppLabel(mPackageManager, activityName);
-        button.setContentDescription(appLabel);
-
-        // Use the View's tag to store metadata for drag and drop.
-        button.setTag(task);
-
-        button.setVisibility(View.VISIBLE);
-        // Load the activity icon on a background thread.
-        AppInfo app = new AppInfo(activityName, new UserHandle(task.userId));
-        new GetActivityIconTask(mPackageManager, button).execute(app);
-
-        final int taskPersistentId = task.persistentId;
-        button.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                // Launch or bring the activity to front.
-                IActivityManager manager = ActivityManagerNative.getDefault();
-                try {
-                    manager.startActivityFromRecents(taskPersistentId, null /* options */);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Exception when activating a recent task", e);
-                } catch (IllegalArgumentException e) {
-                    Slog.e(TAG, "Exception when activating a recent task", e);
-                }
-            }
-        });
-    }
-
-    private static ComponentName getActivityForTask(RecentTaskInfo task) {
-        // If the task was started from an alias, return the actual activity component that was
-        // initially started.
-        if (task.origActivity != null) {
-            return task.origActivity;
-        }
-        // Prefer the first activity of the task.
-        if (task.baseActivity != null) {
-            return task.baseActivity;
-        }
-        // Then goes the activity that started the task.
-        if (task.realActivity != null) {
-            return task.realActivity;
-        }
-        // This should not happen, but fall back to the base intent's activity component name.
-        return task.baseIntent.getComponent();
-    }
-
-    /**
-     * A listener that updates the app buttons whenever the recents task stack changes.
-     * NOTE: This is not the right way to do this.
-     */
-    private class TaskStackListenerImpl extends ITaskStackListener.Stub {
-        // Handler to post messages to the UI thread.
-        private Handler mHandler;
-
-        public TaskStackListenerImpl(Handler handler) {
-            mHandler = handler;
-        }
-
-        @Override
-        public void onTaskStackChanged() throws RemoteException {
-            // Post the message back to the UI thread.
-            mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    updateRecentApps();
-                }
-            });
-        }
-    }
-
-    /** Starts a drag on long-click on an app icon. */
-    private static class AppLongClickListener implements View.OnLongClickListener {
-        private final Context mContext;
-
-        public AppLongClickListener(Context context) {
-            mContext = context;
-        }
-
-        private ComponentName getLaunchComponentForPackage(String packageName, int userId) {
-            // This code is based on ApplicationPackageManager.getLaunchIntentForPackage.
-            PackageManager packageManager = mContext.getPackageManager();
-
-            // First see if the package has an INFO activity; the existence of
-            // such an activity is implied to be the desired front-door for the
-            // overall package (such as if it has multiple launcher entries).
-            Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
-            intentToResolve.addCategory(Intent.CATEGORY_INFO);
-            intentToResolve.setPackage(packageName);
-            List<ResolveInfo> ris = packageManager.queryIntentActivitiesAsUser(
-                    intentToResolve, 0, userId);
-
-            // Otherwise, try to find a main launcher activity.
-            if (ris == null || ris.size() <= 0) {
-                // reuse the intent instance
-                intentToResolve.removeCategory(Intent.CATEGORY_INFO);
-                intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
-                intentToResolve.setPackage(packageName);
-                ris = packageManager.queryIntentActivitiesAsUser(intentToResolve, 0, userId);
-            }
-            if (ris == null || ris.size() <= 0) {
-                Slog.e(TAG, "Failed to build intent for " + packageName);
-                return null;
-            }
-            return new ComponentName(ris.get(0).activityInfo.packageName,
-                    ris.get(0).activityInfo.name);
-        }
-
-        @Override
-        public boolean onLongClick(View v) {
-            ImageView icon = (ImageView) v;
-
-            // The drag will go to the pinned section, which wants to launch the main activity
-            // for the task's package.
-            RecentTaskInfo task = (RecentTaskInfo) v.getTag();
-            ComponentName componentName = getActivityForTask(task);
-            UserHandle taskUser = new UserHandle(task.userId);
-            AppInfo appInfo = new AppInfo(componentName, taskUser);
-
-            if (NavigationBarApps.getModel(mContext).buildAppLaunchIntent(appInfo) == null) {
-                // If task's activity is not launcheable, fall back to a launch component of the
-                // task's package.
-                ComponentName component = getLaunchComponentForPackage(
-                        componentName.getPackageName(), task.userId);
-
-                if (component == null) {
-                    return false;
-                }
-
-                appInfo = new AppInfo(component, taskUser);
-            }
-
-            if (DEBUG) {
-                Slog.d(TAG, "Start drag with " + appInfo.getComponentName().flattenToString());
-            }
-
-            NavigationBarApps.startAppDrag(icon, appInfo);
-            return true;
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 7de7a7b..f37383a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -492,14 +492,6 @@
         }
 
         updateTaskSwitchHelper();
-
-        // If using the app shelf, synchronize the current icons to the data model.
-        NavigationBarApps apps =
-                (NavigationBarApps) mCurrentView.findViewById(R.id.navigation_bar_apps);
-        if (apps != null) {
-            apps.recreateAppButtons();
-        }
-
         setNavigationIconHints(mNavigationIconHints, true);
     }