From 5504622fb01ab9774b5e73d05f86ee03a8b68ab7 Mon Sep 17 00:00:00 2001 From: Makoto Onuki Date: Tue, 8 Mar 2016 10:49:47 -0800 Subject: [PATCH] ShortcutManager: add remaining APIs. - 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 --- api/current.txt | 4 +- api/system-current.txt | 4 +- api/test-current.txt | 4 +- .../android/app/SystemServiceRegistry.java | 11 +- .../android/content/pm/ILauncherApps.aidl | 6 + .../android/content/pm/IShortcutService.aidl | 2 + .../java/android/content/pm/LauncherApps.java | 15 +- .../java/android/content/pm/ShortcutInfo.java | 171 +++-- .../android/content/pm/ShortcutManager.java | 11 + .../content/pm/ShortcutServiceInternal.java | 8 + core/java/android/os/PersistableBundle.java | 2 + .../java/android/graphics/drawable/Icon.java | 5 + .../server/pm/LauncherAppsService.java | 21 + .../android/server/pm/ShortcutService.java | 700 ++++++++++++++---- .../java/com/android/server/SystemServer.java | 5 +- .../res/drawable-nodpi/black_1024x4096.png | Bin 0 -> 12390 bytes .../res/drawable-nodpi/black_16x64.png | Bin 0 -> 160 bytes .../res/drawable-nodpi/black_32x32.png | Bin 0 -> 159 bytes .../res/drawable-nodpi/black_4096x1024.png | Bin 0 -> 14853 bytes .../res/drawable-nodpi/black_4096x4096.png | Bin 0 -> 49129 bytes .../res/drawable-nodpi/black_512x512.png | Bin 0 -> 919 bytes .../res/drawable-nodpi/black_64x16.png | Bin 0 -> 159 bytes .../res/drawable-nodpi/black_64x64.png | Bin 0 -> 168 bytes .../res/drawable-nodpi/icon1.png | Bin 0 -> 5810 bytes .../res/drawable-nodpi/icon2.png | Bin 0 -> 3180 bytes .../server/pm/ShortcutManagerTest.java | 439 ++++++++++- 26 files changed, 1172 insertions(+), 236 deletions(-) create mode 100644 services/tests/servicestests/res/drawable-nodpi/black_1024x4096.png create mode 100644 services/tests/servicestests/res/drawable-nodpi/black_16x64.png create mode 100644 services/tests/servicestests/res/drawable-nodpi/black_32x32.png create mode 100644 services/tests/servicestests/res/drawable-nodpi/black_4096x1024.png create mode 100644 services/tests/servicestests/res/drawable-nodpi/black_4096x4096.png create mode 100644 services/tests/servicestests/res/drawable-nodpi/black_512x512.png create mode 100644 services/tests/servicestests/res/drawable-nodpi/black_64x16.png create mode 100644 services/tests/servicestests/res/drawable-nodpi/black_64x64.png create mode 100644 services/tests/servicestests/res/drawable-nodpi/icon1.png create mode 100644 services/tests/servicestests/res/drawable-nodpi/icon2.png diff --git a/api/current.txt b/api/current.txt index cc386db74ef4e..bc09c6bae68da 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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 getDynamicShortcuts(); + method public int getIconMaxDimensions(); method public int getMaxDynamicShortcutCount(); method public java.util.List 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); diff --git a/api/system-current.txt b/api/system-current.txt index 7823b3cd16ba0..43b9be9121f49 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -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 getDynamicShortcuts(); + method public int getIconMaxDimensions(); method public int getMaxDynamicShortcutCount(); method public java.util.List 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); diff --git a/api/test-current.txt b/api/test-current.txt index a478793a70bf8..2c1e2486d7a02 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -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 getDynamicShortcuts(); + method public int getIconMaxDimensions(); method public int getMaxDynamicShortcutCount(); method public java.util.List 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); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 3a5dd30bfda71..bd321ac4fe80c 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -753,12 +753,11 @@ final class SystemServiceRegistry { registerService(Context.SHORTCUT_SERVICE, ShortcutManager.class, new CachedServiceFetcher() { - @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)); + }}); } /** diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index cf3e298fd6d70..b1d3f207f82f6 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -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); } diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl index 23e671d82f123..8f9dcfc15d632 100644 --- a/core/java/android/content/pm/IShortcutService.aidl +++ b/core/java/android/content/pm/IShortcutService.aidl @@ -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 diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 8e4a0e20097ee..a6a732ea95135 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -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(); + } } /** diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index 65205636c98e8..83a70cd1a7695 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -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; } } @@ -238,6 +259,39 @@ public class ShortcutInfo implements Parcelable { updateTimestamp(); } + /** + * @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. */ @@ -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. + *
    + *
  • Tints are not supported. + *
  • Bitmaps, resources and "content:" URIs are supported. + *
  • "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. + *
