OSDN Git Service

Add support for launcher shortcuts.
authorTony Wickham <twickham@google.com>
Thu, 19 May 2016 18:19:39 +0000 (11:19 -0700)
committerTony Wickham <twickham@google.com>
Tue, 21 Jun 2016 22:49:16 +0000 (15:49 -0700)
- This CL has no UI but provides the necessary backing for one.
- Adds new item type: ITEM_TYPE_DEEP_SHORTCUT, to distinguish from
  ITEM_TYPE_SHORTCUT. We can reconsider these names.
- Adds ShortcutCache, using LruCache for up to 30 dynamic shortcuts
  (pinned shortcuts are always cached in a HashMap).
- DeepShortcutManager queries for shortcuts and other things like
  pin them. In a future CL it will use the cache, but for now it
  simply makes an RPC for all queries.
- LauncherModel maintains counts for pinned shortcuts, pinning and
  unpinning when counts reach 1 or 0, respectively.
- LauncherModel maintains a map of components to lists of shortcut ids,
  which Launcher gets a copy of after it is changed in the background.
  This will allow us to know how many shortcuts an app has immediately,
  and query for details as the UI is animating.

Change-Id: Ic526f374dd10d72a261bae67f07f098fca8d8bca

23 files changed:
build.gradle
src/com/android/launcher3/BubbleTextView.java
src/com/android/launcher3/ItemInfo.java
src/com/android/launcher3/Launcher.java
src/com/android/launcher3/LauncherAppState.java
src/com/android/launcher3/LauncherBackupHelper.java
src/com/android/launcher3/LauncherModel.java
src/com/android/launcher3/LauncherSettings.java
src/com/android/launcher3/ShortcutInfo.java
src/com/android/launcher3/Workspace.java
src/com/android/launcher3/compat/LauncherAppsCompat.java
src/com/android/launcher3/compat/LauncherAppsCompatV16.java
src/com/android/launcher3/compat/LauncherAppsCompatVL.java
src/com/android/launcher3/compat/LauncherAppsCompatVNMR1.java [new file with mode: 0644]
src/com/android/launcher3/folder/Folder.java
src/com/android/launcher3/folder/FolderIcon.java
src/com/android/launcher3/model/GridSizeMigrationTask.java
src/com/android/launcher3/shortcuts/DeepShortcutManager.java [new file with mode: 0644]
src/com/android/launcher3/shortcuts/ShortcutCache.java [new file with mode: 0644]
src/com/android/launcher3/shortcuts/ShortcutInfoCompat.java [new file with mode: 0644]
src/com/android/launcher3/shortcuts/ShortcutKey.java [new file with mode: 0644]
src/com/android/launcher3/util/ManagedProfileHeuristic.java
src/com/android/launcher3/util/MultiHashMap.java [new file with mode: 0644]

