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);
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
}
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();
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);
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);
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
}
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();
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);
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);
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
}
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();
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);
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));
+ }});
}
/**
import android.graphics.Rect;
import android.os.Bundle;
import android.os.UserHandle;
+import android.os.ParcelFileDescriptor;
+
import java.util.List;
/**
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);
}
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
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;
*/
@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();
+ }
}
/**
@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();
+ }
}
/**
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;
/* @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 = {
FLAG_PINNED,
FLAG_HAS_ICON_RES,
FLAG_HAS_ICON_FILE,
+ FLAG_KEY_FIELDS_ONLY,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ShortcutFlags {}
@NonNull
private String mTitle;
+ /**
+ * Intent *with extras removed*.
+ */
@NonNull
private Intent mIntent;
- // Internal use only.
+ /**
+ * Extras for the intent.
+ */
@NonNull
private PersistableBundle mIntentPersistableExtras;
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();
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;
}
mExtras = source.mExtras;
mIconResourceId = source.mIconResourceId;
mBitmapPath = source.mBitmapPath;
+
+ } else {
+ // Set this bit.
+ mFlags |= FLAG_KEY_FIELDS_ONLY;
}
}
}
/**
+ * @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 {
}
/**
- * 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) {
/**
* 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;
}
}
/**
- * 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
}
/**
- * 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) {
}
/**
- * Return the ID of the shortcut.
+ * Return the package name of the creator application.
*/
@NonNull
public String getPackageName() {
*
* 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
*/
/**
* 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;
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();
}
/** @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 */
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);
}
}
+ /**
+ * 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() {
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;
@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);
}
* @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());
return this;
}
+ /** @hide */
+ public boolean hasTint() {
+ return (mTintList != null) || (mTintMode != DEFAULT_TINT_MODE);
+ }
+
/**
* Create an Icon pointing to an image file specified by path.
*
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;
}
@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 {
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;
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;
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;
/**
* 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
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.
@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;
* 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.
*/
/**
* # 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);
}
/**
// 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) {
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);
}
/**
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")
if (getApiCallCount(s) >= s.mMaxDailyUpdates) {
return false;
}
- mApiCallCountInner++;
+ mApiCallCount++;
return true;
}
@GuardedBy("mLock")
public void resetRateLimitingForCommandLine() {
- mApiCallCountInner = 0;
+ mApiCallCount = 0;
mLastResetTime = 0;
}
/**
* 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());
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) {
// 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.
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 {
}
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;
}
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;
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;
}
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;
// === 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);
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;
}
* - 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(
"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 ===
// 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++) {
}
}
userPackageChanged(packageName, userId);
-
return true;
}
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);
}
// Validate the shortcut.
- fixUpIncomingShortcutInfo(newShortcut);
+ fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false);
// Add it.
newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided");
synchronized (mLock) {
- getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(shortcutId);
+ getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId);
}
userPackageChanged(packageName, userId);
}
verifyCaller(packageName, userId);
synchronized (mLock) {
- getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts();
+ getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
}
userPackageChanged(packageName, userId);
}
}
}
+ @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.
*/
mRawLastResetTime = injectCurrentTimeMillis();
}
scheduleSaveBaseState();
+ Slog.i(TAG, "ShortcutManager: throttling counter reset");
}
/**
} 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);
Preconditions.checkNotNull(shortcutIds, "shortcutIds");
synchronized (mLock) {
- getPackageShortcutsLocked(packageName, userId).pinAll(shortcutIds);
+ getPackageShortcutsLocked(packageName, userId).replacePinned(
+ ShortcutService.this, callingPackage, shortcutIds);
}
userPackageChanged(packageName, userId);
}
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();
}
}
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 ===
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));
}
-
}
}
}
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;
}
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) {
}
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
}
@VisibleForTesting
+ void setMaxIconDimensionForTest(int dimension) {
+ mMaxIconDimension = dimension;
+ }
+
+ @VisibleForTesting
public void setResetIntervalForTest(long interval) {
mResetInterval = interval;
}
// 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) {
}
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
+ // LauncherAppsService uses ShortcutService.
+ mSystemServiceManager.startService(ShortcutService.Lifecycle.class);
mSystemServiceManager.startService(LauncherAppsService.class);
}
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;
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.
-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";
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. */
setResetIntervalForTest(INTERVAL);
setMaxDynamicShortcutsForTest(MAX_SHORTCUTS);
setMaxDailyUpdatesForTest(MAX_DAILY_UPDATES);
+ setMaxIconDimensionForTest(MAX_ICON_DIMENSION);
}
@Override
}
@Override
- int injectGetPackageUid(String packageName) {
+ int injectGetPackageUid(String packageName, int userId) {
Integer uid = mInjectedPackageUidMap.get(packageName);
return uid != null ? uid : -1;
}
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. */
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();
/** 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() {
}
/**
+ * 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) {
@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) {
}
@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) {
}
@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);
}
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.
*/
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()));
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
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());
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());
// Now it should work.
mInjectedCurrentTimeLillis++;
- assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1)));
+ assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); // fail
assertEquals(2, mManager.getRemainingCallCount());
mInjectedCurrentTimeLillis++;
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()
// 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
// 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.
// 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
// 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());
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 */);
}