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()));
+ }
}