index 899767f..f98021c 100644 (file)
@@ -18,7 +18,7 @@ android {
     defaultConfig {
         applicationId "com.android.launcher3"
         minSdkVersion 21
-        targetSdkVersion 23
+        targetSdkVersion 'N'
         versionCode 1
         versionName "1.0"
 
index be00aec..97515a8 100644 (file)
@@ -158,7 +158,7 @@ public class BubbleTextView extends TextView
         if (info.isDisabled()) {
             iconDrawable.setState(FastBitmapDrawable.State.DISABLED);
         }
-        setIcon(iconDrawable, mIconSize);
+        setIcon(iconDrawable);
         if (info.contentDescription != null) {
             setContentDescription(info.contentDescription);
         }
@@ -175,7 +175,7 @@ public class BubbleTextView extends TextView
         if (info.isDisabled()) {
             iconDrawable.setState(FastBitmapDrawable.State.DISABLED);
         }
-        setIcon(iconDrawable, mIconSize);
+        setIcon(iconDrawable);
         setText(info.title);
         if (info.contentDescription != null) {
             setContentDescription(info.contentDescription);
@@ -188,7 +188,7 @@ public class BubbleTextView extends TextView
     }
 
     public void applyFromPackageItemInfo(PackageItemInfo info) {
-        setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize);
+        setIcon(mLauncher.createIconDrawable(info.iconBitmap));
         setText(info.title);
         if (info.contentDescription != null) {
             setContentDescription(info.contentDescription);
@@ -205,7 +205,7 @@ public class BubbleTextView extends TextView
      */
     public void applyDummyInfo() {
         ColorDrawable d = new ColorDrawable();
-        setIcon(mLauncher.resizeIconDrawable(d), mIconSize);
+        setIcon(mLauncher.resizeIconDrawable(d));
         setText("");
     }
 
@@ -477,7 +477,7 @@ public class BubbleTextView extends TextView
                     preloadDrawable = (PreloadIconDrawable) mIcon;
                 } else {
                     preloadDrawable = new PreloadIconDrawable(mIcon, getPreloaderTheme());
-                    setIcon(preloadDrawable, mIconSize);
+                    setIcon(preloadDrawable);
                 }
 
                 preloadDrawable.setLevel(progressLevel);
@@ -506,10 +506,10 @@ public class BubbleTextView extends TextView
      * Sets the icon for this view based on the layout direction.
      */
     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
-    private Drawable setIcon(Drawable icon, int iconSize) {
+    public void setIcon(Drawable icon) {
         mIcon = icon;
-        if (iconSize != -1) {
-            mIcon.setBounds(0, 0, iconSize, iconSize);
+        if (mIconSize != -1) {
+            mIcon.setBounds(0, 0, mIconSize, mIconSize);
         }
         if (mLayoutHorizontal) {
             if (Utilities.ATLEAST_JB_MR1) {
@@ -520,7 +520,6 @@ public class BubbleTextView extends TextView
         } else {
             setCompoundDrawables(null, mIcon, null, null);
         }
-        return icon;
     }
 
     @Override
index 286a7f1..f54a2d4 100644 (file)
@@ -32,7 +32,7 @@ public class ItemInfo {
     /**
      * Intent extra to store the profile. Format: UserHandle
      */
-    static final String EXTRA_PROFILE = "profile";
+    public static final String EXTRA_PROFILE = "profile";
 
     public static final int NO_ID = -1;
 
index 156c1b0..cc31de2 100644 (file)
@@ -117,6 +117,7 @@ import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.TestingUtils;
 import com.android.launcher3.util.Thunk;
@@ -293,6 +294,9 @@ public class Launcher extends Activity
     private boolean mHasFocus = false;
     private boolean mAttached = false;
 
+    /** Maps launcher activity components to their list of shortcut ids. */
+    private MultiHashMap<ComponentKey, String> mDeepShortcutMap = new MultiHashMap<>();
+
     private LauncherClings mClings;
 
     private View.OnTouchListener mHapticFeedbackTouchListener;
@@ -2353,7 +2357,7 @@ public class Launcher extends Activity
      * @param itemInfo the {@link ItemInfo} for this view.
      * @param deleteFromDb whether or not to delete this item from the db.
      */
-    public boolean removeItem(View v, ItemInfo itemInfo, boolean deleteFromDb) {
+    public boolean removeItem(View v, final ItemInfo itemInfo, boolean deleteFromDb) {
         if (itemInfo instanceof ShortcutInfo) {
             // Remove the shortcut from the folder before removing it from launcher
             View folderIcon = mWorkspace.getHomescreenIconByItemId(itemInfo.container);
@@ -2381,7 +2385,6 @@ public class Launcher extends Activity
             if (deleteFromDb) {
                 deleteWidgetInfo(widgetInfo);
             }
-
         } else {
             return false;
         }
@@ -2818,8 +2821,16 @@ public class Launcher extends Activity
                 // is enabled by default on NYC.
                 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll()
                         .penaltyLog().build());
-                // Could be launching some bookkeeping activity
-                startActivity(intent, optsBundle);
+
+                if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                    String id = ((ShortcutInfo) info).getDeepShortcutId();
+                    String packageName = intent.getPackage();
+                    LauncherAppsCompat.getInstance(this).startShortcut(
+                                packageName, id, intent.getSourceBounds(), optsBundle, info.user);
+                } else {
+                    // Could be launching some bookkeeping activity
+                    startActivity(intent, optsBundle);
+                }
             } finally {
                 StrictMode.setVmPolicy(oldPolicy);
             }
@@ -2895,8 +2906,9 @@ public class Launcher extends Activity
                     new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1] + v.getHeight()));
         }
         try {
-            if (Utilities.ATLEAST_MARSHMALLOW &&
-                    item != null && item.itemType == Favorites.ITEM_TYPE_SHORTCUT) {
+            if (Utilities.ATLEAST_MARSHMALLOW && item != null
+                    && (item.itemType == Favorites.ITEM_TYPE_SHORTCUT
+                    || item.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
                 // Shortcuts need some special checks due to legacy reasons.
                 startShortcutIntentSafely(intent, optsBundle, item);
             } else if (user == null || user.equals(UserHandleCompat.myUserHandle())) {
@@ -3702,6 +3714,7 @@ public class Launcher extends Activity
             switch (item.itemType) {
                 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                 case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                     ShortcutInfo info = (ShortcutInfo) item;
                     view = createShortcut(info);
                     break;
@@ -4052,6 +4065,16 @@ public class Launcher extends Activity
     }
 
     /**
+     * Copies LauncherModel's map of activities to shortcut ids to Launcher's. This is necessary
+     * because LauncherModel's map is updated in the background, while Launcher runs on the UI.
+     */
+    @Override
+    public void bindDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) {
+        mDeepShortcutMap = deepShortcutMapCopy;
+        if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
+    }
+
+    /**
      * A package was updated.
      *
      * Implementation of the method from LauncherModel.Callbacks.
index c2e7f1a..2ba4982 100644 (file)
@@ -27,6 +27,8 @@ import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.dynamicui.ExtractionUtils;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutCache;
 import com.android.launcher3.util.ConfigMonitor;
 import com.android.launcher3.util.TestingUtils;
 import com.android.launcher3.util.Thunk;
@@ -39,6 +41,7 @@ public class LauncherAppState {
     @Thunk final LauncherModel mModel;
     private final IconCache mIconCache;
     private final WidgetPreviewLoader mWidgetCache;
+    private final DeepShortcutManager mDeepShortcutManager;
 
     @Thunk boolean mWallpaperChangedSinceLastCheck;
 
@@ -92,9 +95,10 @@ public class LauncherAppState {
         mInvariantDeviceProfile = new InvariantDeviceProfile(sContext);
         mIconCache = new IconCache(sContext, mInvariantDeviceProfile);
         mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache);
+        mDeepShortcutManager = new DeepShortcutManager(sContext, new ShortcutCache());
 
         mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class));
-        mModel = new LauncherModel(this, mIconCache, mAppFilter);
+        mModel = new LauncherModel(this, mIconCache, mAppFilter, mDeepShortcutManager);
 
         LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel);
 
@@ -165,6 +169,10 @@ public class LauncherAppState {
         return mWidgetCache;
     }
 
+    public DeepShortcutManager getShortcutManager() {
+        return mDeepShortcutManager;
+    }
+
     public boolean hasWallpaperChangedSinceLastCheck() {
         boolean result = mWallpaperChangedSinceLastCheck;
         mWallpaperChangedSinceLastCheck = false;
index bca2ffb..e987a9b 100644 (file)
@@ -560,7 +560,8 @@ public class LauncherBackupHelper implements BackupHelper {
 
         // Don't backup apps in other profiles for now.
         String where = "(" + Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION + " OR " +
-                Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_SHORTCUT + ") AND " +
+                Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_SHORTCUT + " OR " +
+                Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_DEEP_SHORTCUT + ") AND " +
                 getUserSelectionArg();
         Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
                 where, null, null);
@@ -798,7 +799,8 @@ public class LauncherBackupHelper implements BackupHelper {
         return favorite.container == Favorites.CONTAINER_HOTSEAT
                 && favorite.intent != null
                 && (favorite.itemType == Favorites.ITEM_TYPE_APPLICATION
-                || favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT);
+                || favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT
+                || favorite.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT);
     }
 
     /** Serialize a Favorite for persistence, including a checksum wrapper. */
@@ -835,7 +837,8 @@ public class LauncherBackupHelper implements BackupHelper {
             if (!TextUtils.isEmpty(appWidgetProvider)) {
                 favorite.appWidgetProvider = appWidgetProvider;
             }
-        } else if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT) {
+        } else if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT
+                || favorite.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
             String iconPackage = c.getString(ICON_PACKAGE_INDEX);
             String iconResource = c.getString(ICON_RESOURCE_INDEX);
             if (!TextUtils.isEmpty(iconPackage) && !TextUtils.isEmpty(iconResource)) {
@@ -897,7 +900,8 @@ public class LauncherBackupHelper implements BackupHelper {
         values.put(Favorites.SPANY, favorite.spanY);
         values.put(Favorites.RANK, favorite.rank);
 
-        if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT) {
+        if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT
+                || favorite.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
             values.put(Favorites.ICON_PACKAGE, favorite.iconPackage);
             values.put(Favorites.ICON_RESOURCE, favorite.iconResource);
             values.put(Favorites.ICON, favorite.icon);
index a5e703e..9e87660 100644 (file)
@@ -41,6 +41,7 @@ import android.provider.BaseColumns;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.LongSparseArray;
+import android.util.MutableInt;
 import android.util.Pair;
 
 import com.android.launcher3.compat.AppWidgetManagerCompat;
@@ -59,6 +60,9 @@ import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.GridSizeMigrationTask;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.provider.LauncherDbUtils;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.CursorIconInfo;
 import com.android.launcher3.util.FlagOp;
@@ -68,6 +72,7 @@ import com.android.launcher3.util.ManagedProfileHeuristic;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.StringFilter;
+import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 
@@ -82,6 +87,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.Executor;
@@ -118,8 +124,9 @@ public class LauncherModel extends BroadcastReceiver
     // We start off with everything not loaded.  After that, we assume that
     // our monitoring of the package manager provides all updates and we never
     // need to do a requery.  These are only ever touched from the loader thread.
-    @Thunk boolean mWorkspaceLoaded;
-    @Thunk boolean mAllAppsLoaded;
+    private boolean mWorkspaceLoaded;
+    private boolean mAllAppsLoaded;
+    private boolean mDeepShortcutsLoaded;
 
     /**
      * Set of runnables to be called on the background thread after the workspace binding
@@ -134,6 +141,9 @@ public class LauncherModel extends BroadcastReceiver
     // Entire list of widgets.
     private final WidgetsModel mBgWidgetsModel;
 
+    // Maps all launcher activities to the id's of their shortcuts (if they have any).
+    private final MultiHashMap<ComponentKey, String> mBgDeepShortcutMap = new MultiHashMap<>();
+
     // The lock that must be acquired before referencing any static bg data structures.  Unlike
     // other locks, this one can generally be held long-term because we never expect any of these
     // static data structures to be referenced outside of the worker thread except on the first
@@ -159,16 +169,21 @@ public class LauncherModel extends BroadcastReceiver
     // sBgWorkspaceScreens is the ordered set of workspace screens.
     static final ArrayList<Long> sBgWorkspaceScreens = new ArrayList<Long>();
 
+    // sBgPinnedShortcutCounts is the ComponentKey representing a pinned shortcut to the number of
+    // times it is pinned.
+    static final Map<ShortcutKey, MutableInt> sBgPinnedShortcutCounts = new HashMap<>();
+
     // sPendingPackages is a set of packages which could be on sdcard and are not available yet
     static final HashMap<UserHandleCompat, HashSet<String>> sPendingPackages =
             new HashMap<UserHandleCompat, HashSet<String>>();
 
     // </ only access in worker thread >
 
-    @Thunk IconCache mIconCache;
+    private IconCache mIconCache;
+    private DeepShortcutManager mDeepShortcutManager;
 
-    @Thunk final LauncherAppsCompat mLauncherApps;
-    @Thunk final UserManagerCompat mUserManager;
+    private final LauncherAppsCompat mLauncherApps;
+    private final UserManagerCompat mUserManager;
 
     public interface Callbacks {
         public boolean setLoadOnResume();
@@ -198,18 +213,21 @@ public class LauncherModel extends BroadcastReceiver
         public void bindWidgetsModel(WidgetsModel model);
         public void onPageBoundSynchronously(int page);
         public void executeOnNextDraw(ViewOnDrawExecutor executor);
+        public void bindDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMap);
     }
 
     public interface ItemInfoFilter {
         public boolean filterItem(ItemInfo parent, ItemInfo info, ComponentName cn);
     }
 
-    LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
+    LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter,
+            DeepShortcutManager deepShortcutManager) {
         Context context = app.getContext();
         mApp = app;
         mBgAllAppsList = new AllAppsList(iconCache, appFilter);
         mBgWidgetsModel = new WidgetsModel(context, iconCache, appFilter);
         mIconCache = iconCache;
+        mDeepShortcutManager = deepShortcutManager;
 
         mLauncherApps = LauncherAppsCompat.getInstance(context);
         mUserManager = UserManagerCompat.getInstance(context);
@@ -678,6 +696,7 @@ public class LauncherModel extends BroadcastReceiver
                 switch (modelItem.itemType) {
                     case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                     case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                    case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                     case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                         if (!sBgWorkspaceItems.contains(modelItem)) {
                             sBgWorkspaceItems.add(modelItem);
@@ -891,6 +910,7 @@ public class LauncherModel extends BroadcastReceiver
                             // Fall through
                         case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                         case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                        case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                             if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
                                     item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
                                 sBgWorkspaceItems.add(item);
@@ -902,6 +922,14 @@ public class LauncherModel extends BroadcastReceiver
                                     Log.e(TAG, msg);
                                 }
                             }
+                            if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                                ShortcutInfo shortcutInfo = (ShortcutInfo) item;
+                                ShortcutKey shortcutToPin = new ShortcutKey(
+                                        shortcutInfo.intent.getPackage(),
+                                        shortcutInfo.user,
+                                        shortcutInfo.getDeepShortcutId());
+                                incrementPinnedShortcutCount(shortcutToPin, true /* shouldPin */);
+                            }
                             break;
                         case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                             sBgAppWidgets.add((LauncherAppWidgetInfo) item);
@@ -968,6 +996,14 @@ public class LauncherModel extends BroadcastReceiver
                                 }
                                 sBgWorkspaceItems.remove(item);
                                 break;
+                            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+                                ShortcutInfo shortcutInfo = ((ShortcutInfo) item);
+                                ShortcutKey pinnedShortcut = new ShortcutKey(
+                                        shortcutInfo.intent.getPackage(),
+                                        shortcutInfo.user,
+                                        shortcutInfo.getDeepShortcutId());
+                                decrementPinnedShortcutCount(pinnedShortcut);
+                                // Fall through.
                             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                             case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                                 sBgWorkspaceItems.remove(item);
@@ -985,6 +1021,39 @@ public class LauncherModel extends BroadcastReceiver
     }
 
     /**
+     * Decrement the count for the given pinned shortcut, unpinning it if the count becomes 0.
+     */
+    private static void decrementPinnedShortcutCount(final ShortcutKey pinnedShortcut) {
+        synchronized (sBgLock) {
+            MutableInt count = sBgPinnedShortcutCounts.get(pinnedShortcut);
+            if (count == null || --count.value == 0) {
+                LauncherAppState.getInstance().getShortcutManager().unpinShortcut(pinnedShortcut);
+            }
+        }
+    }
+
+    /**
+     * Increment the count for the given shortcut, pinning it if the count becomes 1.
+     *
+     * As an optimization, the caller can pass shouldPin == false to avoid
+     * unnecessary RPC's if the shortcut is already pinned.
+     */
+    private static void incrementPinnedShortcutCount(ShortcutKey pinnedShortcut, boolean shouldPin) {
+        synchronized (sBgLock) {
+            MutableInt count = sBgPinnedShortcutCounts.get(pinnedShortcut);
+            if (count == null) {
+                count = new MutableInt(1);
+                sBgPinnedShortcutCounts.put(pinnedShortcut, count);
+            } else {
+                count.value++;
+            }
+            if (shouldPin && count.value == 1) {
+                LauncherAppState.getInstance().getShortcutManager().pinShortcut(pinnedShortcut);
+            }
+        }
+    }
+
+    /**
      * Update the order of the workspace screens in the database. The array list contains
      * a list of screen ids in the order that they should appear.
      */
@@ -1076,28 +1145,28 @@ public class LauncherModel extends BroadcastReceiver
     @Override
     public void onPackageChanged(String packageName, UserHandleCompat user) {
         int op = PackageUpdatedTask.OP_UPDATE;
-        enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName },
+        enqueueItemUpdatedTask(new PackageUpdatedTask(op, new String[] { packageName },
                 user));
     }
 
     @Override
     public void onPackageRemoved(String packageName, UserHandleCompat user) {
         int op = PackageUpdatedTask.OP_REMOVE;
-        enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName },
+        enqueueItemUpdatedTask(new PackageUpdatedTask(op, new String[] { packageName },
                 user));
     }
 
     @Override
     public void onPackageAdded(String packageName, UserHandleCompat user) {
         int op = PackageUpdatedTask.OP_ADD;
-        enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName },
+        enqueueItemUpdatedTask(new PackageUpdatedTask(op, new String[] { packageName },
                 user));
     }
 
     @Override
     public void onPackagesAvailable(String[] packageNames, UserHandleCompat user,
             boolean replacing) {
-        enqueuePackageUpdated(
+        enqueueItemUpdatedTask(
                 new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, packageNames, user));
     }
 
