ShortcutManager: deal with changing resource IDs on app update

- When an app is upgraded, all the resource IDs may change.  So
if a shortcut is previously published with an icon for res ID 100
and the publisher is upgraded, resource #100 may refer to something
different.

- So now the service also remembers resource names for icon resources,
as wells as string resources.  When an app is updated, the service
fetch the updated resource IDs by name.

- Also extract all string resources when a shortcut is published
and persist them, so that even when the original string resources are
removed from the app, the launcher can still show the extracted strings.

- When the system locale changes, re-extract all string resources.

- Also really hide the constants in ShortcutInfo that were
accidentally made public.

Change-Id: I23c29b45c1de5d76175229190a1533c9c62c5960
diff --git a/api/current.txt b/api/current.txt
index 83f6131..cb42b86 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -10088,19 +10088,7 @@
     method public boolean isManifestShortcut();
     method public boolean isPinned();
     method public void writeToParcel(android.os.Parcel, int);
-    field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1
-    field public static final int CLONE_REMOVE_FOR_LAUNCHER = 3; // 0x3
-    field public static final int CLONE_REMOVE_NON_KEY_INFO = 4; // 0x4
     field public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR;
-    field public static final int FLAG_DISABLED = 64; // 0x40
-    field public static final int FLAG_DYNAMIC = 1; // 0x1
-    field public static final int FLAG_HAS_ICON_FILE = 8; // 0x8
-    field public static final int FLAG_HAS_ICON_RES = 4; // 0x4
-    field public static final int FLAG_IMMUTABLE = 256; // 0x100
-    field public static final int FLAG_KEY_FIELDS_ONLY = 16; // 0x10
-    field public static final int FLAG_MANIFEST = 32; // 0x20
-    field public static final int FLAG_PINNED = 2; // 0x2
-    field public static final int FLAG_STRINGS_RESOLVED = 128; // 0x80
     field public static final java.lang.String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation";
   }
 
diff --git a/api/system-current.txt b/api/system-current.txt
index e2a0516..8cb9f0d 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -10511,19 +10511,7 @@
     method public boolean isManifestShortcut();
     method public boolean isPinned();
     method public void writeToParcel(android.os.Parcel, int);
-    field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1
-    field public static final int CLONE_REMOVE_FOR_LAUNCHER = 3; // 0x3
-    field public static final int CLONE_REMOVE_NON_KEY_INFO = 4; // 0x4
     field public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR;
-    field public static final int FLAG_DISABLED = 64; // 0x40
-    field public static final int FLAG_DYNAMIC = 1; // 0x1
-    field public static final int FLAG_HAS_ICON_FILE = 8; // 0x8
-    field public static final int FLAG_HAS_ICON_RES = 4; // 0x4
-    field public static final int FLAG_IMMUTABLE = 256; // 0x100
-    field public static final int FLAG_KEY_FIELDS_ONLY = 16; // 0x10
-    field public static final int FLAG_MANIFEST = 32; // 0x20
-    field public static final int FLAG_PINNED = 2; // 0x2
-    field public static final int FLAG_STRINGS_RESOLVED = 128; // 0x80
     field public static final java.lang.String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation";
   }
 
diff --git a/api/test-current.txt b/api/test-current.txt
index e74b845..d20f87a 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -10101,19 +10101,7 @@
     method public boolean isManifestShortcut();
     method public boolean isPinned();
     method public void writeToParcel(android.os.Parcel, int);
-    field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1
-    field public static final int CLONE_REMOVE_FOR_LAUNCHER = 3; // 0x3
-    field public static final int CLONE_REMOVE_NON_KEY_INFO = 4; // 0x4
     field public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR;
-    field public static final int FLAG_DISABLED = 64; // 0x40
-    field public static final int FLAG_DYNAMIC = 1; // 0x1
-    field public static final int FLAG_HAS_ICON_FILE = 8; // 0x8
-    field public static final int FLAG_HAS_ICON_RES = 4; // 0x4
-    field public static final int FLAG_IMMUTABLE = 256; // 0x100
-    field public static final int FLAG_KEY_FIELDS_ONLY = 16; // 0x10
-    field public static final int FLAG_MANIFEST = 32; // 0x20
-    field public static final int FLAG_PINNED = 2; // 0x2
-    field public static final int FLAG_STRINGS_RESOLVED = 128; // 0x80
     field public static final java.lang.String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation";
   }
 
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index 28e7887..da58717 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -22,8 +22,8 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.Parcel;
@@ -31,7 +31,9 @@
 import android.os.PersistableBundle;
 import android.os.UserHandle;
 import android.util.ArraySet;
+import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -54,31 +56,37 @@
  * @see {@link ShortcutManager}.
  */
 public final class ShortcutInfo implements Parcelable {
-    /* @hide */
+    static final String TAG = "Shortcut";
+
+    private static final String RES_TYPE_STRING = "string";
+
+    private static final String ANDROID_PACKAGE_NAME = "android";
+
+    /** @hide */
     public static final int FLAG_DYNAMIC = 1 << 0;
 
-    /* @hide */
+    /** @hide */
     public static final int FLAG_PINNED = 1 << 1;
 
-    /* @hide */
+    /** @hide */
     public static final int FLAG_HAS_ICON_RES = 1 << 2;
 
-    /* @hide */
+    /** @hide */
     public static final int FLAG_HAS_ICON_FILE = 1 << 3;
 
-    /* @hide */
+    /** @hide */
     public static final int FLAG_KEY_FIELDS_ONLY = 1 << 4;
 
-    /* @hide */
+    /** @hide */
     public static final int FLAG_MANIFEST = 1 << 5;
 
-    /* @hide */
+    /** @hide */
     public static final int FLAG_DISABLED = 1 << 6;
 
-    /* @hide */
+    /** @hide */
     public static final int FLAG_STRINGS_RESOLVED = 1 << 7;
 
-    /* @hide */
+    /** @hide */
     public static final int FLAG_IMMUTABLE = 1 << 8;
 
     /** @hide */
@@ -99,20 +107,24 @@
 
     // Cloning options.
 
-    /* @hide */
+    /** @hide */
     private static final int CLONE_REMOVE_ICON = 1 << 0;
 
-    /* @hide */
+    /** @hide */
     private static final int CLONE_REMOVE_INTENT = 1 << 1;
 
-    /* @hide */
+    /** @hide */
     public static final int CLONE_REMOVE_NON_KEY_INFO = 1 << 2;
 
-    /* @hide */
-    public static final int CLONE_REMOVE_FOR_CREATOR = CLONE_REMOVE_ICON;
+    /** @hide */
+    public static final int CLONE_REMOVE_RES_NAMES = 1 << 3;
 
-    /* @hide */
-    public static final int CLONE_REMOVE_FOR_LAUNCHER = CLONE_REMOVE_ICON | CLONE_REMOVE_INTENT;
+    /** @hide */
+    public static final int CLONE_REMOVE_FOR_CREATOR = CLONE_REMOVE_ICON | CLONE_REMOVE_RES_NAMES;
+
+    /** @hide */
+    public static final int CLONE_REMOVE_FOR_LAUNCHER = CLONE_REMOVE_ICON | CLONE_REMOVE_INTENT
+            | CLONE_REMOVE_RES_NAMES;
 
     /** @hide */
     @IntDef(flag = true,
@@ -120,6 +132,7 @@
                     CLONE_REMOVE_ICON,
                     CLONE_REMOVE_INTENT,
                     CLONE_REMOVE_NON_KEY_INFO,
+                    CLONE_REMOVE_RES_NAMES,
                     CLONE_REMOVE_FOR_CREATOR,
                     CLONE_REMOVE_FOR_LAUNCHER
             })
@@ -144,16 +157,22 @@
 
     private int mTitleResId;
 
+    private String mTitleResName;
+
     @Nullable
     private CharSequence mTitle;
 
     private int mTextResId;
 
+    private String mTextResName;
+
     @Nullable
     private CharSequence mText;
 
     private int mDisabledMessageResId;
 
+    private String mDisabledMessageResName;
+
     @Nullable
     private CharSequence mDisabledMessage;
 
@@ -184,7 +203,9 @@
     private int mFlags;
 
     // Internal use only.
-    private int mIconResourceId;
+    private int mIconResId;
+
+    private String mIconResName;
 
     // Internal use only.
     @Nullable
@@ -251,7 +272,7 @@
         mLastChangedTimestamp = source.mLastChangedTimestamp;
 
         // Just always keep it since it's cheep.
-        mIconResourceId = source.mIconResourceId;
+        mIconResId = source.mIconResId;
 
         if ((cloneFlags & CLONE_REMOVE_NON_KEY_INFO) == 0) {
             mActivity = source.mActivity;
@@ -274,37 +295,235 @@
             }
             mRank = source.mRank;
             mExtras = source.mExtras;
+
+            if ((cloneFlags & CLONE_REMOVE_RES_NAMES) == 0) {
+                mTitleResName = source.mTitleResName;
+                mTextResName = source.mTextResName;
+                mDisabledMessageResName = source.mDisabledMessageResName;
+                mIconResName = source.mIconResName;
+            }
         } else {
             // Set this bit.
             mFlags |= FLAG_KEY_FIELDS_ONLY;
         }
     }
 
