Grey out suspended applications.

Grey out application shortcuts and all apps entries
for packages that are suspended.

Bug: 22776761
Change-Id: I1b63da1816aca1de52b9f9bee62d1b162d0cdf4d
diff --git a/proguard.flags b/proguard.flags
index 05963f7..19d2f0c 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -70,3 +70,10 @@
   public float getBackgroundAlpha();
   public void setBackgroundAlpha(float);
 }
+
+# Proguard will strip new callbacks in LauncherApps.Callback from
+# WrappedCallback if compiled against an older SDK. Don't let this happen.
+-keep class com.android.launcher3.compat.** {
+  *;
+}
+
diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java
index 3b25dca..f76c185 100644
--- a/src/com/android/launcher3/AllAppsList.java
+++ b/src/com/android/launcher3/AllAppsList.java
@@ -118,6 +118,25 @@
         }
     }
 
+    /**
+     * Suspend the apps for the given apk identified by packageName.
+     */
+    public void suspendPackage(String packageName, UserHandleCompat user, boolean suspend) {
+        final List<AppInfo> data = this.data;
+        for (int i = data.size() - 1; i >= 0; i--) {
+            AppInfo info = data.get(i);
+            final ComponentName component = info.intent.getComponent();
+            if (info.user.equals(user) && packageName.equals(component.getPackageName())) {
+                if (suspend) {
+                    info.isDisabled |= ShortcutInfo.FLAG_DISABLED_SUSPENDED;
+                } else {
+                    info.isDisabled &= ~ShortcutInfo.FLAG_DISABLED_SUSPENDED;
+                }
+                modified.add(info);
+            }
+        }
+    }
+
     public void updateIconsAndLabels(HashSet<String> packages, UserHandleCompat user,
             ArrayList<AppInfo> outUpdates) {
         for (AppInfo info : data) {
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index 1923df7..32be12f 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -56,6 +56,11 @@
 
     int flags = 0;
 
+    /**
+     * {@see ShortcutInfo#isDisabled}
+     */
+    int isDisabled = ShortcutInfo.DEFAULT;
+
     AppInfo() {
         itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT;
     }
@@ -75,8 +80,10 @@
             IconCache iconCache) {
         this.componentName = info.getComponentName();
         this.container = ItemInfo.NO_ID;
-
         flags = initFlags(info);
+        if ((info.getApplicationInfo().flags & LauncherActivityInfoCompat.FLAG_SUSPENDED) != 0) {
+            isDisabled |= ShortcutInfo.FLAG_DISABLED_SUSPENDED;
+        }
         iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */);
         intent = makeLaunchIntent(context, info, user);
         this.user = user;
@@ -101,6 +108,7 @@
         title = Utilities.trim(info.title);
         intent = new Intent(info.intent);
         flags = info.flags;
+        isDisabled = info.isDisabled;
         iconBitmap = info.iconBitmap;
     }
 
@@ -140,4 +148,9 @@
             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
             .putExtra(EXTRA_PROFILE, serialNumber);
     }
+
+    @Override
+    public boolean isDisabled() {
+        return isDisabled != 0;
+    }
 }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index ce7e392..8842d5c 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -150,7 +150,7 @@
         Bitmap b = info.getIcon(iconCache);
 
         FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b);
