diff options
author | Tony Wickham <twickham@google.com> | 2016-05-19 11:19:39 -0700 |
---|---|---|
committer | Tony Wickham <twickham@google.com> | 2016-06-21 15:49:16 -0700 |
commit | bfbf7f9f4a0b300613f0ff27a4eb592d88c08325 (patch) | |
tree | 6906865484b6a03199eecd0f352eba24345af33e /src/com/android | |
parent | ae50284e0a838139c67caf0064a0977c871497fa (diff) | |
download | android_packages_apps_Trebuchet-bfbf7f9f4a0b300613f0ff27a4eb592d88c08325.tar.gz android_packages_apps_Trebuchet-bfbf7f9f4a0b300613f0ff27a4eb592d88c08325.tar.bz2 android_packages_apps_Trebuchet-bfbf7f9f4a0b300613f0ff27a4eb592d88c08325.zip |
Add support for launcher shortcuts.
- 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
Diffstat (limited to 'src/com/android')
22 files changed, 946 insertions, 60 deletions
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index be00aec34..97515a8d4 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -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 diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java index 286a7f104..f54a2d47a 100644 --- a/src/com/android/launcher3/ItemInfo.java +++ b/src/com/android/launcher3/ItemInfo.java @@ -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; diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 156c1b0b0..cc31de208 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -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. diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index c2e7f1aac..2ba4982b4 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -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; diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index bca2ffbe0..e987a9bdf 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -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); diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index a5e703eb0..9e8766005 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -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 diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 52668d721..c213c8d77 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -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> diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index d051665f3..63f49e0d5 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -16,20 +16,23 @@ 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; diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 0e4fe8b19..47cf1237e 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -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(); diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java index 237a9e9fb..338106427 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompat.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java @@ -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); } diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java index 4e2fc055e..1a144e859 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java @@ -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); } diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java index 7270d023b..d97bf2f74 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java @@ -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 index 000000000..0c1db1385 --- /dev/null +++ b/src/com/android/launcher3/compat/LauncherAppsCompatVNMR1.java @@ -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)); + } + } +} + diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index 93238de86..2035f9960 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -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()); } diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index d7952c570..d08cf548e 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -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); } diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java index 8e8e551eb..9d3399fc3 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationTask.java +++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java @@ -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 index 000000000..e2e06af9a --- /dev/null +++ b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java @@ -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 index 000000000..fc118a86e --- /dev/null +++ b/src/com/android/launcher3/shortcuts/ShortcutCache.java @@ -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 index 000000000..8dbeaa741 --- /dev/null +++ b/src/com/android/launcher3/shortcuts/ShortcutInfoCompat.java @@ -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 index 000000000..c9d66eb7c --- /dev/null +++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java @@ -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()); + } +} diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java index df23abe08..7dbc0e7a8 100644 --- a/src/com/android/launcher3/util/ManagedProfileHeuristic.java +++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java @@ -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 index 000000000..f54ab8840 --- /dev/null +++ b/src/com/android/launcher3/util/MultiHashMap.java @@ -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); + } + } +} |