-    /** @hide */
-    public void resolveStringsRequiringCrossUser(Context context) throws NameNotFoundException {
+    /**
+     * Load a string resource from the publisher app.
+     *
+     * @param resId resource ID
+     * @param defValue default value to be returned when the specified resource isn't found.
+     */
+    private CharSequence getResourceString(Resources res, int resId, CharSequence defValue) {
+        try {
+            return res.getString(resId);
+        } catch (NotFoundException e) {
+            Log.e(TAG, "Resource for ID=" + resId + " not found in package " + mPackageName);
+            return defValue;
+        }
+    }
+
+    /**
+     * Load the string resources for the text fields and set them to the actual value fields.
+     * This will set {@link #FLAG_STRINGS_RESOLVED}.
+     *
+     * @param res {@link Resources} for the publisher.  Must have been loaded with
+     * {@link PackageManager#getResourcesForApplicationAsUser}.
+     *
+     * @hide
+     */
+    public void resolveResourceStrings(@NonNull Resources res) {
         mFlags |= FLAG_STRINGS_RESOLVED;
 
         if ((mTitleResId == 0) && (mTextResId == 0) && (mDisabledMessageResId == 0)) {
             return; // Bail early.
         }
-        final Resources res = context.getPackageManager().getResourcesForApplicationAsUser(
-                mPackageName, mUserId);
 
         if (mTitleResId != 0) {
-            mTitle = res.getString(mTitleResId);
-            mTitleResId = 0;
+            mTitle = getResourceString(res, mTitleResId, mTitle);
         }
         if (mTextResId != 0) {
-            mText = res.getString(mTextResId);
-            mTextResId = 0;
+            mText = getResourceString(res, mTextResId, mText);
         }
         if (mDisabledMessageResId != 0) {
-            mDisabledMessage = res.getString(mDisabledMessageResId);
-            mDisabledMessageResId = 0;
+            mDisabledMessage = getResourceString(res, mDisabledMessageResId, mDisabledMessage);
         }
     }
 
     /**
+     * Look up resource name for a given resource ID.
+     *
+     * @return a simple resource name (e.g. "text_1") when {@code withType} is false, or with the
+     * type (e.g. "string/text_1").
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public static String lookUpResourceName(@NonNull Resources res, int resId, boolean withType,
+            @NonNull String packageName) {
+        if (resId == 0) {
+            return null;
+        }
+        try {
+            final String fullName = res.getResourceName(resId);
+
+            if (ANDROID_PACKAGE_NAME.equals(getResourcePackageName(fullName))) {
+                // If it's a framework resource, the value won't change, so just return the ID
+                // value as a string.
+                return String.valueOf(resId);
+            }
+            return withType ? getResourceTypeAndEntryName(fullName)
+                    : getResourceEntryName(fullName);
+        } catch (NotFoundException e) {
+            Log.e(TAG, "Resource name for ID=" + resId + " not found in package " + packageName
+                    + ". Resource IDs may change when the application is upgraded, and the system"
+                    + " may not be able to find the correct resource.");
+            return null;
+        }
+    }
+
+    /**
+     * Extract the package name from a fully-donated resource name.
+     * e.g. "com.android.app1:drawable/icon1" -> "com.android.app1"
+     * @hide
+     */
+    @VisibleForTesting
+    public static String getResourcePackageName(@NonNull String fullResourceName) {
+        final int p1 = fullResourceName.indexOf(':');
+        if (p1 < 0) {
+            return null;
+        }
+        return fullResourceName.substring(0, p1);
+    }
+
+    /**
+     * Extract the type name from a fully-donated resource name.
+     * e.g. "com.android.app1:drawable/icon1" -> "drawable"
+     * @hide
+     */
+    @VisibleForTesting
+    public static String getResourceTypeName(@NonNull String fullResourceName) {
+        final int p1 = fullResourceName.indexOf(':');
+        if (p1 < 0) {
+            return null;
+        }
+        final int p2 = fullResourceName.indexOf('/', p1 + 1);
+        if (p2 < 0) {
+            return null;
+        }
+        return fullResourceName.substring(p1 + 1, p2);
+    }
+
+    /**
+     * Extract the type name + the entry name from a fully-donated resource name.
+     * e.g. "com.android.app1:drawable/icon1" -> "drawable/icon1"
+     * @hide
+     */
+    @VisibleForTesting
+    public static String getResourceTypeAndEntryName(@NonNull String fullResourceName) {
+        final int p1 = fullResourceName.indexOf(':');
+        if (p1 < 0) {
+            return null;
+        }
+        return fullResourceName.substring(p1 + 1);
+    }
+
+    /**
+     * Extract the entry name from a fully-donated resource name.
+     * e.g. "com.android.app1:drawable/icon1" -> "icon1"
+     * @hide
+     */
+    @VisibleForTesting
+    public static String getResourceEntryName(@NonNull String fullResourceName) {
+        final int p1 = fullResourceName.indexOf('/');
+        if (p1 < 0) {
+            return null;
+        }
+        return fullResourceName.substring(p1 + 1);
+    }
+
+    /**
+     * Return the resource ID for a given resource ID.
+     *
+     * Basically its' a wrapper over {@link Resources#getIdentifier(String, String, String)}, except
+     * if {@code resourceName} is an integer then it'll just return its value.  (Which also the
+     * aforementioned method would do internally, but not documented, so doing here explicitly.)
+     *
+     * @param res {@link Resources} for the publisher.  Must have been loaded with
+     * {@link PackageManager#getResourcesForApplicationAsUser}.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public static int lookUpResourceId(@NonNull Resources res, @Nullable String resourceName,
+            @Nullable String resourceType, String packageName) {
+        if (resourceName == null) {
+            return 0;
+        }
+        try {
+            try {
+                // It the name can be parsed as an integer, just use it.
+                return Integer.parseInt(resourceName);
+            } catch (NumberFormatException ignore) {
+            }
+
+            return res.getIdentifier(resourceName, resourceType, packageName);
+        } catch (NotFoundException e) {
+            Log.e(TAG, "Resource ID for name=" + resourceName + " not found in package "
+                    + packageName);
+            return 0;
+        }
+    }
+
+    /**
+     * Look up resource names from the resource IDs for the icon res and the text fields, and fill
+     * in the resource name fields.
+     *
+     * @param res {@link Resources} for the publisher.  Must have been loaded with
+     * {@link PackageManager#getResourcesForApplicationAsUser}.
+     *
+     * @hide
+     */
+    public void lookupAndFillInResourceNames(@NonNull Resources res) {
+        if ((mTitleResId == 0) && (mTextResId == 0) && (mDisabledMessageResId == 0)
+                && (mIconResId == 0)) {
+            return; // Bail early.
+        }
+
+        // We don't need types for strings because their types are always "string".
+        mTitleResName = lookUpResourceName(res, mTitleResId, /*withType=*/ false, mPackageName);
+        mTextResName = lookUpResourceName(res, mTextResId, /*withType=*/ false, mPackageName);
+        mDisabledMessageResName = lookUpResourceName(res, mDisabledMessageResId,
+                /*withType=*/ false, mPackageName);
+
+        // But icons have multiple possible types, so include the type.
+        mIconResName = lookUpResourceName(res, mIconResId, /*withType=*/ true, mPackageName);
+    }
+
+    /**
+     * Look up resource IDs from the resource names for the icon res and the text fields, and fill
+     * in the resource ID fields.
+     *
+     * This is called when an app is updated.
+     *
+     * @hide
+     */
+    public void lookupAndFillInResourceIds(@NonNull Resources res) {
+        if ((mTitleResName == null) && (mTextResName == null) && (mDisabledMessageResName == null)
+                && (mIconResName == null)) {
+            return; // Bail early.
+        }
+
+        mTitleResId = lookUpResourceId(res, mTitleResName, RES_TYPE_STRING, mPackageName);
+        mTextResId = lookUpResourceId(res, mTextResName, RES_TYPE_STRING, mPackageName);
+        mDisabledMessageResId = lookUpResourceId(res, mDisabledMessageResName, RES_TYPE_STRING,
+                mPackageName);
+
+        // mIconResName already contains the type, so the third argument is not needed.
+        mIconResId = lookUpResourceId(res, mIconResName, null, mPackageName);
+    }
+
+    /**
      * Copy a {@link ShortcutInfo}, optionally removing fields.
      * @hide
      */
@@ -344,27 +563,38 @@
 
         if (source.mIcon != null) {
             mIcon = source.mIcon;
+
+            mIconResId = 0;
+            mIconResName = null;
+            mBitmapPath = null;
         }
         if (source.mTitle != null) {
             mTitle = source.mTitle;
             mTitleResId = 0;
+            mTitleResName = null;
         } else if (source.mTitleResId != 0) {
             mTitle = null;
             mTitleResId = source.mTitleResId;
+            mTitleResName = null;
         }
+
         if (source.mText != null) {
             mText = source.mText;
             mTextResId = 0;
+            mTextResName = null;
         } else if (source.mTextResId != 0) {
             mText = null;
             mTextResId = source.mTextResId;
+            mTextResName = null;
         }
         if (source.mDisabledMessage != null) {
             mDisabledMessage = source.mDisabledMessage;
             mDisabledMessageResId = 0;
+            mDisabledMessageResName = null;
         } else if (source.mDisabledMessageResId != 0) {
             mDisabledMessage = null;
             mDisabledMessageResId = source.mDisabledMessageResId;
+            mDisabledMessageResName = null;
         }
         if (source.mCategories != null) {
             mCategories = clone(source.mCategories);
@@ -983,14 +1213,17 @@
 
     /** @hide */
     public void setIconResourceId(int iconResourceId) {
-        mIconResourceId = iconResourceId;
+        if (mIconResId != iconResourceId) {
+            mIconResName = null;
+        }
+        mIconResId = iconResourceId;
     }
 
     /**
      * Get the resource ID for the icon, valid only when {@link #hasIconResource()} } is true.
      */
     public int getIconResourceId() {
-        return mIconResourceId;
+        return mIconResId;
     }
 
     /** @hide */
@@ -1005,6 +1238,9 @@
 
     /** @hide */
     public void setDisabledMessageResId(int disabledMessageResId) {
+        if (mDisabledMessageResId != disabledMessageResId) {
+            mDisabledMessageResName = null;
+        }
         mDisabledMessageResId = disabledMessageResId;
         mDisabledMessage = null;
     }
@@ -1013,6 +1249,47 @@
     public void setDisabledMessage(String disabledMessage) {
         mDisabledMessage = disabledMessage;
         mDisabledMessageResId = 0;
+        mDisabledMessageResName = null;
+    }
+
+    /** @hide */
+    public String getTitleResName() {
+        return mTitleResName;
+    }
+
+    /** @hide */
+    public void setTitleResName(String titleResName) {
+        mTitleResName = titleResName;
+    }
+
+    /** @hide */
+    public String getTextResName() {
+        return mTextResName;
+    }
+
+    /** @hide */
+    public void setTextResName(String textResName) {
+        mTextResName = textResName;
+    }
+
+    /** @hide */
+    public String getDisabledMessageResName() {
+        return mDisabledMessageResName;
+    }
+
+    /** @hide */
+    public void setDisabledMessageResName(String disabledMessageResName) {
+        mDisabledMessageResName = disabledMessageResName;
+    }
+
+    /** @hide */
+    public String getIconResName() {
+        return mIconResName;
+    }
+
+    /** @hide */
+    public void setIconResName(String iconResName) {
+        mIconResName = iconResName;
     }
 
     private ShortcutInfo(Parcel source) {
@@ -1035,9 +1312,14 @@
         mExtras = source.readParcelable(cl);
         mLastChangedTimestamp = source.readLong();
         mFlags = source.readInt();
-        mIconResourceId = source.readInt();
+        mIconResId = source.readInt();
         mBitmapPath = source.readString();
 
+        mIconResName = source.readString();
+        mTitleResName = source.readString();
+        mTextResName = source.readString();
+        mDisabledMessageResName = source.readString();
+
         int N = source.readInt();
         if (N == 0) {
             mCategories = null;
@@ -1069,9 +1351,14 @@
         dest.writeParcelable(mExtras, flags);
         dest.writeLong(mLastChangedTimestamp);
         dest.writeInt(mFlags);
-        dest.writeInt(mIconResourceId);
+        dest.writeInt(mIconResId);
         dest.writeString(mBitmapPath);
 
+        dest.writeString(mIconResName);
+        dest.writeString(mTitleResName);
+        dest.writeString(mTextResName);
+        dest.writeString(mDisabledMessageResName);
+
         if (mCategories != null) {
             final int N = mCategories.size();
             dest.writeInt(N);
@@ -1160,16 +1447,25 @@
         sb.append(secure ? "***" : mTitle);
         sb.append(", resId=");
         sb.append(mTitleResId);
+        sb.append("[");
+        sb.append(mTitleResName);
+        sb.append("]");
 
         sb.append(", longLabel=");
         sb.append(secure ? "***" : mText);
         sb.append(", resId=");
         sb.append(mTextResId);
+        sb.append("[");
+        sb.append(mTextResName);
+        sb.append("]");
 
         sb.append(", disabledMessage=");
         sb.append(secure ? "***" : mDisabledMessage);
         sb.append(", resId=");
         sb.append(mDisabledMessageResId);
+        sb.append("[");
+        sb.append(mDisabledMessageResName);
+        sb.append("]");
 
         sb.append(", categories=");
         sb.append(mCategories);
@@ -1195,7 +1491,10 @@
         if (includeInternalData) {
 
             sb.append(", iconRes=");
-            sb.append(mIconResourceId);
+            sb.append(mIconResId);
+            sb.append("[");
+            sb.append(mIconResName);
+            sb.append("]");
 
             sb.append(", bitmapPath=");
             sb.append(mBitmapPath);
@@ -1208,11 +1507,13 @@
     /** @hide */
     public ShortcutInfo(
             @UserIdInt int userId, String id, String packageName, ComponentName activity,
-            Icon icon, CharSequence title, int titleResId, CharSequence text, int textResId,
-            CharSequence disabledMessage, int disabledMessageResId, Set<String> categories,
+            Icon icon, CharSequence title, int titleResId, String titleResName,
+            CharSequence text, int textResId, String textResName,
+            CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName,
+            Set<String> categories,
             Intent intent, PersistableBundle intentPersistableExtras,
             int rank, PersistableBundle extras, long lastChangedTimestamp,
-            int flags, int iconResId, String bitmapPath) {
+            int flags, int iconResId, String iconResName, String bitmapPath) {
         mUserId = userId;
         mId = id;
         mPackageName = packageName;
@@ -1220,10 +1521,13 @@
         mIcon = icon;
         mTitle = title;
         mTitleResId = titleResId;
+        mTitleResName = titleResName;
         mText = text;
         mTextResId = textResId;
+        mTextResName = textResName;
         mDisabledMessage = disabledMessage;
         mDisabledMessageResId = disabledMessageResId;
+        mDisabledMessageResName = disabledMessageResName;
         mCategories = clone(categories);
         mIntent = intent;
         mIntentPersistableExtras = intentPersistableExtras;
@@ -1231,7 +1535,8 @@
         mExtras = extras;
         mLastChangedTimestamp = lastChangedTimestamp;
         mFlags = flags;
-        mIconResourceId = iconResId;
+        mIconResId = iconResId;
+        mIconResName = iconResName;
         mBitmapPath = bitmapPath;
     }
 }
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index e0c28fa..f336ff3 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -19,9 +19,12 @@
 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;
 import android.text.format.Formatter;
 import android.util.ArrayMap;
@@ -48,6 +51,8 @@
 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}.
@@ -72,15 +77,19 @@
     private static final String ATTR_ACTIVITY = "activity";
     private static final String ATTR_TITLE = "title";
     private static final String ATTR_TITLE_RES_ID = "titleid";
+    private static final String ATTR_TITLE_RES_NAME = "titlename";
     private static final String ATTR_TEXT = "text";
     private static final String ATTR_TEXT_RES_ID = "textid";
+    private static final String ATTR_TEXT_RES_NAME = "textname";
     private static final String ATTR_DISABLED_MESSAGE = "dmessage";
     private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid";
+    private static final String ATTR_DISABLED_MESSAGE_RES_NAME = "dmessagename";
     private static final String ATTR_INTENT = "intent";
     private static final String ATTR_RANK = "rank";
     private static final String ATTR_TIMESTAMP = "timestamp";
     private static final String ATTR_FLAGS = "flags";
-    private static final String ATTR_ICON_RES = "icon-res";
+    private static final String ATTR_ICON_RES_ID = "icon-res";
+    private static final String ATTR_ICON_RES_NAME = "icon-resname";
     private static final String ATTR_BITMAP_PATH = "bitmap-path";
 
     private static final String NAME_CATEGORIES = "categories";
@@ -153,6 +162,12 @@
         }
     }
 
+    @Nullable
+    public Resources getPackageResources() {
+        return mShortcutUser.mService.injectGetResourcesForApplicationAsUser(
+                getPackageName(), getPackageUserId());
+    }
+
     @Override
     protected void onRestoreBlocked() {
         // Can't restore due to version/signature mismatch.  Remove all shortcuts.
@@ -209,8 +224,13 @@
     }
 
     private void addShortcutInner(@NonNull ShortcutInfo newShortcut) {
+        final ShortcutService s = mShortcutUser.mService;
+
         deleteShortcutInner(newShortcut.getId());
-        mShortcutUser.mService.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut);
+
+        // Extract Icon and update the icon res ID and the bitmap path.
+        s.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut);
+        s.fixUpShortcutResourceNamesAndValues(newShortcut);
         mShortcuts.put(newShortcut.getId(), newShortcut);
     }
 
