Keeping icons in disabled state when SD-card is unmounted

> changing shortcutInfo.isDisabled to be a flag based variable
> on received OnPackageUnavailable, icons are disabled from desktop
instead of being removed. Icons in all apps are removed

Bug: 15852084
Bug: 16238283
Change-Id: I126d23c709682a917d4bbb84de71032593dce8f9
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ff3509b..dd9b170 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -40,6 +40,8 @@
     <string name="folder_name"></string>
     <!-- Displayed when user selects a shortcut for an app that was uninstalled [CHAR_LIMIT=none]-->
     <string name="activity_not_found">App isn\'t installed.</string>
+    <!-- Displayed when user selects a shortcut for an app that is current not available [CHAR_LIMIT=none]-->
+    <string name="activity_not_available">App isn\'t available</string>
     <!-- SafeMode shortcut error string -->
     <string name="safemode_shortcut_error">Downloaded app disabled in Safe mode</string>
     <!--  Labels for the tabs in the customize drawer -->
diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java
index ac9a125..72c6693 100644
--- a/src/com/android/launcher3/AllAppsList.java
+++ b/src/com/android/launcher3/AllAppsList.java
@@ -105,7 +105,7 @@
     /**
      * Remove the apps for the given apk identified by packageName.
      */
-    public void removePackage(String packageName, UserHandleCompat user) {
+    public void removePackage(String packageName, UserHandleCompat user, boolean clearCache) {
         final List<AppInfo> data = this.data;
         for (int i = data.size() - 1; i >= 0; i--) {
             AppInfo info = data.get(i);
@@ -115,7 +115,9 @@
                 data.remove(i);
             }
         }
-        mIconCache.remove(packageName, user);
+        if (clearCache) {
+            mIconCache.remove(packageName, user);
+        }
     }
 
     /**
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 73c7831..07f3045 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -122,7 +122,7 @@
         LauncherAppState app = LauncherAppState.getInstance();
 
         FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b);
-        iconDrawable.setGhostModeEnabled(info.isDisabled);
+        iconDrawable.setGhostModeEnabled(info.isDisabled != 0);
 
         setCompoundDrawables(null, iconDrawable, null, null);
         if (setDefaultPadding) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index aa403db..9bf536e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -88,13 +88,11 @@
 import android.view.ViewAnimationUtils;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
-import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Advanceable;
 import android.widget.FrameLayout;
@@ -2593,6 +2591,16 @@
 
         // Open shortcut
         final ShortcutInfo shortcut = (ShortcutInfo) tag;
+
+        if (shortcut.isDisabled != 0) {
+            int error = R.string.activity_not_available;
+            if ((shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_SAFEMODE) != 0) {
+                error = R.string.safemode_shortcut_error;
+            }
+            Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
+            return;
+        }
+
         final Intent intent = shortcut.intent;
 
         // Check for special shortcuts
@@ -4769,24 +4777,30 @@
      * we only remove specific components from the workspace, where as
      * package-removal should clear all items by package name.
      *
+     * @param reason if non-zero, the icons are not permanently removed, rather marked as disabled.
      * Implementation of the method from LauncherModel.Callbacks.
      */
