Offer to delete broken promise icons.

Track state of promise in the info, not the view.
Fix bugs around moving promises to folders.
Fix bugs around filterign and removing promises.

Bug: 12764789
Change-Id: If5e8b6d315e463154b5bafe8aef7ef4f9889bb95
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ad3a1c4..b7f4505 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -277,4 +277,21 @@
     <string name="package_state_unknown">Unknown</string>
     <!-- Label on an icon that references an uninstalled package, for which restore from market has failed. [CHAR_LIMIT=15] -->
     <string name="package_state_error">Not restored</string>
+
+    <!-- Button for abandoned promises dialog, that removes all abandoned promise icons. -->
+    <string name="abandoned_clean_all">Remove All</string>
+    <!-- Button for abandoned promises dialog, to removes this abandoned promise icon. -->
+    <string name="abandoned_clean_this">Remove</string>
+    <!-- Button for abandoned promise dialog, to search in the market for the missing package. -->
+    <string name="abandoned_search">Search</string>
+    <!-- Title for abandoned promise dialog. -->
+    <string name="abandoned_promises_title">This Package is not Installed</string>
+    <!-- Explanation for abandoned promise dialog. -->
+    <plurals name="abandoned_promises_explanation">
+        <item quantity="one">The package for this icon is not installed.  You many remove it, or
+            attempt to search for the package and install it manually.</item>
+        <item quantity="other">The package for this icon is not installed.  You many remove all
+            similarly broken icons, remove only this icon, or attempt to search for the package and
+            install it manually.</item>
+    </plurals>
 </resources>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 95300c1..992ac58 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -74,8 +74,6 @@
     private CheckLongPressHelper mLongPressHelper;
     private int mInstallState;
 
-    private int mState;
-
     private CharSequence mDefaultText = "";
 
     public BubbleTextView(Context context) {
@@ -124,10 +122,9 @@
         Drawable iconDrawable = Utilities.createIconDrawable(b);
         setCompoundDrawables(null, iconDrawable, null, null);
         setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
-        setText(info.title);
         setTag(info);
         if (info.isPromise()) {
-            setState(ShortcutInfo.PACKAGE_STATE_UNKNOWN); // TODO: persist this state somewhere
+            applyState();
         }
     }
 
@@ -150,6 +147,11 @@
             LauncherModel.checkItemInfo((ItemInfo) tag);
         }
         super.setTag(tag);
+        if (tag instanceof ShortcutInfo) {
+            final ShortcutInfo info = (ShortcutInfo) tag;
+            mDefaultText = info.title;
+            setText(mDefaultText);
+        }
     }
 
     @Override
@@ -415,19 +417,12 @@
         mLongPressHelper.cancelLongPress();
     }
 
-    public void setState(int state) {
-        if (mState == ShortcutInfo.PACKAGE_STATE_DEFAULT && mState != state) {
-            mDefaultText = getText();
-        }
-        mState = state;
-        applyState();
-    }
-
-    private void applyState() {
+    public void applyState() {
         int alpha = getResources().getInteger(R.integer.promise_icon_alpha);
-        if (DEBUG) Log.d(TAG, "applying icon state: " + mState);
+        final int state = getState();
+        if (DEBUG) Log.d(TAG, "applying icon state: " + state);
 
-        switch(mState) {
+        switch(state) {
             case ShortcutInfo.PACKAGE_STATE_DEFAULT:
                 super.setText(mDefaultText);
                 alpha = 255;
@@ -462,4 +457,13 @@
             }
         }
     }
+
+    private int getState() {
+        if (! (getTag() instanceof ShortcutInfo)) {
+            return ShortcutInfo.PACKAGE_STATE_DEFAULT;
+        } else {
+            ShortcutInfo info = (ShortcutInfo) getTag();
+            return info.getState();
+        }
+    }
 }
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java
index e900c2b..896be70 100644
--- a/src/com/android/launcher3/Folder.java
+++ b/src/com/android/launcher3/Folder.java
@@ -575,6 +575,7 @@
         textView.setTextColor(getResources().getColor(R.color.folder_items_text_color));
         textView.setShadowsEnabled(false);
         textView.setGlowColor(getResources().getColor(R.color.folder_items_glow_color));
+        textView.applyState();
 
         textView.setOnClickListener(this);
         textView.setOnLongClickListener(this);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index f8c9f7b..4ca3c50 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -28,6 +28,7 @@
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
+import android.app.AlertDialog;
 import android.app.SearchManager;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