@@ -248,7 +268,7 @@
         // TODO Check max dynamic count.
         // mShortcutUser.mService.enforceMaxDynamicShortcuts(newDynamicCount);
 
-        // Okay, make it dynamic and add.
+        // If it was originally pinned, the new one should be pinned too.
         if (wasPinned) {
             newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
         }
@@ -318,6 +338,8 @@
                 disabled.setDisabledMessage(disabledMessage);
             } else if (disabledMessageResId != 0) {
                 disabled.setDisabledMessageResId(disabledMessageResId);
+
+                mShortcutUser.mService.fixUpShortcutResourceNamesAndValues(disabled);
             }
         }
     }
@@ -622,10 +644,25 @@
 
         // For existing shortcuts, update timestamps if they have any resources.
         if (!isNewApp) {
+            Resources publisherRes = null;
+
             for (int i = mShortcuts.size() - 1; i >= 0; i--) {
                 final ShortcutInfo si = mShortcuts.valueAt(i);
 
                 if (si.hasAnyResources()) {
+                    if (!si.isOriginallyFromManifest()) {
+                        if (publisherRes == null) {
+                            publisherRes = getPackageResources();
+                            if (publisherRes == null) {
+                                break; // Resources couldn't be loaded.
+                            }
+                        }
+
+                        // If this shortcut is not from a manifest, then update all resource IDs
+                        // from resource names.  (We don't allow resource strings for
+                        // non-manifest at the moment, but icons can still be resources.)
+                        si.lookupAndFillInResourceIds(publisherRes);
+                    }
                     changed = true;
                     si.setTimestamp(s.injectCurrentTimeMillis());
                 }
@@ -869,7 +906,9 @@
             final ComponentName newActivity = newShortcut.getActivity();
             if (newActivity == null) {
                 if (operation != ShortcutService.OPERATION_UPDATE) {
-                    service.wtf("null Activity found for non-update");
+                    // This method may be called before validating shortcuts, so this may happen,
+                    // and is a caller side error.
+                    throw new NullPointerException("activity must be provided");
                 }
                 continue; // Activity can be null for update.
             }
@@ -907,6 +946,36 @@
         }
     }
 