@@ -1105,7 +1174,7 @@ public class LauncherModel extends BroadcastReceiver
     public void onPackagesUnavailable(String[] packageNames, UserHandleCompat user,
             boolean replacing) {
         if (!replacing) {
-            enqueuePackageUpdated(new PackageUpdatedTask(
+            enqueueItemUpdatedTask(new PackageUpdatedTask(
                     PackageUpdatedTask.OP_UNAVAILABLE, packageNames,
                     user));
         }
@@ -1113,18 +1182,24 @@ public class LauncherModel extends BroadcastReceiver
 
     @Override
     public void onPackagesSuspended(String[] packageNames, UserHandleCompat user) {
-        enqueuePackageUpdated(new PackageUpdatedTask(
+        enqueueItemUpdatedTask(new PackageUpdatedTask(
                 PackageUpdatedTask.OP_SUSPEND, packageNames,
                 user));
     }
 
     @Override
     public void onPackagesUnsuspended(String[] packageNames, UserHandleCompat user) {
-        enqueuePackageUpdated(new PackageUpdatedTask(
+        enqueueItemUpdatedTask(new PackageUpdatedTask(
                 PackageUpdatedTask.OP_UNSUSPEND, packageNames,
                 user));
     }
 
+    @Override
+    public void onShortcutsChanged(String packageName, List<ShortcutInfoCompat> shortcuts,
+            UserHandleCompat user) {
+        enqueueItemUpdatedTask(new ShortcutsChangedTask(packageName, shortcuts, user));
+    }
+
     /**
      * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and
      * ACTION_PACKAGE_CHANGED.
@@ -1145,7 +1220,7 @@ public class LauncherModel extends BroadcastReceiver
                 LauncherAppsCompat.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
             UserHandleCompat user = UserHandleCompat.fromIntent(intent);
             if (user != null) {
-                enqueuePackageUpdated(new PackageUpdatedTask(
+                enqueueItemUpdatedTask(new PackageUpdatedTask(
                         PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE,
                         new String[0], user));
             }
@@ -1170,6 +1245,8 @@ public class LauncherModel extends BroadcastReceiver
             stopLoaderLocked();
             if (resetAllAppsLoaded) mAllAppsLoaded = false;
             if (resetWorkspaceLoaded) mWorkspaceLoaded = false;
+            // Always reset deep shortcuts loaded.
+            mDeepShortcutsLoaded = false;
         }
     }
 
@@ -1256,6 +1333,7 @@ public class LauncherModel extends BroadcastReceiver
      *   - workspace icons
      *   - widgets
      *   - all apps icons
+     *   - deep shortcuts within apps
      */
     private class LoaderTask implements Runnable {
         private Context mContext;
@@ -1387,6 +1465,12 @@ public class LauncherModel extends BroadcastReceiver
                 // second step
                 if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");
                 loadAndBindAllApps();
+
+                waitForIdle();
+
+                // third step
+                if (DEBUG_LOADERS) Log.d(TAG, "step 3: loading deep shortcuts");
+                loadAndBindDeepShortcuts();
             }
 
             // Clear out this reference, otherwise we end up holding it until all of the
@@ -1538,6 +1622,7 @@ public class LauncherModel extends BroadcastReceiver
                 sBgFolders.clear();
                 sBgItemsIdMap.clear();
                 sBgWorkspaceScreens.clear();
+                sBgPinnedShortcutCounts.clear();
             }
         }
 
@@ -1581,6 +1666,7 @@ public class LauncherModel extends BroadcastReceiver
 
                 final ArrayList<Long> itemsToRemove = new ArrayList<>();
                 final ArrayList<Long> restoredRows = new ArrayList<>();
+                Map<ShortcutKey, ShortcutInfoCompat> shortcutKeyToPinnedShortcuts = new HashMap<>();
                 final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI;
                 if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri);
                 final Cursor c = contentResolver.query(contentUri, null, null, null, null);
@@ -1631,6 +1717,13 @@ public class LauncherModel extends BroadcastReceiver
                         long serialNo = mUserManager.getSerialNumberForUser(user);
                         allUsers.put(serialNo, user);
                         quietMode.put(serialNo, mUserManager.isQuietModeEnabled(user));
+
+                        List<ShortcutInfoCompat> pinnedShortcuts = mDeepShortcutManager
+                                .queryForPinnedShortcuts(null, user);
+                        for (ShortcutInfoCompat shortcut : pinnedShortcuts) {
+                            shortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
+                                    shortcut);
+                        }
                     }
 
                     ShortcutInfo info;
@@ -1653,6 +1746,7 @@ public class LauncherModel extends BroadcastReceiver
                             switch (itemType) {
                             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                             case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                                 id = c.getLong(idIndex);
                                 intentDescription = c.getString(intentIndex);
                                 serialNumber = c.getInt(profileIdIndex);
@@ -1815,7 +1909,37 @@ public class LauncherModel extends BroadcastReceiver
                                     info = getAppShortcutInfo(intent, user, context, c,
                                             cursorIconInfo.iconIndex, titleIndex,
                                             allowMissingTarget, useLowResIcon);
-                                } else {
+                                } else if (itemType ==
+                                        LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                                    String shortcutId = intent.getStringExtra(
+                                            ShortcutInfoCompat.EXTRA_SHORTCUT_ID);
+                                    String packageName = intent.getPackage();
+                                    ShortcutKey key = new ShortcutKey(intent.getPackage(),
+                                            user, shortcutId);
+                                    ShortcutInfoCompat pinnedShortcut =
+                                            shortcutKeyToPinnedShortcuts.get(key);
+                                    boolean shouldPin = false; // It's already pinned.
+                                    if (pinnedShortcut == null) {
+                                        // It shouldn't be possible for a shortcut to be on the
+                                        // workspace without being pinned, but if one somehow is,
+                                        // we should pin it now to get back to a good state.
+                                        Log.w(TAG, "Shortcut was on workspace but wasn't pinned");
+                                        // Get full details; incrementing the count will pin it.
+                                        List<ShortcutInfoCompat> fullDetails = mDeepShortcutManager
+                                                .queryForFullDetails(packageName,
+                                                Collections.singletonList(shortcutId), user);
+                                        if (fullDetails == null || fullDetails.isEmpty()) {
+                                            itemsToRemove.add(id);
+                                            continue;
+                                        } else {
+                                            pinnedShortcut = fullDetails.get(0);
+                                            shouldPin = true;
+                                        }
+                                    }
+                                    incrementPinnedShortcutCount(key, shouldPin);
+                                    info = ShortcutInfo.fromDeepShortcutInfo(pinnedShortcut,
+                                            context, launcherApps);
+                                } else { // item type == ITEM_TYPE_SHORTCUT
                                     info = getShortcutInfo(c, context, titleIndex, cursorIconInfo);
 
                                     // Shortcuts are only available on the primary profile
@@ -2094,6 +2218,15 @@ public class LauncherModel extends BroadcastReceiver
                     }
                 }
 
+                // Unpin shortcuts that don't exist on the workspace.
+                for (ShortcutKey key : shortcutKeyToPinnedShortcuts.keySet()) {
+                    MutableInt numTimesPinned = sBgPinnedShortcutCounts.get(key);
+                    if (numTimesPinned == null || numTimesPinned.value == 0) {
+                        // Shortcut is pinned but doesn't exist on the workspace; unpin it.
+                        mDeepShortcutManager.unpinShortcut(key);
+                    }
+                }
+
                 // Sort all the folder items and make sure the first 3 items are high resolution.
                 for (FolderInfo folder : sBgFolders) {
                     Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
@@ -2622,6 +2755,27 @@ public class LauncherModel extends BroadcastReceiver
             }
         }
 