* - * TODO Only allow Bitmap, Resource and URI types. byte[] type can easily go over - * binder size limit. + *

For performance reasons, icons will NOT 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 NOT 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. + * + *

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. + * + *

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. + *

    + *
  • {@link #getId()} + *
  • {@link #getPackageName()} + *
  • {@link #getLastChangedTimestamp()} + *
  • {@link #isDynamic()} + *
  • {@link #isPinned()} + *
  • {@link #hasIconResource()} + *
  • {@link #hasIconFile()} + *
+ */ + 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; + public void clearIcon() { + mIcon = null; } /** @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; - } - - /** @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); diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index 4c51d499ab4a2..b247f65c94c3f 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -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() { diff --git a/core/java/android/content/pm/ShortcutServiceInternal.java b/core/java/android/content/pm/ShortcutServiceInternal.java index 3d6028a6ffe59..918c763545fab 100644 --- a/core/java/android/content/pm/ShortcutServiceInternal.java +++ b/core/java/android/content/pm/ShortcutServiceInternal.java @@ -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); } diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java index f36bb2989c72c..5872f7438904e 100644 --- a/core/java/android/os/PersistableBundle.java +++ b/core/java/android/os/PersistableBundle.java @@ -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()); diff --git a/graphics/java/android/graphics/drawable/Icon.java b/graphics/java/android/graphics/drawable/Icon.java index 0de4c2cffa17e..51221b4a303e9 100644 --- a/graphics/java/android/graphics/drawable/Icon.java +++ b/graphics/java/android/graphics/drawable/Icon.java @@ -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. * diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 56128708599b3..e90fb3208d858 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -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; @@ -328,6 +329,26 @@ public class LauncherAppsService extends SystemService { ids, user.getIdentifier()); } + @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) diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 423767ad87b44..8982632666b7f 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -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 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 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 shortcutIds) { + public void replacePinned(@NonNull ShortcutService s, String launcherPackage, + List 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 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 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,24 +1276,60 @@ public class ShortcutService extends IShortcutService.Stub { "Activity package name mismatch"); } - shortcut.enforceMandatoryFields(); - - final Intent intent = shortcut.getIntent(); - final Bundle intentExtras = intent.getExtras(); - if (intentExtras != null && intentExtras.size() > 0) { - intent.replaceExtras((Bundle) null); - - // PersistableBundle's constructor will throw IllegalArgumentException if original - // extras contain something not persistable. - shortcut.setIntentPersistableExtras(new PersistableBundle(intentExtras)); + if (!forUpdate) { + shortcut.enforceMandatoryFields(); + } + if (shortcut.getIcon() != null) { + ShortcutInfo.validateIcon(shortcut.getIcon()); } - // TODO Save the icon - shortcut.setIcon(null); + validateForXml(shortcut.getId()); + validateForXml(shortcut.getTitle()); + validatePersistableBundleForXml(shortcut.getIntentPersistableExtras()); + validatePersistableBundleForXml(shortcut.getExtras()); 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); + } + } + } + + 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); + } + } + } + + private static boolean isAllowedInXml(char c) { + return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); + } + // === APIs === @Override @@ -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 newShortcuts = (List) 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 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 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 @@ -1523,6 +1926,11 @@ public class ShortcutService extends IShortcutService.Stub { mMaxDailyUpdates = max; } + @VisibleForTesting + void setMaxIconDimensionForTest(int dimension) { + mMaxIconDimension = dimension; + } + @VisibleForTesting public void setResetIntervalForTest(long interval) { mResetInterval = interval; diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 59f8284a52002..e7daaa15d2473 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -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 index 0000000000000000000000000000000000000000..f70032698896e90964d79f9115cab0d1a88646e4 GIT binary patch literal 12390 zcmeI&y-Gtd6u|Kld$k|ZR&=oHa(BhuP3^t*W@x#HqHapPV27$`K&M)Da#B}uba2wq z*>?~`LFfZW-@t0(1Goz1e;}OXKuGc%54nhIm3(e8Cn9+r15(ej@)Wnk16ovB4k_-iRPv3y>Mm}+% yA}LQ7#}JR>Zx1#CIShvv{0xqN$PmiNz)-`+`sf17=nRa|z8>=0!ju$r9Iy66gHf+|;}h2Ir#G#FEq$h4Rdj3vD vPZ!4!kK=C-8ZrWThZcPGf1Aj=0LaYaQD9)~@sn8!WHWfW`njxgN@xNASrjG> literal 0 HcmV?d00001 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 index 0000000000000000000000000000000000000000..f6750301773439aef38b7811f2191049550798bf GIT binary patch literal 14853 zcmeHNzb`{k82$Q#KK&ttL@S0jbdX4-i5M^_ZPkmkN+cL0ZD>+MOLz%`;prk0e}Rw? zJFyebW;D?-uvye37V+a=^!X%WGM<|>_xoP*Uel9v&i77p*Hg*3&)w`MBA+%G9VMz0 zrIQr9*d9`zZL!tN4^A!;c^c%$Mw?rHanYq~iJ0rwQAJ&C%EfTExD?Q16MAGWpU)O_ ziY#O^dUi%#n$>e^TuY?JS8JPyRMMh-W6M|f*M0S*dd-k|a+jGt9tu9*^mMqyLn^CO z)+(jkp98#8P2Q2Mnzu@bcVWZETf3Kcb!pxiO(umXY= zzzPUf04pF^fk-x{l@(-xOoHS^$d`l^4J#m6!T(yp%2U~JiXjs|f4eLzk89TC38_{o z3e(^nIplwO*77P=DGJ;Oa2OxJ9H@iQ0oI{%G@uTsgYyA{m7@W5KpmV9PzOf?>JaMa zJZKl0C9o*Z0(^sm41x^u%?6}bBy`3GLIzPe9GD}&zhxcHxq=d>;_FEvzSHacz^wXn z`@+vJkGNSY9kxnQ7`7kB#c(6QVF-XZPzR#}tOLHv=z!UQ!OG}BczNJxKpjvA=L6Ki z(SSOj4$cRtgQEd;KpmV9PzOij4|U}B(v2I%k&t`fwAEh~5qIq#xnItvT1xT(S}Yme I?@t%s0doexr2qf` literal 0 HcmV?d00001 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 index 0000000000000000000000000000000000000000..999d8585fc6fe13dc429c2d435f1ee9d79f6e54a GIT binary patch literal 49129 zcmeI*J!=#}7y#f|b0KFWF(SdQ!V?5bku+C$m`m%~H0oEHVRW`W-K<|M+_>DlR2YjVYG>~B?+Kw0;!1hy=7-g#D7~chXnp=u zKE1m_fB*pk1PBlyK!5-N0t5(TTA;OjuPdDl2!-~$leN6Zy1 ze4cCMvJsLf2oNAZfB*pk1PBlyK!CviBd|D^Tlj&s0G&VvzpM;z%nQ73FYcP^)Jr`9 z0t5&UAV7cs0RjXF5Fn5lf%g&#WM(V{LVy4P0t5&UAV7cs0Rq_-cs@9E{3q4|bcB&e zpd*b20t5&UAV7cs0RjXF5Fn6A0f_`MIjn*rK!5-N0t5&UAV7csfqzus=EaktaqI2DMxYLc!wY@}$3J8UWdw0X!DtAKxDYsy&$NYs@&65$0Fc)`UHx3v IIVCg!0Fn+U8UO$Q literal 0 HcmV?d00001 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 index 0000000000000000000000000000000000000000..58830151997536b9fa126e23770cceebf4860c16 GIT binary patch literal 159 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 literal 0 HcmV?d00001 diff --git a/services/tests/servicestests/res/drawable-nodpi/icon2.png b/services/tests/servicestests/res/drawable-nodpi/icon2.png new file mode 100644 index 0000000000000000000000000000000000000000..75024841d327c4fbaefef7c8e9c8d0e892895f93 GIT binary patch literal 3180 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 S`hb-H0000 assertShortcutIds(@NonNull List actualShortcuts, String... expectedIds) { + assertEquals(expectedIds.length, actualShortcuts.size()); final HashSet expected = new HashSet<>(Arrays.asList(expectedIds)); final HashSet actual = new HashSet<>(); for (ShortcutInfo s : actualShortcuts) { @@ -460,6 +515,35 @@ public class ShortcutManagerTest extends AndroidTestCase { return actualShortcuts; } + @NonNull + private List assertAllNotHaveIcon( + @NonNull List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertNull("ID " + s.getId(), s.getIcon()); + } + return actualShortcuts; + } + + @NonNull + private List assertAllHaveIconResId( + @NonNull List 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 assertAllHaveIconFile( + @NonNull List 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 assertAllHaveFlags(@NonNull List actualShortcuts, int shortcutFlags) { @@ -469,6 +553,24 @@ public class ShortcutManagerTest extends AndroidTestCase { return actualShortcuts; } + @NonNull + private List assertAllKeyFieldsOnly( + @NonNull List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), s.hasKeyFieldsOnly()); + } + return actualShortcuts; + } + + @NonNull + private List assertAllNotKeyFieldsOnly( + @NonNull List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertFalse("ID " + s.getId(), s.hasKeyFieldsOnly()); + } + return actualShortcuts; + } + @NonNull private List assertAllDynamic(@NonNull List 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 void assertAllUnique(Collection list) { + final Set 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 */); }