+    /**
+     * For all the text fields, refresh the string values if they're from resources.
+     */
+    public void resolveResourceStrings() {
+        final ShortcutService s = mShortcutUser.mService;
+        boolean changed = false;
+
+        Resources publisherRes = null;
+        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+            final ShortcutInfo si = mShortcuts.valueAt(i);
+
+            if (si.hasStringResources()) {
+                changed = true;
+
+                if (publisherRes == null) {
+                    publisherRes = getPackageResources();
+                    if (publisherRes == null) {
+                        break; // Resources couldn't be loaded.
+                    }
+                }
+
+                si.resolveResourceStrings(publisherRes);
+                si.setTimestamp(s.injectCurrentTimeMillis());
+            }
+        }
+        if (changed) {
+            s.scheduleSaveUser(getPackageUserId());
+        }
+    }
+
     public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
         pw.println();
 
@@ -1008,11 +1077,15 @@
         // writeAttr(out, "icon", si.getIcon());  // We don't save it.
         ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
         ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId());
+        ShortcutService.writeAttr(out, ATTR_TITLE_RES_NAME, si.getTitleResName());
         ShortcutService.writeAttr(out, ATTR_TEXT, si.getText());
         ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId());
+        ShortcutService.writeAttr(out, ATTR_TEXT_RES_NAME, si.getTextResName());
         ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE, si.getDisabledMessage());
         ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_ID,
                 si.getDisabledMessageResourceId());
+        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,
@@ -1025,7 +1098,8 @@
                             | ShortcutInfo.FLAG_DYNAMIC));
         } else {
             ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
-            ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
+            ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId());
+            ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName());
             ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
         }
 
@@ -1096,17 +1170,21 @@
         // Icon icon;
         String title;
         int titleResId;
+        String titleResName;
         String text;
         int textResId;
+        String textResName;
         String disabledMessage;
         int disabledMessageResId;
+        String disabledMessageResName;
         Intent intent;
         PersistableBundle intentPersistableExtras = null;
         int rank;
         PersistableBundle extras = null;
         long lastChangedTimestamp;
         int flags;
-        int iconRes;
+        int iconResId;
+        String iconResName;
         String bitmapPath;
         ArraySet<String> categories = null;
 
@@ -1115,16 +1193,21 @@
                 ATTR_ACTIVITY);
         title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
         titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID);
+        titleResName = ShortcutService.parseStringAttribute(parser, ATTR_TITLE_RES_NAME);
         text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT);
         textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID);
+        textResName = ShortcutService.parseStringAttribute(parser, ATTR_TEXT_RES_NAME);
         disabledMessage = ShortcutService.parseStringAttribute(parser, ATTR_DISABLED_MESSAGE);
         disabledMessageResId = ShortcutService.parseIntAttribute(parser,
                 ATTR_DISABLED_MESSAGE_RES_ID);
+        disabledMessageResName = ShortcutService.parseStringAttribute(parser,
+                ATTR_DISABLED_MESSAGE_RES_NAME);
         intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT);
         rank = (int) ShortcutService.parseLongAttribute(parser, ATTR_RANK);
         lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP);
         flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
-        iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES);
+        iconResId = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES_ID);
+        iconResName = ShortcutService.parseStringAttribute(parser, ATTR_ICON_RES_NAME);
         bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
 
         final int outerDepth = parser.getDepth();
@@ -1167,10 +1250,11 @@
 
         return new ShortcutInfo(
                 userId, id, packageName, activityComponent, /* icon =*/ null,
-                title, titleResId, text, textResId, disabledMessage, disabledMessageResId,
+                title, titleResId, titleResName, text, textResId, textResName,
+                disabledMessage, disabledMessageResId, disabledMessageResName,
                 categories, intent,
                 intentPersistableExtras, rank, extras, lastChangedTimestamp, flags,
-                iconRes, bitmapPath);
+                iconResId, iconResName, bitmapPath);
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java
index 3eda13e..470d4af 100644
--- a/services/core/java/com/android/server/pm/ShortcutParser.java
+++ b/services/core/java/com/android/server/pm/ShortcutParser.java
@@ -221,6 +221,8 @@
                 | ShortcutInfo.FLAG_IMMUTABLE
                 | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0);
 
+        // Note we don't need to set resource names here yet.  They'll be set when they're about
+        // to be published.
         return new ShortcutInfo(
                 userId,
                 id,
@@ -229,10 +231,13 @@
                 null, // icon
                 null, // title string
                 titleResId,
+                null, // title res name
                 null, // text string
                 textResId,
+                null, // text res name
                 null, // disabled message string
                 disabledMessageResId,
+                null, // disabled message res name
                 categories,
                 intent,
                 null, // intent extras
@@ -241,6 +246,7 @@
                 service.injectCurrentTimeMillis(),
                 flags,
                 iconResId,
+                null, // icon res name
                 null); // bitmap path
     }
 }
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 7835231..47fb2c7 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -42,6 +42,7 @@
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
+import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.CompressFormat;
@@ -119,6 +120,9 @@
 
 /**
  * TODO:
+ * - Deal with the async nature of PACKAGE_ADD.  Basically when a publisher does anything after
+ *   it's upgraded, the manager should make sure the upgrade process has been executed.
+ *
  * - HandleUnlockUser needs to be async.  Wait on it in onCleanupUser.
  *
  * - Implement reportShortcutUsed().
@@ -318,8 +322,10 @@
         int GET_ACTIVITIES_WITH_METADATA = 6;
         int GET_INSTALLED_APPLICATIONS = 7;
         int CHECK_PACKAGE_CHANGES = 8;
+        int GET_APPLICATION_RESOURCES = 9;
+        int RESOURCE_NAME_LOOKUP = 10;
 
-        int COUNT = CHECK_PACKAGE_CHANGES + 1;
+        int COUNT = RESOURCE_NAME_LOOKUP + 1;
     }
 
     final Object mStatLock = new Object();
@@ -1265,6 +1271,24 @@
         return scaledBitmap;
     }
 
+    /**
+     * For a shortcut, update all resource names from resource IDs, and also update all
+     * resource-based strings.
+     */
+    void fixUpShortcutResourceNamesAndValues(ShortcutInfo si) {
+        final Resources publisherRes = injectGetResourcesForApplicationAsUser(
+                si.getPackage(), si.getUserId());
+        if (publisherRes != null) {
+            final long start = injectElapsedRealtime();
+            try {
+                si.lookupAndFillInResourceNames(publisherRes);
+            } finally {
+                logDurationStat(Stats.RESOURCE_NAME_LOOKUP, start);
+            }
+            si.resolveResourceStrings(publisherRes);
+        }
+    }
+
     // === Caller validation ===
 
     private boolean isCallerSystem() {
@@ -1327,7 +1351,8 @@
         throw new SecurityException("Calling package name mismatch");
     }
 
-    void postToHandler(Runnable r) {
+    // Overridden in unit tests to execute r synchronously.
+    void injectPostToHandler(Runnable r) {
         mHandler.post(r);
     }
 
@@ -1370,7 +1395,7 @@
         } finally {
             injectRestoreCallingIdentity(token);
         }
-        postToHandler(() -> {
+        injectPostToHandler(() -> {
             final ArrayList<ShortcutChangeListener> copy;
             synchronized (mLock) {
                 copy = new ArrayList<>(mListeners);
@@ -1537,12 +1562,18 @@
                         // TODO When activity is changing, check the dynamic count.
                     }
 
-                    // Note copyNonNullFieldsFrom() does the "udpatable with?" check too.
+                    // Note copyNonNullFieldsFrom() does the "updatable with?" check too.
                     target.copyNonNullFieldsFrom(source);
 
                     if (replacingIcon) {
                         saveIconAndFixUpShortcut(userId, target);
                     }
+
+                    // When we're updating any resource related fields, re-extract the res names and
+                    // the values.
+                    if (replacingIcon || source.hasStringResources()) {
+                        fixUpShortcutResourceNamesAndValues(target);
+                    }
                 }
             }
         }
@@ -1980,21 +2011,6 @@
                     });
                 }
             }
-            // Resolve all strings if needed.
-            if (!cloneKeyFieldOnly) {
-                final long token = injectClearCallingIdentity();
-                try {
-                    for (int i = ret.size() - 1; i >= 0; i--) {
-                        try {
-                            ret.get(i).resolveStringsRequiringCrossUser(mContext);
-                        } catch (NameNotFoundException e) {
-                            continue;
-                        }
-                    }
-                } finally {
-                    injectRestoreCallingIdentity(token);
-                }
-            }
             return ret;
         }
 
@@ -2217,11 +2233,25 @@
                 if (DEBUG) {
                     Slog.d(TAG, "onSystemLocaleChangedNoLock: " + mLocaleChangeSequenceNumber.get());
                 }
-                postToHandler(() -> scheduleSaveBaseState());
+                injectPostToHandler(() -> handleLocaleChanged());
             }
         }
     }
 