+        private void loadAndBindDeepShortcuts() {
+            if (DEBUG_LOADERS) {
+                Log.d(TAG, "loadAndBindDeepShortcuts mDeepShortcutsLoaded=" + mDeepShortcutsLoaded);
+            }
+            if (!mDeepShortcutsLoaded) {
+                mBgDeepShortcutMap.clear();
+                for (UserHandleCompat user : mUserManager.getUserProfiles()) {
+                    List<ShortcutInfoCompat> shortcuts = mDeepShortcutManager
+                            .queryForAllShortcuts(user);
+                    updateDeepShortcutMap(null, shortcuts);
+                }
+                synchronized (LoaderTask.this) {
+                    if (mStopped) {
+                        return;
+                    }
+                    mDeepShortcutsLoaded = true;
+                }
+            }
+            bindDeepShortcutMapOnMainThread();
+        }
+
         public void dumpState() {
             synchronized (sBgLock) {
                 Log.d(TAG, "mLoaderTask.mContext=" + mContext);
@@ -2632,6 +2786,40 @@ public class LauncherModel extends BroadcastReceiver
         }
     }
 
+    // Clear all the shortcuts for the given package, and re-add the new shortcuts.
+    private void updateDeepShortcutMap(String packageName, List<ShortcutInfoCompat> shortcuts) {
+        // Remove all keys associated with the given package.
+        if (packageName != null) {
+            Iterator<ComponentKey> keysIter = mBgDeepShortcutMap.keySet().iterator();
+            while (keysIter.hasNext()) {
+                if (keysIter.next().componentName.getPackageName().equals(packageName)) {
+                    keysIter.remove();
+                }
+            }
+        }
+
+        // Now add the new shortcuts to the map.
+        for (ShortcutInfoCompat shortcut : shortcuts) {
+            ComponentKey targetComponent
+                    = new ComponentKey(shortcut.getActivity(), shortcut.getUserHandle());
+            mBgDeepShortcutMap.addToList(targetComponent, shortcut.getId());
+        }
+    }
+
+    private void bindDeepShortcutMapOnMainThread() {
+        final MultiHashMap<ComponentKey, String> shortcutMapCopy = new MultiHashMap<>();
+        shortcutMapCopy.putAll(mBgDeepShortcutMap);
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                Callbacks callbacks = getCallback();
+                if (callbacks != null) {
+                    callbacks.bindDeepShortcutMap(shortcutMapCopy);
+                }
+            }
+        });
+    }
+
     /**
      * Called when the icons for packages have been updated in the icon cache.
      */