+    @Override
     public void bindComponentsRemoved(final ArrayList<String> packageNames,
-            final ArrayList<AppInfo> appInfos, final UserHandleCompat user) {
+            final ArrayList<AppInfo> appInfos, final UserHandleCompat user, final int reason) {
         Runnable r = new Runnable() {
             public void run() {
-                bindComponentsRemoved(packageNames, appInfos, user);
+                bindComponentsRemoved(packageNames, appInfos, user, reason);
             }
         };
         if (waitUntilResume(r)) {
             return;
         }
 
-        if (!packageNames.isEmpty()) {
-            mWorkspace.removeItemsByPackageName(packageNames, user);
-        }
-        if (!appInfos.isEmpty()) {
-            mWorkspace.removeItemsByApplicationInfo(appInfos, user);
+        if (reason == 0) {
+            if (!packageNames.isEmpty()) {
+                mWorkspace.removeItemsByPackageName(packageNames, user);
+            }
+            if (!appInfos.isEmpty()) {
+                mWorkspace.removeItemsByApplicationInfo(appInfos, user);
+            }
+        } else {
+            mWorkspace.disableShortcutsByPackageName(packageNames, user, reason);
         }
 
         // Notify the drag controller
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index eb4210f..0b7ee2e 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -202,7 +202,7 @@
         public void updatePackageState(ArrayList<PackageInstallInfo> installInfo);
         public void updatePackageBadge(String packageName);
         public void bindComponentsRemoved(ArrayList<String> packageNames,
-                        ArrayList<AppInfo> appInfos, UserHandleCompat user);
+                        ArrayList<AppInfo> appInfos, UserHandleCompat user, int reason);
         public void bindPackagesUpdated(ArrayList<Object> widgetsAndShortcuts);
         public void bindSearchablesChanged();
         public boolean isAllAppsButtonRank(int rank);
@@ -1981,6 +1981,7 @@
                                 long serialNumber = c.getInt(profileIdIndex);
                                 user = mUserManager.getUserForSerialNumber(serialNumber);
                                 int promiseType = c.getInt(restoredIndex);
+                                int disabledState = 0;
                                 if (user == null) {
                                     // User has been deleted remove the item.
                                     itemsToRemove.add(id);
@@ -2054,14 +2055,13 @@
                                                 itemsToRemove.add(id);
                                                 continue;
                                             }
-                                        } else if (isSdCardReady) {
-                                            // Do not wait for external media load anymore.
-                                            // Log the invalid package, and remove it
-                                            Launcher.addDumpLog(TAG,
-                                                    "Invalid package removed: " + cn, true);
-                                            itemsToRemove.add(id);
-                                            continue;
-                                        } else {
+                                        } else if (launcherApps.isAppEnabled(
+                                                manager, cn.getPackageName(),
+                                                PackageManager.GET_UNINSTALLED_PACKAGES)) {
+                                            // Package is present but not available.
+                                            allowMissingTarget = true;
+                                            disabledState = ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE;
+                                        } else if (!isSdCardReady) {
                                             // SdCard is not ready yet. Package might get available,
                                             // once it is ready.
                                             Launcher.addDumpLog(TAG, "Invalid package: " + cn
@@ -2074,6 +2074,14 @@
                                             pkgs.add(cn.getPackageName());
                                             allowMissingTarget = true;
                                             // Add the icon on the workspace anyway.
+
+                                        } else {
+                                            // Do not wait for external media load anymore.
+                                            // Log the invalid package, and remove it
+                                            Launcher.addDumpLog(TAG,
+                                                    "Invalid package removed: " + cn, true);
+                                            itemsToRemove.add(id);
+                                            continue;
                                         }
                                     } else if (cn == null) {
                                         // For shortcuts with no component, keep them as they are
@@ -2131,8 +2139,10 @@
                                     info.spanX = 1;
                                     info.spanY = 1;
                                     info.intent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber);
-                                    info.isDisabled = isSafeMode
-                                            && !Utilities.isSystemApp(context, intent);
+                                    info.isDisabled = disabledState;
+                                    if (isSafeMode && !Utilities.isSystemApp(context, intent)) {
+                                        info.isDisabled |= ShortcutInfo.FLAG_DISABLED_SAFEMODE;
+                                    }
 
                                     // check & update map of what's occupied
                                     deleteOnInvalidPlacement.set(false);