+    void handleLocaleChanged() {
+        if (DEBUG) {
+            Slog.d(TAG, "handleLocaleChanged");
+        }
+        scheduleSaveBaseState();
+
+        final long token = injectClearCallingIdentity();
+        try {
+            forEachLoadedUserLocked(u -> u.forAllPackages(p -> p.resolveResourceStrings()));
+        } finally {
+            injectRestoreCallingIdentity(token);
+        }
+    }
+
     /**
      * Package event callbacks.
      */
@@ -2468,10 +2498,26 @@
 
     @Nullable
     XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) {
-// TODO Doesn't seem like ACROSS_USER is needed, but double check.
         return activityInfo.loadXmlMetaData(mContext.getPackageManager(), key);
     }
 
+    @Nullable
+    Resources injectGetResourcesForApplicationAsUser(String packageName, int userId) {
+        final long start = injectElapsedRealtime();
+        final long token = injectClearCallingIdentity();
+        try {
+            return mContext.getPackageManager().getResourcesForApplicationAsUser(
+                    packageName, userId);
+        } catch (NameNotFoundException e) {
+            Slog.e(TAG, "Resources for package " + packageName + " not found");
+            return null;
+        } finally {
+            injectRestoreCallingIdentity(token);
+
+            logDurationStat(Stats.GET_APPLICATION_RESOURCES, start);
+        }
+    }
+
     // === Backup & restore ===
 
     boolean shouldBackupApp(String packageName, int userId) {
@@ -2614,6 +2660,8 @@
                 dumpStatLS(pw, p, Stats.GET_ACTIVITIES_WITH_METADATA, "getActivities+metadata");
                 dumpStatLS(pw, p, Stats.GET_INSTALLED_APPLICATIONS, "getInstalledApplications");
                 dumpStatLS(pw, p, Stats.CHECK_PACKAGE_CHANGES, "checkPackageChanges");
+                dumpStatLS(pw, p, Stats.GET_APPLICATION_RESOURCES, "getApplicationResources");
+                dumpStatLS(pw, p, Stats.RESOURCE_NAME_LOOKUP, "resourceNameLookup");
             }
 
             for (int i = 0; i < mUsers.size(); i++) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index ed53b77..fa2461b 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -22,6 +22,8 @@
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.set;
 
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
@@ -92,6 +94,7 @@
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiPredicate;
@@ -317,7 +320,7 @@
         }
 
         @Override
-        void postToHandler(Runnable r) {
+        void injectPostToHandler(Runnable r) {
             final long token = mContext.injectClearCallingIdentity();
             r.run();
             mContext.injectRestoreCallingIdentity(token);
@@ -441,6 +444,8 @@
 
     protected boolean mInjectedIsLowRamDevice;
 
+    protected Locale mInjectedLocale = Locale.ENGLISH;
+
     protected int mInjectedCallingUid;
     protected String mInjectedClientPackage;
 
@@ -597,6 +602,9 @@
         // User 0 is always running.
         when(mMockUserManager.isUserRunning(eq(USER_0))).thenAnswer(new AnswerIsUserRunning(true));
 
+        setUpAppResources();
+
+        // Start the service.
         initService();
         setCaller(CALLING_PACKAGE_1);
 
@@ -624,6 +632,53 @@
         }
     }
 
+    protected void setUpAppResources() throws Exception {
+        setUpAppResources(/* offset = */ 0);
+    }
+
+    protected void setUpAppResources(int ressIdOffset) throws Exception {
+        // ressIdOffset is used to adjust resource IDs to emulate the case where an updated app
+        // has resource IDs changed.
+
+        doAnswer(pmInvocation -> {
+            assertEquals(Process.SYSTEM_UID, mInjectedCallingUid);
+
+            final String packageName = (String) pmInvocation.getArguments()[0];
+            final int userId = (Integer) pmInvocation.getArguments()[1];
+
+            final Resources res = mock(Resources.class);
+
+            doAnswer(resInvocation -> {
+                final int argResId = (Integer) resInvocation.getArguments()[0];
+
+                return "string-" + packageName + "-user:" + userId + "-res:" + argResId
+                        + "/" + mInjectedLocale;
+            }).when(res).getString(anyInt());
+
+            doAnswer(resInvocation -> {
+                final int resId = (Integer) resInvocation.getArguments()[0];
+
+                // Always use the "string" resource type.  The type doesn't matter during the test.
+                return packageName + ":string/r" + resId;
+            }).when(res).getResourceName(anyInt());
+
+            doAnswer(resInvocation -> {
+                final String argResName = (String) resInvocation.getArguments()[0];
+                final String argType = (String) resInvocation.getArguments()[1];
+                final String argPackageName = (String) resInvocation.getArguments()[2];
+
+                // See the above code.  getResourceName() will just use "r" + res ID as the entry
+                // name.
+                String entryName = argResName;
+                if (entryName.contains("/")) {
+                    entryName = ShortcutInfo.getResourceEntryName(entryName);
+                }
+                return Integer.parseInt(entryName.substring(1)) + ressIdOffset;
+            }).when(res).getIdentifier(anyString(), anyString(), anyString());
+            return res;
+        }).when(mMockPackageManager).getResourcesForApplicationAsUser(anyString(), anyInt());
+    }
+
     protected static UserInfo withProfileGroupId(UserInfo in, int groupId) {
         in.profileGroupId = groupId;
         return in;
@@ -667,8 +722,16 @@
         mLauncherApps = null;
         mLauncherAppsMap.clear();
 
-        // Load the setting file.
+        // Send boot sequence events.
         mService.onBootPhase(SystemService.PHASE_LOCK_SETTINGS_READY);
+
+        // Make sure a call to onSystemLocaleChangedNoLock() before PHASE_BOOT_COMPLETED will be
+        // ignored.
+        final long origSequenceNumber = mService.getLocaleChangeSequenceNumber();
+        mInternal.onSystemLocaleChangedNoLock();
+        assertEquals(origSequenceNumber, mService.getLocaleChangeSequenceNumber());
+
+        mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
     }
 
     protected void shutdownServices() {
@@ -870,6 +933,18 @@
         setCaller(previousPackage, previousUserId);
     }
 
+    protected void runWithSystemUid(Runnable r) {
+        final int origUid = mInjectedCallingUid;
+        mInjectedCallingUid = Process.SYSTEM_UID;
+        r.run();
+        mInjectedCallingUid = origUid;
+    }
+
+    protected void lookupAndFillInResourceNames(ShortcutInfo si) {
+        runWithSystemUid(() -> si.lookupAndFillInResourceNames(
+                mService.injectGetResourcesForApplicationAsUser(si.getPackage(), si.getUserId())));
+    }
+
     protected int getCallingUserId() {
         return UserHandle.getUserId(mInjectedCallingUid);
     }
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index c11be7a..fe2d1ec 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -95,6 +95,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * Tests for ShortcutService and ShortcutManager.
@@ -922,6 +923,7 @@
             ShortcutInfo s = getCallerShortcut("s2");
             assertTrue(s.hasIconResource());
             assertEquals(R.drawable.black_32x32, s.getIconResourceId());
+            assertEquals("string/r" + R.drawable.black_32x32, s.getIconResName());
             assertEquals("Title-s2", s.getTitle());
 
             s = getCallerShortcut("s4");
@@ -1091,21 +1093,6 @@
     }
 
     public void testGetShortcuts_resolveStrings() throws Exception {
-        doAnswer(pmInvocation -> {
-            assertEquals(Process.SYSTEM_UID, mInjectedCallingUid);
-
-            final String packageName = (String) pmInvocation.getArguments()[0];
-            final int userId = (Integer) pmInvocation.getArguments()[1];
-
-            final Resources res = mock(Resources.class);
-            doAnswer(resInvocation -> {
-                final int resId = (Integer) resInvocation.getArguments()[0];
-
-                return "string-" + packageName + "-user:" + userId + "-res:" + resId;
-            }).when(res).getString(anyInt());
-            return res;
-        }).when(mMockPackageManager).getResourcesForApplicationAsUser(anyString(), anyInt());
-
         runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
             ShortcutInfo si = new ShortcutInfo.Builder(mClientContext)
                     .setId("id")
@@ -1137,18 +1124,18 @@
             List<ShortcutInfo> ret = assertShortcutIds(
                     assertAllStringsResolved(mLauncherApps.getShortcuts(q, HANDLE_USER_0)),
                     "id");
-            assertEquals("string-com.android.test.1-user:0-res:10", ret.get(0).getTitle());
-            assertEquals("string-com.android.test.1-user:0-res:11", ret.get(0).getText());
-            assertEquals("string-com.android.test.1-user:0-res:12",
+            assertEquals("string-com.android.test.1-user:0-res:10/en", ret.get(0).getTitle());
+            assertEquals("string-com.android.test.1-user:0-res:11/en", ret.get(0).getText());
+            assertEquals("string-com.android.test.1-user:0-res:12/en",
                     ret.get(0).getDisabledMessage());
 
             // USER P0
             ret = assertShortcutIds(
                     assertAllStringsResolved(mLauncherApps.getShortcuts(q, HANDLE_USER_P0)),
                     "id");
-            assertEquals("string-com.android.test.1-user:20-res:10", ret.get(0).getTitle());
-            assertEquals("string-com.android.test.1-user:20-res:11", ret.get(0).getText());
-            assertEquals("string-com.android.test.1-user:20-res:12",
+            assertEquals("string-com.android.test.1-user:20-res:10/en", ret.get(0).getTitle());
+            assertEquals("string-com.android.test.1-user:20-res:11/en", ret.get(0).getText());
+            assertEquals("string-com.android.test.1-user:20-res:12/en",
                     ret.get(0).getDisabledMessage());
         });
     }
@@ -3602,6 +3589,84 @@
                 findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp());
     }
 