@@ -2657,34 +2845,40 @@ public class LauncherModel extends BroadcastReceiver
             mBgAllAppsList.updateIconsAndLabels(updatedPackages, user, updatedApps);
         }
 
-        if (!updatedShortcuts.isEmpty()) {
-            final UserHandleCompat userFinal = user;
+        bindUpdatedShortcuts(updatedShortcuts, user);
+
+        if (!updatedApps.isEmpty()) {
             mHandler.post(new Runnable() {
 
                 public void run() {
                     Callbacks cb = getCallback();
                     if (cb != null && callbacks == cb) {
-                        cb.bindShortcutsChanged(updatedShortcuts,
-                                new ArrayList<ShortcutInfo>(), userFinal);
+                        cb.bindAppsUpdated(updatedApps);
                     }
                 }
             });
         }
+    }
 
-        if (!updatedApps.isEmpty()) {
+    private void bindUpdatedShortcuts(final ArrayList<ShortcutInfo> updatedShortcuts,
+            UserHandleCompat user) {
+        if (!updatedShortcuts.isEmpty()) {
+            final Callbacks callbacks = getCallback();
+            final UserHandleCompat userFinal = user;
             mHandler.post(new Runnable() {
 
                 public void run() {
                     Callbacks cb = getCallback();
                     if (cb != null && callbacks == cb) {
-                        cb.bindAppsUpdated(updatedApps);
+                        cb.bindShortcutsChanged(updatedShortcuts,
+                                new ArrayList<ShortcutInfo>(), userFinal);
                     }
                 }
             });
         }
     }
 
-    void enqueuePackageUpdated(PackageUpdatedTask task) {
+    void enqueueItemUpdatedTask(Runnable task) {
         sWorker.post(task);
     }
 
@@ -2712,11 +2906,11 @@ public class LauncherModel extends BroadcastReceiver
                         }
                     }
                     if (!packagesRemoved.isEmpty()) {
-                        enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_REMOVE,
+                        enqueueItemUpdatedTask(new PackageUpdatedTask(PackageUpdatedTask.OP_REMOVE,
                                 packagesRemoved.toArray(new String[packagesRemoved.size()]), user));
                     }
                     if (!packagesUnavailable.isEmpty()) {
-                        enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_UNAVAILABLE,
+                        enqueueItemUpdatedTask(new PackageUpdatedTask(PackageUpdatedTask.OP_UNAVAILABLE,
                                 packagesUnavailable.toArray(new String[packagesUnavailable.size()]), user));
                     }
                 }
@@ -3074,6 +3268,60 @@ public class LauncherModel extends BroadcastReceiver
         }
     }
 