@@ -38,6 +39,7 @@
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
@@ -2517,7 +2519,7 @@
      *
      * @param v The view that was clicked. Must be a tagged with a {@link ShortcutInfo}.
      */
-    protected void onClickAppShortcut(View v) {
+    protected void onClickAppShortcut(final View v) {
         if (LOGD) Log.d(TAG, "onClickAppShortcut");
         Object tag = v.getTag();
         if (!(tag instanceof ShortcutInfo)) {
@@ -2541,7 +2543,55 @@
             }
         }
 
+        // Check for abandoned promise
+        if (shortcut.isAbandoned() && v instanceof BubbleTextView) {
+            final ArrayList<BubbleTextView> abandoned =
+                    mWorkspace.getAbandonedPromises(new ArrayList<BubbleTextView>());
+            AlertDialog.Builder builder = new AlertDialog.Builder(this);
+            builder.setTitle(R.string.abandoned_promises_title);
+            builder.setMessage(getResources().getQuantityString(
+                    R.plurals.abandoned_promises_explanation, abandoned.size()));
+            builder.setPositiveButton(R.string.abandoned_search,
+                    new DialogInterface.OnClickListener() {
+                        public void onClick(DialogInterface dialog, int id) {
+                            startAppShortcutActivity(v);
+                        }
+                    }
+            );
+            if (abandoned.size() > 1) {
+                builder.setNegativeButton(R.string.abandoned_clean_all,
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int id) {
+                                final UserHandleCompat user = UserHandleCompat.myUserHandle();
+                                mWorkspace.removeAbandonedPromises(abandoned, user);
+                            }
+                        }
+                );
+            }
+            builder.setNeutralButton(R.string.abandoned_clean_this,
+                    new DialogInterface.OnClickListener() {
+                        public void onClick(DialogInterface dialog, int id) {
+                            final BubbleTextView bubble = (BubbleTextView) v;
+                            final UserHandleCompat user = UserHandleCompat.myUserHandle();
+                            mWorkspace.removeAbandonedPromise(bubble, user);
+                        }
+                    });
+            builder.create().show();
+            return;
+        }
+
         // Start activities
+        startAppShortcutActivity(v);
+    }
+
+    private void startAppShortcutActivity(View v) {
+        Object tag = v.getTag();
+        if (!(tag instanceof ShortcutInfo)) {
+            throw new IllegalArgumentException("Input must be a Shortcut");
+        }
+        final ShortcutInfo shortcut = (ShortcutInfo) tag;
+        final Intent intent = shortcut.intent;
+
         int[] pos = new int[2];
         v.getLocationOnScreen(pos);
         intent.setSourceBounds(new Rect(pos[0], pos[1],
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 07389c9..4d0ec78 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -2975,6 +2975,7 @@
         info.setIcon(mIconCache.getIcon(intent, info.title.toString(), info.user));
         info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
         info.restoredIntent = intent;
+        info.setState(ShortcutInfo.PACKAGE_STATE_UNKNOWN);
         return info;
     }
 
@@ -3088,6 +3089,9 @@
             if (i instanceof ShortcutInfo) {
                 ShortcutInfo info = (ShortcutInfo) i;
                 ComponentName cn = info.intent.getComponent();
+                if (info.restoredIntent != null) {
+                    cn = info.restoredIntent.getComponent();
+                }
                 if (cn != null && f.filterItem(null, info, cn)) {
                     filtered.add(info);
                 }
@@ -3095,6 +3099,9 @@
                 FolderInfo info = (FolderInfo) i;
                 for (ShortcutInfo s : info.contents) {
                     ComponentName cn = s.intent.getComponent();
+                    if (s.restoredIntent != null) {
+                        cn = s.restoredIntent.getComponent();
+                    }
                     if (cn != null && f.filterItem(info, s, cn)) {
                         filtered.add(s);
                     }
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index f40cf9f..a84a903 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -36,22 +36,22 @@
  */
 public class ShortcutInfo extends ItemInfo {
 
-    /** This package is not installed, and there is no other information available. */
+    /** {@link #mState} meaning this package is not installed, and there is no other information. */
     public static final int PACKAGE_STATE_UNKNOWN = -2;
 
-    /** This package is not installed, because installation failed. */
+    /** {@link #mState} meaning this package is not installed, because installation failed. */
     public static final int PACKAGE_STATE_ERROR = -1;
 
-    /** This package is installed.  This is the typical case */
+    /** {@link #mState} meaning this package is installed.  This is the typical case. */
     public static final int PACKAGE_STATE_DEFAULT = 0;
 
-    /** This package is not installed, but some external entity has promised to install it. */
+    /** {@link #mState} meaning some external entity has promised to install this package. */
     public static final int PACKAGE_STATE_ENQUEUED = 1;
 
-    /** This package is not installed, but some external entity is downloading it. */
+    /** {@link #mState} meaning but some external entity is downloading this package. */
     public static final int PACKAGE_STATE_DOWNLOADING = 2;
 
-    /** This package is not installed, but some external entity is installing it. */
+    /** {@link #mState} meaning some external entity is installing this package. */
     public static final int PACKAGE_STATE_INSTALLING = 3;
 
     /**
@@ -82,6 +82,11 @@
      */
     private Bitmap mIcon;
 
+    /**
+     * The installation state of the package that this shortcut represents.
+     */
+    protected int mState;
+
     long firstInstallTime;
     int flags = 0;
 
@@ -110,6 +115,7 @@
         if (restoredIntent != null) {
             intent = restoredIntent;
             restoredIntent = null;
+            mState = PACKAGE_STATE_DEFAULT;
         }
     }
 
@@ -218,5 +224,19 @@
                 && pkgName != null
                 && pkgName.equals(restoredIntent.getComponent().getPackageName());
     }
+
+    public boolean isAbandoned() {
+        return isPromise()
+                && (mState == ShortcutInfo.PACKAGE_STATE_ERROR
+                        || mState == ShortcutInfo.PACKAGE_STATE_UNKNOWN);
+    }
+
+    public int getState() {
+        return mState;
+    }
+
+    public void setState(int state) {
+        mState = state;
+    }
 }
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 6afea82..74ef1d4 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -98,8 +98,8 @@
 
     private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f;
 
-    private static final boolean MAP_NO_RECURSE = false;
-    private static final boolean MAP_RECURSE = true;
+    static final boolean MAP_NO_RECURSE = false;
+    static final boolean MAP_RECURSE = true;
 
     // These animators are used to fade the children's outlines
     private ObjectAnimator mChildrenOutlineFadeInAnimation;
@@ -4857,6 +4857,40 @@
         });
     }
 