+    /**
+     * Test the case where an updated app has resource IDs changed.
+     */
+    public void testHandlePackageUpdate_resIdChanged() throws Exception {
+        final Icon icon1 = Icon.createWithResource(getTestContext(), /* res ID */ 1000);
+        final Icon icon2 = Icon.createWithResource(getTestContext(), /* res ID */ 1001);
+
+        // Set up shortcuts.
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            // Note resource strings are not officially supported (they're hidden), but
+            // should work.
+
+            final ShortcutInfo s1 = new ShortcutInfo.Builder(mClientContext)
+                    .setId("s1")
+                    .setActivity(makeComponent(ShortcutActivity.class))
+                    .setIntent(new Intent(Intent.ACTION_VIEW))
+                    .setIcon(icon1)
+                    .setTitleResId(10000)
+                    .setTextResId(10001)
+                    .setDisabledMessageResId(10002)
+                    .build();
+
+            final ShortcutInfo s2 = new ShortcutInfo.Builder(mClientContext)
+                    .setId("s2")
+                    .setActivity(makeComponent(ShortcutActivity.class))
+                    .setIntent(new Intent(Intent.ACTION_VIEW))
+                    .setIcon(icon2)
+                    .setTitleResId(20000)
+                    .build();
+
+            assertTrue(mManager.setDynamicShortcuts(list(s1, s2)));
+        });
+
+        // Verify.
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            final ShortcutInfo s1 = getCallerShortcut("s1");
+            final ShortcutInfo s2 = getCallerShortcut("s2");
+
+            assertEquals(1000, s1.getIconResourceId());
+            assertEquals(10000, s1.getTitleResId());
+            assertEquals(10001, s1.getTextResId());
+            assertEquals(10002, s1.getDisabledMessageResourceId());
+
+            assertEquals(1001, s2.getIconResourceId());
+            assertEquals(20000, s2.getTitleResId());
+            assertEquals(0, s2.getTextResId());
+            assertEquals(0, s2.getDisabledMessageResourceId());
+        });
+
+        mService.saveDirtyInfo();
+        initService();
+
+        // Set up the mock resources again, with an "adjustment".
+        // When the package is updated, the service will fetch the updated res-IDs with res-names,
+        // and the new IDs will have this offset.
+        setUpAppResources(10);
+
+        // Update the package.
+        updatePackageVersion(CALLING_PACKAGE_1, 1);
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            final ShortcutInfo s1 = getCallerShortcut("s1");
+            final ShortcutInfo s2 = getCallerShortcut("s2");
+
+            assertEquals(1010, s1.getIconResourceId());
+            assertEquals(10010, s1.getTitleResId());
+            assertEquals(10011, s1.getTextResId());
+            assertEquals(10012, s1.getDisabledMessageResourceId());
+
+            assertEquals(1011, s2.getIconResourceId());
+            assertEquals(20010, s2.getTitleResId());
+            assertEquals(0, s2.getTextResId());
+            assertEquals(0, s2.getDisabledMessageResourceId());
+        });
+    }
+
     protected void prepareForBackupTest() {
 
         prepareCrossProfileDataSet();
@@ -4785,27 +4850,35 @@
             assertEquals(R.drawable.icon1, si.getIconResourceId());
             assertEquals(new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
                     si.getActivity());
+
             assertEquals(R.string.shortcut_title1, si.getTitleResId());
+            assertEquals("r" + R.string.shortcut_title1, si.getTitleResName());
             assertEquals(R.string.shortcut_text1, si.getTextResId());
+            assertEquals("r" + R.string.shortcut_text1, si.getTextResName());
             assertEquals(R.string.shortcut_disabled_message1, si.getDisabledMessageResourceId());
+            assertEquals("r" + R.string.shortcut_disabled_message1, si.getDisabledMessageResName());
+
             assertEquals(set("android.shortcut.conversation", "android.shortcut.media"),
                     si.getCategories());
             assertEquals("action1", si.getIntent().getAction());
             assertEquals(Uri.parse("http://a.b.c/1"), si.getIntent().getData());
-            assertEquals(0, si.getRank());
 
             // check another
             si = getCallerShortcut("ms2");
 
             assertEquals("ms2", si.getId());
             assertEquals(R.drawable.icon2, si.getIconResourceId());
+
             assertEquals(R.string.shortcut_title2, si.getTitleResId());
+            assertEquals("r" + R.string.shortcut_title2, si.getTitleResName());
             assertEquals(R.string.shortcut_text2, si.getTextResId());
+            assertEquals("r" + R.string.shortcut_text2, si.getTextResName());
             assertEquals(R.string.shortcut_disabled_message2, si.getDisabledMessageResourceId());
+            assertEquals("r" + R.string.shortcut_disabled_message2, si.getDisabledMessageResName());
+
             assertEquals(set("android.shortcut.conversation"), si.getCategories());
             assertEquals("action2", si.getIntent().getAction());
             assertEquals(null, si.getIntent().getData());
-            assertEquals(1, si.getRank());
 
             // check another
             si = getCallerShortcut("ms3");
@@ -4813,12 +4886,113 @@
             assertEquals("ms3", si.getId());
             assertEquals(0, si.getIconResourceId());
             assertEquals(R.string.shortcut_title1, si.getTitleResId());
+            assertEquals("r" + R.string.shortcut_title1, si.getTitleResName());
+
             assertEquals(0, si.getTextResId());
+            assertEquals(null, si.getTextResName());
             assertEquals(0, si.getDisabledMessageResourceId());
+            assertEquals(null, si.getDisabledMessageResName());
+
             assertEquals(null, si.getCategories());
             assertEquals("android.intent.action.VIEW", si.getIntent().getAction());
             assertEquals(null, si.getIntent().getData());
-            assertEquals(2, si.getRank());
+        });
+    }
+
+    public void testManifestShortcuts_localeChange() {
+        mService.handleUnlockUser(USER_0);
+
+        // Package 1 updated, which has one valid manifest shortcut and one invalid.
+        addManifestShortcutResource(
+                new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+                R.xml.shortcut_2);
+        updatePackageVersion(CALLING_PACKAGE_1, 1);
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            mManager.setDynamicShortcuts(list(makeShortcutWithTitle("s1", "title")));
+
+            assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled(
+                    mManager.getManifestShortcuts()))),
+                    "ms1", "ms2");
+
+            // check first shortcut.
+            ShortcutInfo si = getCallerShortcut("ms1");
+
+            assertEquals("ms1", si.getId());
+            assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_title1 + "/en",
+                    si.getTitle());
+            assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_text1 + "/en",
+                    si.getText());
+            assertEquals("string-com.android.test.1-user:0-res:"
+                            + R.string.shortcut_disabled_message1 + "/en",
+                    si.getDisabledMessage());
+            assertEquals(START_TIME, si.getLastChangedTimestamp());
+
+            // check another
+            si = getCallerShortcut("ms2");
+
+            assertEquals("ms2", si.getId());
+            assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_title2 + "/en",
+                    si.getTitle());
+            assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_text2 + "/en",
+                    si.getText());
+            assertEquals("string-com.android.test.1-user:0-res:"
+                            + R.string.shortcut_disabled_message2 + "/en",
+                    si.getDisabledMessage());
+            assertEquals(START_TIME, si.getLastChangedTimestamp());
+
+            // Check the dynamic one.
+            si = getCallerShortcut("s1");
+
+            assertEquals("s1", si.getId());
+            assertEquals("title", si.getTitle());
+            assertEquals(null, si.getText());
+            assertEquals(null, si.getDisabledMessage());
+            assertEquals(START_TIME, si.getLastChangedTimestamp());
+        });
+
+        mInjectedCurrentTimeMillis++;
+
+        mInjectedLocale = Locale.JAPANESE;
+        mInternal.onSystemLocaleChangedNoLock();
+
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            // check first shortcut.
+            ShortcutInfo si = getCallerShortcut("ms1");
+
+            assertEquals("ms1", si.getId());
+            assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_title1 + "/ja",
+                    si.getTitle());
+            assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_text1 + "/ja",
+                    si.getText());
+            assertEquals("string-com.android.test.1-user:0-res:"
+                            + R.string.shortcut_disabled_message1 + "/ja",
+                    si.getDisabledMessage());
+            assertEquals(START_TIME + 1, si.getLastChangedTimestamp());
+
+            // check another
+            si = getCallerShortcut("ms2");
+
+            assertEquals("ms2", si.getId());
+            assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_title2 + "/ja",
+                    si.getTitle());
+            assertEquals("string-com.android.test.1-user:0-res:" + R.string.shortcut_text2 + "/ja",
+                    si.getText());
+            assertEquals("string-com.android.test.1-user:0-res:"
+                            + R.string.shortcut_disabled_message2 + "/ja",
+                    si.getDisabledMessage());
+            assertEquals(START_TIME + 1, si.getLastChangedTimestamp());
+
+            // Check the dynamic one.  (locale change shouldn't affect.)
+            si = getCallerShortcut("s1");
+
+            assertEquals("s1", si.getId());
+            assertEquals("title", si.getTitle());
+            assertEquals(null, si.getText());
+            assertEquals(null, si.getDisabledMessage());
+            assertEquals(START_TIME, si.getLastChangedTimestamp()); // Not changed.
         });
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
index 1702ca4..399fddf 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -32,6 +32,7 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ShortcutInfo;
+import android.content.res.Resources;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.Icon;
 import android.os.PersistableBundle;
