OSDN Git Service

ShortcutManager: add remaining APIs.
authorMakoto Onuki <omakoto@google.com>
Tue, 8 Mar 2016 18:49:47 +0000 (10:49 -0800)
committerMakoto Onuki <omakoto@google.com>
Wed, 9 Mar 2016 02:37:32 +0000 (18:37 -0800)
- Icons are now persisted. (under /data/system_ce, as PNGs)
- the "load icon" APIs in LauncherApps are supported.
- Implement updateShortcuts()

- Addressed all the comments on the previous CL
- @hide the newly added constructor for PersistableBundle

- Enhance incoming shortcut validation
- A lot of internal clean-up.

Bug 27548047

Change-Id: I8e3c1ccd3e0a997a6d271c84d81170f0c022b60e

26 files changed:
api/current.txt
api/system-current.txt
api/test-current.txt
core/java/android/app/SystemServiceRegistry.java
core/java/android/content/pm/ILauncherApps.aidl
core/java/android/content/pm/IShortcutService.aidl
core/java/android/content/pm/LauncherApps.java
core/java/android/content/pm/ShortcutInfo.java
core/java/android/content/pm/ShortcutManager.java
core/java/android/content/pm/ShortcutServiceInternal.java
core/java/android/os/PersistableBundle.java
graphics/java/android/graphics/drawable/Icon.java
services/core/java/com/android/server/pm/LauncherAppsService.java
services/core/java/com/android/server/pm/ShortcutService.java
services/java/com/android/server/SystemServer.java
services/tests/servicestests/res/drawable-nodpi/black_1024x4096.png [new file with mode: 0644]
services/tests/servicestests/res/drawable-nodpi/black_16x64.png [new file with mode: 0644]
services/tests/servicestests/res/drawable-nodpi/black_32x32.png [new file with mode: 0644]
services/tests/servicestests/res/drawable-nodpi/black_4096x1024.png [new file with mode: 0644]
services/tests/servicestests/res/drawable-nodpi/black_4096x4096.png [new file with mode: 0644]
services/tests/servicestests/res/drawable-nodpi/black_512x512.png [new file with mode: 0644]
services/tests/servicestests/res/drawable-nodpi/black_64x16.png [new file with mode: 0644]
services/tests/servicestests/res/drawable-nodpi/black_64x64.png [new file with mode: 0644]
services/tests/servicestests/res/drawable-nodpi/icon1.png [new file with mode: 0644]
services/tests/servicestests/res/drawable-nodpi/icon2.png [new file with mode: 0644]
services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java

