Loading core/java/android/content/pm/ShortcutInfo.java +71 −8 Original line number Diff line number Diff line Loading @@ -62,6 +62,13 @@ public final class ShortcutInfo implements Parcelable { private static final String ANDROID_PACKAGE_NAME = "android"; private static final int IMPLICIT_RANK_MASK = 0x7fffffff; private static final int RANK_CHANGED_BIT = ~IMPLICIT_RANK_MASK; /** @hide */ public static final int RANK_NOT_SET = Integer.MAX_VALUE; /** @hide */ public static final int FLAG_DYNAMIC = 1 << 0; Loading Loading @@ -193,6 +200,15 @@ public final class ShortcutInfo implements Parcelable { private int mRank; /** * Internally used for auto-rank-adjustment. * * RANK_CHANGED_BIT is used to denote that the rank of a shortcut is changing. * The rest of the bits are used to denote the order in which shortcuts are passed to * APIs, which is used to preserve the argument order when ranks are tie. */ private int mImplicitRank; @Nullable private PersistableBundle mExtras; Loading Loading @@ -544,7 +560,8 @@ public final class ShortcutInfo implements Parcelable { /** * Copy non-null/zero fields from another {@link ShortcutInfo}. Only "public" information * will be overwritten. The timestamp will be updated. * will be overwritten. The timestamp will *not* be updated to be consistent with other * setters (and also the clock is not injectable in this file). * * - Flags will not change * - mBitmapPath will not change Loading Loading @@ -603,14 +620,12 @@ public final class ShortcutInfo implements Parcelable { mIntent = source.mIntent; mIntentPersistableExtras = source.mIntentPersistableExtras; } if (source.mRank != 0) { if (source.mRank != RANK_NOT_SET) { mRank = source.mRank; } if (source.mExtras != null) { mExtras = source.mExtras; } updateTimestamp(); } /** Loading Loading @@ -665,7 +680,7 @@ public final class ShortcutInfo implements Parcelable { private Intent mIntent; private int mRank; private int mRank = RANK_NOT_SET; private PersistableBundle mExtras; Loading Loading @@ -825,15 +840,18 @@ public final class ShortcutInfo implements Parcelable { @NonNull public Builder setIntent(@NonNull Intent intent) { mIntent = Preconditions.checkNotNull(intent, "intent"); Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set."); Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set"); return this; } /** * TODO javadoc. * "Rank" of a shortcut, which is a non-negative value that's used by the launcher app * to sort shortcuts. */ @NonNull public Builder setRank(int rank) { Preconditions.checkArgument((0 <= rank), "Rank cannot be negative or bigger than MAX_RANK"); mRank = rank; return this; } Loading Loading @@ -1014,12 +1032,57 @@ public final class ShortcutInfo implements Parcelable { } /** * TODO Javadoc * "Rank" of a shortcut, which is a non-negative, sequential value that's unique for each * {@link #getActivity} for each of the two kinds, dynamic shortcuts and manifest shortcuts. * * <p>Because manifest shortcuts and dynamic shortcuts have overlapping ranks, * when a launcher application shows shortcuts for an activity, it should first show * the manifest shortcuts followed by the dynamic shortcuts. Within each of those categories, * shortcuts should be sorted by rank in ascending order. * * <p>"Floating" shortcuts (i.e. shortcuts that are neither dynamic nor manifest) will all * have rank 0, because there's no sorting for them. */ public int getRank() { return mRank; } /** @hide */ public boolean hasRank() { return mRank != RANK_NOT_SET; } /** @hide */ public void setRank(int rank) { mRank = rank; } /** @hide */ public void clearImplicitRankAndRankChangedFlag() { mImplicitRank = 0; } /** @hide */ public void setImplicitRank(int rank) { // Make sure to keep RANK_CHANGED_BIT. mImplicitRank = (mImplicitRank & RANK_CHANGED_BIT) | (rank & IMPLICIT_RANK_MASK); } /** @hide */ public int getImplicitRank() { return mImplicitRank & IMPLICIT_RANK_MASK; } /** @hide */ public void setRankChanged() { mImplicitRank |= RANK_CHANGED_BIT; } /** @hide */ public boolean isRankChanged() { return (mImplicitRank & RANK_CHANGED_BIT) != 0; } /** * Optional values that application can set. */ Loading services/core/java/com/android/server/pm/ShortcutPackage.java +152 −16 Original line number Diff line number Diff line Loading @@ -19,10 +19,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ShortcutInfo; import android.content.res.Resources; import android.os.PersistableBundle; Loading Loading @@ -51,8 +49,6 @@ import java.util.List; import java.util.Set; import java.util.function.Predicate; import sun.misc.Resource; /** * Package information used by {@link ShortcutService}. * User information used by {@link ShortcutService}. Loading @@ -63,6 +59,7 @@ import sun.misc.Resource; */ class ShortcutPackage extends ShortcutPackageItem { private static final String TAG = ShortcutService.TAG; private static final String TAG_VERIFY = ShortcutService.TAG + ".verify"; static final String TAG_ROOT = "package"; private static final String TAG_INTENT_EXTRAS = "intent-extras"; Loading Loading @@ -303,12 +300,17 @@ class ShortcutPackage extends ShortcutPackageItem { * Remove all dynamic shortcuts. */ public void deleteAllDynamicShortcuts() { final long now = mShortcutUser.mService.injectCurrentTimeMillis(); boolean changed = false; for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); if (si.isDynamic()) { changed = true; si.setTimestamp(now); si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); si.setRank(0); // It may still be pinned, so clear the rank. } } if (changed) { Loading Loading @@ -356,10 +358,14 @@ class ShortcutPackage extends ShortcutPackageItem { ensureNotImmutable(oldShortcut); } if (oldShortcut.isPinned()) { oldShortcut.setRank(0); oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST); if (disable) { oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED); } oldShortcut.setTimestamp(mShortcutUser.mService.injectCurrentTimeMillis()); return oldShortcut; } else { deleteShortcutInner(shortcutId); Loading Loading @@ -829,7 +835,7 @@ class ShortcutPackage extends ShortcutPackageItem { if (!a.isManifestShortcut() && b.isManifestShortcut()) { return 1; } return a.getRank() - b.getRank(); return Integer.compare(a.getRank(), b.getRank()); }; /** Loading @@ -837,8 +843,6 @@ class ShortcutPackage extends ShortcutPackageItem { * contain "floating" shortcuts because they don't belong on any activities. */ private ArrayMap<ComponentName, ArrayList<ShortcutInfo>> sortShortcutsToActivities() { final int maxShortcuts = mShortcutUser.mService.getMaxActivityShortcuts(); final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> activitiesToShortcuts = new ArrayMap<>(); for (int i = mShortcuts.size() - 1; i >= 0; i--) { Loading @@ -851,7 +855,7 @@ class ShortcutPackage extends ShortcutPackageItem { ArrayList<ShortcutInfo> list = activitiesToShortcuts.get(activity); if (list == null) { list = new ArrayList<>(maxShortcuts * 2); list = new ArrayList<>(); activitiesToShortcuts.put(activity, list); } list.add(si); Loading Loading @@ -976,6 +980,96 @@ class ShortcutPackage extends ShortcutPackageItem { } } /** Clears the implicit ranks for all shortcuts. */ public void clearAllImplicitRanks() { for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); si.clearImplicitRankAndRankChangedFlag(); } } /** * Used to sort shortcuts for rank auto-adjusting. */ final Comparator<ShortcutInfo> mShortcutRankComparator = (ShortcutInfo a, ShortcutInfo b) -> { // First, sort by rank. int ret = Integer.compare(a.getRank(), b.getRank()); if (ret != 0) { return ret; } // When ranks are tie, then prioritize the ones that have just been assigned new ranks. // e.g. when there are 3 shortcuts, "s1" "s2" and "s3" with rank 0, 1, 2 respectively, // adding a shortcut "s4" with rank 1 will "insert" it between "s1" and "s2", because // "s2" and "s4" have the same rank 1 but s4 has isRankChanged() set. // Similarly, updating s3's rank to 1 will insert it between s1 and s2. if (a.isRankChanged() != b.isRankChanged()) { return a.isRankChanged() ? -1 : 1; } // If they're still tie, sort by implicit rank -- i.e. preserve the order in which // they're passed to the API. ret = Integer.compare(a.getImplicitRank(), b.getImplicitRank()); if (ret != 0) { return ret; } // If they're stil tie, just sort by their IDs. // This may happen with updateShortcuts() -- see // the testUpdateShortcuts_noManifestShortcuts() test. return a.getId().compareTo(b.getId()); }; /** * Re-calculate the ranks for all shortcuts. */ public void adjustRanks() { final ShortcutService s = mShortcutUser.mService; final long now = s.injectCurrentTimeMillis(); // First, clear ranks for floating shortcuts. for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); if (si.isFloating()) { if (si.getRank() != 0) { si.setTimestamp(now); si.setRank(0); } } } // Then adjust ranks. Ranks are unique for each activity, so we first need to sort // shortcuts to each activity. // Then sort the shortcuts within each activity with mShortcutRankComparator, and // assign ranks from 0. final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all = sortShortcutsToActivities(); for (int outer = all.size() - 1; outer >= 0; outer--) { // For each activity. final ArrayList<ShortcutInfo> list = all.valueAt(outer); // Sort by ranks and other signals. Collections.sort(list, mShortcutRankComparator); int rank = 0; final int size = list.size(); for (int i = 0; i < size; i++) { final ShortcutInfo si = list.get(i); if (si.isManifestShortcut()) { // Don't adjust ranks for manifest shortcuts. continue; } // At this point, it must be dynamic. if (!si.isDynamic()) { s.wtf("Non-dynamic shortcut found."); continue; } final int thisRank = rank++; if (si.getRank() != thisRank) { si.setTimestamp(now); si.setRank(thisRank); } } } } public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(); Loading Loading @@ -1087,7 +1181,6 @@ class ShortcutPackage extends ShortcutPackageItem { ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_NAME, si.getDisabledMessageResName()); ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras()); ShortcutService.writeAttr(out, ATTR_RANK, si.getRank()); ShortcutService.writeAttr(out, ATTR_TIMESTAMP, si.getLastChangedTimestamp()); if (forBackup) { Loading @@ -1097,6 +1190,10 @@ class ShortcutPackage extends ShortcutPackageItem { ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES | ShortcutInfo.FLAG_DYNAMIC)); } else { // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored // as dynamic. ShortcutService.writeAttr(out, ATTR_RANK, si.getRank()); ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags()); ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId()); ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName()); Loading Loading @@ -1272,35 +1369,74 @@ class ShortcutPackage extends ShortcutPackageItem { sortShortcutsToActivities(); // Make sure each activity won't have more than max shortcuts. for (int i = all.size() - 1; i >= 0; i--) { if (all.valueAt(i).size() > mShortcutUser.mService.getMaxActivityShortcuts()) { for (int outer = all.size() - 1; outer >= 0; outer--) { final ArrayList<ShortcutInfo> list = all.valueAt(outer); if (list.size() > mShortcutUser.mService.getMaxActivityShortcuts()) { failed = true; Log.e(TAG, "Package " + getPackageName() + ": activity " + all.keyAt(i) + " has " + all.valueAt(i).size() + " shortcuts."); Log.e(TAG_VERIFY, "Package " + getPackageName() + ": activity " + all.keyAt(outer) + " has " + all.valueAt(outer).size() + " shortcuts."); } // Sort by rank. Collections.sort(list, (a, b) -> Integer.compare(a.getRank(), b.getRank())); // Split into two arrays for each kind. final ArrayList<ShortcutInfo> dynamicList = new ArrayList<>(list); dynamicList.removeIf((si) -> !si.isDynamic()); final ArrayList<ShortcutInfo> manifestList = new ArrayList<>(list); dynamicList.removeIf((si) -> !si.isManifestShortcut()); verifyRanksSequential(dynamicList); verifyRanksSequential(manifestList); } // Verify each shortcut's status. for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); if (!(si.isManifestShortcut() || si.isDynamic() || si.isPinned())) { failed = true; Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId() Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is not manifest, dynamic or pinned."); } if (si.isManifestShortcut() && si.isDynamic()) { failed = true; Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is both dynamic and manifest at the same time."); } if (si.getActivity() == null) { failed = true; Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId() Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " has null activity."); } if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) { failed = true; Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId() Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is not floating, but is disabled."); } if (si.isFloating() && si.getRank() != 0) { failed = true; Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is floating, but has rank=" + si.getRank()); } } if (failed) { throw new IllegalStateException("See logcat for errors"); } } private boolean verifyRanksSequential(List<ShortcutInfo> list) { boolean failed = false; for (int i = 0; i < list.size(); i++) { final ShortcutInfo si = list.get(i); if (si.getRank() != i) { failed = true; Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " rank=" + si.getRank() + " but expected to be "+ i); } } return failed; } } services/core/java/com/android/server/pm/ShortcutParser.java +2 −1 Original line number Diff line number Diff line Loading @@ -103,7 +103,7 @@ public class ShortcutParser { } if (depth == 2 && TAG_SHORTCUT.equals(tag)) { final ShortcutInfo si = parseShortcutAttributes( service, attrs, packageName, activity, userId, rank++); service, attrs, packageName, activity, userId, rank); if (ShortcutService.DEBUG) { Slog.d(TAG, "Shortcut=" + si); } Loading @@ -128,6 +128,7 @@ public class ShortcutParser { } result.add(si); numShortcuts++; rank++; } continue; } Loading services/core/java/com/android/server/pm/ShortcutService.java +77 −29 Original line number Diff line number Diff line Loading @@ -114,7 +114,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Predicate; Loading Loading @@ -223,7 +222,7 @@ public class ShortcutService extends IShortcutService.Stub { String KEY_MAX_ICON_DIMENSION_DP_LOWRAM = "max_icon_dimension_dp_lowram"; /** * Key name for the max dynamic shortcuts per app. (int) * Key name for the max dynamic shortcuts per activity. (int) */ String KEY_MAX_SHORTCUTS = "max_shortcuts"; Loading Loading @@ -1479,6 +1478,12 @@ public class ShortcutService extends IShortcutService.Stub { return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); } private void assignImplicitRanks(List<ShortcutInfo> shortcuts) { for (int i = shortcuts.size() - 1; i >= 0; i--) { shortcuts.get(i).setImplicitRank(i); } } // === APIs === @Override Loading @@ -1501,10 +1506,9 @@ public class ShortcutService extends IShortcutService.Stub { return false; } // Validate the shortcuts. for (int i = 0; i < size; i++) { fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false); } // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). ps.clearAllImplicitRanks(); assignImplicitRanks(newShortcuts); // First, remove all un-pinned; dynamic shortcuts ps.deleteAllDynamicShortcuts(); Loading @@ -1514,6 +1518,9 @@ public class ShortcutService extends IShortcutService.Stub { final ShortcutInfo newShortcut = newShortcuts.get(i); ps.addOrUpdateDynamicShortcut(newShortcut); } // Lastly, adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading Loading @@ -1542,17 +1549,31 @@ public class ShortcutService extends IShortcutService.Stub { return false; } // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). ps.clearAllImplicitRanks(); assignImplicitRanks(newShortcuts); for (int i = 0; i < size; i++) { final ShortcutInfo source = newShortcuts.get(i); fixUpIncomingShortcutInfo(source, /* forUpdate= */ true); final ShortcutInfo target = ps.findShortcutById(source.getId()); if (target != null) { if (target == null) { continue; } if (target.isEnabled() != source.isEnabled()) { Slog.w(TAG, "ShortcutInfo.enabled cannot be changed with updateShortcuts()"); } // When updating the rank, we need to insert between existing ranks, so set // this setRankChanged, and also copy the implicit rank fo adjustRanks(). if (source.hasRank()) { target.setRankChanged(); target.setImplicitRank(source.getImplicitRank()); } final boolean replacingIcon = (source.getIcon() != null); if (replacingIcon) { removeIcon(userId, target); Loading @@ -1565,6 +1586,7 @@ public class ShortcutService extends IShortcutService.Stub { // Note copyNonNullFieldsFrom() does the "updatable with?" check too. target.copyNonNullFieldsFrom(source); target.setTimestamp(injectCurrentTimeMillis()); if (replacingIcon) { saveIconAndFixUpShortcut(userId, target); Loading @@ -1576,7 +1598,9 @@ public class ShortcutService extends IShortcutService.Stub { fixUpShortcutResourceNamesAndValues(target); } } } // Lastly, adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading @@ -1600,6 +1624,10 @@ public class ShortcutService extends IShortcutService.Stub { ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD); // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). ps.clearAllImplicitRanks(); assignImplicitRanks(newShortcuts); // Throttling. if (!ps.tryApiCall()) { return false; Loading @@ -1610,9 +1638,16 @@ public class ShortcutService extends IShortcutService.Stub { // Validate the shortcut. fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false); // When ranks are changing, we need to insert between ranks, so set the // "rank changed" flag. newShortcut.setRankChanged(); // Add it. ps.addOrUpdateDynamicShortcut(newShortcut); } // Lastly, adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading @@ -1637,6 +1672,9 @@ public class ShortcutService extends IShortcutService.Stub { disabledMessage, disabledMessageResId, /* overrideImmutable=*/ false); } // We may have removed dynamic shortcuts which may have left a gap, so adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading Loading @@ -1677,6 +1715,9 @@ public class ShortcutService extends IShortcutService.Stub { ps.deleteDynamicWithId( Preconditions.checkStringNotEmpty((String) shortcutIds.get(i))); } // We may have removed dynamic shortcuts which may have left a gap, so adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading Loading @@ -2328,6 +2369,7 @@ public class ShortcutService extends IShortcutService.Stub { } finally { logDurationStat(Stats.CHECK_PACKAGE_CHANGES, start); } verifyStates(); } private void handlePackageAdded(String packageName, @UserIdInt int userId) { Loading @@ -2339,6 +2381,7 @@ public class ShortcutService extends IShortcutService.Stub { user.attemptToRestoreIfNeededAndSave(this, packageName, userId); user.handlePackageAddedOrUpdated(packageName); } verifyStates(); } private void handlePackageUpdateFinished(String packageName, @UserIdInt int userId) { Loading @@ -2354,6 +2397,7 @@ public class ShortcutService extends IShortcutService.Stub { user.handlePackageAddedOrUpdated(packageName); } } verifyStates(); } private void handlePackageRemoved(String packageName, @UserIdInt int packageUserId) { Loading @@ -2362,6 +2406,8 @@ public class ShortcutService extends IShortcutService.Stub { packageUserId)); } cleanUpPackageForAllLoadedUsers(packageName, packageUserId); verifyStates(); } private void handlePackageDataCleared(String packageName, int packageUserId) { Loading @@ -2370,6 +2416,8 @@ public class ShortcutService extends IShortcutService.Stub { packageUserId)); } cleanUpPackageForAllLoadedUsers(packageName, packageUserId); verifyStates(); } // === PackageManager interaction === Loading services/tests/servicestests/res/xml/shortcut_5_reverse.xml 0 → 100644 +53 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <shortcuts xmlns:android="http://schemas.android.com/apk/res/android" > <shortcut android:shortcutId="ms5" android:enabled="true" android:shortcutIcon="@drawable/icon1" android:shortcutShortLabel="@string/shortcut_title1" android:shortcutLongLabel="@string/shortcut_text1" android:shortcutDisabledMessage="@string/shortcut_disabled_message1" android:shortcutCategories="android.shortcut.conversation:android.shortcut.media" android:shortcutIntentAction="action1" android:shortcutIntentData="http://a.b.c/1" /> <shortcut android:shortcutId="ms4" android:enabled="true" android:shortcutIcon="@drawable/icon2" android:shortcutShortLabel="@string/shortcut_title2" android:shortcutLongLabel="@string/shortcut_text2" android:shortcutDisabledMessage="@string/shortcut_disabled_message2" android:shortcutCategories="android.shortcut.conversation" android:shortcutIntentAction="action2" /> <shortcut android:shortcutId="ms3" android:shortcutShortLabel="@string/shortcut_title1" android:shortcutIntentAction="android.intent.action.VIEW" /> <shortcut android:shortcutId="ms2" android:shortcutShortLabel="@string/shortcut_title1" android:shortcutIntentAction="android.intent.action.VIEW" /> <shortcut android:shortcutId="ms1" android:shortcutShortLabel="@string/shortcut_title1" android:shortcutIntentAction="android.intent.action.VIEW" /> </shortcuts> Loading
core/java/android/content/pm/ShortcutInfo.java +71 −8 Original line number Diff line number Diff line Loading @@ -62,6 +62,13 @@ public final class ShortcutInfo implements Parcelable { private static final String ANDROID_PACKAGE_NAME = "android"; private static final int IMPLICIT_RANK_MASK = 0x7fffffff; private static final int RANK_CHANGED_BIT = ~IMPLICIT_RANK_MASK; /** @hide */ public static final int RANK_NOT_SET = Integer.MAX_VALUE; /** @hide */ public static final int FLAG_DYNAMIC = 1 << 0; Loading Loading @@ -193,6 +200,15 @@ public final class ShortcutInfo implements Parcelable { private int mRank; /** * Internally used for auto-rank-adjustment. * * RANK_CHANGED_BIT is used to denote that the rank of a shortcut is changing. * The rest of the bits are used to denote the order in which shortcuts are passed to * APIs, which is used to preserve the argument order when ranks are tie. */ private int mImplicitRank; @Nullable private PersistableBundle mExtras; Loading Loading @@ -544,7 +560,8 @@ public final class ShortcutInfo implements Parcelable { /** * Copy non-null/zero fields from another {@link ShortcutInfo}. Only "public" information * will be overwritten. The timestamp will be updated. * will be overwritten. The timestamp will *not* be updated to be consistent with other * setters (and also the clock is not injectable in this file). * * - Flags will not change * - mBitmapPath will not change Loading Loading @@ -603,14 +620,12 @@ public final class ShortcutInfo implements Parcelable { mIntent = source.mIntent; mIntentPersistableExtras = source.mIntentPersistableExtras; } if (source.mRank != 0) { if (source.mRank != RANK_NOT_SET) { mRank = source.mRank; } if (source.mExtras != null) { mExtras = source.mExtras; } updateTimestamp(); } /** Loading Loading @@ -665,7 +680,7 @@ public final class ShortcutInfo implements Parcelable { private Intent mIntent; private int mRank; private int mRank = RANK_NOT_SET; private PersistableBundle mExtras; Loading Loading @@ -825,15 +840,18 @@ public final class ShortcutInfo implements Parcelable { @NonNull public Builder setIntent(@NonNull Intent intent) { mIntent = Preconditions.checkNotNull(intent, "intent"); Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set."); Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set"); return this; } /** * TODO javadoc. * "Rank" of a shortcut, which is a non-negative value that's used by the launcher app * to sort shortcuts. */ @NonNull public Builder setRank(int rank) { Preconditions.checkArgument((0 <= rank), "Rank cannot be negative or bigger than MAX_RANK"); mRank = rank; return this; } Loading Loading @@ -1014,12 +1032,57 @@ public final class ShortcutInfo implements Parcelable { } /** * TODO Javadoc * "Rank" of a shortcut, which is a non-negative, sequential value that's unique for each * {@link #getActivity} for each of the two kinds, dynamic shortcuts and manifest shortcuts. * * <p>Because manifest shortcuts and dynamic shortcuts have overlapping ranks, * when a launcher application shows shortcuts for an activity, it should first show * the manifest shortcuts followed by the dynamic shortcuts. Within each of those categories, * shortcuts should be sorted by rank in ascending order. * * <p>"Floating" shortcuts (i.e. shortcuts that are neither dynamic nor manifest) will all * have rank 0, because there's no sorting for them. */ public int getRank() { return mRank; } /** @hide */ public boolean hasRank() { return mRank != RANK_NOT_SET; } /** @hide */ public void setRank(int rank) { mRank = rank; } /** @hide */ public void clearImplicitRankAndRankChangedFlag() { mImplicitRank = 0; } /** @hide */ public void setImplicitRank(int rank) { // Make sure to keep RANK_CHANGED_BIT. mImplicitRank = (mImplicitRank & RANK_CHANGED_BIT) | (rank & IMPLICIT_RANK_MASK); } /** @hide */ public int getImplicitRank() { return mImplicitRank & IMPLICIT_RANK_MASK; } /** @hide */ public void setRankChanged() { mImplicitRank |= RANK_CHANGED_BIT; } /** @hide */ public boolean isRankChanged() { return (mImplicitRank & RANK_CHANGED_BIT) != 0; } /** * Optional values that application can set. */ Loading
services/core/java/com/android/server/pm/ShortcutPackage.java +152 −16 Original line number Diff line number Diff line Loading @@ -19,10 +19,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ShortcutInfo; import android.content.res.Resources; import android.os.PersistableBundle; Loading Loading @@ -51,8 +49,6 @@ import java.util.List; import java.util.Set; import java.util.function.Predicate; import sun.misc.Resource; /** * Package information used by {@link ShortcutService}. * User information used by {@link ShortcutService}. Loading @@ -63,6 +59,7 @@ import sun.misc.Resource; */ class ShortcutPackage extends ShortcutPackageItem { private static final String TAG = ShortcutService.TAG; private static final String TAG_VERIFY = ShortcutService.TAG + ".verify"; static final String TAG_ROOT = "package"; private static final String TAG_INTENT_EXTRAS = "intent-extras"; Loading Loading @@ -303,12 +300,17 @@ class ShortcutPackage extends ShortcutPackageItem { * Remove all dynamic shortcuts. */ public void deleteAllDynamicShortcuts() { final long now = mShortcutUser.mService.injectCurrentTimeMillis(); boolean changed = false; for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); if (si.isDynamic()) { changed = true; si.setTimestamp(now); si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); si.setRank(0); // It may still be pinned, so clear the rank. } } if (changed) { Loading Loading @@ -356,10 +358,14 @@ class ShortcutPackage extends ShortcutPackageItem { ensureNotImmutable(oldShortcut); } if (oldShortcut.isPinned()) { oldShortcut.setRank(0); oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST); if (disable) { oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED); } oldShortcut.setTimestamp(mShortcutUser.mService.injectCurrentTimeMillis()); return oldShortcut; } else { deleteShortcutInner(shortcutId); Loading Loading @@ -829,7 +835,7 @@ class ShortcutPackage extends ShortcutPackageItem { if (!a.isManifestShortcut() && b.isManifestShortcut()) { return 1; } return a.getRank() - b.getRank(); return Integer.compare(a.getRank(), b.getRank()); }; /** Loading @@ -837,8 +843,6 @@ class ShortcutPackage extends ShortcutPackageItem { * contain "floating" shortcuts because they don't belong on any activities. */ private ArrayMap<ComponentName, ArrayList<ShortcutInfo>> sortShortcutsToActivities() { final int maxShortcuts = mShortcutUser.mService.getMaxActivityShortcuts(); final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> activitiesToShortcuts = new ArrayMap<>(); for (int i = mShortcuts.size() - 1; i >= 0; i--) { Loading @@ -851,7 +855,7 @@ class ShortcutPackage extends ShortcutPackageItem { ArrayList<ShortcutInfo> list = activitiesToShortcuts.get(activity); if (list == null) { list = new ArrayList<>(maxShortcuts * 2); list = new ArrayList<>(); activitiesToShortcuts.put(activity, list); } list.add(si); Loading Loading @@ -976,6 +980,96 @@ class ShortcutPackage extends ShortcutPackageItem { } } /** Clears the implicit ranks for all shortcuts. */ public void clearAllImplicitRanks() { for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); si.clearImplicitRankAndRankChangedFlag(); } } /** * Used to sort shortcuts for rank auto-adjusting. */ final Comparator<ShortcutInfo> mShortcutRankComparator = (ShortcutInfo a, ShortcutInfo b) -> { // First, sort by rank. int ret = Integer.compare(a.getRank(), b.getRank()); if (ret != 0) { return ret; } // When ranks are tie, then prioritize the ones that have just been assigned new ranks. // e.g. when there are 3 shortcuts, "s1" "s2" and "s3" with rank 0, 1, 2 respectively, // adding a shortcut "s4" with rank 1 will "insert" it between "s1" and "s2", because // "s2" and "s4" have the same rank 1 but s4 has isRankChanged() set. // Similarly, updating s3's rank to 1 will insert it between s1 and s2. if (a.isRankChanged() != b.isRankChanged()) { return a.isRankChanged() ? -1 : 1; } // If they're still tie, sort by implicit rank -- i.e. preserve the order in which // they're passed to the API. ret = Integer.compare(a.getImplicitRank(), b.getImplicitRank()); if (ret != 0) { return ret; } // If they're stil tie, just sort by their IDs. // This may happen with updateShortcuts() -- see // the testUpdateShortcuts_noManifestShortcuts() test. return a.getId().compareTo(b.getId()); }; /** * Re-calculate the ranks for all shortcuts. */ public void adjustRanks() { final ShortcutService s = mShortcutUser.mService; final long now = s.injectCurrentTimeMillis(); // First, clear ranks for floating shortcuts. for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); if (si.isFloating()) { if (si.getRank() != 0) { si.setTimestamp(now); si.setRank(0); } } } // Then adjust ranks. Ranks are unique for each activity, so we first need to sort // shortcuts to each activity. // Then sort the shortcuts within each activity with mShortcutRankComparator, and // assign ranks from 0. final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all = sortShortcutsToActivities(); for (int outer = all.size() - 1; outer >= 0; outer--) { // For each activity. final ArrayList<ShortcutInfo> list = all.valueAt(outer); // Sort by ranks and other signals. Collections.sort(list, mShortcutRankComparator); int rank = 0; final int size = list.size(); for (int i = 0; i < size; i++) { final ShortcutInfo si = list.get(i); if (si.isManifestShortcut()) { // Don't adjust ranks for manifest shortcuts. continue; } // At this point, it must be dynamic. if (!si.isDynamic()) { s.wtf("Non-dynamic shortcut found."); continue; } final int thisRank = rank++; if (si.getRank() != thisRank) { si.setTimestamp(now); si.setRank(thisRank); } } } } public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(); Loading Loading @@ -1087,7 +1181,6 @@ class ShortcutPackage extends ShortcutPackageItem { ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_NAME, si.getDisabledMessageResName()); ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras()); ShortcutService.writeAttr(out, ATTR_RANK, si.getRank()); ShortcutService.writeAttr(out, ATTR_TIMESTAMP, si.getLastChangedTimestamp()); if (forBackup) { Loading @@ -1097,6 +1190,10 @@ class ShortcutPackage extends ShortcutPackageItem { ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES | ShortcutInfo.FLAG_DYNAMIC)); } else { // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored // as dynamic. ShortcutService.writeAttr(out, ATTR_RANK, si.getRank()); ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags()); ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId()); ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName()); Loading Loading @@ -1272,35 +1369,74 @@ class ShortcutPackage extends ShortcutPackageItem { sortShortcutsToActivities(); // Make sure each activity won't have more than max shortcuts. for (int i = all.size() - 1; i >= 0; i--) { if (all.valueAt(i).size() > mShortcutUser.mService.getMaxActivityShortcuts()) { for (int outer = all.size() - 1; outer >= 0; outer--) { final ArrayList<ShortcutInfo> list = all.valueAt(outer); if (list.size() > mShortcutUser.mService.getMaxActivityShortcuts()) { failed = true; Log.e(TAG, "Package " + getPackageName() + ": activity " + all.keyAt(i) + " has " + all.valueAt(i).size() + " shortcuts."); Log.e(TAG_VERIFY, "Package " + getPackageName() + ": activity " + all.keyAt(outer) + " has " + all.valueAt(outer).size() + " shortcuts."); } // Sort by rank. Collections.sort(list, (a, b) -> Integer.compare(a.getRank(), b.getRank())); // Split into two arrays for each kind. final ArrayList<ShortcutInfo> dynamicList = new ArrayList<>(list); dynamicList.removeIf((si) -> !si.isDynamic()); final ArrayList<ShortcutInfo> manifestList = new ArrayList<>(list); dynamicList.removeIf((si) -> !si.isManifestShortcut()); verifyRanksSequential(dynamicList); verifyRanksSequential(manifestList); } // Verify each shortcut's status. for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); if (!(si.isManifestShortcut() || si.isDynamic() || si.isPinned())) { failed = true; Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId() Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is not manifest, dynamic or pinned."); } if (si.isManifestShortcut() && si.isDynamic()) { failed = true; Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is both dynamic and manifest at the same time."); } if (si.getActivity() == null) { failed = true; Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId() Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " has null activity."); } if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) { failed = true; Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId() Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is not floating, but is disabled."); } if (si.isFloating() && si.getRank() != 0) { failed = true; Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is floating, but has rank=" + si.getRank()); } } if (failed) { throw new IllegalStateException("See logcat for errors"); } } private boolean verifyRanksSequential(List<ShortcutInfo> list) { boolean failed = false; for (int i = 0; i < list.size(); i++) { final ShortcutInfo si = list.get(i); if (si.getRank() != i) { failed = true; Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " rank=" + si.getRank() + " but expected to be "+ i); } } return failed; } }
services/core/java/com/android/server/pm/ShortcutParser.java +2 −1 Original line number Diff line number Diff line Loading @@ -103,7 +103,7 @@ public class ShortcutParser { } if (depth == 2 && TAG_SHORTCUT.equals(tag)) { final ShortcutInfo si = parseShortcutAttributes( service, attrs, packageName, activity, userId, rank++); service, attrs, packageName, activity, userId, rank); if (ShortcutService.DEBUG) { Slog.d(TAG, "Shortcut=" + si); } Loading @@ -128,6 +128,7 @@ public class ShortcutParser { } result.add(si); numShortcuts++; rank++; } continue; } Loading
services/core/java/com/android/server/pm/ShortcutService.java +77 −29 Original line number Diff line number Diff line Loading @@ -114,7 +114,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Predicate; Loading Loading @@ -223,7 +222,7 @@ public class ShortcutService extends IShortcutService.Stub { String KEY_MAX_ICON_DIMENSION_DP_LOWRAM = "max_icon_dimension_dp_lowram"; /** * Key name for the max dynamic shortcuts per app. (int) * Key name for the max dynamic shortcuts per activity. (int) */ String KEY_MAX_SHORTCUTS = "max_shortcuts"; Loading Loading @@ -1479,6 +1478,12 @@ public class ShortcutService extends IShortcutService.Stub { return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); } private void assignImplicitRanks(List<ShortcutInfo> shortcuts) { for (int i = shortcuts.size() - 1; i >= 0; i--) { shortcuts.get(i).setImplicitRank(i); } } // === APIs === @Override Loading @@ -1501,10 +1506,9 @@ public class ShortcutService extends IShortcutService.Stub { return false; } // Validate the shortcuts. for (int i = 0; i < size; i++) { fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false); } // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). ps.clearAllImplicitRanks(); assignImplicitRanks(newShortcuts); // First, remove all un-pinned; dynamic shortcuts ps.deleteAllDynamicShortcuts(); Loading @@ -1514,6 +1518,9 @@ public class ShortcutService extends IShortcutService.Stub { final ShortcutInfo newShortcut = newShortcuts.get(i); ps.addOrUpdateDynamicShortcut(newShortcut); } // Lastly, adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading Loading @@ -1542,17 +1549,31 @@ public class ShortcutService extends IShortcutService.Stub { return false; } // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). ps.clearAllImplicitRanks(); assignImplicitRanks(newShortcuts); for (int i = 0; i < size; i++) { final ShortcutInfo source = newShortcuts.get(i); fixUpIncomingShortcutInfo(source, /* forUpdate= */ true); final ShortcutInfo target = ps.findShortcutById(source.getId()); if (target != null) { if (target == null) { continue; } if (target.isEnabled() != source.isEnabled()) { Slog.w(TAG, "ShortcutInfo.enabled cannot be changed with updateShortcuts()"); } // When updating the rank, we need to insert between existing ranks, so set // this setRankChanged, and also copy the implicit rank fo adjustRanks(). if (source.hasRank()) { target.setRankChanged(); target.setImplicitRank(source.getImplicitRank()); } final boolean replacingIcon = (source.getIcon() != null); if (replacingIcon) { removeIcon(userId, target); Loading @@ -1565,6 +1586,7 @@ public class ShortcutService extends IShortcutService.Stub { // Note copyNonNullFieldsFrom() does the "updatable with?" check too. target.copyNonNullFieldsFrom(source); target.setTimestamp(injectCurrentTimeMillis()); if (replacingIcon) { saveIconAndFixUpShortcut(userId, target); Loading @@ -1576,7 +1598,9 @@ public class ShortcutService extends IShortcutService.Stub { fixUpShortcutResourceNamesAndValues(target); } } } // Lastly, adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading @@ -1600,6 +1624,10 @@ public class ShortcutService extends IShortcutService.Stub { ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD); // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). ps.clearAllImplicitRanks(); assignImplicitRanks(newShortcuts); // Throttling. if (!ps.tryApiCall()) { return false; Loading @@ -1610,9 +1638,16 @@ public class ShortcutService extends IShortcutService.Stub { // Validate the shortcut. fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false); // When ranks are changing, we need to insert between ranks, so set the // "rank changed" flag. newShortcut.setRankChanged(); // Add it. ps.addOrUpdateDynamicShortcut(newShortcut); } // Lastly, adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading @@ -1637,6 +1672,9 @@ public class ShortcutService extends IShortcutService.Stub { disabledMessage, disabledMessageResId, /* overrideImmutable=*/ false); } // We may have removed dynamic shortcuts which may have left a gap, so adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading Loading @@ -1677,6 +1715,9 @@ public class ShortcutService extends IShortcutService.Stub { ps.deleteDynamicWithId( Preconditions.checkStringNotEmpty((String) shortcutIds.get(i))); } // We may have removed dynamic shortcuts which may have left a gap, so adjust the ranks. ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); Loading Loading @@ -2328,6 +2369,7 @@ public class ShortcutService extends IShortcutService.Stub { } finally { logDurationStat(Stats.CHECK_PACKAGE_CHANGES, start); } verifyStates(); } private void handlePackageAdded(String packageName, @UserIdInt int userId) { Loading @@ -2339,6 +2381,7 @@ public class ShortcutService extends IShortcutService.Stub { user.attemptToRestoreIfNeededAndSave(this, packageName, userId); user.handlePackageAddedOrUpdated(packageName); } verifyStates(); } private void handlePackageUpdateFinished(String packageName, @UserIdInt int userId) { Loading @@ -2354,6 +2397,7 @@ public class ShortcutService extends IShortcutService.Stub { user.handlePackageAddedOrUpdated(packageName); } } verifyStates(); } private void handlePackageRemoved(String packageName, @UserIdInt int packageUserId) { Loading @@ -2362,6 +2406,8 @@ public class ShortcutService extends IShortcutService.Stub { packageUserId)); } cleanUpPackageForAllLoadedUsers(packageName, packageUserId); verifyStates(); } private void handlePackageDataCleared(String packageName, int packageUserId) { Loading @@ -2370,6 +2416,8 @@ public class ShortcutService extends IShortcutService.Stub { packageUserId)); } cleanUpPackageForAllLoadedUsers(packageName, packageUserId); verifyStates(); } // === PackageManager interaction === Loading
services/tests/servicestests/res/xml/shortcut_5_reverse.xml 0 → 100644 +53 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <shortcuts xmlns:android="http://schemas.android.com/apk/res/android" > <shortcut android:shortcutId="ms5" android:enabled="true" android:shortcutIcon="@drawable/icon1" android:shortcutShortLabel="@string/shortcut_title1" android:shortcutLongLabel="@string/shortcut_text1" android:shortcutDisabledMessage="@string/shortcut_disabled_message1" android:shortcutCategories="android.shortcut.conversation:android.shortcut.media" android:shortcutIntentAction="action1" android:shortcutIntentData="http://a.b.c/1" /> <shortcut android:shortcutId="ms4" android:enabled="true" android:shortcutIcon="@drawable/icon2" android:shortcutShortLabel="@string/shortcut_title2" android:shortcutLongLabel="@string/shortcut_text2" android:shortcutDisabledMessage="@string/shortcut_disabled_message2" android:shortcutCategories="android.shortcut.conversation" android:shortcutIntentAction="action2" /> <shortcut android:shortcutId="ms3" android:shortcutShortLabel="@string/shortcut_title1" android:shortcutIntentAction="android.intent.action.VIEW" /> <shortcut android:shortcutId="ms2" android:shortcutShortLabel="@string/shortcut_title1" android:shortcutIntentAction="android.intent.action.VIEW" /> <shortcut android:shortcutId="ms1" android:shortcutShortLabel="@string/shortcut_title1" android:shortcutIntentAction="android.intent.action.VIEW" /> </shortcuts>