@@ -40,7 +41,6 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.frameworks.servicestests.R;
-import com.android.server.SystemService;
 
 /**
  * Tests for ShortcutService and ShortcutManager.
@@ -138,6 +138,13 @@
         assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
         assertEquals("abc", si.getBitmapPath());
         assertEquals(456, si.getIconResourceId());
+
+        assertEquals(0, si.getTitleResId());
+        assertEquals(null, si.getTitleResName());
+        assertEquals(0, si.getTextResId());
+        assertEquals(null, si.getTextResName());
+        assertEquals(0, si.getDisabledMessageResourceId());
+        assertEquals(null, si.getDisabledMessageResName());
     }
 
     public void testShortcutInfoParcel_resId() {
@@ -163,6 +170,8 @@
         si.setBitmapPath("abc");
         si.setIconResourceId(456);
 
+        lookupAndFillInResourceNames(si);
+
         si = parceled(si);
 
         assertEquals(getTestContext().getPackageName(), si.getPackage());
@@ -170,8 +179,11 @@
         assertEquals(new ComponentName("a", "b"), si.getActivity());
         assertEquals(123, si.getIcon().getResId());
         assertEquals(10, si.getTitleResId());
+        assertEquals("r10", si.getTitleResName());
         assertEquals(11, si.getTextResId());
+        assertEquals("r11", si.getTextResName());
         assertEquals(12, si.getDisabledMessageResourceId());
+        assertEquals("r12", si.getDisabledMessageResName());
         assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
         assertEquals("action", si.getIntent().getAction());
         assertEquals("val", si.getIntent().getStringExtra("key"));
@@ -181,6 +193,7 @@
         assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
         assertEquals("abc", si.getBitmapPath());
         assertEquals(456, si.getIconResourceId());
+        assertEquals("string/r456", si.getIconResName());
     }
 
     public void testShortcutInfoClone() {
@@ -204,6 +217,8 @@
         sorig.setBitmapPath("abc");
         sorig.setIconResourceId(456);
 
+        lookupAndFillInResourceNames(sorig);
+
         ShortcutInfo si = sorig.clone(/* clone flags*/ 0);
 
         assertEquals(USER_11, si.getUserId());
@@ -224,6 +239,7 @@
         assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
         assertEquals("abc", si.getBitmapPath());
         assertEquals(456, si.getIconResourceId());
+        assertEquals("string/r456", si.getIconResName());
 
         si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR);
 
@@ -244,6 +260,7 @@
         assertEquals(null, si.getBitmapPath());
 
         assertEquals(456, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
 
         si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
 
@@ -263,6 +280,7 @@
         assertEquals(null, si.getBitmapPath());
 
         assertEquals(456, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
 
         si = sorig.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
 
@@ -282,6 +300,7 @@
         assertEquals(null, si.getBitmapPath());
 
         assertEquals(456, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
     }
 
     public void testShortcutInfoClone_resId() {
@@ -305,6 +324,8 @@
         sorig.setBitmapPath("abc");
         sorig.setIconResourceId(456);
 
+        lookupAndFillInResourceNames(sorig);
+
         ShortcutInfo si = sorig.clone(/* clone flags*/ 0);
 
         assertEquals(USER_11, si.getUserId());
@@ -314,8 +335,11 @@
         assertEquals(new ComponentName("a", "b"), si.getActivity());
         assertEquals(123, si.getIcon().getResId());
         assertEquals(10, si.getTitleResId());
+        assertEquals("r10", si.getTitleResName());
         assertEquals(11, si.getTextResId());
+        assertEquals("r11", si.getTextResName());
         assertEquals(12, si.getDisabledMessageResourceId());
+        assertEquals("r12", si.getDisabledMessageResName());
         assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
         assertEquals("action", si.getIntent().getAction());
         assertEquals("val", si.getIntent().getStringExtra("key"));
@@ -325,6 +349,7 @@
         assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
         assertEquals("abc", si.getBitmapPath());
         assertEquals(456, si.getIconResourceId());
+        assertEquals("string/r456", si.getIconResName());
 
         si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR);
 
@@ -333,8 +358,11 @@
         assertEquals(new ComponentName("a", "b"), si.getActivity());
         assertEquals(null, si.getIcon());
         assertEquals(10, si.getTitleResId());
+        assertEquals(null, si.getTitleResName());
         assertEquals(11, si.getTextResId());
+        assertEquals(null, si.getTextResName());
         assertEquals(12, si.getDisabledMessageResourceId());
+        assertEquals(null, si.getDisabledMessageResName());
         assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
         assertEquals("action", si.getIntent().getAction());
         assertEquals("val", si.getIntent().getStringExtra("key"));
@@ -345,6 +373,7 @@
         assertEquals(null, si.getBitmapPath());
 
         assertEquals(456, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
 
         si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
 
@@ -353,8 +382,11 @@
         assertEquals(new ComponentName("a", "b"), si.getActivity());
         assertEquals(null, si.getIcon());
         assertEquals(10, si.getTitleResId());
+        assertEquals(null, si.getTitleResName());
         assertEquals(11, si.getTextResId());
+        assertEquals(null, si.getTextResName());
         assertEquals(12, si.getDisabledMessageResourceId());
+        assertEquals(null, si.getDisabledMessageResName());
         assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
         assertEquals(null, si.getIntent());
         assertEquals(123, si.getRank());
@@ -364,6 +396,7 @@
         assertEquals(null, si.getBitmapPath());
 
         assertEquals(456, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
 
         si = sorig.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
 
@@ -372,8 +405,11 @@
         assertEquals(null, si.getActivity());
         assertEquals(null, si.getIcon());
         assertEquals(0, si.getTitleResId());
+        assertEquals(null, si.getTitleResName());
         assertEquals(0, si.getTextResId());
+        assertEquals(null, si.getTextResName());
         assertEquals(0, si.getDisabledMessageResourceId());
+        assertEquals(null, si.getDisabledMessageResName());
         assertEquals(null, si.getCategories());
         assertEquals(null, si.getIntent());
         assertEquals(0, si.getRank());
@@ -383,6 +419,7 @@
         assertEquals(null, si.getBitmapPath());
 
         assertEquals(456, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
     }
 
     public void testShortcutInfoClone_minimum() {
@@ -445,6 +482,8 @@
         sorig.setBitmapPath("abc");
         sorig.setIconResourceId(456);
 
+        lookupAndFillInResourceNames(sorig);
+
         ShortcutInfo si;
 
         si = sorig.clone(/* flags=*/ 0);
@@ -458,6 +497,9 @@
                 .setIcon(Icon.createWithResource(mClientContext, 456)).build());
         assertEquals("text", si.getText());
         assertEquals(456, si.getIcon().getResId());
+        assertEquals(0, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
+        assertEquals(null, si.getBitmapPath());
 
         si = sorig.clone(/* flags=*/ 0);
         si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
@@ -586,6 +628,9 @@
                 .setIcon(Icon.createWithResource(mClientContext, 456)).build());
         assertEquals(11, si.getTextResId());
         assertEquals(456, si.getIcon().getResId());
+        assertEquals(0, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
+        assertEquals(null, si.getBitmapPath());
 
         si = sorig.clone(/* flags=*/ 0);
         si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
@@ -593,6 +638,7 @@
         assertEquals(11, si.getTextResId());
         assertEquals("xyz", si.getTitle());
         assertEquals(0, si.getTitleResId());
+        assertEquals(null, si.getTitleResName());
 
         si = sorig.clone(/* flags=*/ 0);
         si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
@@ -600,6 +646,7 @@
         assertEquals(11, si.getTextResId());
         assertEquals(null, si.getTitle());
         assertEquals(123, si.getTitleResId());
+        assertEquals(null, si.getTitleResName());
 
         si = sorig.clone(/* flags=*/ 0);
         si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
@@ -607,6 +654,7 @@
         assertEquals(123, si.getRank());
         assertEquals("xxx", si.getText());
         assertEquals(0, si.getTextResId());
+        assertEquals(null, si.getTextResName());
 
         si = sorig.clone(/* flags=*/ 0);
         si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
@@ -614,6 +662,7 @@
         assertEquals(123, si.getRank());
         assertEquals(null, si.getText());
         assertEquals(1111, si.getTextResId());
+        assertEquals(null, si.getTextResName());
 
         si = sorig.clone(/* flags=*/ 0);
         si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
@@ -621,6 +670,7 @@
         assertEquals(123, si.getRank());
         assertEquals("xxx", si.getDisabledMessage());
         assertEquals(0, si.getDisabledMessageResourceId());
+        assertEquals(null, si.getDisabledMessageResName());
 
         si = sorig.clone(/* flags=*/ 0);
         si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
@@ -628,6 +678,7 @@
         assertEquals(123, si.getRank());
         assertEquals(null, si.getDisabledMessage());
         assertEquals(11111, si.getDisabledMessageResourceId());
+        assertEquals(null, si.getDisabledMessageResName());
 
         si = sorig.clone(/* flags=*/ 0);
         si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id")
@@ -731,7 +782,8 @@
         assertEquals(123, si.getRank());
         assertEquals(1, si.getExtras().getInt("k"));
 
-        assertEquals(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_HAS_ICON_FILE, si.getFlags());
+        assertEquals(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_HAS_ICON_FILE
+                | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags());
         assertNotNull(si.getBitmapPath()); // Something should be set.
         assertEquals(0, si.getIconResourceId());
         assertTrue(si.getLastChangedTimestamp() < now);
@@ -740,15 +792,14 @@
     public void testShortcutInfoSaveAndLoad_resId() throws InterruptedException {
         setCaller(CALLING_PACKAGE_1, USER_10);
 
-        final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
-                getTestContext().getResources(), R.drawable.black_32x32));
+        final Icon res32x32 = Icon.createWithResource(mClientContext, R.drawable.black_32x32);
 
         PersistableBundle pb = new PersistableBundle();
         pb.putInt("k", 1);
         ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext)
                 .setId("id")
                 .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
-                .setIcon(bmp32x32)
+                .setIcon(res32x32)
                 .setTitleResId(10)
                 .setTextResId(11)
                 .setDisabledMessageResId(12)
@@ -778,17 +829,21 @@
         assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName());
         assertEquals(null, si.getIcon());
         assertEquals(10, si.getTitleResId());
+        assertEquals("r10", si.getTitleResName());
         assertEquals(11, si.getTextResId());
+        assertEquals("r11", si.getTextResName());
         assertEquals(12, si.getDisabledMessageResourceId());
+        assertEquals("r12", si.getDisabledMessageResName());
         assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
         assertEquals("action", si.getIntent().getAction());
         assertEquals("val", si.getIntent().getStringExtra("key"));
         assertEquals(123, si.getRank());
         assertEquals(1, si.getExtras().getInt("k"));
 
-        assertEquals(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_HAS_ICON_FILE, si.getFlags());
-        assertNotNull(si.getBitmapPath()); // Something should be set.
-        assertEquals(0, si.getIconResourceId());
+        assertEquals(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_HAS_ICON_RES
+                | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags());
+        assertNull(si.getBitmapPath());
+        assertEquals(R.drawable.black_32x32, si.getIconResourceId());
         assertTrue(si.getLastChangedTimestamp() < now);
     }
 
@@ -840,7 +895,7 @@
         assertEquals(123, si.getRank());
         assertEquals(1, si.getExtras().getInt("k"));
 
-        assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+        assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags());
         assertNull(si.getBitmapPath()); // No icon.
         assertEquals(0, si.getIconResourceId());
     }