-        if (info.isDisabled != 0) {
+        if (info.isDisabled()) {
             iconDrawable.setState(FastBitmapDrawable.State.DISABLED);
         }
         setIcon(iconDrawable, mIconSize);
@@ -166,7 +166,11 @@
     }
 
     public void applyFromApplicationInfo(AppInfo info) {
-        setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize);
+        FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(info.iconBitmap);
+        if (info.isDisabled()) {
+            iconDrawable.setState(FastBitmapDrawable.State.DISABLED);
+        }
+        setIcon(iconDrawable, mIconSize);
         setText(info.title);
         if (info.contentDescription != null) {
             setContentDescription(info.contentDescription);
@@ -250,11 +254,11 @@
     private void updateIconState() {
         if (mIcon instanceof FastBitmapDrawable) {
             FastBitmapDrawable d = (FastBitmapDrawable) mIcon;
-            if (isPressed() || mStayPressed) {
-                d.animateState(FastBitmapDrawable.State.PRESSED);
-            } else if (getTag() instanceof ShortcutInfo
-                    && ((ShortcutInfo) getTag()).isDisabled != 0) {
+            if (getTag() instanceof ItemInfo
+                    && ((ItemInfo) getTag()).isDisabled()) {
                 d.animateState(FastBitmapDrawable.State.DISABLED);
+            } else if (isPressed() || mStayPressed) {
+                d.animateState(FastBitmapDrawable.State.PRESSED);
             } else {
                 d.animateState(FastBitmapDrawable.State.NORMAL);
             }
diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java
index aa5a18d..1ba09e1 100644
--- a/src/com/android/launcher3/ItemInfo.java
+++ b/src/com/android/launcher3/ItemInfo.java
@@ -198,4 +198,11 @@
             + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX
             + " spanY=" + spanY + " user=" + user + ")";
     }
+
+    /**
+     * Whether this item is disabled.
+     */
+    public boolean isDisabled() {
+        return false;
+    }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 0df3084..6a548c3 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2652,12 +2652,16 @@
         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;
+            if ((shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_SUSPENDED) != 0) {
+                // Launch activity anyway, framework will tell the user why the app is suspended.
+            } else {
+                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;
             }
-            Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
-            return;
         }
 
         // Check for abandoned promise
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 92ef3ea..52d2dca 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -1198,6 +1198,20 @@
         }
     }
 