+    ArrayList<BubbleTextView> getAbandonedPromises(final ArrayList<BubbleTextView> abandoned) {
+        mapOverShortcuts(Workspace.MAP_RECURSE, new Workspace.ShortcutOperator() {
+            @Override
+            public boolean evaluate(ItemInfo info, View view, View parent) {
+                if (info instanceof ShortcutInfo
+                        && ((ShortcutInfo) info).isAbandoned()
+                        && view instanceof BubbleTextView) {
+                    abandoned.add((BubbleTextView) view);
+                }
+                return false;
+            }
+        });
+        return abandoned;
+    }
+    public void removeAbandonedPromise(BubbleTextView view, UserHandleCompat user) {
+        ArrayList<BubbleTextView> views = new ArrayList<BubbleTextView>(1);
+        views.add(view);
+        removeAbandonedPromises(views, user);
+    }
+
+    public void removeAbandonedPromises(ArrayList<BubbleTextView> views, UserHandleCompat user) {
+        HashSet<ComponentName> cns = new HashSet<ComponentName>(views.size());
+        for (final BubbleTextView bubble : views) {
+            if (bubble.getTag() != null && bubble.getTag() instanceof ShortcutInfo) {
+                final ShortcutInfo shortcut = (ShortcutInfo) bubble.getTag();
+                if (shortcut.isAbandoned()) {
+                    cns.add(shortcut.getRestoredIntent().getComponent());
+                    LauncherModel.deleteItemFromDatabase(mLauncher, shortcut);
+                }
+            }
+        }
+        removeItemsByComponentName(cns, user);
+    }
+
     public void updatePackageState(final String pkgName, final int state) {
         mapOverShortcuts(MAP_RECURSE, new ShortcutOperator() {
             @Override
@@ -4864,7 +4898,8 @@
                 if (info instanceof ShortcutInfo
                         && ((ShortcutInfo) info).isPromiseFor(pkgName)
                         && v instanceof BubbleTextView) {
-                    ((BubbleTextView)v).setState(state);
+                    ((ShortcutInfo) info).setState(state);
+                    ((BubbleTextView)v).applyState();
                 }
                 // process all the shortcuts
                 return false;