@@ -848,15 +903,14 @@
     public void testShortcutInfoSaveAndLoad_forBackup_resId() {
         setCaller(CALLING_PACKAGE_1, USER_0);
 
-        final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
-                getTestContext().getResources(), R.drawable.black_32x32));
+        final Icon res32x32 = Icon.createWithResource(mClientContext, R.drawable.black_32x32);
 
         PersistableBundle pb = new PersistableBundle();
         pb.putInt("k", 1);
         ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext)
                 .setId("id")
                 .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
-                .setIcon(bmp32x32)
+                .setIcon(res32x32)
                 .setTitleResId(10)
                 .setTextResId(11)
                 .setDisabledMessageResId(12)
@@ -885,20 +939,23 @@
         assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName());
         assertEquals(null, si.getIcon());
         assertEquals(10, si.getTitleResId());
+        assertEquals("r10", si.getTitleResName());
         assertEquals(11, si.getTextResId());
+        assertEquals("r11", si.getTextResName());
         assertEquals(12, si.getDisabledMessageResourceId());
+        assertEquals("r12", si.getDisabledMessageResName());
         assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
         assertEquals("action", si.getIntent().getAction());
         assertEquals("val", si.getIntent().getStringExtra("key"));
         assertEquals(123, si.getRank());
         assertEquals(1, si.getExtras().getInt("k"));
 
-        assertEquals(ShortcutInfo.FLAG_PINNED, si.getFlags());
+        assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags());
         assertNull(si.getBitmapPath()); // No icon.
         assertEquals(0, si.getIconResourceId());
+        assertEquals(null, si.getIconResName());
     }
 
-
     public void testThrottling() {
         final ShortcutInfo si1 = makeShortcut("shortcut1");
 
@@ -1086,11 +1143,6 @@
 
         final long origSequenceNumber = mService.getLocaleChangeSequenceNumber();
 
-        // onSystemLocaleChangedNoLock before boot completed will be ignored.
-        mInternal.onSystemLocaleChangedNoLock();
-        assertEquals(origSequenceNumber, mService.getLocaleChangeSequenceNumber());
-
-        mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
         mInternal.onSystemLocaleChangedNoLock();
         assertEquals(origSequenceNumber + 1, mService.getLocaleChangeSequenceNumber());
 
@@ -1467,4 +1519,103 @@
 
         });
     }
+
+    // Test for a ShortcutInfo method.
+    public void testGetResourcePackageName() {
+        assertEquals(null, ShortcutInfo.getResourcePackageName(""));
+        assertEquals(null, ShortcutInfo.getResourcePackageName("abc"));
+        assertEquals("p", ShortcutInfo.getResourcePackageName("p:"));
+        assertEquals("p", ShortcutInfo.getResourcePackageName("p:xx"));
+        assertEquals("pac", ShortcutInfo.getResourcePackageName("pac:"));
+    }
+
+    // Test for a ShortcutInfo method.
+    public void testGetResourceTypeName() {
+        assertEquals(null, ShortcutInfo.getResourceTypeName(""));
+        assertEquals(null, ShortcutInfo.getResourceTypeName(":"));
+        assertEquals(null, ShortcutInfo.getResourceTypeName("/"));
+        assertEquals(null, ShortcutInfo.getResourceTypeName("/:"));
+        assertEquals("a", ShortcutInfo.getResourceTypeName(":a/"));
+        assertEquals("type", ShortcutInfo.getResourceTypeName("xxx:type/yyy"));
+    }
+
+    // Test for a ShortcutInfo method.
+    public void testGetResourceTypeAndEntryName() {
+        assertEquals(null, ShortcutInfo.getResourceTypeAndEntryName(""));
+        assertEquals(null, ShortcutInfo.getResourceTypeAndEntryName("abc"));
+        assertEquals("", ShortcutInfo.getResourceTypeAndEntryName("p:"));
+        assertEquals("x", ShortcutInfo.getResourceTypeAndEntryName(":x"));
+        assertEquals("x", ShortcutInfo.getResourceTypeAndEntryName("p:x"));
+        assertEquals("xyz", ShortcutInfo.getResourceTypeAndEntryName("pac:xyz"));
+    }
+
+    // Test for a ShortcutInfo method.
+    public void testGetResourceEntryName() {
+        assertEquals(null, ShortcutInfo.getResourceEntryName(""));
+        assertEquals(null, ShortcutInfo.getResourceEntryName("ab:"));
+        assertEquals("", ShortcutInfo.getResourceEntryName("/"));
+        assertEquals("abc", ShortcutInfo.getResourceEntryName("/abc"));
+        assertEquals("abc", ShortcutInfo.getResourceEntryName("xyz/abc"));
+    }
+
+    // Test for a ShortcutInfo method.
+    public void testLookUpResourceName_systemResources() {
+        // For android system resources, lookUpResourceName will simply return the value as a
+        // string, regardless of "withType".
+        final Resources res = getTestContext().getResources();
+
+        assertEquals("" + android.R.string.cancel, ShortcutInfo.lookUpResourceName(res,
+                android.R.string.cancel, true, getTestContext().getPackageName()));
+        assertEquals("" + android.R.drawable.alert_dark_frame, ShortcutInfo.lookUpResourceName(res,
+                android.R.drawable.alert_dark_frame, true, getTestContext().getPackageName()));
+        assertEquals("" + android.R.string.cancel, ShortcutInfo.lookUpResourceName(res,
+                android.R.string.cancel, false, getTestContext().getPackageName()));
+    }
+
+    public void testLookUpResourceName_appResources() {
+        final Resources res = getTestContext().getResources();
+
+        assertEquals("shortcut_text1", ShortcutInfo.lookUpResourceName(res,
+                R.string.shortcut_text1, false, getTestContext().getPackageName()));
+        assertEquals("string/shortcut_text1", ShortcutInfo.lookUpResourceName(res,
+                R.string.shortcut_text1, true, getTestContext().getPackageName()));
+
+        assertEquals("black_16x64", ShortcutInfo.lookUpResourceName(res,
+                R.drawable.black_16x64, false, getTestContext().getPackageName()));
+        assertEquals("drawable/black_16x64", ShortcutInfo.lookUpResourceName(res,
+                R.drawable.black_16x64, true, getTestContext().getPackageName()));
+    }
+
+    // Test for a ShortcutInfo method.
+    public void testLookUpResourceId_systemResources() {
+        final Resources res = getTestContext().getResources();
+
+        assertEquals(android.R.string.cancel, ShortcutInfo.lookUpResourceId(res,
+                "" + android.R.string.cancel, null,
+                getTestContext().getPackageName()));
+        assertEquals(android.R.drawable.alert_dark_frame, ShortcutInfo.lookUpResourceId(res,
+                "" + android.R.drawable.alert_dark_frame, null,
+                getTestContext().getPackageName()));
+    }
+
+    // Test for a ShortcutInfo method.
+    public void testLookUpResourceId_appResources() {
+        final Resources res = getTestContext().getResources();
+
+        assertEquals(R.string.shortcut_text1,
+                ShortcutInfo.lookUpResourceId(res, "shortcut_text1", "string",
+                        getTestContext().getPackageName()));
+
+        assertEquals(R.string.shortcut_text1,
+                ShortcutInfo.lookUpResourceId(res, "string/shortcut_text1", null,
+                        getTestContext().getPackageName()));
+
+        assertEquals(R.drawable.black_16x64,
+                ShortcutInfo.lookUpResourceId(res, "black_16x64", "drawable",
+                        getTestContext().getPackageName()));
+
+        assertEquals(R.drawable.black_16x64,
+                ShortcutInfo.lookUpResourceId(res, "drawable/black_16x64", null,
+                        getTestContext().getPackageName()));
+    }
 }