@@ -2930,20 +2940,34 @@
             synchronized (sBgLock) {
                 final LauncherAppsCompat launcherApps = LauncherAppsCompat
                         .getInstance(mApp.getContext());
-                ArrayList<String> packagesRemoved;
+                final PackageManager manager = context.getPackageManager();
+                final ArrayList<String> packagesRemoved = new ArrayList<String>();
+                final ArrayList<String> packagesUnavailable = new ArrayList<String>();
                 for (Entry<UserHandleCompat, HashSet<String>> entry : sPendingPackages.entrySet()) {
                     UserHandleCompat user = entry.getKey();
-                    packagesRemoved = new ArrayList<String>();
+                    packagesRemoved.clear();
+                    packagesUnavailable.clear();
                     for (String pkg : entry.getValue()) {
                         if (!launcherApps.isPackageEnabledForProfile(pkg, user)) {
-                            Launcher.addDumpLog(TAG, "Package not found: " + pkg, true);
-                            packagesRemoved.add(pkg);
+                            boolean packageOnSdcard = launcherApps.isAppEnabled(
+                                    manager, pkg, PackageManager.GET_UNINSTALLED_PACKAGES);
+                            if (packageOnSdcard) {
+                                Launcher.addDumpLog(TAG, "Package found on sd-card: " + pkg, true);
+                                packagesUnavailable.add(pkg);
+                            } else {
+                                Launcher.addDumpLog(TAG, "Package not found: " + pkg, true);
+                                packagesRemoved.add(pkg);
+                            }
                         }
                     }
                     if (!packagesRemoved.isEmpty()) {
                         enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_REMOVE,
                                 packagesRemoved.toArray(new String[packagesRemoved.size()]), user));
                     }
+                    if (!packagesUnavailable.isEmpty()) {
+                        enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_UNAVAILABLE,
+                                packagesUnavailable.toArray(new String[packagesUnavailable.size()]), user));
+                    }
                 }
                 sPendingPackages.clear();
             }
@@ -2991,9 +3015,10 @@
                     break;
                 case OP_REMOVE:
                 case OP_UNAVAILABLE:
+                    boolean clearCache = mOp == OP_REMOVE;
                     for (int i=0; i<N; i++) {
                         if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
-                        mBgAllAppsList.removePackage(packages[i], mUser);
+                        mBgAllAppsList.removePackage(packages[i], mUser, clearCache);
                         WidgetPreviewLoader.removePackageFromDb(
                                 mApp.getWidgetPreviewCacheDb(), packages[i]);
                     }