+    private class ShortcutsChangedTask implements Runnable {
+        private String mPackageName;
+        private List<ShortcutInfoCompat> mShortcuts;
+        private UserHandleCompat mUser;
+
+        public ShortcutsChangedTask(String packageName, List<ShortcutInfoCompat> shortcuts,
+                UserHandleCompat user) {
+            mPackageName = packageName;
+            mShortcuts = shortcuts;
+            mUser = user;
+        }
+
+        @Override
+        public void run() {
+            mDeepShortcutManager.onShortcutsChanged(mShortcuts);
+
+            Map<String, ShortcutInfoCompat> idsToShortcuts = new HashMap<>();
+            for (ShortcutInfoCompat shortcut : mShortcuts) {
+                idsToShortcuts.put(shortcut.getId(), shortcut);
+            }
+
+            // Find ShortcutInfo's that have changed on the workspace.
+            MultiHashMap<String, ShortcutInfo> idsToWorkspaceShortcutInfos = new MultiHashMap<>();
+            for (ItemInfo itemInfo : sBgItemsIdMap) {
+                if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                    ShortcutInfo si = (ShortcutInfo) itemInfo;
+                    String shortcutId = si.getDeepShortcutId();
+                    if (idsToShortcuts.containsKey(shortcutId)) {
+                        idsToWorkspaceShortcutInfos.addToList(shortcutId, si);
+                    }
+                }
+            }
+
+            // Update the workspace to reflect the changes to updated shortcuts residing on it.
+            List<ShortcutInfoCompat> shortcuts = mDeepShortcutManager.queryForFullDetails(
+                    mPackageName, new ArrayList<>(idsToWorkspaceShortcutInfos.keySet()), mUser);
+            ArrayList<ShortcutInfo> updatedShortcutInfos = new ArrayList<>();
+            Context context = LauncherAppState.getInstance().getContext();
+            for (ShortcutInfoCompat fullDetails : shortcuts) {
+                List<ShortcutInfo> shortcutInfos = idsToWorkspaceShortcutInfos
+                        .get(fullDetails.getId());
+                for (ShortcutInfo shortcutInfo : shortcutInfos) {
+                    shortcutInfo.updateFromDeepShortcutInfo(fullDetails, context, mLauncherApps);
+                    updatedShortcutInfos.add(shortcutInfo);
+                }
+            }
+            bindUpdatedShortcuts(updatedShortcutInfos, mUser);
+
+            // Update the deep shortcut map, in case the list of ids has changed for an activity.
+            updateDeepShortcutMap(mPackageName, mShortcuts);
+            bindDeepShortcutMapOnMainThread();
+        }
+    }
+
     private void bindWidgetsModel(final Callbacks callbacks, final WidgetsModel model) {
         mHandler.post(new Runnable() {
             @Override
index 52668d7..c213c8d 100644 (file)
@@ -211,6 +211,11 @@ public class LauncherSettings {
         public static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5;
 
         /**
+         * The gesture is an application created deep shortcut
+         */
+        public static final int ITEM_TYPE_DEEP_SHORTCUT = 6;
+
+        /**
          * The appWidgetId of the widget
          *
          * <P>Type: INTEGER</P>
index d051665..63f49e0 100644 (file)
 
 package com.android.launcher3;
 
+import android.annotation.TargetApi;
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
-import android.util.Log;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.TextUtils;
 
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.compat.LauncherActivityInfoCompat;
+import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.folder.FolderIcon;
-
-import java.util.ArrayList;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
 
 /**
  * Represents a launchable icon on the workspaces and in folders.
@@ -274,6 +277,46 @@ public class ShortcutInfo extends ItemInfo {
         return shortcut;
     }
 
+    /**
+     * Creates a {@link ShortcutInfo} from a {@link ShortcutInfoCompat}. Pardon the overloaded name.
+     */
+    @TargetApi(Build.VERSION_CODES.N)
+    public static ShortcutInfo fromDeepShortcutInfo(ShortcutInfoCompat shortcutInfo,
+            Context context, LauncherAppsCompat launcherApps) {
+        ShortcutInfo si = new ShortcutInfo();
+        si.user = shortcutInfo.getUserHandle();
+        si.itemType = LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+        si.intent = shortcutInfo.makeIntent(context);
+        si.flags = 0;
+        si.updateFromDeepShortcutInfo(shortcutInfo, context, launcherApps);
+        return si;
+    }
+
+    public void updateFromDeepShortcutInfo(ShortcutInfoCompat shortcutInfo,
+            Context context, LauncherAppsCompat launcherApps) {
+        title = shortcutInfo.getShortLabel();
+
+        CharSequence label = shortcutInfo.getLongLabel();
+        if (TextUtils.isEmpty(label)) {
+            label = shortcutInfo.getShortLabel();
+        }
+        this.contentDescription = UserManagerCompat.getInstance(context)
+                .getBadgedLabelForUser(label, user);
+
+        LauncherAppState launcherAppState = LauncherAppState.getInstance();
+        Drawable unbadgedIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, launcherAppState
+                .getInvariantDeviceProfile().fillResIconDpi);
+        Bitmap icon = unbadgedIcon == null ? null
+                : Utilities.createBadgedIconBitmap(unbadgedIcon, user, context);
+        setIcon(icon != null ? icon : launcherAppState.getIconCache().getDefaultIcon(user));
+    }
+
+    /** Returns the ShortcutInfo id associated with the deep shortcut. */
+    public String getDeepShortcutId() {
+        return itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT ?
+                intent.getStringExtra(ShortcutInfoCompat.EXTRA_SHORTCUT_ID) : null;
+    }
+
     @Override
     public boolean isDisabled() {
         return isDisabled != 0;
index 0e4fe8b..47cf123 100644 (file)
@@ -59,7 +59,6 @@ import android.widget.TextView;
 import com.android.launcher3.Launcher.CustomContentCallbacks;
 import com.android.launcher3.Launcher.LauncherOverlay;
 import com.android.launcher3.UninstallDropTarget.DropTargetSource;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource;
 import com.android.launcher3.accessibility.OverviewScreenAccessibilityDelegate;
 import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
@@ -2554,7 +2553,8 @@ public class Workspace extends PagedView
         boolean aboveShortcut = (dropOverView.getTag() instanceof ShortcutInfo);
         boolean willBecomeShortcut =
                 (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
-                        info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT);
+                        info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
+                        info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT);
 
         return (aboveShortcut && willBecomeShortcut);
     }
@@ -3488,6 +3488,7 @@ public class Workspace extends PagedView
             switch (info.itemType) {
             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
             case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                 if (info.container == NO_ID && info instanceof AppInfo) {
                     // Came from all apps -- make a copy
                     info = ((AppInfo) info).makeShortcut();
index 237a9e9..3381064 100644 (file)
@@ -19,10 +19,13 @@ package com.android.launcher3.compat;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 
 import com.android.launcher3.Utilities;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
 
 import java.util.List;
 
@@ -45,6 +48,8 @@ public abstract class LauncherAppsCompat {
         void onPackagesUnavailable(String[] packageNames, UserHandleCompat user, boolean replacing);
         void onPackagesSuspended(String[] packageNames, UserHandleCompat user);
         void onPackagesUnsuspended(String[] packageNames, UserHandleCompat user);
+        void onShortcutsChanged(String packageName, List<ShortcutInfoCompat> shortcuts,
+                UserHandleCompat user);
     }
 
     protected LauncherAppsCompat() {
@@ -56,7 +61,9 @@ public abstract class LauncherAppsCompat {
     public static LauncherAppsCompat getInstance(Context context) {
         synchronized (sInstanceLock) {
             if (sInstance == null) {
-                if (Utilities.ATLEAST_LOLLIPOP) {
+                if (Utilities.isNycOrAbove()) {
+                    sInstance = new LauncherAppsCompatVNMR1(context.getApplicationContext());
+                } else if (Utilities.ATLEAST_LOLLIPOP) {
                     sInstance = new LauncherAppsCompatVL(context.getApplicationContext());
                 } else {
                     sInstance = new LauncherAppsCompatV16(context.getApplicationContext());
@@ -79,4 +86,11 @@ public abstract class LauncherAppsCompat {
     public abstract boolean isActivityEnabledForProfile(ComponentName component,
             UserHandleCompat user);
     public abstract boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user);
+    public abstract List<ShortcutInfoCompat> getShortcuts(LauncherApps.ShortcutQuery q,
+            UserHandleCompat userHandle);
+    public abstract void pinShortcuts(String packageName, List<String> pinnedIds,
+            UserHandleCompat userHandle);
+    public abstract void startShortcut(String packageName, String id, Rect sourceBounds,
+            Bundle startActivityOptions, UserHandleCompat user);
+    public abstract Drawable getShortcutIconDrawable(ShortcutInfoCompat shortcutInfo, int density);
 }
index 4e2fc05..1a144e8 100644 (file)
@@ -22,15 +22,18 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.Settings;
 
 import com.android.launcher3.Utilities;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Thunk;
 
@@ -130,6 +133,29 @@ public class LauncherAppsCompatV16 extends LauncherAppsCompat {
         return false;
     }
 
+    @Override
+    public List<ShortcutInfoCompat> getShortcuts(LauncherApps.ShortcutQuery q,
+            UserHandleCompat userHandle) {
+        return null;
+    }
+
+    @Override
+    public void pinShortcuts(String packageName, List<String> pinnedIds,
+            UserHandleCompat userHandle) {
+        // Not supported, so do nothing.
+    }
+
+    @Override
+    public void startShortcut(String packageName, String id, Rect sourceBounds,
+            Bundle startActivityOptions, UserHandleCompat user) {
+        // Not supported, so do nothing.
+    }
+
+    @Override
+    public Drawable getShortcutIconDrawable(ShortcutInfoCompat shortcutInfo, int density) {
+        return null;
+    }
+
     private void unregisterForPackageIntents() {
         mContext.unregisterReceiver(mPackageMonitor);
     }
index 7270d02..d97bf2f 100644 (file)
@@ -22,11 +22,14 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.UserHandle;
 
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -34,7 +37,7 @@ import java.util.List;
 import java.util.Map;
 
 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-public class LauncherAppsCompatVL extends LauncherAppsCompat {
+public class LauncherAppsCompatVL extends LauncherAppsCompatV16 {
 
     protected LauncherApps mLauncherApps;
 
@@ -42,7 +45,7 @@ public class LauncherAppsCompatVL extends LauncherAppsCompat {
             = new HashMap<OnAppsChangedCallbackCompat, WrappedCallback>();
 
     LauncherAppsCompatVL(Context context) {
-        super();
+        super(context);
         mLauncherApps = (LauncherApps) context.getSystemService("launcherapps");
     }
 
@@ -146,6 +149,18 @@ public class LauncherAppsCompatVL extends LauncherAppsCompat {
         public void onPackagesUnsuspended(String[] packageNames, UserHandle user) {
             mCallback.onPackagesUnsuspended(packageNames, UserHandleCompat.fromUser(user));
         }
+
+        @Override
+        public void onShortcutsChanged(String packageName, List<ShortcutInfo> shortcuts,
+                UserHandle user) {
+            List<ShortcutInfoCompat> shortcutInfoCompats = new ArrayList<>(shortcuts.size());
+            for (ShortcutInfo shortcutInfo : shortcuts) {
+                shortcutInfoCompats.add(new ShortcutInfoCompat(shortcutInfo));
+            }
+
+            mCallback.onShortcutsChanged(packageName, shortcutInfoCompats,
+                    UserHandleCompat.fromUser(user));
+        }
     }
 }
 
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVNMR1.java b/src/com/android/launcher3/compat/LauncherAppsCompatVNMR1.java
new file mode 100644 (file)
index 0000000..0c1db13
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@TargetApi(Build.VERSION_CODES.N)
+public class LauncherAppsCompatVNMR1 extends LauncherAppsCompatVL {
+
+    LauncherAppsCompatVNMR1(Context context) {
+        super(context);
+    }
+
+    @Override
+    public List<ShortcutInfoCompat> getShortcuts(LauncherApps.ShortcutQuery q,
+            UserHandleCompat userHandle) {
+        List<ShortcutInfo> shortcutInfos = mLauncherApps.getShortcuts(q, userHandle.getUser());
+        if (shortcutInfos == null) {
+            return null;
+        }
+        List<ShortcutInfoCompat> shortcutInfoCompats = new ArrayList<>(shortcutInfos.size());
+        for (ShortcutInfo shortcutInfo : shortcutInfos) {
+            shortcutInfoCompats.add(new ShortcutInfoCompat(shortcutInfo));
+        }
+        return shortcutInfoCompats;
+    }
+
+    @Override
+    public void pinShortcuts(String packageName, List<String> pinnedIds,
+            UserHandleCompat userHandle) {
+        mLauncherApps.pinShortcuts(packageName, pinnedIds, userHandle.getUser());
+    }
+
+    @Override
+    public void startShortcut(String packageName, String id, Rect sourceBounds,
+            Bundle startActivityOptions, UserHandleCompat user) {
+        mLauncherApps.startShortcut(packageName, id, sourceBounds,
+                startActivityOptions, user.getUser());
+    }
+
+    @Override
+    public Drawable getShortcutIconDrawable(ShortcutInfoCompat shortcutInfo, int density) {
+        return mLauncherApps.getShortcutIconDrawable(shortcutInfo.getShortcutInfo(), density);
+    }
+
+    private static class WrappedCallback extends LauncherApps.Callback {
+        private OnAppsChangedCallbackCompat mCallback;
+
+        public WrappedCallback(OnAppsChangedCallbackCompat callback) {
+            mCallback = callback;
+        }
+
+        public void onPackageRemoved(String packageName, UserHandle user) {
+            mCallback.onPackageRemoved(packageName, UserHandleCompat.fromUser(user));
+        }
+
+        public void onPackageAdded(String packageName, UserHandle user) {
+            mCallback.onPackageAdded(packageName, UserHandleCompat.fromUser(user));
+        }
+
+        public void onPackageChanged(String packageName, UserHandle user) {
+            mCallback.onPackageChanged(packageName, UserHandleCompat.fromUser(user));
+        }
+
+        public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) {
+            mCallback.onPackagesAvailable(packageNames, UserHandleCompat.fromUser(user), replacing);
+        }
+
+        public void onPackagesUnavailable(String[] packageNames, UserHandle user,
+                boolean replacing) {
+            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));
+        }
+
+        @Override
+        public void onShortcutsChanged(String packageName, List<ShortcutInfo> shortcuts,
+                UserHandle user) {
+            List<ShortcutInfoCompat> shortcutInfoCompats = new ArrayList<>(shortcuts.size());
+            for (ShortcutInfo shortcutInfo : shortcuts) {
+                shortcutInfoCompats.add(new ShortcutInfoCompat(shortcutInfo));
+            }
+
+            mCallback.onShortcutsChanged(packageName, shortcutInfoCompats,
+                    UserHandleCompat.fromUser(user));
+        }
+    }
+}
+
index 93238de..2035f99 100644 (file)
@@ -510,7 +510,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
         }
 
         // This is set to true in close(), but isn't reset to false until onDropCompleted(). This
-        // leads to an consistent state if you drag out of the folder and drag back in without
+        // leads to an inconsistent state if you drag out of the folder and drag back in without
         // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
         mDeleteFolderOnDropCompleted = false;
 
@@ -737,7 +737,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
         final ItemInfo item = d.dragInfo;
         final int itemType = item.itemType;
         return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
-                    itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
+                itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
+                itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) &&
                     !isFull());
     }
 
index d7952c5..d08cf54 100644 (file)
@@ -213,7 +213,8 @@ public class FolderIcon extends FrameLayout implements FolderListener {
     private boolean willAcceptItem(ItemInfo item) {
         final int itemType = item.itemType;
         return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
-                itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
+                itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
+                itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) &&
                 !mFolder.isFull() && item != mInfo && !mInfo.opened);
     }
 
index 8e8e551..9d3399f 100644 (file)
@@ -641,10 +641,11 @@ public class GridSizeMigrationTask {
                 // calculate weight
                 switch (entry.itemType) {
                     case Favorites.ITEM_TYPE_SHORTCUT:
+                    case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                     case Favorites.ITEM_TYPE_APPLICATION: {
                         verifyIntent(c.getString(indexIntent));
-                        entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT
-                                ? WT_SHORTCUT : WT_APPLICATION;
+                        entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ?
+                                WT_APPLICATION : WT_SHORTCUT;
                         break;
                     }
                     case Favorites.ITEM_TYPE_FOLDER: {
@@ -715,10 +716,11 @@ public class GridSizeMigrationTask {
                 // calculate weight
                 switch (entry.itemType) {
                     case Favorites.ITEM_TYPE_SHORTCUT:
+                    case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                     case Favorites.ITEM_TYPE_APPLICATION: {
                         verifyIntent(c.getString(indexIntent));
-                        entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT
-                            ? WT_SHORTCUT : WT_APPLICATION;
+                        entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ?
+                                WT_APPLICATION : WT_SHORTCUT;
                         break;
                     }
                     case Favorites.ITEM_TYPE_APPWIDGET: {
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
new file mode 100644 (file)
index 0000000..e2e06af
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * 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.shortcuts;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.LauncherApps.ShortcutQuery;
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class DeepShortcutManager {
+    private static final int FLAG_GET_ALL = ShortcutQuery.FLAG_GET_DYNAMIC
+            | ShortcutQuery.FLAG_GET_PINNED | ShortcutQuery.FLAG_GET_MANIFEST;
+
+    private final LauncherAppsCompat mLauncherApps;
+
+    public DeepShortcutManager(Context context, ShortcutCache shortcutCache) {
+        mLauncherApps = LauncherAppsCompat.getInstance(context);
+    }
+
+    public void onShortcutsChanged(List<ShortcutInfoCompat> shortcuts) {
+        // mShortcutCache.removeShortcuts(shortcuts);
+    }
+
+    /**
+     * Queries for the shortcuts with the package name and provided ids.
+     *
+     * This method is intended to get the full details for shortcuts when they are added or updated,
+     * because we only get "key" fields in onShortcutsChanged().
+     */
+    public List<ShortcutInfoCompat> queryForFullDetails(String packageName,
+            List<String> shortcutIds, UserHandleCompat user) {
+        return query(FLAG_GET_ALL, packageName, null, shortcutIds, user);
+    }
+
+    /**
+     * Gets all the shortcuts associated with the given package and user.
+     */
+    public List<ShortcutInfoCompat> queryForAllAppShortcuts(ComponentName activity,
+            List<String> ids, UserHandleCompat user) {
+        return query(FLAG_GET_ALL, activity.getPackageName(), activity, ids, user);
+    }
+
+    /**
+     * Removes the given shortcut from the current list of pinned shortcuts.
+     * (Runs on background thread)
+     */
+    public void unpinShortcut(final ShortcutKey key) {
+        String packageName = key.componentName.getPackageName();
+        String id = key.id;
+        UserHandleCompat user = key.user;
+        List<String> pinnedIds = extractIds(queryForPinnedShortcuts(packageName, user));
+        pinnedIds.remove(id);
+        mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
+    }
+
+    /**
+     * Adds the given shortcut to the current list of pinned shortcuts.
+     * (Runs on background thread)
+     */
+    public void pinShortcut(final ShortcutKey key) {
+        String packageName = key.componentName.getPackageName();
+        String id = key.id;
+        UserHandleCompat user = key.user;
+        List<String> pinnedIds = extractIds(queryForPinnedShortcuts(packageName, user));
+        pinnedIds.add(id);
+        mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
+    }
+
+    /**
+     * Returns the id's of pinned shortcuts associated with the given package and user.
+     *
+     * If packageName is null, returns all pinned shortcuts regardless of package.
+     */
+    public List<ShortcutInfoCompat> queryForPinnedShortcuts(String packageName,
+            UserHandleCompat user) {
+        return query(ShortcutQuery.FLAG_GET_PINNED, packageName, null, null, user);
+    }
+
+    public List<ShortcutInfoCompat> queryForAllShortcuts(UserHandleCompat user) {
+        return query(FLAG_GET_ALL, null, null, null, user);
+    }
+
+    private List<String> extractIds(List<ShortcutInfoCompat> shortcuts) {
+        List<String> shortcutIds = new ArrayList<>(shortcuts.size());
+        for (ShortcutInfoCompat shortcut : shortcuts) {
+            shortcutIds.add(shortcut.getId());
+        }
+        return shortcutIds;
+    }
+
+    /**
+     * Query the system server for all the shortcuts matching the given parameters.
+     * If packageName == null, we query for all shortcuts with the passed flags, regardless of app.
+     *
+     * TODO: Use the cache to optimize this so we don't make an RPC every time.
+     */
+    private List<ShortcutInfoCompat> query(int flags, String packageName,
+            ComponentName activity, List<String> shortcutIds, UserHandleCompat user) {
+        ShortcutQuery q = new ShortcutQuery();
+        q.setQueryFlags(flags);
+        if (packageName != null) {
+            q.setPackage(packageName);
+            q.setActivity(activity);
+            q.setShortcutIds(shortcutIds);
+        }
+        return mLauncherApps.getShortcuts(q, user);
+    }
+}
diff --git a/src/com/android/launcher3/shortcuts/ShortcutCache.java b/src/com/android/launcher3/shortcuts/ShortcutCache.java
new file mode 100644 (file)
index 0000000..fc118a8
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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.shortcuts;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.UserHandle;
+import android.util.LruCache;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Loads {@link ShortcutInfoCompat}s on demand (e.g. when launcher
+ * loads for pinned shortcuts and on long-press for dynamic shortcuts), and caches them
+ * for handful of apps in an LruCache while launcher lives.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class ShortcutCache {
+    private static final String TAG = "ShortcutCache";
+    private static final boolean LOGD = false;
+
+    private static final int CACHE_SIZE = 30; // Max number shortcuts we cache.
+
+    private LruCache<ShortcutKey, ShortcutInfoCompat> mCachedShortcuts;
+    // We always keep pinned shortcuts in the cache.
+    private HashMap<ShortcutKey, ShortcutInfoCompat> mPinnedShortcuts;
+
+    public ShortcutCache() {
+        mCachedShortcuts = new LruCache<>(CACHE_SIZE);
+        mPinnedShortcuts = new HashMap<>();
+    }
+
+    /**
+     * Removes shortcuts from the cache when shortcuts change for a given package.
+     *
+     * Returns a map of ids to their evicted shortcuts.
+     *
+     * @see android.content.pm.LauncherApps.Callback#onShortcutsChanged(String, List, UserHandle).
+     */
+    public void removeShortcuts(List<ShortcutInfoCompat> shortcuts) {
+        for (ShortcutInfoCompat shortcut : shortcuts) {
+            ShortcutKey key = ShortcutKey.fromInfo(shortcut);
+            mCachedShortcuts.remove(key);
+        }
+    }
+
+    public ShortcutInfoCompat get(ShortcutKey key) {
+        if (mPinnedShortcuts.containsKey(key)) {
+            return mPinnedShortcuts.get(key);
+        }
+        return mCachedShortcuts.get(key);
+    }
+
+    public void put(ShortcutKey key, ShortcutInfoCompat shortcut) {
+        if (shortcut.isPinned()) {
+            mPinnedShortcuts.put(key, shortcut);
+        } else {
+            mCachedShortcuts.put(key, shortcut);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/shortcuts/ShortcutInfoCompat.java b/src/com/android/launcher3/shortcuts/ShortcutInfoCompat.java
new file mode 100644 (file)
index 0000000..8dbeaa7
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * 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.shortcuts;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.os.Build;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.ComponentKey;
+
+/**
+ * Wrapper class for {@link android.content.pm.ShortcutInfo}, representing deep shortcuts into apps.
+ *
+ * Not to be confused with {@link com.android.launcher3.ShortcutInfo}.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class ShortcutInfoCompat {
+    private static final String INTENT_CATEGORY = "com.android.launcher3.DEEP_SHORTCUT";
+    public static final String EXTRA_SHORTCUT_ID = "shortcut_id";
+
+    private ShortcutInfo mShortcutInfo;
+
+    public ShortcutInfoCompat(ShortcutInfo shortcutInfo) {
+        mShortcutInfo = shortcutInfo;
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    public Intent makeIntent(Context context) {
+        long serialNumber = UserManagerCompat.getInstance(context)
+                .getSerialNumberForUser(getUserHandle());
+        return new Intent(Intent.ACTION_MAIN)
+                .addCategory(INTENT_CATEGORY)
+                .setComponent(getActivity())
+                .setPackage(getPackage())
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+                .putExtra(ItemInfo.EXTRA_PROFILE, serialNumber)
+                .putExtra(EXTRA_SHORTCUT_ID, getId());
+    }
+
+    public ShortcutInfo getShortcutInfo() {
+        return mShortcutInfo;
+    }
+
+    public String getPackage() {
+        return mShortcutInfo.getPackage();
+    }
+
+    public String getId() {
+        return mShortcutInfo.getId();
+    }
+
+    public CharSequence getShortLabel() {
+        return mShortcutInfo.getShortLabel();
+    }
+
+    public CharSequence getLongLabel() {
+        return mShortcutInfo.getLongLabel();
+    }
+
+    public long getLastChangedTimestamp() {
+        return mShortcutInfo.getLastChangedTimestamp();
+    }
+
+    public ComponentName getActivity() {
+        return mShortcutInfo.getActivity();
+    }
+
+    public UserHandleCompat getUserHandle() {
+        return UserHandleCompat.fromUser(mShortcutInfo.getUserHandle());
+    }
+
+    public boolean hasKeyFieldsOnly() {
+        return mShortcutInfo.hasKeyFieldsOnly();
+    }
+
+    public boolean isPinned() {
+        return mShortcutInfo.isPinned();
+    }
+
+    @Override
+    public String toString() {
+        return mShortcutInfo.toString();
+    }
+}
diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java
new file mode 100644 (file)
index 0000000..c9d66eb
--- /dev/null
@@ -0,0 +1,30 @@
+package com.android.launcher3.shortcuts;
+
+import android.content.ComponentName;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.util.ComponentKey;
+
+/**
+ * A key that uniquely identifies a shortcut using its package, id, and user handle.
+ */
+public class ShortcutKey extends ComponentKey {
+    final String id;
+
+    public ShortcutKey(String packageName, UserHandleCompat user, String id) {
+        // Use the id as the class name.
+        super(new ComponentName(packageName, id), user);
+        this.id = id;
+    }
+
+    public static ShortcutKey fromInfo(ShortcutInfoCompat shortcutInfo) {
+        return new ShortcutKey(shortcutInfo.getPackage(), shortcutInfo.getUserHandle(),
+                shortcutInfo.getId());
+    }
+
+    @Override
+    public String toString() {
+        return flattenToString(LauncherAppState.getInstance().getContext());
+    }
+}
index df23abe..7dbc0e7 100644 (file)
@@ -29,6 +29,7 @@ import com.android.launcher3.R;
 import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.LauncherActivityInfoCompat;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 
@@ -180,6 +181,12 @@ public class ManagedProfileHeuristic {
                 saveWorkFolderShortcuts(workFolder.id, 0, workFolderApps);
             }
         }
+
+        @Override
+        public void onShortcutsChanged(String packageName, List<ShortcutInfoCompat> shortcuts,
+                UserHandleCompat user) {
+            // Do nothing
+        }
     }
 
     /**
diff --git a/src/com/android/launcher3/util/MultiHashMap.java b/src/com/android/launcher3/util/MultiHashMap.java
new file mode 100644 (file)
index 0000000..f54ab88
--- /dev/null
@@ -0,0 +1,20 @@
+package com.android.launcher3.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * A utility map from keys to an ArrayList of values.
+ */
+public class MultiHashMap<K, V> extends HashMap<K, ArrayList<V>> {
+    public void addToList(K key, V value) {
+        ArrayList<V> list = get(key);
+        if (list == null) {
+            list = new ArrayList<>();
+            list.add(value);
+            put(key, list);
+        } else {
+            list.add(value);
+        }
+    }
+}