+    @Override
+    public void onPackagesSuspended(String[] packageNames, UserHandleCompat user) {
+        enqueuePackageUpdated(new PackageUpdatedTask(
+                PackageUpdatedTask.OP_SUSPEND, packageNames,
+                user));
+    }
+
+    @Override
+    public void onPackagesUnsuspended(String[] packageNames, UserHandleCompat user) {
+        enqueuePackageUpdated(new PackageUpdatedTask(
+                PackageUpdatedTask.OP_UNSUSPEND, packageNames,
+                user));
+    }
+
     /**
      * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and
      * ACTION_PACKAGE_CHANGED.
@@ -1777,6 +1791,11 @@
                                                 restoredRows.add(id);
                                                 restored = false;
                                             }
+                                            boolean isSuspended = launcherApps.isPackageSuspendedForProfile(
+                                                    cn.getPackageName(), user);
+                                            if (isSuspended) {
+                                                disabledState = ShortcutInfo.FLAG_DISABLED_SUSPENDED;
+                                            }
                                         } else if (validPkg) {
                                             intent = null;
                                             if ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) {
@@ -2892,6 +2911,8 @@
         public static final int OP_UPDATE = 2;
         public static final int OP_REMOVE = 3; // uninstlled
         public static final int OP_UNAVAILABLE = 4; // external media unmounted
+        public static final int OP_SUSPEND = 5; // package suspended
+        public static final int OP_UNSUSPEND = 6; // package unsuspended
 
 
         public PackageUpdatedTask(int op, String[] packages, UserHandleCompat user) {
@@ -2949,6 +2970,15 @@
                         mApp.getWidgetCache().removePackage(packages[i], mUser);
                     }
                     break;
+                case OP_SUSPEND:
+                case OP_UNSUSPEND:
+                    boolean suspend = mOp == OP_SUSPEND;
+                    for (int i=0; i<N; i++) {
+                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.suspendPackage "
+                                + suspend + " " + packages[i]);
+                        mBgAllAppsList.suspendPackage(packages[i], mUser, suspend);
+                    }
+                    break;
             }
 
             ArrayList<AppInfo> added = null;
@@ -3001,7 +3031,7 @@
             }
 
             // Update shortcut infos
-            if (mOp == OP_ADD || mOp == OP_UPDATE) {
+            if (mOp == OP_ADD || mOp == OP_UPDATE || mOp == OP_UNSUSPEND) {
                 final ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<ShortcutInfo>();
                 final ArrayList<ShortcutInfo> removedShortcuts = new ArrayList<ShortcutInfo>();
                 final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<LauncherAppWidgetInfo>();
@@ -3014,6 +3044,11 @@
                             boolean infoUpdated = false;
                             boolean shortcutUpdated = false;
 
+                            if (mOp == OP_UNSUSPEND) {
+                                si.isDisabled &= ~ ShortcutInfo.FLAG_DISABLED_SUSPENDED;
+                                infoUpdated = true;
+                            }
+
                             // Update shortcuts which use iconResource.
                             if ((si.iconResource != null)
                                     && packageSet.contains(si.iconResource.packageName)) {
@@ -3139,7 +3174,7 @@
 
             final ArrayList<String> removedPackageNames =
                     new ArrayList<String>();
-            if (mOp == OP_REMOVE || mOp == OP_UNAVAILABLE) {
+            if (mOp == OP_REMOVE || mOp == OP_UNAVAILABLE || mOp == OP_SUSPEND) {
                 // Mark all packages in the broadcast to be removed
                 removedPackageNames.addAll(Arrays.asList(packages));
             } else if (mOp == OP_UPDATE) {
@@ -3155,6 +3190,8 @@
                 final int removeReason;
                 if (mOp == OP_UNAVAILABLE) {
                     removeReason = ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE;
+                } else if (mOp == OP_SUSPEND) {
+                    removeReason = ShortcutInfo.FLAG_DISABLED_SUSPENDED;
                 } else {
                     // Remove all the components associated with this package
                     for (String pn : removedPackageNames) {
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index baa3cd0..0d6708b 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -112,6 +112,11 @@
     public static final int FLAG_DISABLED_NOT_AVAILABLE = 2;
 
     /**
+     * Indicates that the icon is disabled as the app is suspended
+     */
+    public static final int FLAG_DISABLED_SUSPENDED = 4;
+
+    /**
      * Could be disabled, if the the app is installed but unavailable (eg. in safe mode or when
      * sd-card is not available).
      */
@@ -177,6 +182,7 @@
         intent = new Intent(info.intent);
         customIcon = false;
         flags = info.flags;
+        isDisabled = info.isDisabled;
     }
 
     public void setIcon(Bitmap b) {
@@ -278,5 +284,10 @@
         shortcut.flags = AppInfo.initFlags(info);
         return shortcut;
     }
+
+    @Override
+    public boolean isDisabled() {
+        return isDisabled != 0;
+    }
 }
 
diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java
index 0bc9588..aaf756e 100644
--- a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java
+++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java
@@ -24,6 +24,8 @@
 
 public abstract class LauncherActivityInfoCompat {
 
+    public static final int FLAG_SUSPENDED = 1<<30;
+
     LauncherActivityInfoCompat() {
     }
 
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java
index 95e3ba9..da3eb8f 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompat.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java
@@ -42,6 +42,8 @@
         void onPackageChanged(String packageName, UserHandleCompat user);
         void onPackagesAvailable(String[] packageNames, UserHandleCompat user, boolean replacing);
         void onPackagesUnavailable(String[] packageNames, UserHandleCompat user, boolean replacing);
+        void onPackagesSuspended(String[] packageNames, UserHandleCompat user);
+        void onPackagesUnsuspended(String[] packageNames, UserHandleCompat user);
     }
 
     protected LauncherAppsCompat() {
@@ -53,7 +55,9 @@
     public static LauncherAppsCompat getInstance(Context context) {
         synchronized (sInstanceLock) {
             if (sInstance == null) {
-                if (Utilities.ATLEAST_LOLLIPOP) {
+                if (Utilities.isNycOrAbove()) {
+                    sInstance = new LauncherAppsCompatVN(context.getApplicationContext());
+                } else if (Utilities.ATLEAST_LOLLIPOP) {
                     sInstance = new LauncherAppsCompatVL(context.getApplicationContext());
                 } else {
                     sInstance = new LauncherAppsCompatV16(context.getApplicationContext());
@@ -75,6 +79,7 @@
     public abstract boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user);
     public abstract boolean isActivityEnabledForProfile(ComponentName component,
             UserHandleCompat user);
+    public abstract boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user);
 
     public boolean isAppEnabled(PackageManager pm, String packageName, int flags) {
         try {
@@ -84,4 +89,4 @@
             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 339c457..2d0778d 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
@@ -126,6 +126,10 @@
         }
     }
 
+    public boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user) {
+        return false;
+    }
+
     private void unregisterForPackageIntents() {
         mContext.unregisterReceiver(mPackageMonitor);
     }
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
index fbf91b5..7270d02 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
@@ -36,7 +36,7 @@
 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
 public class LauncherAppsCompatVL extends LauncherAppsCompat {
 
-    private LauncherApps mLauncherApps;
+    protected LauncherApps mLauncherApps;
 
     private Map<OnAppsChangedCallbackCompat, WrappedCallback> mCallbacks
             = new HashMap<OnAppsChangedCallbackCompat, WrappedCallback>();
@@ -106,6 +106,10 @@
         return mLauncherApps.isActivityEnabled(component, user.getUser());
     }
 
+    public boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user) {
+        return false;
+    }
+
     private static class WrappedCallback extends LauncherApps.Callback {
         private LauncherAppsCompat.OnAppsChangedCallbackCompat mCallback;
 
@@ -134,6 +138,14 @@
             mCallback.onPackagesUnavailable(packageNames, UserHandleCompat.fromUser(user),
                     replacing);
         }
+
+        public void onPackagesSuspended(String[] packageNames, UserHandle user) {
+            mCallback.onPackagesSuspended(packageNames, UserHandleCompat.fromUser(user));
+        }
+
+        public void onPackagesUnsuspended(String[] packageNames, UserHandle user) {
+            mCallback.onPackagesUnsuspended(packageNames, UserHandleCompat.fromUser(user));
+        }
     }
 }
 
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVN.java b/src/com/android/launcher3/compat/LauncherAppsCompatVN.java
new file mode 100644
index 0000000..0d883b6
--- /dev/null
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatVN.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 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.launcher3.compat;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.os.UserHandle;
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+//TODO: Once gogole3 SDK is updated to N, add @TargetApi(Build.VERSION_CODES.N)
+public class LauncherAppsCompatVN extends LauncherAppsCompatVL {
+
+    private static final String TAG = "LauncherAppsCompatVN";
+
+    LauncherAppsCompatVN(Context context) {
+        super(context);
+    }
+
+    @Override
+    public boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user) {
+        if (user != null && packageName != null) {
+            try {
+                //TODO: Replace with proper API call once google3 SDK is updated.
+                Method getApplicationInfoMethod = LauncherApps.class.getMethod("getApplicationInfo",
+                        String.class, int.class, UserHandle.class);
+
+                ApplicationInfo info = (ApplicationInfo) getApplicationInfoMethod.invoke(
+                        mLauncherApps, packageName, 0, user.getUser());
+                if (info != null) {
+                    return (info.flags & LauncherActivityInfoCompat.FLAG_SUSPENDED) != 0;
+                }
+            } catch (NoSuchMethodError | NoSuchMethodException | IllegalAccessException
+                    | IllegalArgumentException | InvocationTargetException e) {
+                Log.e(TAG, "Running on N without getApplicationInfo", e);
+            }
+        }
+        return false;
+    }
+}