@@ -3041,7 +3066,7 @@
                     ArrayList<ItemInfo> infos =
                             getItemInfoForComponentName(a.componentName, mUser);
                     for (ItemInfo i : infos) {
-                        if (isShortcutInfoUpdateable(i)) {
+                        if (i instanceof ShortcutInfo && isShortcutAppTarget((ShortcutInfo) i)) {
                             ShortcutInfo info = (ShortcutInfo) i;
                             info.title = a.title.toString();
                             info.contentDescription = a.contentDescription;
@@ -3064,7 +3089,7 @@
             if (mOp == OP_ADD || mOp == OP_UPDATE) {
                 final ArrayList<ShortcutInfo> iconsChanged = new ArrayList<ShortcutInfo>();
                 HashSet<String> packageSet = new HashSet<String>(Arrays.asList(packages));
-                // We need to iteration over the items here, so that we can avoid new Bitmap
+                // We need to iterate over the items here, so that we can avoid new Bitmap
                 // creation on the UI thread.
                 synchronized (sBgLock) {
                     for (ItemInfo info : sBgWorkspaceItems) {
@@ -3099,28 +3124,35 @@
 
             final ArrayList<String> removedPackageNames =
                     new ArrayList<String>();
-            if (mOp == OP_REMOVE) {
+            if (mOp == OP_REMOVE || mOp == OP_UNAVAILABLE) {
                 // Mark all packages in the broadcast to be removed
                 removedPackageNames.addAll(Arrays.asList(packages));
             } else if (mOp == OP_UPDATE) {
                 // Mark disabled packages in the broadcast to be removed
-                final PackageManager pm = context.getPackageManager();
                 for (int i=0; i<N; i++) {
                     if (isPackageDisabled(context, packages[i], mUser)) {
                         removedPackageNames.add(packages[i]);
                     }
                 }
             }
-            // Remove all the components associated with this package
-            for (String pn : removedPackageNames) {
-                deletePackageFromDatabase(context, pn, mUser);
-            }
-            // Remove all the specific components
-            for (AppInfo a : removedApps) {
-                ArrayList<ItemInfo> infos = getItemInfoForComponentName(a.componentName, mUser);
-                deleteItemsFromDatabase(context, infos);
-            }
+
             if (!removedPackageNames.isEmpty() || !removedApps.isEmpty()) {
+                final int removeReason;
+                if (mOp == OP_UNAVAILABLE) {
+                    removeReason = ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE;
+                } else {
+                    // Remove all the components associated with this package
+                    for (String pn : removedPackageNames) {
+                        deletePackageFromDatabase(context, pn, mUser);
+                    }
+                    // Remove all the specific components
+                    for (AppInfo a : removedApps) {
+                        ArrayList<ItemInfo> infos = getItemInfoForComponentName(a.componentName, mUser);
+                        deleteItemsFromDatabase(context, infos);
+                    }
+                    removeReason = 0;
+                }
+
                 // Remove any queued items from the install queue
                 String spKey = LauncherAppState.getSharedPreferencesKey();
                 SharedPreferences sp =
@@ -3131,7 +3163,8 @@
                     public void run() {
                         Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
                         if (callbacks == cb && cb != null) {
-                            callbacks.bindComponentsRemoved(removedPackageNames, removedApps, mUser);
+                            callbacks.bindComponentsRemoved(
+                                    removedPackageNames, removedApps, mUser, removeReason);
                         }
                     }
                 });
@@ -3381,24 +3414,18 @@
         return filterItemInfos(sBgItemsIdMap.values(), filter);
     }
 
-    public static boolean isShortcutInfoUpdateable(ItemInfo i) {
-        if (i instanceof ShortcutInfo) {
-            ShortcutInfo info = (ShortcutInfo) i;
-            // We need to check for ACTION_MAIN otherwise getComponent() might
-            // return null for some shortcuts (for instance, for shortcuts to
-            // web pages.)
-            Intent intent = info.intent;
-            ComponentName name = intent.getComponent();
-            if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
-                    Intent.ACTION_MAIN.equals(intent.getAction()) && name != null) {
-                return true;
-            }
-            // placeholder shortcuts get special treatment, let them through too.
-            if (info.isPromise()) {
-                return true;
-            }
-        }
-        return false;
+    /**
+     * @return true if the ShortcutInfo points to an app shortcut target, i.e. it has been added by
+     * dragging from AllApps list.
+     */
+    public static boolean isShortcutAppTarget(ShortcutInfo info) {
+        // We need to check for ACTION_MAIN otherwise getComponent() might
+        // return null for some shortcuts (for instance, for shortcuts to
+        // web pages.)
+        Intent intent = info.promisedIntent != null ? info.promisedIntent : info.intent;
+        ComponentName name = intent.getComponent();
+        return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
+                Intent.ACTION_MAIN.equals(intent.getAction()) && name != null;
     }
 
     /**
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index daf3434..01f7931 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -88,10 +88,20 @@
     private Bitmap mIcon;
 
     /**
+     * Indicates that the icon is disabled due to safe mode restrictions.
+     */
+    public static final int FLAG_DISABLED_SAFEMODE = 1;
+
+    /**
+     * Indicates that the icon is disabled as the app is not available.
+     */
+    public static final int FLAG_DISABLED_NOT_AVAILABLE = 2;
+
+    /**
      * Could be disabled, if the the app is installed but unavailable (eg. in safe mode or when
      * sd-card is not available).
      */
-    boolean isDisabled = false;
+    int isDisabled = DEFAULT;
 
     int status;
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 5951be6..965eaae 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -4629,6 +4629,34 @@
         });
     }
 
+    public void disableShortcutsByPackageName(final ArrayList<String> packages,
+            final UserHandleCompat user, final int reason) {
+        final HashSet<String> packageNames = new HashSet<String>();
+        packageNames.addAll(packages);
+
+        mapOverItems(MAP_RECURSE, new ItemOperator() {
+            @Override
+            public boolean evaluate(ItemInfo info, View v, View parent) {
+                if (info instanceof ShortcutInfo && v instanceof BubbleTextView) {
+                    ShortcutInfo shortcutInfo = (ShortcutInfo) info;
+                    ComponentName cn = shortcutInfo.getTargetComponent();
+                    if (user.equals(shortcutInfo.user) && cn != null
+                            && packageNames.contains(cn.getPackageName())) {
+                        shortcutInfo.isDisabled |= reason;
+                        BubbleTextView shortcut = (BubbleTextView) v;
+                        shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache, true, false);
+
+                        if (parent != null) {
+                            parent.invalidate();
+                        }
+                    }
+                }
+                // process all the shortcuts
+                return false;
+            }
+        });
+    }
+
     // Removes ALL items that match a given package name, this is usually called when a package
     // has been removed and we want to remove all components (widgets, shortcuts, apps) that
     // belong to that package.
@@ -4859,7 +4887,6 @@
                     ComponentName cn = shortcutInfo.getTargetComponent();
                     AppInfo appInfo = appsMap.get(cn);
                     if (user.equals(shortcutInfo.user) && cn != null
-                            && LauncherModel.isShortcutInfoUpdateable(info)
                             && pkgNames.contains(cn.getPackageName())) {
                         boolean promiseStateChanged = false;
                         boolean infoUpdated = false;
@@ -4904,8 +4931,14 @@
                             LauncherModel.updateItemInDatabase(getContext(), shortcutInfo);
                         }
 
+                        if ((shortcutInfo.isDisabled & ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE) != 0) {
+                            // Since package was just updated, the target must be available now.
+                            shortcutInfo.isDisabled &= ~ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE;
+                            infoUpdated = true;
+                        }
 
-                        if (appInfo != null) {
+                        // Only update the icon and labels if the shortcuts points to an app target
+                        if ((appInfo != null) && LauncherModel.isShortcutAppTarget(shortcutInfo)) {
                             shortcutInfo.updateIcon(mIconCache);
                             shortcutInfo.title = appInfo.title.toString();
                             shortcutInfo.contentDescription = appInfo.contentDescription;
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java
index 6efcc00..5858bc8 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompat.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java
@@ -19,8 +19,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.graphics.Rect;
-import android.os.Build;
 import android.os.Bundle;
 
 import com.android.launcher3.Utilities;
@@ -73,4 +75,13 @@
     public abstract boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user);
     public abstract boolean isActivityEnabledForProfile(ComponentName component,
             UserHandleCompat user);
+
+    public boolean isAppEnabled(PackageManager pm, String packageName, int flags) {
+        try {
+            ApplicationInfo info = pm.getApplicationInfo(packageName, flags);
+            return info != null && info.enabled;
+        } catch (NameNotFoundException e) {
+            return false;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
index 7e5e6bf..e47b9a5 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
@@ -22,7 +22,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
-import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
@@ -112,12 +111,7 @@
     }
 
     public boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user) {
-        try {
-            PackageInfo info = mPm.getPackageInfo(packageName, 0);
-            return info != null && info.applicationInfo.enabled;
-        } catch (NameNotFoundException e) {
-            return false;
-        }
+        return isAppEnabled(mPm, packageName, 0);
     }
 
     public boolean isActivityEnabledForProfile(ComponentName component, UserHandleCompat user) {
@@ -198,8 +192,13 @@
                     callback.onPackagesAvailable(packages, user, replacing);
                 }
             } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
-                final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING,
-                        Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT);
+                // This intent is broadcasted when moving a package or mounting/un-mounting
+                // external storage.
+                // However on Kitkat this is also sent when a package is being updated, and
+                // contains an extra Intent.EXTRA_REPLACING=true for that case.
+                // Using false as default for Intent.EXTRA_REPLACING gives correct value on
+                // lower devices as the intent is not sent when the app is updating/replacing.
+                final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
                 String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
                 for (OnAppsChangedCallbackCompat callback : getCallbacks()) {
                     callback.onPackagesUnavailable(packages, user, replacing);