Implementing app-centric Shelf.

We finally get a UX decision for the Shelf.
Shelf won't have 2 parts - pinned and recents; it will have only 1 list where
all pinned and recent activities of the same kind are grouped under a single icon.
Clicking at that icon activates the latest running activity if it's present, or
starts a new one otherwise.

The above part is implemented in this CL. I removed stuff related to the separate
Recents pane, and moved surviving code to NavigationBarApps.

Later, we'll have menus popping up from the icon, which will allow pinning and unpinning,
and choosing a concrete activity to activate.

Bug: 20024603
Change-Id: Ia08fe62939f92c7ee4f102c07a31e7168a11f010
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);
     }