index cc386db..bc09c6b 100644 (file)
@@ -9994,6 +9994,7 @@ package android.content.pm {
     method public int getWeight();
     method public boolean hasIconFile();
     method public boolean hasIconResource();
+    method public boolean hasKeyFieldsOnly();
     method public boolean isDynamic();
     method public boolean isPinned();
     method public void writeToParcel(android.os.Parcel, int);
@@ -10004,6 +10005,7 @@ package android.content.pm {
     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_KEY_FIELDS_ONLY = 16; // 0x10
     field public static final int FLAG_PINNED = 2; // 0x2
   }
 
@@ -10024,6 +10026,7 @@ package android.content.pm {
     method public void deleteAllDynamicShortcuts();
     method public void deleteDynamicShortcut(java.lang.String);
     method public java.util.List<android.content.pm.ShortcutInfo> getDynamicShortcuts();
+    method public int getIconMaxDimensions();
     method public int getMaxDynamicShortcutCount();
     method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
     method public long getRateLimitResetTime();
@@ -29080,7 +29083,6 @@ package android.os {
     ctor public PersistableBundle();
     ctor public PersistableBundle(int);
     ctor public PersistableBundle(android.os.PersistableBundle);
-    ctor public PersistableBundle(android.os.Bundle);
     method public java.lang.Object clone();
     method public int describeContents();
     method public android.os.PersistableBundle getPersistableBundle(java.lang.String);
index 7823b3c..43b9be9 100644 (file)
@@ -10388,6 +10388,7 @@ package android.content.pm {
     method public int getWeight();
     method public boolean hasIconFile();
     method public boolean hasIconResource();
+    method public boolean hasKeyFieldsOnly();
     method public boolean isDynamic();
     method public boolean isPinned();
     method public void writeToParcel(android.os.Parcel, int);
@@ -10398,6 +10399,7 @@ package android.content.pm {
     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_KEY_FIELDS_ONLY = 16; // 0x10
     field public static final int FLAG_PINNED = 2; // 0x2
   }
 
@@ -10418,6 +10420,7 @@ package android.content.pm {
     method public void deleteAllDynamicShortcuts();
     method public void deleteDynamicShortcut(java.lang.String);
     method public java.util.List<android.content.pm.ShortcutInfo> getDynamicShortcuts();
+    method public int getIconMaxDimensions();
     method public int getMaxDynamicShortcutCount();
     method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
     method public long getRateLimitResetTime();
@@ -31360,7 +31363,6 @@ package android.os {
     ctor public PersistableBundle();
     ctor public PersistableBundle(int);
     ctor public PersistableBundle(android.os.PersistableBundle);
-    ctor public PersistableBundle(android.os.Bundle);
     method public java.lang.Object clone();
     method public int describeContents();
     method public android.os.PersistableBundle getPersistableBundle(java.lang.String);
index a478793..2c1e248 100644 (file)
@@ -10004,6 +10004,7 @@ package android.content.pm {
     method public int getWeight();
     method public boolean hasIconFile();
     method public boolean hasIconResource();
+    method public boolean hasKeyFieldsOnly();
     method public boolean isDynamic();
     method public boolean isPinned();
     method public void writeToParcel(android.os.Parcel, int);
@@ -10014,6 +10015,7 @@ package android.content.pm {
     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_KEY_FIELDS_ONLY = 16; // 0x10
     field public static final int FLAG_PINNED = 2; // 0x2
   }
 
@@ -10034,6 +10036,7 @@ package android.content.pm {
     method public void deleteAllDynamicShortcuts();
     method public void deleteDynamicShortcut(java.lang.String);
     method public java.util.List<android.content.pm.ShortcutInfo> getDynamicShortcuts();
+    method public int getIconMaxDimensions();
     method public int getMaxDynamicShortcutCount();
     method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
     method public long getRateLimitResetTime();
@@ -29091,7 +29094,6 @@ package android.os {
     ctor public PersistableBundle();
     ctor public PersistableBundle(int);
     ctor public PersistableBundle(android.os.PersistableBundle);
-    ctor public PersistableBundle(android.os.Bundle);
     method public java.lang.Object clone();
     method public int describeContents();
     method public android.os.PersistableBundle getPersistableBundle(java.lang.String);
index 3a5dd30..bd321ac 100644 (file)
@@ -753,12 +753,11 @@ final class SystemServiceRegistry {
 
         registerService(Context.SHORTCUT_SERVICE, ShortcutManager.class,
                 new CachedServiceFetcher<ShortcutManager>() {
-                    @Override
-                    public ShortcutManager createService(ContextImpl ctx) {
-                        IBinder b = ServiceManager.getService(Context.SHORTCUT_SERVICE);
-                        return new ShortcutManager(ctx,
-                                IShortcutService.Stub.asInterface(b));
-                    }});
+            @Override
+            public ShortcutManager createService(ContextImpl ctx) {
+                IBinder b = ServiceManager.getService(Context.SHORTCUT_SERVICE);
+                return new ShortcutManager(ctx, IShortcutService.Stub.asInterface(b));
+            }});
     }
 
     /**
index cf3e298..b1d3f20 100644 (file)
@@ -26,6 +26,8 @@ import android.content.pm.ShortcutInfo;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.os.ParcelFileDescriptor;
+
 import java.util.List;
 
 /**
@@ -52,4 +54,8 @@ interface ILauncherApps {
             in UserHandle user);
     boolean startShortcut(String callingPackage, String packageName, String id,
             in Rect sourceBounds, in Bundle startActivityOptions, in UserHandle user);
+
+    int getShortcutIconResId(String callingPackage, in ShortcutInfo shortcut, in UserHandle user);
+    ParcelFileDescriptor getShortcutIconFd(String callingPackage, in ShortcutInfo shortcut,
+            in UserHandle user);
 }
index 23e671d..8f9dcfc 100644 (file)
@@ -44,5 +44,7 @@ interface IShortcutService {
 
     long getRateLimitResetTime(String packageName, int userId);
 
+    int getIconMaxDimensions(String packageName, int userId);
+
     void resetThrottling(); // system only API for developer opsions
 }
\ No newline at end of file
index 8e4a0e2..a6a732e 100644 (file)
@@ -183,7 +183,8 @@ public class LauncherApps {
         public static final int FLAG_GET_PINNED = 1 << 1;
 
         /**
-         * Requests "key" fields only.
+         * Requests "key" fields only.  See {@link ShortcutInfo#hasKeyFieldsOnly()} for which
+         * fields are available.
          */
         public static final int FLAG_GET_KEY_FIELDS_ONLY = 1 << 2;
 
@@ -473,7 +474,11 @@ public class LauncherApps {
      */
     @RequiresPermission(permission.BIND_APPWIDGET)
     public int getShortcutIconResId(@NonNull ShortcutInfo shortcut, @NonNull UserHandle user) {
-        throw new RuntimeException("not implemented yet");
+        try {
+            return mService.getShortcutIconResId(mContext.getPackageName(), shortcut, user);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -488,7 +493,11 @@ public class LauncherApps {
     @RequiresPermission(permission.BIND_APPWIDGET)
     public ParcelFileDescriptor getShortcutIconFd(
             @NonNull ShortcutInfo shortcut, @NonNull UserHandle user) {
-        throw new RuntimeException("not implemented yet");
+        try {
+            return mService.getShortcutIconFd(mContext.getPackageName(), shortcut, user);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
index 6520563..83a70cd 100644 (file)
@@ -19,9 +19,11 @@ import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Icon;
+import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
@@ -60,6 +62,9 @@ public class ShortcutInfo implements Parcelable {
     /* @hide */
     public static final int FLAG_HAS_ICON_FILE = 1 << 3;
 
+    /* @hide */
+    public static final int FLAG_KEY_FIELDS_ONLY = 1 << 4;
+
     /** @hide */
     @IntDef(flag = true,
             value = {
@@ -67,6 +72,7 @@ public class ShortcutInfo implements Parcelable {
             FLAG_PINNED,
             FLAG_HAS_ICON_RES,
             FLAG_HAS_ICON_FILE,
+            FLAG_KEY_FIELDS_ONLY,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ShortcutFlags {}
@@ -114,10 +120,15 @@ public class ShortcutInfo implements Parcelable {
     @NonNull
     private String mTitle;
 
+    /**
+     * Intent *with extras removed*.
+     */
     @NonNull
     private Intent mIntent;
 
-    // Internal use only.
+    /**
+     * Extras for the intent.
+     */
     @NonNull
     private PersistableBundle mIntentPersistableExtras;
 
@@ -149,6 +160,11 @@ public class ShortcutInfo implements Parcelable {
         mIcon = b.mIcon;
         mTitle = b.mTitle;
         mIntent = b.mIntent;
+        final Bundle intentExtras = mIntent.getExtras();
+        if (intentExtras != null) {
+            mIntent.replaceExtras((Bundle) null);
+            mIntentPersistableExtras = new PersistableBundle(intentExtras);
+        }
         mWeight = b.mWeight;
         mExtras = b.mExtras;
         updateTimestamp();
@@ -170,11 +186,12 @@ public class ShortcutInfo implements Parcelable {
     private ShortcutInfo(ShortcutInfo source, @CloneFlags int cloneFlags) {
         mId = source.mId;
         mPackageName = source.mPackageName;
-        mActivityComponent = source.mActivityComponent;
         mFlags = source.mFlags;
         mLastChangedTimestamp = source.mLastChangedTimestamp;
 
         if ((cloneFlags & CLONE_REMOVE_NON_KEY_INFO) == 0) {
+            mActivityComponent = source.mActivityComponent;
+
             if ((cloneFlags & CLONE_REMOVE_ICON) == 0) {
                 mIcon = source.mIcon;
             }
@@ -188,6 +205,10 @@ public class ShortcutInfo implements Parcelable {
             mExtras = source.mExtras;
             mIconResourceId = source.mIconResourceId;
             mBitmapPath = source.mBitmapPath;
+
+        } else {
+            // Set this bit.
+            mFlags |= FLAG_KEY_FIELDS_ONLY;
         }
     }
 
@@ -239,6 +260,39 @@ public class ShortcutInfo implements Parcelable {
     }
 
     /**
+     * @hide
+     */
+    public static Icon validateIcon(Icon icon) {
+        switch (icon.getType()) {
+            case Icon.TYPE_RESOURCE:
+            case Icon.TYPE_BITMAP:
+                break; // OK
+            case Icon.TYPE_URI:
+                if (ContentResolver.SCHEME_CONTENT.equals(icon.getUri().getScheme())) {
+                    break;
+                }
+                // Note "file:" is not supported, because depending on the path, system server
+                // cannot access it. // TODO Revisit "file:" icon support
+
+                // fall through
+            default:
+                throw getInvalidIconException();
+        }
+        if (icon.hasTint()) {
+            // TODO support it
+            throw new IllegalArgumentException("Icons with tints are not supported");
+        }
+
+        return icon;
+    }
+
+    /** @hide */
+    public static IllegalArgumentException getInvalidIconException() {
+        return new IllegalArgumentException("Unsupported icon type:"
+                +" only bitmap, resource and content URI are supported");
+    }
+
+    /**
      * Builder class for {@link ShortcutInfo} objects.
      */
     public static class Builder {
@@ -273,7 +327,9 @@ public class ShortcutInfo implements Parcelable {
         }
 
         /**
-         * Optionally sets the target activity.
+         * Optionally sets the target activity.  If it's not set, and if the caller application
+         * has multiple launcher icons, this shortcut will be shown on all those icons.
+         * If it's set, this shortcut will be only shown on this activity.
          */
         @NonNull
         public Builder setActivityComponent(@NonNull ComponentName activityComponent) {
@@ -284,15 +340,22 @@ public class ShortcutInfo implements Parcelable {
         /**
          * Optionally sets an icon.
          *
-         * - Tint is not supported TODO Either check and throw, or support it.
-         * - URI icons will be converted into Bitmap icons at the registration time.
+         * <ul>
+         *     <li>Tints are not supported.
+         *     <li>Bitmaps, resources and "content:" URIs are supported.
+         *     <li>"content:" URI will be fetched when a shortcut is registered to
+         *         {@link ShortcutManager}.  Changing the content from the same URI later will
+         *         not be reflected to launcher icons.
+         * </ul>
          *
-         * TODO Only allow Bitmap, Resource and URI types.  byte[] type can easily go over
-         * binder size limit.
+         * <p>For performance reasons, icons will <b>NOT</b> be available on instances
+         * returned by {@link ShortcutManager} or {@link LauncherApps}.  Launcher applications
+         * need to use {@link LauncherApps#getShortcutIconFd(ShortcutInfo, UserHandle)}
+         * and {@link LauncherApps#getShortcutIconResId(ShortcutInfo, UserHandle)}.
          */
         @NonNull
         public Builder setIcon(Icon icon) {
-            mIcon = icon;
+            mIcon = validateIcon(icon);
             return this;
         }
 
@@ -316,7 +379,7 @@ public class ShortcutInfo implements Parcelable {
         }
 
         /**
-         * Optionally sets the weight of a shortcut, which will be used by Launcher for sorting.
+         * Optionally sets the weight of a shortcut, which will be used by the launcher for sorting.
          * The larger the weight, the more "important" a shortcut is.
          */
         @NonNull
@@ -326,8 +389,8 @@ public class ShortcutInfo implements Parcelable {
         }
 
         /**
-         * Optional values that application can set.
-         * TODO: reserve keys starting with "android."
+         * Optional values that applications can set.  Applications can store any meta-data of
+         * shortcuts in this, and retrieve later from {@link ShortcutInfo#getExtras()}.
          */
         @NonNull
         public Builder setExtras(@NonNull PersistableBundle extras) {
@@ -353,7 +416,7 @@ public class ShortcutInfo implements Parcelable {
     }
 
     /**
-     * Return the ID of the shortcut.
+     * Return the package name of the creator application.
      */
     @NonNull
     public String getPackageName() {
@@ -374,7 +437,7 @@ public class ShortcutInfo implements Parcelable {
      *
      * For performance reasons, this will <b>NOT</b> be available when an instance is returned
      * by {@link ShortcutManager} or {@link LauncherApps}.  A launcher application needs to use
-     * other APIs in LauncherApps to fetch the bitmap.  TODO Add a precondition for it.
+     * other APIs in LauncherApps to fetch the bitmap.
      *
      * @hide
      */
@@ -385,22 +448,46 @@ public class ShortcutInfo implements Parcelable {
 
     /**
      * Return the shortcut title.
+     *
+     * <p>All shortcuts must have a non-empty title, but this method will return null when
+     * {@link #hasKeyFieldsOnly()} is true.
      */
-    @NonNull
+    @Nullable
     public String getTitle() {
         return mTitle;
     }
 
     /**
      * Return the intent.
-     * TODO Set mIntentPersistableExtras and before returning.
+     *
+     * <p>All shortcuts must have an intent, but this method will return null when
+     * {@link #hasKeyFieldsOnly()} is true.
      */
-    @NonNull
+    @Nullable
     public Intent getIntent() {
+        if (mIntent == null) {
+            return null;
+        }
+        final Intent intent = new Intent(mIntent);
+        intent.replaceExtras(
+                mIntentPersistableExtras != null ? new Bundle(mIntentPersistableExtras) : null);
+        return intent;
+    }
+
+    /**
+     * Return "raw" intent, which is the original intent without the extras.
+     * @hide
+     */
+    @Nullable
+    public Intent getIntentNoExtras() {
         return mIntent;
     }
 
-    /** @hide */
+    /**
+     * The extras in the intent.  We convert extras into {@link PersistableBundle} so we can
+     * persist them.
+     * @hide
+     */
     @Nullable
     public PersistableBundle getIntentPersistableExtras() {
         return mIntentPersistableExtras;
@@ -483,6 +570,23 @@ public class ShortcutInfo implements Parcelable {
         return hasFlags(FLAG_HAS_ICON_FILE);
     }
 
+    /**
+     * Return whether a shortcut only contains "key" information only or not.  If true, only the
+     * following fields are available.
+     * <ul>
+     *     <li>{@link #getId()}
+     *     <li>{@link #getPackageName()}
+     *     <li>{@link #getLastChangedTimestamp()}
+     *     <li>{@link #isDynamic()}
+     *     <li>{@link #isPinned()}
+     *     <li>{@link #hasIconResource()}
+     *     <li>{@link #hasIconFile()}
+     * </ul>
+     */
+    public boolean hasKeyFieldsOnly() {
+        return hasFlags(FLAG_KEY_FIELDS_ONLY);
+    }
+
     /** @hide */
     public void updateTimestamp() {
         mLastChangedTimestamp = System.currentTimeMillis();
@@ -495,33 +599,13 @@ public class ShortcutInfo implements Parcelable {
     }
 
     /** @hide */
-    public void setIcon(Icon icon) {
-        mIcon = icon;
-    }
-
-    /** @hide */
-    public void setTitle(String title) {
-        mTitle = title;
-    }
-
-    /** @hide */
-    public void setIntent(Intent intent) {
-        mIntent = intent;
-    }
-
-    /** @hide */
-    public void setIntentPersistableExtras(PersistableBundle intentPersistableExtras) {
-        mIntentPersistableExtras = intentPersistableExtras;
-    }
-
-    /** @hide */
-    public void setWeight(int weight) {
-        mWeight = weight;
+    public void clearIcon() {
+        mIcon = null;
     }
 
     /** @hide */
-    public void setExtras(PersistableBundle extras) {
-        mExtras = extras;
+    public void setIconResourceId(int iconResourceId) {
+        mIconResourceId = iconResourceId;
     }
 
     /** @hide */
@@ -643,9 +727,10 @@ public class ShortcutInfo implements Parcelable {
         sb.append(", extras=");
         sb.append(mExtras);
 
+        sb.append(", flags=");
+        sb.append(mFlags);
+
         if (includeInternalData) {
-            sb.append(", flags=");
-            sb.append(mFlags);
 
             sb.append(", iconRes=");
             sb.append(mIconResourceId);
index 4c51d49..b247f65 100644 (file)
@@ -239,6 +239,17 @@ public class ShortcutManager {
         }
     }
 
+    /**
+     * Return the max width and height for icons, in pixels.
+     */
+    public int getIconMaxDimensions() {
+        try {
+            return mService.getIconMaxDimensions(mContext.getPackageName(), injectMyUserId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /** @hide injection point */
     @VisibleForTesting
     protected int injectMyUserId() {
index 3d6028a..918c763 100644 (file)
@@ -22,6 +22,8 @@ import android.annotation.UserIdInt;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.LauncherApps.ShortcutQuery;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
 
 import java.util.List;
 
@@ -55,4 +57,10 @@ public abstract class ShortcutServiceInternal {
             @NonNull String packageName, @NonNull String shortcutId, int userId);
 
     public abstract void addListener(@NonNull ShortcutChangeListener listener);
+
+    public abstract int getShortcutIconResId(@NonNull String callingPackage,
+            @NonNull ShortcutInfo shortcut, int userId);
+
+    public abstract ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage,
+            @NonNull ShortcutInfo shortcut, int userId);
 }
index f36bb29..5872f74 100644 (file)
@@ -86,6 +86,8 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa
      * @param b a Bundle to be copied.
      *
      * @throws IllegalArgumentException if any element of {@code b} cannot be persisted.
+     *
+     * @hide
      */
     public PersistableBundle(Bundle b) {
         this(b.getMap());
index 0de4c2c..51221b4 100644 (file)
@@ -627,6 +627,11 @@ public final class Icon implements Parcelable {
         return this;
     }
 
+    /** @hide */
+    public boolean hasTint() {
+        return (mTintList != null) || (mTintMode != DEFAULT_TINT_MODE);
+    }
+
     /**
      * Create an Icon pointing to an image file specified by path.
      *
index 5612870..e90fb32 100644 (file)
@@ -42,6 +42,7 @@ import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IInterface;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -329,6 +330,26 @@ public class LauncherAppsService extends SystemService {
         }
 
         @Override
+        public int getShortcutIconResId(String callingPackage, ShortcutInfo shortcut,
+                UserHandle user) {
+            enforceShortcutPermission(user);
+            verifyCallingPackage(callingPackage);
+
+            return mShortcutServiceInternal.getShortcutIconResId(callingPackage, shortcut,
+                    user.getIdentifier());
+        }
+
+        @Override
+        public ParcelFileDescriptor getShortcutIconFd(String callingPackage, ShortcutInfo shortcut,
+                UserHandle user) {
+            enforceShortcutPermission(user);
+            verifyCallingPackage(callingPackage);
+
+            return mShortcutServiceInternal.getShortcutIconFd(callingPackage, shortcut,
+                    user.getIdentifier());
+        }
+
+        @Override
         public boolean startShortcut(String callingPackage, String packageName, String shortcutId,
                 Rect sourceBounds, Bundle startActivityOptions, UserHandle user)
                 throws RemoteException {
index 423767a..8982632 100644 (file)
@@ -18,7 +18,9 @@ package com.android.server.pm;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.app.ActivityManager;
 import android.content.ComponentName;
+import android.content.ContentProvider;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.IShortcutService;
@@ -30,24 +32,33 @@ import android.content.pm.ParceledListSlice;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.RectF;
 import android.graphics.drawable.Icon;
+import android.net.Uri;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
+import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.os.SELinux;
 import android.os.ShellCommand;
 import android.os.UserHandle;
 import android.text.TextUtils;
+import android.text.format.Formatter;
 import android.text.format.Time;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.TypedValue;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -70,6 +81,7 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.PrintWriter;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
@@ -79,22 +91,28 @@ import java.util.function.Predicate;
 
 /**
  * TODO:
- * - Make save async
  *
- * - Add Bitmap support
+ * - Implement launchShortcut
  *
- * - Implement updateShortcuts
+ * - Detect when already registered instances are passed to APIs again, which might break
+ *   internal bitmap handling.
  *
  * - Listen to PACKAGE_*, remove orphan info, update timestamp for icon res
+ *   -> Need to scan all packages when a user starts too.
+ *   -> Clear data -> remove all dynamic?  but not the pinned?
  *
  * - Pinned per each launcher package (multiple launchers)
  *
- * - Dev option to reset all counts for QA (for now use "adb shell cmd shortcut reset-throttling")
- *
  * - Load config from settings
+ *
+ * - Make save async (should we?)
+ *
+ * - Scan and remove orphan bitmaps (just in case).
+ *
+ * - Backup & restore
  */
 public class ShortcutService extends IShortcutService.Stub {
-    private static final String TAG = "ShortcutService";
+    static final String TAG = "ShortcutService";
 
     private static final boolean DEBUG = true; // STOPSHIP if true
     private static final boolean DEBUG_LOAD = true; // STOPSHIP if true
@@ -102,6 +120,8 @@ public class ShortcutService extends IShortcutService.Stub {
     private static final int DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day
     private static final int DEFAULT_MAX_DAILY_UPDATES = 10;
     private static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 5;
+    private static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
+    private static final int DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP = 48;
 
     private static final int SAVE_DELAY_MS = 5000; // in milliseconds.
 
@@ -114,11 +134,29 @@ public class ShortcutService extends IShortcutService.Stub {
     @VisibleForTesting
     static final String FILENAME_USER_PACKAGES = "shortcuts.xml";
 
-    private static final String DIRECTORY_BITMAPS = "bitmaps";
+    static final String DIRECTORY_BITMAPS = "bitmaps";
 
     private static final String TAG_ROOT = "root";
+    private static final String TAG_PACKAGE = "package";
     private static final String TAG_LAST_RESET_TIME = "last_reset_time";
+    private static final String TAG_INTENT_EXTRAS = "intent-extras";
+    private static final String TAG_EXTRAS = "extras";
+    private static final String TAG_SHORTCUT = "shortcut";
+
     private static final String ATTR_VALUE = "value";
+    private static final String ATTR_NAME = "name";
+    private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
+    private static final String ATTR_CALL_COUNT = "call-count";
+    private static final String ATTR_LAST_RESET = "last-reset";
+    private static final String ATTR_ID = "id";
+    private static final String ATTR_ACTIVITY = "activity";
+    private static final String ATTR_TITLE = "title";
+    private static final String ATTR_INTENT = "intent";
+    private static final String ATTR_WEIGHT = "weight";
+    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_BITMAP_PATH = "bitmap-path";
 
     private final Context mContext;
 
@@ -136,8 +174,16 @@ public class ShortcutService extends IShortcutService.Stub {
      * All the information relevant to shortcuts from a single package (per-user).
      *
      * TODO Move the persisting code to this class.
+     *
+     * Only save/load/dump should look/touch inside this class.
      */
     private static class PackageShortcuts {
+        @UserIdInt
+        private final int mUserId;
+
+        @NonNull
+        private final String mPackageName;
+
         /**
          * All the shortcuts from the package, keyed on IDs.
          */
@@ -151,20 +197,38 @@ public class ShortcutService extends IShortcutService.Stub {
         /**
          * # of times the package has called rate-limited APIs.
          */
-        private int mApiCallCountInner;
+        private int mApiCallCount;
 
         /**
-         * When {@link #mApiCallCountInner} was reset last time.
+         * When {@link #mApiCallCount} was reset last time.
          */
         private long mLastResetTime;
 
-        /**
-         * @return the all shortcuts.  Note DO NOT add/remove or touch the flags of the result
-         * directly, which would cause {@link #mDynamicShortcutCount} to be out of sync.
-         */
+        private PackageShortcuts(int userId, String packageName) {
+            mUserId = userId;
+            mPackageName = packageName;
+        }
+
         @GuardedBy("mLock")
-        public ArrayMap<String, ShortcutInfo> getShortcuts() {
-            return mShortcuts;
+        @Nullable
+        public ShortcutInfo findShortcutById(String id) {
+            return mShortcuts.get(id);
+        }
+
+        private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
+                @NonNull String id) {
+            final ShortcutInfo shortcut = mShortcuts.remove(id);
+            if (shortcut != null) {
+                s.removeIcon(mUserId, shortcut);
+                shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
+            }
+            return shortcut;
+        }
+
+        void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
+            deleteShortcut(s, newShortcut.getId());
+            s.saveIconAndFixUpShortcut(mUserId, newShortcut);
+            mShortcuts.put(newShortcut.getId(), newShortcut);
         }
 
         /**
@@ -195,40 +259,44 @@ public class ShortcutService extends IShortcutService.Stub {
             // Okay, make it dynamic and add.
             newShortcut.addFlags(oldFlags);
 
-            mShortcuts.put(newShortcut.getId(), newShortcut);
+            addShortcut(s, newShortcut);
             mDynamicShortcutCount = newDynamicCount;
         }
 
-        @GuardedBy("mLock")
-        public void deleteAllDynamicShortcuts() {
+        /**
+         * Remove all shortcuts that aren't pinned nor dynamic.
+         */
+        private void removeOrphans(@NonNull ShortcutService s) {
             ArrayList<String> removeList = null; // Lazily initialize.
 
             for (int i = mShortcuts.size() - 1; i >= 0; i--) {
                 final ShortcutInfo si = mShortcuts.valueAt(i);
 
-                if (!si.isDynamic()) {
-                    continue;
-                }
-                if (si.isPinned()) {
-                    // Still pinned, so don't remove; just make it non-dynamic.
-                    si.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
-                } else {
-                    if (removeList == null) {
-                        removeList = new ArrayList<>();
-                    }
-                    removeList.add(si.getId());
+                if (si.isPinned() || si.isDynamic()) continue;
+
+                if (removeList == null) {
+                    removeList = new ArrayList<>();
                 }
+                removeList.add(si.getId());
             }
             if (removeList != null) {
                 for (int i = removeList.size() - 1 ; i >= 0; i--) {
-                    mShortcuts.remove(removeList.get(i));
+                    deleteShortcut(s, removeList.get(i));
                 }
             }
+        }
+
+        @GuardedBy("mLock")
+        public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
+            for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+                mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
+            }
+            removeOrphans(s);
             mDynamicShortcutCount = 0;
         }
 
         @GuardedBy("mLock")
-        public void deleteDynamicWithId(@NonNull String shortcutId) {
+        public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
             final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
 
             if (oldShortcut == null) {
@@ -240,18 +308,30 @@ public class ShortcutService extends IShortcutService.Stub {
             if (oldShortcut.isPinned()) {
                 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
             } else {
-                mShortcuts.remove(shortcutId);
+                deleteShortcut(s, shortcutId);
             }
         }
 
         @GuardedBy("mLock")
-        public void pinAll(List<String> shortcutIds) {
+        public void replacePinned(@NonNull ShortcutService s, String launcherPackage,
+                List<String> shortcutIds) {
+
+            // TODO Should be per launcherPackage.
+
+            // First, un-pin all shortcuts
+            for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+                mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
+            }
+
+            // Then pin ALL
             for (int i = shortcutIds.size() - 1; i >= 0; i--) {
                 final ShortcutInfo shortcut = mShortcuts.get(shortcutIds.get(i));
                 if (shortcut != null) {
                     shortcut.addFlags(ShortcutInfo.FLAG_PINNED);
                 }
             }
+
+            removeOrphans(s);
         }
 
         /**
@@ -261,16 +341,22 @@ public class ShortcutService extends IShortcutService.Stub {
         public int getApiCallCount(@NonNull ShortcutService s) {
             final long last = s.getLastResetTimeLocked();
 
+            final long now = s.injectCurrentTimeMillis();
+            if (mLastResetTime > now) {
+                // Clock rewound. // TODO Test it
+                mLastResetTime = now;
+            }
+
             // If not reset yet, then reset.
             if (mLastResetTime < last) {
-                mApiCallCountInner = 0;
+                mApiCallCount = 0;
                 mLastResetTime = last;
             }
-            return mApiCallCountInner;
+            return mApiCallCount;
         }
 
         /**
-         * If the caller app hasn't been throttled yet, increment {@link #mApiCallCountInner}
+         * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
          * and return true.  Otherwise just return false.
          */
         @GuardedBy("mLock")
@@ -278,13 +364,13 @@ public class ShortcutService extends IShortcutService.Stub {
             if (getApiCallCount(s) >= s.mMaxDailyUpdates) {
                 return false;
             }
-            mApiCallCountInner++;
+            mApiCallCount++;
             return true;
         }
 
         @GuardedBy("mLock")
         public void resetRateLimitingForCommandLine() {
-            mApiCallCountInner = 0;
+            mApiCallCount = 0;
             mLastResetTime = 0;
         }
 
@@ -313,21 +399,27 @@ public class ShortcutService extends IShortcutService.Stub {
     /**
      * Max number of dynamic shortcuts that each application can have at a time.
      */
-    @GuardedBy("mLock")
     private int mMaxDynamicShortcuts;
 
     /**
      * Max number of updating API calls that each application can make a day.
      */
-    @GuardedBy("mLock")
     private int mMaxDailyUpdates;
 
     /**
      * Actual throttling-reset interval.  By default it's a day.
      */
-    @GuardedBy("mLock")
     private long mResetInterval;
 
+    /**
+     * Icon max width/height in pixels.
+     */
+    private int mMaxIconDimension;
+
+    private CompressFormat mIconPersistFormat = CompressFormat.PNG;
+
+    private int mIconPersistQuality = 100;
+
     public ShortcutService(Context context) {
         mContext = Preconditions.checkNotNull(context);
         LocalServices.addService(ShortcutServiceInternal.class, new LocalService());
@@ -417,9 +509,15 @@ public class ShortcutService extends IShortcutService.Stub {
         mResetInterval = DEFAULT_RESET_INTERVAL_SEC * 1000L;
         mMaxDailyUpdates = DEFAULT_MAX_DAILY_UPDATES;
         mMaxDynamicShortcuts = DEFAULT_MAX_SHORTCUTS_PER_APP;
+
+        final int iconDimensionDp = (injectIsLowRamDevice()
+                ? DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP : DEFAULT_MAX_ICON_DIMENSION_DP);
+        mMaxIconDimension =
+                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, iconDimensionDp,
+                        mContext.getResources().getDisplayMetrics());
     }
 
-    // === Persistings ===
+    // === Persisting ===
 
     @Nullable
     private String parseStringAttribute(XmlPullParser parser, String attribute) {
@@ -605,23 +703,24 @@ public class ShortcutService extends IShortcutService.Stub {
             // Body.
             for (int i = 0; i < packages.size(); i++) {
                 final String packageName = packages.keyAt(i);
-                final PackageShortcuts shortcuts = packages.valueAt(i);
+                final PackageShortcuts packageShortcuts = packages.valueAt(i);
 
                 // TODO Move this to PackageShortcuts.
 
-                out.startTag(null, "package");
+                out.startTag(null, TAG_PACKAGE);
 
-                writeAttr(out, "name", packageName);
-                writeAttr(out, "dynamic-count", shortcuts.mDynamicShortcutCount);
-                writeAttr(out, "call-count", shortcuts.mApiCallCountInner);
-                writeAttr(out, "last-reset", shortcuts.mLastResetTime);
+                writeAttr(out, ATTR_NAME, packageName);
+                writeAttr(out, ATTR_DYNAMIC_COUNT, packageShortcuts.mDynamicShortcutCount);
+                writeAttr(out, ATTR_CALL_COUNT, packageShortcuts.mApiCallCount);
+                writeAttr(out, ATTR_LAST_RESET, packageShortcuts.mLastResetTime);
 
-                final int size = shortcuts.getShortcuts().size();
+                final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts;
+                final int size = shortcuts.size();
                 for (int j = 0; j < size; j++) {
-                    saveShortcut(out, shortcuts.getShortcuts().valueAt(j));
+                    saveShortcut(out, shortcuts.valueAt(j));
                 }
 
-                out.endTag(null, "package");
+                out.endTag(null, TAG_PACKAGE);
             }
 
             // Epilogue.
@@ -638,23 +737,23 @@ public class ShortcutService extends IShortcutService.Stub {
 
     private void saveShortcut(XmlSerializer out, ShortcutInfo si)
             throws IOException, XmlPullParserException {
-        out.startTag(null, "shortcut");
-        writeAttr(out, "id", si.getId());
+        out.startTag(null, TAG_SHORTCUT);
+        writeAttr(out, ATTR_ID, si.getId());
         // writeAttr(out, "package", si.getPackageName()); // not needed
-        writeAttr(out, "activity", si.getActivityComponent());
+        writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
         // writeAttr(out, "icon", si.getIcon());  // We don't save it.
-        writeAttr(out, "title", si.getTitle());
-        writeAttr(out, "intent", si.getIntent());
-        writeAttr(out, "weight", si.getWeight());
-        writeAttr(out, "timestamp", si.getLastChangedTimestamp());
-        writeAttr(out, "flags", si.getFlags());
-        writeAttr(out, "icon-res", si.getIconResourceId());
-        writeAttr(out, "bitmap-path", si.getBitmapPath());
+        writeAttr(out, ATTR_TITLE, si.getTitle());
+        writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
+        writeAttr(out, ATTR_WEIGHT, si.getWeight());
+        writeAttr(out, ATTR_TIMESTAMP, si.getLastChangedTimestamp());
+        writeAttr(out, ATTR_FLAGS, si.getFlags());
+        writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
+        writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
 
-        writeTagExtra(out, "intent-extras", si.getIntentPersistableExtras());
-        writeTagExtra(out, "extras", si.getExtras());
+        writeTagExtra(out, TAG_INTENT_EXTRAS, si.getIntentPersistableExtras());
+        writeTagExtra(out, TAG_EXTRAS, si.getExtras());
 
-        out.endTag(null, "shortcut");
+        out.endTag(null, TAG_SHORTCUT);
     }
 
     private static IOException throwForInvalidTag(int depth, String tag) throws IOException {
@@ -710,24 +809,27 @@ public class ShortcutService extends IShortcutService.Stub {
                     }
                     case 2: {
                         switch (tag) {
-                            case "package":
-                                packageName = parseStringAttribute(parser, "name");
-                                shortcuts = new PackageShortcuts();
+                            case TAG_PACKAGE:
+                                packageName = parseStringAttribute(parser, ATTR_NAME);
+                                shortcuts = new PackageShortcuts(userId, packageName);
                                 ret.put(packageName, shortcuts);
 
                                 shortcuts.mDynamicShortcutCount =
-                                        (int) parseLongAttribute(parser, "dynamic-count");
-                                shortcuts.mApiCallCountInner =
-                                        (int) parseLongAttribute(parser, "call-count");
-                                shortcuts.mLastResetTime = parseLongAttribute(parser, "last-reset");
+                                        (int) parseLongAttribute(parser, ATTR_DYNAMIC_COUNT);
+                                shortcuts.mApiCallCount =
+                                        (int) parseLongAttribute(parser, ATTR_CALL_COUNT);
+                                shortcuts.mLastResetTime = parseLongAttribute(parser,
+                                        ATTR_LAST_RESET);
                                 continue;
                         }
                         break;
                     }
                     case 3: {
                         switch (tag) {
-                            case "shortcut":
+                            case TAG_SHORTCUT:
                                 final ShortcutInfo si = parseShortcut(parser, packageName);
+
+                                // Don't use addShortcut(), we don't need to save the icon.
                                 shortcuts.mShortcuts.put(si.getId(), si);
                                 continue;
                         }
@@ -760,15 +862,15 @@ public class ShortcutService extends IShortcutService.Stub {
         int iconRes;
         String bitmapPath;
 
-        id = parseStringAttribute(parser, "id");
-        activityComponent = parseComponentNameAttribute(parser, "activity");
-        title = parseStringAttribute(parser, "title");
-        intent = parseIntentAttribute(parser, "intent");
-        weight = (int) parseLongAttribute(parser, "weight");
-        lastChangedTimestamp = (int) parseLongAttribute(parser, "timestamp");
-        flags = (int) parseLongAttribute(parser, "flags");
-        iconRes = (int) parseLongAttribute(parser, "icon-res");
-        bitmapPath = parseStringAttribute(parser, "bitmap-path");
+        id = parseStringAttribute(parser, ATTR_ID);
+        activityComponent = parseComponentNameAttribute(parser, ATTR_ACTIVITY);
+        title = parseStringAttribute(parser, ATTR_TITLE);
+        intent = parseIntentAttribute(parser, ATTR_INTENT);
+        weight = (int) parseLongAttribute(parser, ATTR_WEIGHT);
+        lastChangedTimestamp = (int) parseLongAttribute(parser, ATTR_TIMESTAMP);
+        flags = (int) parseLongAttribute(parser, ATTR_FLAGS);
+        iconRes = (int) parseLongAttribute(parser, ATTR_ICON_RES);
+        bitmapPath = parseStringAttribute(parser, ATTR_BITMAP_PATH);
 
         final int outerDepth = parser.getDepth();
         int type;
@@ -784,10 +886,10 @@ public class ShortcutService extends IShortcutService.Stub {
                         depth, type, tag));
             }
             switch (tag) {
-                case "intent-extras":
+                case TAG_INTENT_EXTRAS:
                     intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
                     continue;
-                case "extras":
+                case TAG_EXTRAS:
                     extras = PersistableBundle.restoreFromXml(parser);
                     continue;
             }
@@ -875,7 +977,7 @@ public class ShortcutService extends IShortcutService.Stub {
         final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId);
         PackageShortcuts shortcuts = userPackages.get(packageName);
         if (shortcuts == null) {
-            shortcuts = new PackageShortcuts();
+            shortcuts = new PackageShortcuts(userId, packageName);
             userPackages.put(packageName, shortcuts);
         }
         return shortcuts;
@@ -883,6 +985,195 @@ public class ShortcutService extends IShortcutService.Stub {
 
     // === Caller validation ===
 
+    void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) {
+        if (shortcut.getBitmapPath() != null) {
+            if (DEBUG) {
+                Slog.d(TAG, "Removing " + shortcut.getBitmapPath());
+            }
+            new File(shortcut.getBitmapPath()).delete();
+
+            shortcut.setBitmapPath(null);
+            shortcut.setIconResourceId(0);
+            shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES);
+        }
+    }
+
+    @VisibleForTesting
+    static class FileOutputStreamWithPath extends FileOutputStream {
+        private final File mFile;
+
+        public FileOutputStreamWithPath(File file) throws FileNotFoundException {
+            super(file);
+            mFile = file;
+        }
+
+        public File getFile() {
+            return mFile;
+        }
+    }
+
+    /**
+     * Build the cached bitmap filename for a shortcut icon.
+     *
+     * The filename will be based on the ID, except certain characters will be escaped.
+     */
+    @VisibleForTesting
+    FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut)
+            throws IOException {
+        final File packagePath = new File(getUserBitmapFilePath(userId),
+                shortcut.getPackageName());
+        if (!packagePath.isDirectory()) {
+            packagePath.mkdirs();
+            if (!packagePath.isDirectory()) {
+                throw new IOException("Unable to create directory " + packagePath);
+            }
+            SELinux.restorecon(packagePath);
+        }
+
+        final String baseName = String.valueOf(injectCurrentTimeMillis());
+        for (int suffix = 0;; suffix++) {
+            final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png";
+            final File file = new File(packagePath, filename);
+            if (!file.exists()) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Saving icon to " + file.getAbsolutePath());
+                }
+                return new FileOutputStreamWithPath(file);
+            }
+        }
+    }
+
+    void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) {
+        if (shortcut.hasIconFile() || shortcut.hasIconResource()) {
+            return;
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            // Clear icon info on the shortcut.
+            shortcut.setIconResourceId(0);
+            shortcut.setBitmapPath(null);
+
+            final Icon icon = shortcut.getIcon();
+            if (icon == null) {
+                return; // has no icon
+            }
+
+            Bitmap bitmap = null;
+            try {
+                switch (icon.getType()) {
+                    case Icon.TYPE_RESOURCE: {
+                        injectValidateIconResPackage(shortcut, icon);
+
+                        shortcut.setIconResourceId(icon.getResId());
+                        shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES);
+                        return;
+                    }
+                    case Icon.TYPE_BITMAP: {
+                        bitmap = icon.getBitmap();
+                        break;
+                    }
+                    case Icon.TYPE_URI: {
+                        final Uri uri = ContentProvider.maybeAddUserId(icon.getUri(), userId);
+
+                        try (InputStream is = mContext.getContentResolver().openInputStream(uri)) {
+
+                            bitmap = BitmapFactory.decodeStream(is);
+
+                        } catch (IOException e) {
+                            Slog.e(TAG, "Unable to load icon from " + uri);
+                            return;
+                        }
+                        break;
+                    }
+                    default:
+                        // This shouldn't happen because we've already validated the icon, but
+                        // just in case.
+                        throw ShortcutInfo.getInvalidIconException();
+                }
+                if (bitmap == null) {
+                    Slog.e(TAG, "Null bitmap detected");
+                    return;
+                }
+                // Shrink and write to the file.
+                File path = null;
+                try {
+                    final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut);
+                    try {
+                        path = out.getFile();
+
+                        shrinkBitmap(bitmap, mMaxIconDimension)
+                                .compress(mIconPersistFormat, mIconPersistQuality, out);
+
+                        shortcut.setBitmapPath(out.getFile().getAbsolutePath());
+                        shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE);
+                    } finally {
+                        IoUtils.closeQuietly(out);
+                    }
+                } catch (IOException|RuntimeException e) {
+                    // STOPSHIP Change wtf to e
+                    Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
+                    if (path != null && path.exists()) {
+                        path.delete();
+                    }
+                }
+            } finally {
+                if (bitmap != null) {
+                    bitmap.recycle();
+                }
+                // Once saved, we won't use the original icon information, so null it out.
+                shortcut.clearIcon();
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    // Unfortunately we can't do this check in unit tests because we fake creator package names,
+    // so override in unit tests.
+    // TODO CTS this case.
+    void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) {
+        if (!shortcut.getPackageName().equals(icon.getResPackage())) {
+            throw new IllegalArgumentException(
+                    "Icon resource must reside in shortcut owner package");
+        }
+    }
+
+    @VisibleForTesting
+    static Bitmap shrinkBitmap(Bitmap in, int maxSize) {
+        // Original width/height.
+        final int ow = in.getWidth();
+        final int oh = in.getHeight();
+        if ((ow <= maxSize) && (oh <= maxSize)) {
+            if (DEBUG) {
+                Slog.d(TAG, String.format("Icon size %dx%d, no need to shrink", ow, oh));
+            }
+            return in;
+        }
+        final int longerDimension = Math.max(ow, oh);
+
+        // New width and height.
+        final int nw = ow * maxSize / longerDimension;
+        final int nh = oh * maxSize / longerDimension;
+        if (DEBUG) {
+            Slog.d(TAG, String.format("Icon size %dx%d, shrinking to %dx%d",
+                    ow, oh, nw, nh));
+        }
+
+        final Bitmap scaledBitmap = Bitmap.createBitmap(nw, nh, Bitmap.Config.ARGB_8888);
+        final Canvas c = new Canvas(scaledBitmap);
+
+        final RectF dst = new RectF(0, 0, nw, nh);
+
+        c.drawBitmap(in, /*src=*/ null, dst, /* paint =*/ null);
+
+        in.recycle();
+
+        return scaledBitmap;
+    }
+
+    // === Caller validation ===
+
     private boolean isCallerSystem() {
         final int callingUid = injectBinderCallingUid();
          return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
@@ -915,31 +1206,21 @@ public class ShortcutService extends IShortcutService.Stub {
         if (UserHandle.getUserId(callingUid) != userId) {
             throw new SecurityException("Invalid user-ID");
         }
-        verifyCallingPackage(packageName);
-    }
-
-    private void verifyCallingPackage(@NonNull String packageName) {
-        Preconditions.checkStringNotEmpty(packageName, "packageName");
-
-        if (isCallerSystem()) {
-            return; // no check
-        }
-
-        if (injectGetPackageUid(packageName) == injectBinderCallingUid()) {
+        if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) {
             return; // Caller is valid.
         }
         throw new SecurityException("Caller UID= doesn't own " + packageName);
     }
 
     // Test overrides it.
-    int injectGetPackageUid(String packageName) {
+    int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) {
         try {
 
             // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info?
 
-            return mContext.getPackageManager().getPackageUid(packageName,
+            return mContext.getPackageManager().getPackageUidAsUser(packageName,
                     PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE
-                            | PackageManager.MATCH_UNINSTALLED_PACKAGES);
+                            | PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
         } catch (NameNotFoundException e) {
             return -1;
         }
@@ -983,8 +1264,10 @@ public class ShortcutService extends IShortcutService.Stub {
      * - Make sure the intent's extras are persistable, and them to set
      *  {@link ShortcutInfo#mIntentPersistableExtras}.  Also clear its extras.
      * - Clear flags.
+     *
+     * TODO Detailed unit tests
      */
-    private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut) {
+    private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) {
         Preconditions.checkNotNull(shortcut, "Null shortcut detected");
         if (shortcut.getActivityComponent() != null) {
             Preconditions.checkState(
@@ -993,22 +1276,58 @@ public class ShortcutService extends IShortcutService.Stub {
                     "Activity package name mismatch");
         }
 
-        shortcut.enforceMandatoryFields();
+        if (!forUpdate) {
+            shortcut.enforceMandatoryFields();
+        }
+        if (shortcut.getIcon() != null) {
+            ShortcutInfo.validateIcon(shortcut.getIcon());
+        }
 
-        final Intent intent = shortcut.getIntent();
-        final Bundle intentExtras = intent.getExtras();
-        if (intentExtras != null && intentExtras.size() > 0) {
-            intent.replaceExtras((Bundle) null);
+        validateForXml(shortcut.getId());
+        validateForXml(shortcut.getTitle());
+        validatePersistableBundleForXml(shortcut.getIntentPersistableExtras());
+        validatePersistableBundleForXml(shortcut.getExtras());
 
-            // PersistableBundle's constructor will throw IllegalArgumentException if original
-            // extras contain something not persistable.
-            shortcut.setIntentPersistableExtras(new PersistableBundle(intentExtras));
+        shortcut.setFlags(0);
+    }
+
+    // KXmlSerializer is strict and doesn't allow certain characters, so we disallow those
+    // characters.
+
+    private static void validatePersistableBundleForXml(PersistableBundle b) {
+        if (b == null || b.size() == 0) {
+            return;
         }
+        for (String key : b.keySet()) {
+            validateForXml(key);
+            final Object value = b.get(key);
+            if (value == null) {
+                continue;
+            } else if (value instanceof String) {
+                validateForXml((String) value);
+            } else if (value instanceof String[]) {
+                for (String v : (String[]) value) {
+                    validateForXml(v);
+                }
+            } else if (value instanceof PersistableBundle) {
+                validatePersistableBundleForXml((PersistableBundle) value);
+            }
+        }
+    }
 
-        // TODO Save the icon
-        shortcut.setIcon(null);
+    private static void validateForXml(String s) {
+        if (TextUtils.isEmpty(s)) {
+            return;
+        }
+        for (int i = s.length() - 1; i >= 0; i--) {
+            if (!isAllowedInXml(s.charAt(i))) {
+                throw new IllegalArgumentException("Unsupported character detected in: " + s);
+            }
+        }
+    }
 
-        shortcut.setFlags(0);
+    private static boolean isAllowedInXml(char c) {
+        return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
     }
 
     // === APIs ===
@@ -1032,11 +1351,11 @@ public class ShortcutService extends IShortcutService.Stub {
 
             // Validate the shortcuts.
             for (int i = 0; i < size; i++) {
-                fixUpIncomingShortcutInfo(newShortcuts.get(i));
+                fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false);
             }
 
             // First, remove all un-pinned; dynamic shortcuts
-            ps.deleteAllDynamicShortcuts();
+            ps.deleteAllDynamicShortcuts(this);
 
             // Then, add/update all.  We need to make sure to take over "pinned" flag.
             for (int i = 0; i < size; i++) {
@@ -1046,7 +1365,6 @@ public class ShortcutService extends IShortcutService.Stub {
             }
         }
         userPackageChanged(packageName, userId);
-
         return true;
     }
 
@@ -1056,15 +1374,34 @@ public class ShortcutService extends IShortcutService.Stub {
         verifyCaller(packageName, userId);
 
         final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
+        final int size = newShortcuts.size();
 
         synchronized (mLock) {
+            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
 
-            if (true) {
-                throw new RuntimeException("not implemented yet");
+            // Throttling.
+            if (!ps.tryApiCall(this)) {
+                return false;
             }
 
-            // TODO Similar to setDynamicShortcuts, but don't add new ones, and don't change flags.
-            // Update non-null fields only.
+            for (int i = 0; i < size; i++) {
+                final ShortcutInfo source = newShortcuts.get(i);
+                fixUpIncomingShortcutInfo(source, /* forUpdate= */ true);
+
+                final ShortcutInfo target = ps.findShortcutById(source.getId());
+                if (target != null) {
+                    final boolean replacingIcon = (source.getIcon() != null);
+                    if (replacingIcon) {
+                        removeIcon(userId, target);
+                    }
+
+                    target.copyNonNullFieldsFrom(source);
+
+                    if (replacingIcon) {
+                        saveIconAndFixUpShortcut(userId, target);
+                    }
+                }
+            }
         }
         userPackageChanged(packageName, userId);
 
@@ -1085,7 +1422,7 @@ public class ShortcutService extends IShortcutService.Stub {
             }
 
             // Validate the shortcut.
-            fixUpIncomingShortcutInfo(newShortcut);
+            fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false);
 
             // Add it.
             newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
@@ -1103,7 +1440,7 @@ public class ShortcutService extends IShortcutService.Stub {
         Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided");
 
         synchronized (mLock) {
-            getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(shortcutId);
+            getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId);
         }
         userPackageChanged(packageName, userId);
     }
@@ -1113,7 +1450,7 @@ public class ShortcutService extends IShortcutService.Stub {
         verifyCaller(packageName, userId);
 
         synchronized (mLock) {
-            getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts();
+            getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
         }
         userPackageChanged(packageName, userId);
     }
@@ -1177,6 +1514,13 @@ public class ShortcutService extends IShortcutService.Stub {
         }
     }
 
+    @Override
+    public int getIconMaxDimensions(String packageName, int userId) throws RemoteException {
+        synchronized (mLock) {
+            return mMaxIconDimension;
+        }
+    }
+
     /**
      * Reset all throttling, for developer options and command line.  Only system/shell can call it.
      */
@@ -1193,6 +1537,7 @@ public class ShortcutService extends IShortcutService.Stub {
             mRawLastResetTime = injectCurrentTimeMillis();
         }
         scheduleSaveBaseState();
+        Slog.i(TAG, "ShortcutManager: throttling counter reset");
     }
 
     /**
@@ -1217,7 +1562,7 @@ public class ShortcutService extends IShortcutService.Stub {
                 } else {
                     final ArrayMap<String, PackageShortcuts> packages =
                             getUserShortcutsLocked(userId);
-                    for (int i = 0; i < packages.size(); i++) {
+                    for (int i = packages.size() - 1; i >= 0; i--) {
                         getShortcutsInnerLocked(
                                 packages.keyAt(i),
                                 changedSince, componentName, queryFlags, userId, ret, cloneFlag);
@@ -1274,7 +1619,8 @@ public class ShortcutService extends IShortcutService.Stub {
             Preconditions.checkNotNull(shortcutIds, "shortcutIds");
 
             synchronized (mLock) {
-                getPackageShortcutsLocked(packageName, userId).pinAll(shortcutIds);
+                getPackageShortcutsLocked(packageName, userId).replacePinned(
+                        ShortcutService.this, callingPackage, shortcutIds);
             }
             userPackageChanged(packageName, userId);
         }
@@ -1289,18 +1635,8 @@ public class ShortcutService extends IShortcutService.Stub {
             synchronized (mLock) {
                 final ShortcutInfo fullShortcut =
                         getPackageShortcutsLocked(packageName, userId)
-                        .getShortcuts().get(shortcutId);
-                if (fullShortcut == null) {
-                    return null;
-                } else {
-                    final Intent intent = fullShortcut.getIntent();
-                    final PersistableBundle extras = fullShortcut.getIntentPersistableExtras();
-                    if (extras != null) {
-                        intent.replaceExtras(new Bundle(extras));
-                    }
-
-                    return intent;
-                }
+                        .findShortcutById(shortcutId);
+                return fullShortcut == null ? null : fullShortcut.getIntent();
             }
         }
 
@@ -1310,6 +1646,41 @@ public class ShortcutService extends IShortcutService.Stub {
                 mListeners.add(Preconditions.checkNotNull(listener));
             }
         }
+
+        @Override
+        public int getShortcutIconResId(@NonNull String callingPackage,
+                @NonNull ShortcutInfo shortcut, int userId) {
+            Preconditions.checkNotNull(shortcut, "shortcut");
+
+            synchronized (mLock) {
+                final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
+                        shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
+                return (shortcutInfo != null && shortcutInfo.hasIconResource())
+                        ? shortcutInfo.getIconResourceId() : 0;
+            }
+        }
+
+        @Override
+        public ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage,
+                @NonNull ShortcutInfo shortcut, int userId) {
+            Preconditions.checkNotNull(shortcut, "shortcut");
+
+            synchronized (mLock) {
+                final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
+                        shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
+                if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
+                    return null;
+                }
+                try {
+                    return ParcelFileDescriptor.open(
+                            new File(shortcutInfo.getBitmapPath()),
+                            ParcelFileDescriptor.MODE_READ_ONLY);
+                } catch (FileNotFoundException e) {
+                    Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath());
+                    return null;
+                }
+            }
+        }
     }
 
     // === Dump ===
@@ -1336,30 +1707,38 @@ public class ShortcutService extends IShortcutService.Stub {
             pw.print(now);
             pw.print("] ");
             pw.print(formatTime(now));
+
             pw.print("  Raw last reset: [");
             pw.print(mRawLastResetTime);
             pw.print("] ");
             pw.print(formatTime(mRawLastResetTime));
 
             final long last = getLastResetTimeLocked();
-            final long next = getNextResetTimeLocked();
             pw.print("  Last reset: [");
             pw.print(last);
             pw.print("] ");
             pw.print(formatTime(last));
 
+            final long next = getNextResetTimeLocked();
             pw.print("  Next reset: [");
             pw.print(next);
             pw.print("] ");
             pw.print(formatTime(next));
             pw.println();
 
+            pw.print("  Max icon dim: ");
+            pw.print(mMaxIconDimension);
+            pw.print("  Icon format: ");
+            pw.print(mIconPersistFormat);
+            pw.print("  Icon quality: ");
+            pw.print(mIconPersistQuality);
+            pw.println();
+
             pw.println();
 
             for (int i = 0; i < mShortcuts.size(); i++) {
                 dumpUserLocked(pw, mShortcuts.keyAt(i));
             }
-
         }
     }
 
@@ -1379,8 +1758,8 @@ public class ShortcutService extends IShortcutService.Stub {
     }
 
     private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) {
-        final PackageShortcuts shortcuts = mShortcuts.get(userId).get(packageName);
-        if (shortcuts == null) {
+        final PackageShortcuts packageShortcuts = mShortcuts.get(userId).get(packageName);
+        if (packageShortcuts == null) {
             return;
         }
 
@@ -1389,22 +1768,38 @@ public class ShortcutService extends IShortcutService.Stub {
         pw.println();
 
         pw.print("      Calls: ");
-        pw.print(shortcuts.getApiCallCount(this));
+        pw.print(packageShortcuts.getApiCallCount(this));
         pw.println();
 
         // This should be after getApiCallCount(), which may update it.
         pw.print("      Last reset: [");
-        pw.print(shortcuts.mLastResetTime);
+        pw.print(packageShortcuts.mLastResetTime);
         pw.print("] ");
-        pw.print(formatTime(shortcuts.mLastResetTime));
+        pw.print(formatTime(packageShortcuts.mLastResetTime));
         pw.println();
 
         pw.println("      Shortcuts:");
-        final int size = shortcuts.getShortcuts().size();
+        long totalBitmapSize = 0;
+        final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts;
+        final int size = shortcuts.size();
         for (int i = 0; i < size; i++) {
+            final ShortcutInfo si = shortcuts.valueAt(i);
             pw.print("        ");
-            pw.println(shortcuts.getShortcuts().valueAt(i).toInsecureString());
+            pw.println(si.toInsecureString());
+            if (si.hasIconFile()) {
+                final long len = new File(si.getBitmapPath()).length();
+                pw.print("          ");
+                pw.print("bitmap size=");
+                pw.println(len);
+
+                totalBitmapSize += len;
+            }
         }
+        pw.print("      Total bitmap size: ");
+        pw.print(totalBitmapSize);
+        pw.print(" (");
+        pw.print(Formatter.formatFileSize(mContext, totalBitmapSize));
+        pw.println(")");
     }
 
     private static String formatTime(long time) {
@@ -1505,7 +1900,15 @@ public class ShortcutService extends IShortcutService.Stub {
     }
 
     File injectUserDataPath(@UserIdInt int userId) {
-        return new File(Environment.getDataSystemDeDirectory(userId), DIRECTORY_PER_USER);
+        return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER);
+    }
+
+    boolean injectIsLowRamDevice() {
+        return ActivityManager.isLowRamDeviceStatic();
+    }
+
+    File getUserBitmapFilePath(@UserIdInt int userId) {
+        return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS);
     }
 
     @VisibleForTesting
@@ -1524,6 +1927,11 @@ public class ShortcutService extends IShortcutService.Stub {
     }
 
     @VisibleForTesting
+    void setMaxIconDimensionForTest(int dimension) {
+        mMaxIconDimension = dimension;
+    }
+
+    @VisibleForTesting
     public void setResetIntervalForTest(long interval) {
         mResetInterval = interval;
     }
index 59f8284..e7daaa1 100644 (file)
@@ -726,9 +726,6 @@ public final class SystemServer {
                 // Always start the Device Policy Manager, so that the API is compatible with
                 // API8.
                 mSystemServiceManager.startService(DevicePolicyManagerService.Lifecycle.class);
-
-// TODO is this a good place?
-                mSystemServiceManager.startService(ShortcutService.Lifecycle.class);
             }
 
             if (!disableSystemUI) {
@@ -1139,6 +1136,8 @@ public final class SystemServer {
                 }
                 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
             }
+            // LauncherAppsService uses ShortcutService.
+            mSystemServiceManager.startService(ShortcutService.Lifecycle.class);
 
             mSystemServiceManager.startService(LauncherAppsService.class);
         }
diff --git a/services/tests/servicestests/res/drawable-nodpi/black_1024x4096.png b/services/tests/servicestests/res/drawable-nodpi/black_1024x4096.png
new file mode 100644 (file)
index 0000000..f700326
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/black_1024x4096.png differ
diff --git a/services/tests/servicestests/res/drawable-nodpi/black_16x64.png b/services/tests/servicestests/res/drawable-nodpi/black_16x64.png
new file mode 100644 (file)
index 0000000..315763e
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/black_16x64.png differ
diff --git a/services/tests/servicestests/res/drawable-nodpi/black_32x32.png b/services/tests/servicestests/res/drawable-nodpi/black_32x32.png
new file mode 100644 (file)
index 0000000..8958f6b
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/black_32x32.png differ
diff --git a/services/tests/servicestests/res/drawable-nodpi/black_4096x1024.png b/services/tests/servicestests/res/drawable-nodpi/black_4096x1024.png
new file mode 100644 (file)
index 0000000..f675030
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/black_4096x1024.png differ
diff --git a/services/tests/servicestests/res/drawable-nodpi/black_4096x4096.png b/services/tests/servicestests/res/drawable-nodpi/black_4096x4096.png
new file mode 100644 (file)
index 0000000..999d858
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/black_4096x4096.png differ
diff --git a/services/tests/servicestests/res/drawable-nodpi/black_512x512.png b/services/tests/servicestests/res/drawable-nodpi/black_512x512.png
new file mode 100644 (file)
index 0000000..40d1c2c
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/black_512x512.png differ
diff --git a/services/tests/servicestests/res/drawable-nodpi/black_64x16.png b/services/tests/servicestests/res/drawable-nodpi/black_64x16.png
new file mode 100644 (file)
index 0000000..5883015
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/black_64x16.png differ
diff --git a/services/tests/servicestests/res/drawable-nodpi/black_64x64.png b/services/tests/servicestests/res/drawable-nodpi/black_64x64.png
new file mode 100644 (file)
index 0000000..71cfafc
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/black_64x64.png differ
diff --git a/services/tests/servicestests/res/drawable-nodpi/icon1.png b/services/tests/servicestests/res/drawable-nodpi/icon1.png
new file mode 100644 (file)
index 0000000..64eb294
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/icon1.png differ
diff --git a/services/tests/servicestests/res/drawable-nodpi/icon2.png b/services/tests/servicestests/res/drawable-nodpi/icon2.png
new file mode 100644 (file)
index 0000000..7502484
Binary files /dev/null and b/services/tests/servicestests/res/drawable-nodpi/icon2.png differ
index 1f805e9..21daa1b 100644 (file)
@@ -25,19 +25,26 @@ import android.content.pm.LauncherApps.ShortcutQuery;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
 import android.content.pm.ShortcutServiceInternal;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
 import android.os.UserHandle;
 import android.test.AndroidTestCase;
 import android.test.mock.MockContext;
+import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Log;
 
 import com.android.frameworks.servicestests.R;
 import com.android.internal.util.Preconditions;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
+
+import libcore.io.IoUtils;
 
 import org.junit.Assert;
 
@@ -45,13 +52,16 @@ import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileReader;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Tests for ShortcutService and ShortcutManager.
@@ -61,7 +71,11 @@ import java.util.Map;
  -r -g ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk &&
  adb shell am instrument -e class com.android.server.pm.ShortcutManagerTest \
  -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
+
+ * TODO: Add checks with assertAllNotHaveIcon()
+ * TODO: Cross-user test (do in CTS?)
  */
+@SmallTest
 public class ShortcutManagerTest extends AndroidTestCase {
     private static final String TAG = "ShortcutManagerTest";
 
@@ -77,10 +91,19 @@ public class ShortcutManagerTest extends AndroidTestCase {
         public String getPackageName() {
             return mInjectedClientPackage;
         }
+
+        @Override
+        public Resources getResources() {
+            return ShortcutManagerTest.this.getContext().getResources();
+        }
     }
 
     /** Context used in the service side */
     private final class ServiceContext extends MockContext {
+        @Override
+        public Resources getResources() {
+            return ShortcutManagerTest.this.getContext().getResources();
+        }
     }
 
     /** ShortcutService with injection override methods. */
@@ -95,6 +118,7 @@ public class ShortcutManagerTest extends AndroidTestCase {
             setResetIntervalForTest(INTERVAL);
             setMaxDynamicShortcutsForTest(MAX_SHORTCUTS);
             setMaxDailyUpdatesForTest(MAX_DAILY_UPDATES);
+            setMaxIconDimensionForTest(MAX_ICON_DIMENSION);
         }
 
         @Override
@@ -108,7 +132,7 @@ public class ShortcutManagerTest extends AndroidTestCase {
         }
 
         @Override
-        int injectGetPackageUid(String packageName) {
+        int injectGetPackageUid(String packageName, int userId) {
             Integer uid = mInjectedPackageUidMap.get(packageName);
             return uid != null ? uid : -1;
         }
@@ -122,6 +146,11 @@ public class ShortcutManagerTest extends AndroidTestCase {
         File injectUserDataPath(@UserIdInt int userId) {
             return new File(mInjectedFilePathRoot, "user-" + userId);
         }
+
+        @Override
+        void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) {
+            // Can't check
+        }
     }
 
     /** ShortcutManager with injection override methods. */
@@ -181,10 +210,12 @@ public class ShortcutManagerTest extends AndroidTestCase {
 
     private static final long INTERVAL = 10000;
 
-    private static final int MAX_SHORTCUTS = 5;
+    private static final int MAX_SHORTCUTS = 10;
 
     private static final int MAX_DAILY_UPDATES = 3;
 
+    private static final int MAX_ICON_DIMENSION = 128;
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -233,7 +264,8 @@ public class ShortcutManagerTest extends AndroidTestCase {
     /** Replace the current calling package */
     private void setCaller(String packageName) {
         mInjectedClientPackage = packageName;
-        mInjectedCallingUid = Preconditions.checkNotNull(mInjectedPackageUidMap.get(packageName));
+        mInjectedCallingUid = Preconditions.checkNotNull(mInjectedPackageUidMap.get(packageName),
+                "Unknown package");
     }
 
     private String getCallingPackage() {
@@ -343,6 +375,28 @@ public class ShortcutManagerTest extends AndroidTestCase {
     }
 
     /**
+     * Make a shortcut with an ID and icon.
+     */
+    private ShortcutInfo makeShortcutWithIcon(String id, Icon icon) {
+        return makeShortcut(
+                id, "Title-" + id, /* activity =*/ null, icon,
+                makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0);
+    }
+
+    private ShortcutInfo makePackageShortcut(String packageName, String id) {
+        String origCaller = getCallingPackage();
+
+        setCaller(packageName);
+        ShortcutInfo s = makeShortcut(
+                id, "Title-" + id, /* activity =*/ null, /* icon =*/ null,
+                makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0);
+        setCaller(origCaller); // restore the caller
+
+        return s;
+    }
+
+
+    /**
      * Make multiple shortcuts with IDs.
      */
     private List<ShortcutInfo> makeShortcuts(String... ids) {
@@ -413,6 +467,7 @@ public class ShortcutManagerTest extends AndroidTestCase {
     @NonNull
     private List<ShortcutInfo> assertShortcutIds(@NonNull List<ShortcutInfo> actualShortcuts,
             String... expectedIds) {
+        assertEquals(expectedIds.length, actualShortcuts.size());
         final HashSet<String> expected = new HashSet<>(Arrays.asList(expectedIds));
         final HashSet<String> actual = new HashSet<>();
         for (ShortcutInfo s : actualShortcuts) {
@@ -461,6 +516,35 @@ public class ShortcutManagerTest extends AndroidTestCase {
     }
 
     @NonNull
+    private List<ShortcutInfo> assertAllNotHaveIcon(
+            @NonNull List<ShortcutInfo> actualShortcuts) {
+        for (ShortcutInfo s : actualShortcuts) {
+            assertNull("ID " + s.getId(), s.getIcon());
+        }
+        return actualShortcuts;
+    }
+
+    @NonNull
+    private List<ShortcutInfo> assertAllHaveIconResId(
+            @NonNull List<ShortcutInfo> actualShortcuts) {
+        for (ShortcutInfo s : actualShortcuts) {
+            assertTrue("ID " + s.getId() + " not have icon res ID", s.hasIconResource());
+            assertFalse("ID " + s.getId() + " shouldn't have icon FD", s.hasIconFile());
+        }
+        return actualShortcuts;
+    }
+
+    @NonNull
+    private List<ShortcutInfo> assertAllHaveIconFile(
+            @NonNull List<ShortcutInfo> actualShortcuts) {
+        for (ShortcutInfo s : actualShortcuts) {
+            assertFalse("ID " + s.getId() + " shouldn't have icon res ID", s.hasIconResource());
+            assertTrue("ID " + s.getId() + " not have icon FD", s.hasIconFile());
+        }
+        return actualShortcuts;
+    }
+
+    @NonNull
     private List<ShortcutInfo> assertAllHaveFlags(@NonNull List<ShortcutInfo> actualShortcuts,
             int shortcutFlags) {
         for (ShortcutInfo s : actualShortcuts) {
@@ -470,6 +554,24 @@ public class ShortcutManagerTest extends AndroidTestCase {
     }
 
     @NonNull
+    private List<ShortcutInfo> assertAllKeyFieldsOnly(
+            @NonNull List<ShortcutInfo> actualShortcuts) {
+        for (ShortcutInfo s : actualShortcuts) {
+            assertTrue("ID " + s.getId(), s.hasKeyFieldsOnly());
+        }
+        return actualShortcuts;
+    }
+
+    @NonNull
+    private List<ShortcutInfo> assertAllNotKeyFieldsOnly(
+            @NonNull List<ShortcutInfo> actualShortcuts) {
+        for (ShortcutInfo s : actualShortcuts) {
+            assertFalse("ID " + s.getId(), s.hasKeyFieldsOnly());
+        }
+        return actualShortcuts;
+    }
+
+    @NonNull
     private List<ShortcutInfo> assertAllDynamic(@NonNull List<ShortcutInfo> actualShortcuts) {
         return assertAllHaveFlags(actualShortcuts, ShortcutInfo.FLAG_DYNAMIC);
     }
@@ -488,6 +590,31 @@ public class ShortcutManagerTest extends AndroidTestCase {
         return actualShortcuts;
     }
 
+    private void assertBitmapSize(int expectedWidth, int expectedHeight, @NonNull Bitmap bitmap) {
+        assertEquals("width", expectedWidth, bitmap.getWidth());
+        assertEquals("height", expectedHeight, bitmap.getHeight());
+    }
+
+    private <T> void assertAllUnique(Collection<T> list) {
+        final Set<Object> set = new HashSet<>();
+        for (T item : list) {
+            if (set.contains(item)) {
+                fail("Duplicate item found: " + item + " (in the list: " + list + ")");
+            }
+            set.add(item);
+        }
+    }
+
+    @NonNull
+    private Bitmap pfdToBitmap(@NonNull ParcelFileDescriptor pfd) {
+        Preconditions.checkNotNull(pfd);
+        try {
+            return BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
+        } finally {
+            IoUtils.closeQuietly(pfd);
+        }
+    }
+
     /**
      * Test for the first launch path, no settings file available.
      */
@@ -594,13 +721,17 @@ public class ShortcutManagerTest extends AndroidTestCase {
         final ShortcutInfo si3 = makeShortcut("shortcut3");
 
         assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2)));
-        assertEquals(2, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut1", "shortcut2");
         assertEquals(2, mManager.getRemainingCallCount());
 
         // TODO: Check fields
 
         assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1)));
-        assertEquals(1, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut1");
         assertEquals(1, mManager.getRemainingCallCount());
 
         assertTrue(mManager.setDynamicShortcuts(Arrays.asList()));
@@ -629,16 +760,22 @@ public class ShortcutManagerTest extends AndroidTestCase {
 
         assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1)));
         assertEquals(2, mManager.getRemainingCallCount());
-        assertEquals(1, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut1");
 
         assertTrue(mManager.addDynamicShortcut(si2));
         assertEquals(1, mManager.getRemainingCallCount());
-        assertEquals(2, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut1", "shortcut2");
 
         // Add with the same ID
         assertTrue(mManager.addDynamicShortcut(makeShortcut("shortcut1")));
         assertEquals(0, mManager.getRemainingCallCount());
-        assertEquals(2, mManager.getDynamicShortcuts().size()); // Still 2
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut1", "shortcut2");
 
         // TODO Check max number
 
@@ -651,24 +788,35 @@ public class ShortcutManagerTest extends AndroidTestCase {
         final ShortcutInfo si3 = makeShortcut("shortcut3");
 
         assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2, si3)));
-        assertEquals(3, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut1", "shortcut2", "shortcut3");
 
         assertEquals(2, mManager.getRemainingCallCount());
 
         mManager.deleteDynamicShortcut("shortcut1");
-        assertEquals(2, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut2", "shortcut3");
 
         mManager.deleteDynamicShortcut("shortcut1");
-        assertEquals(2, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut2", "shortcut3");
 
         mManager.deleteDynamicShortcut("shortcutXXX");
-        assertEquals(2, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut2", "shortcut3");
 
         mManager.deleteDynamicShortcut("shortcut2");
-        assertEquals(1, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut3");
 
         mManager.deleteDynamicShortcut("shortcut3");
-        assertEquals(0, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()));
 
         // Still 2 calls left.
         assertEquals(2, mManager.getRemainingCallCount());
@@ -682,7 +830,9 @@ public class ShortcutManagerTest extends AndroidTestCase {
         final ShortcutInfo si3 = makeShortcut("shortcut3");
 
         assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2, si3)));
-        assertEquals(3, mManager.getDynamicShortcuts().size());
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
+                mManager.getDynamicShortcuts()),
+                "shortcut1", "shortcut2", "shortcut3");
 
         assertEquals(2, mManager.getRemainingCallCount());
 
@@ -732,7 +882,7 @@ public class ShortcutManagerTest extends AndroidTestCase {
 
         // Now it should work.
         mInjectedCurrentTimeLillis++;
-        assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1)));
+        assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); // fail
         assertEquals(2, mManager.getRemainingCallCount());
 
         mInjectedCurrentTimeLillis++;
@@ -831,6 +981,226 @@ public class ShortcutManagerTest extends AndroidTestCase {
         assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si2)));
     }
 
+    public void testIcons() {
+        final Icon res32x32 = Icon.createWithResource(mContext, R.drawable.black_32x32);
+        final Icon res64x64 = Icon.createWithResource(mContext, R.drawable.black_64x64);
+        final Icon res512x512 = Icon.createWithResource(mContext, R.drawable.black_512x512);
+
+        final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), R.drawable.black_32x32));
+        final Icon bmp64x64 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), R.drawable.black_64x64));
+        final Icon bmp512x512 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                mContext.getResources(), R.drawable.black_512x512));
+
+        // Set from package 1
+        setCaller(CALLING_PACKAGE_1);
+        assertTrue(mManager.setDynamicShortcuts(Arrays.asList(
+                makeShortcutWithIcon("res32x32", res32x32),
+                makeShortcutWithIcon("res64x64", res64x64),
+                makeShortcutWithIcon("bmp32x32", bmp32x32),
+                makeShortcutWithIcon("bmp64x64", bmp64x64),
+                makeShortcutWithIcon("bmp512x512", bmp512x512),
+                makeShortcut("none")
+        )));
+
+        // getDynamicShortcuts() shouldn't return icons, thus assertAllNotHaveIcon().
+        assertShortcutIds(assertAllNotHaveIcon(mManager.getDynamicShortcuts()),
+                "res32x32",
+                "res64x64",
+                "bmp32x32",
+                "bmp64x64",
+                "bmp512x512",
+                "none");
+
+        // Call from another caller with the same ID, just to make sure storage is per-package.
+        setCaller(CALLING_PACKAGE_2);
+        assertTrue(mManager.setDynamicShortcuts(Arrays.asList(
+                makeShortcutWithIcon("res32x32", res512x512),
+                makeShortcutWithIcon("res64x64", res512x512),
+                makeShortcutWithIcon("none", res512x512)
+        )));
+        assertShortcutIds(assertAllNotHaveIcon(mManager.getDynamicShortcuts()),
+                "res32x32",
+                "res64x64",
+                "none");
+
+        dumpsysOnLogcat();
+
+        // Load from launcher.
+        Bitmap bmp;
+
+        setCaller(LAUNCHER_1);
+
+        // Check hasIconResource()/hasIconFile().
+        assertShortcutIds(assertAllHaveIconResId(mInternal.getShortcutInfo(
+                getCallingPackage(), CALLING_PACKAGE_1, Arrays.asList("res32x32"),
+                getCallingUserId())), "res32x32");
+
+        assertShortcutIds(assertAllHaveIconResId(mInternal.getShortcutInfo(
+                getCallingPackage(), CALLING_PACKAGE_1, Arrays.asList("res64x64"),
+                getCallingUserId())), "res64x64");
+
+        assertShortcutIds(assertAllHaveIconFile(mInternal.getShortcutInfo(
+                getCallingPackage(), CALLING_PACKAGE_1, Arrays.asList("bmp32x32"),
+                getCallingUserId())), "bmp32x32");
+        assertShortcutIds(assertAllHaveIconFile(mInternal.getShortcutInfo(
+                getCallingPackage(), CALLING_PACKAGE_1, Arrays.asList("bmp64x64"),
+                getCallingUserId())), "bmp64x64");
+        assertShortcutIds(assertAllHaveIconFile(mInternal.getShortcutInfo(
+                getCallingPackage(), CALLING_PACKAGE_1, Arrays.asList("bmp512x512"),
+                getCallingUserId())), "bmp512x512");
+
+        // Check
+        assertEquals(
+                R.drawable.black_32x32,
+                mInternal.getShortcutIconResId(getCallingPackage(),
+                        makePackageShortcut(CALLING_PACKAGE_1, "res32x32"), getCallingUserId()));
+
+        assertEquals(
+                R.drawable.black_64x64,
+                mInternal.getShortcutIconResId(
+                        getCallingPackage(),
+                        makePackageShortcut(CALLING_PACKAGE_1, "res64x64"), getCallingUserId()));
+
+        assertEquals(
+                0, // because it's not a resource
+                mInternal.getShortcutIconResId(
+                        getCallingPackage(),
+                        makePackageShortcut(CALLING_PACKAGE_1, "bmp32x32"), getCallingUserId()));
+        assertEquals(
+                0, // because it's not a resource
+                mInternal.getShortcutIconResId(
+                        getCallingPackage(),
+                        makePackageShortcut(CALLING_PACKAGE_1, "bmp64x64"), getCallingUserId()));
+        assertEquals(
+                0, // because it's not a resource
+                mInternal.getShortcutIconResId(
+                        getCallingPackage(),
+                        makePackageShortcut(CALLING_PACKAGE_1, "bmp512x512"), getCallingUserId()));
+
+        bmp = pfdToBitmap(mInternal.getShortcutIconFd(
+                getCallingPackage(),
+                makePackageShortcut(CALLING_PACKAGE_1, "bmp32x32"), getCallingUserId()));
+        assertBitmapSize(32, 32, bmp);
+
+        bmp = pfdToBitmap(mInternal.getShortcutIconFd(
+                getCallingPackage(),
+                makePackageShortcut(CALLING_PACKAGE_1, "bmp64x64"), getCallingUserId()));
+        assertBitmapSize(64, 64, bmp);
+
+        bmp = pfdToBitmap(mInternal.getShortcutIconFd(
+                getCallingPackage(),
+                makePackageShortcut(CALLING_PACKAGE_1, "bmp512x512"), getCallingUserId()));
+        assertBitmapSize(128, 128, bmp);
+
+        // TODO Test the content URI case too.
+    }
+
+    private void checkShrinkBitmap(int expectedWidth, int expectedHeight, int resId, int maxSize) {
+        assertBitmapSize(expectedWidth, expectedHeight,
+                ShortcutService.shrinkBitmap(BitmapFactory.decodeResource(
+                        mContext.getResources(), resId),
+                        maxSize));
+    }
+
+    public void testShrinkBitmap() {
+        checkShrinkBitmap(32, 32, R.drawable.black_512x512, 32);
+        checkShrinkBitmap(511, 511, R.drawable.black_512x512, 511);
+        checkShrinkBitmap(512, 512, R.drawable.black_512x512, 512);
+
+        checkShrinkBitmap(1024, 4096, R.drawable.black_1024x4096, 4096);
+        checkShrinkBitmap(1024, 4096, R.drawable.black_1024x4096, 4100);
+        checkShrinkBitmap(512, 2048, R.drawable.black_1024x4096, 2048);
+
+        checkShrinkBitmap(4096, 1024, R.drawable.black_4096x1024, 4096);
+        checkShrinkBitmap(4096, 1024, R.drawable.black_4096x1024, 4100);
+        checkShrinkBitmap(2048, 512, R.drawable.black_4096x1024, 2048);
+    }
+
+    private File openIconFileForWriteAndGetPath(int userId, String packageName)
+            throws IOException {
+        // Shortcut IDs aren't used in the path, so just pass the same ID.
+        final FileOutputStreamWithPath out =
+                mService.openIconFileForWrite(userId, makePackageShortcut(packageName, "id"));
+        out.close();
+        return out.getFile();
+    }
+
+    public void testOpenIconFileForWrite() throws IOException {
+        mInjectedCurrentTimeLillis = 1000;
+
+        final File p10_1_1 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+        final File p10_1_2 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+
+        final File p10_2_1 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_2);
+        final File p10_2_2 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_2);
+
+        final File p11_1_1 = openIconFileForWriteAndGetPath(11, CALLING_PACKAGE_1);
+        final File p11_1_2 = openIconFileForWriteAndGetPath(11, CALLING_PACKAGE_1);
+
+        mInjectedCurrentTimeLillis++;
+
+        final File p10_1_3 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+        final File p10_1_4 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+        final File p10_1_5 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
+
+        final File p10_2_3 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_2);
+        final File p11_1_3 = openIconFileForWriteAndGetPath(11, CALLING_PACKAGE_1);
+
+        // Make sure their paths are all unique
+        assertAllUnique(Arrays.asList(
+                p10_1_1,
+                p10_1_2,
+                p10_1_3,
+                p10_1_4,
+                p10_1_5,
+
+                p10_2_1,
+                p10_2_2,
+                p10_2_3,
+
+                p11_1_1,
+                p11_1_2,
+                p11_1_3
+        ));
+
+        // Check each set has the same parent.
+        assertEquals(p10_1_1.getParent(), p10_1_2.getParent());
+        assertEquals(p10_1_1.getParent(), p10_1_3.getParent());
+        assertEquals(p10_1_1.getParent(), p10_1_4.getParent());
+        assertEquals(p10_1_1.getParent(), p10_1_5.getParent());
+
+        assertEquals(p10_2_1.getParent(), p10_2_2.getParent());
+        assertEquals(p10_2_1.getParent(), p10_2_3.getParent());
+
+        assertEquals(p11_1_1.getParent(), p11_1_2.getParent());
+        assertEquals(p11_1_1.getParent(), p11_1_3.getParent());
+
+        // Check the parents are still unique.
+        assertAllUnique(Arrays.asList(
+                p10_1_1.getParent(),
+                p10_2_1.getParent(),
+                p11_1_1.getParent()
+        ));
+
+        // All files created at the same time for the same package/user, expcet for the first ones,
+        // will have "_" in the path.
+        assertFalse(p10_1_1.getName().contains("_"));
+        assertTrue(p10_1_2.getName().contains("_"));
+        assertFalse(p10_1_3.getName().contains("_"));
+        assertTrue(p10_1_4.getName().contains("_"));
+        assertTrue(p10_1_5.getName().contains("_"));
+
+        assertFalse(p10_2_1.getName().contains("_"));
+        assertTrue(p10_2_2.getName().contains("_"));
+        assertFalse(p10_2_3.getName().contains("_"));
+
+        assertFalse(p11_1_1.getName().contains("_"));
+        assertTrue(p11_1_2.getName().contains("_"));
+        assertFalse(p11_1_3.getName().contains("_"));
+    }
+
     // TODO: updateShortcuts()
     // TODO: getPinnedShortcuts()
 
@@ -860,9 +1230,10 @@ public class ShortcutManagerTest extends AndroidTestCase {
 
         // Get dynamic
         assertAllDynamic(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
+                assertAllNotKeyFieldsOnly(
                 mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_1,
                         /* activity =*/ null,
-                    ShortcutQuery.FLAG_GET_DYNAMIC, getCallingUserId()),
+                    ShortcutQuery.FLAG_GET_DYNAMIC, getCallingUserId())),
                 "s1", "s2"))));
 
         // Get pinned
@@ -874,18 +1245,20 @@ public class ShortcutManagerTest extends AndroidTestCase {
 
         // Get both, with timestamp
         assertAllDynamic(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
-                mInternal.getShortcuts(getCallingPackage(),  /* time =*/ 1000, CALLING_PACKAGE_2,
+                assertAllNotKeyFieldsOnly(mInternal.getShortcuts(getCallingPackage(),
+                        /* time =*/ 1000, CALLING_PACKAGE_2,
                         /* activity =*/ null,
                         ShortcutQuery.FLAG_GET_PINNED | ShortcutQuery.FLAG_GET_DYNAMIC,
-                        getCallingUserId()),
+                        getCallingUserId())),
                 "s2", "s3"))));
 
         // FLAG_GET_KEY_FIELDS_ONLY
         assertAllDynamic(assertAllNotHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
-                mInternal.getShortcuts(getCallingPackage(), /* time =*/ 1000, CALLING_PACKAGE_2,
+                assertAllKeyFieldsOnly(mInternal.getShortcuts(getCallingPackage(),
+                        /* time =*/ 1000, CALLING_PACKAGE_2,
                         /* activity =*/ null,
                         ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY,
-                        getCallingUserId()),
+                        getCallingUserId())),
                 "s2", "s3"))));
 
         // Pin some shortcuts.
@@ -894,19 +1267,20 @@ public class ShortcutManagerTest extends AndroidTestCase {
 
         // Pinned ones only
         assertAllPinned(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds(
-                mInternal.getShortcuts(getCallingPackage(), /* time =*/ 1000, CALLING_PACKAGE_2,
+                assertAllNotKeyFieldsOnly(mInternal.getShortcuts(getCallingPackage(),
+                        /* time =*/ 1000, CALLING_PACKAGE_2,
                         /* activity =*/ null,
                         ShortcutQuery.FLAG_GET_PINNED,
-                        getCallingUserId()),
+                        getCallingUserId())),
                 "s3"))));
 
         // All packages.
-        assertShortcutIds(
+        assertShortcutIds(assertAllNotKeyFieldsOnly(
                 mInternal.getShortcuts(getCallingPackage(),
                         /* time =*/ 5000, /* package= */ null,
                         /* activity =*/ null,
                         ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED,
-                        getCallingUserId()),
+                        getCallingUserId())),
                 "s1", "s3");
 
         // TODO More tests: pinned but dynamic, filter by activity
@@ -968,8 +1342,9 @@ public class ShortcutManagerTest extends AndroidTestCase {
 
         // Note we don't guarantee the orders.
         list = assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents(
+                assertAllNotKeyFieldsOnly(
                 mInternal.getShortcutInfo(getCallingPackage(), CALLING_PACKAGE_1,
-                Arrays.asList("s2", "s1", "s3", null), getCallingUserId()))),
+                Arrays.asList("s2", "s1", "s3", null), getCallingUserId())))),
                 "s1", "s2");
         assertEquals("Title 1", findById(list, "s1").getTitle());
         assertEquals("Title 2", findById(list, "s2").getTitle());
@@ -1036,19 +1411,19 @@ public class ShortcutManagerTest extends AndroidTestCase {
         setCaller(LAUNCHER_1);
 
         // CALLING_PACKAGE_1 deleted s2, but it's pinned, so it still exists.
-        assertShortcutIds(assertAllPinned(
+        assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
                 mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_1,
-                /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId())),
+                /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId()))),
                 "s2");
 
-        assertShortcutIds(assertAllPinned(
+        assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
                 mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_2,
-                /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId())),
+                /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId()))),
                 "s3", "s4");
 
-        assertShortcutIds(assertAllPinned(
+        assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(
                 mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_3,
-                /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId()))
+                /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId())))
                 /* none */);
     }