diff --git a/Android.mk b/Android.mk index 3ac5889f61c33..93ea66134cd50 100644 --- a/Android.mk +++ b/Android.mk @@ -149,6 +149,7 @@ LOCAL_SRC_FILES += \ core/java/android/content/pm/IPackageMoveObserver.aidl \ core/java/android/content/pm/IPackageStatsObserver.aidl \ core/java/android/content/pm/IOnPermissionsChangeListener.aidl \ + core/java/android/content/pm/IShortcutService.aidl \ core/java/android/database/IContentObserver.aidl \ ../av/camera/aidl/android/hardware/ICameraService.aidl \ ../av/camera/aidl/android/hardware/ICameraServiceListener.aidl \ @@ -650,6 +651,7 @@ aidl_files := \ frameworks/base/core/java/android/content/pm/ProviderInfo.aidl \ frameworks/base/core/java/android/content/pm/PackageStats.aidl \ frameworks/base/core/java/android/content/pm/PermissionGroupInfo.aidl \ + frameworks/base/core/java/android/content/pm/ShortcutInfo.aidl \ frameworks/base/core/java/android/content/pm/LabeledIntent.aidl \ frameworks/base/core/java/android/content/ComponentName.aidl \ frameworks/base/core/java/android/content/SyncStats.aidl \ diff --git a/api/current.txt b/api/current.txt index 799ad0fd18512..27935d6c89b1a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -8138,6 +8138,7 @@ package android.content { field public static final java.lang.String RESTRICTIONS_SERVICE = "restrictions"; field public static final java.lang.String SEARCH_SERVICE = "search"; field public static final java.lang.String SENSOR_SERVICE = "sensor"; + field public static final java.lang.String SHORTCUT_SERVICE = "shortcut"; field public static final java.lang.String STORAGE_SERVICE = "storage"; field public static final java.lang.String TELECOM_SERVICE = "telecom"; field public static final java.lang.String TELEPHONY_SERVICE = "phone"; @@ -9443,13 +9444,19 @@ package android.content.pm { public class LauncherApps { method public java.util.List getActivityList(java.lang.String, android.os.UserHandle); method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int, android.os.UserHandle); + method public android.os.ParcelFileDescriptor getShortcutIconFd(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public int getShortcutIconResId(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public java.util.List getShortcutInfo(java.lang.String, java.util.List, android.os.UserHandle); + method public java.util.List getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); method public boolean isActivityEnabled(android.content.ComponentName, android.os.UserHandle); method public boolean isPackageEnabled(java.lang.String, android.os.UserHandle); + method public void pinShortcuts(java.lang.String, java.util.List, android.os.UserHandle); method public void registerCallback(android.content.pm.LauncherApps.Callback); method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle); method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); + method public void startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle, android.os.UserHandle); method public void unregisterCallback(android.content.pm.LauncherApps.Callback); } @@ -9462,6 +9469,18 @@ package android.content.pm { method public void onPackagesSuspended(java.lang.String[], android.os.UserHandle); method public abstract void onPackagesUnavailable(java.lang.String[], android.os.UserHandle, boolean); method public void onPackagesUnsuspended(java.lang.String[], android.os.UserHandle); + method public void onShortcutsChanged(java.lang.String, java.util.List, android.os.UserHandle); + } + + public static class LauncherApps.ShortcutQuery { + ctor public LauncherApps.ShortcutQuery(); + method public void setActivity(android.content.ComponentName); + method public void setChangedSince(long); + method public void setPackage(java.lang.String); + method public void setQueryFlags(int); + field public static final int FLAG_GET_DYNAMIC = 1; // 0x1 + field public static final int FLAG_GET_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_GET_PINNED = 2; // 0x2 } public class PackageInfo implements android.os.Parcelable { @@ -9961,6 +9980,56 @@ package android.content.pm { field public java.lang.String permission; } + public class ShortcutInfo implements android.os.Parcelable { + method public int describeContents(); + method public android.content.ComponentName getActivityComponent(); + method public android.os.PersistableBundle getExtras(); + method public java.lang.String getId(); + method public android.content.Intent getIntent(); + method public long getLastChangedTimestamp(); + method public java.lang.String getPackageName(); + method public java.lang.String getTitle(); + method public int getWeight(); + method public boolean hasIconFile(); + method public boolean hasIconResource(); + method public boolean isDynamic(); + method public boolean isPinned(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1 + field public static final int CLONE_REMOVE_FOR_LAUNCHER = 3; // 0x3 + field public static final int CLONE_REMOVE_NON_KEY_INFO = 4; // 0x4 + field public static final android.os.Parcelable.Creator CREATOR; + 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_PINNED = 2; // 0x2 + } + + public static class ShortcutInfo.Builder { + ctor public ShortcutInfo.Builder(android.content.Context); + method public android.content.pm.ShortcutInfo build(); + method public android.content.pm.ShortcutInfo.Builder setActivityComponent(android.content.ComponentName); + method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); + method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); + method public android.content.pm.ShortcutInfo.Builder setId(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setIntent(android.content.Intent); + method public android.content.pm.ShortcutInfo.Builder setTitle(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setWeight(int); + } + + public class ShortcutManager { + method public boolean addDynamicShortcut(android.content.pm.ShortcutInfo); + method public void deleteAllDynamicShortcuts(); + method public void deleteDynamicShortcut(java.lang.String); + method public java.util.List getDynamicShortcuts(); + method public int getMaxDynamicShortcutCount(); + method public java.util.List getPinnedShortcuts(); + method public long getRateLimitResetTime(); + method public int getRemainingCallCount(); + method public boolean setDynamicShortcuts(java.util.List); + method public boolean updateShortcuts(java.util.List); + } + public class Signature implements android.os.Parcelable { ctor public Signature(byte[]); ctor public Signature(java.lang.String); @@ -29003,6 +29072,7 @@ 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 9a84d1ade8ce8..27de329418ad6 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -8444,6 +8444,7 @@ package android.content { field public static final java.lang.String RESTRICTIONS_SERVICE = "restrictions"; field public static final java.lang.String SEARCH_SERVICE = "search"; field public static final java.lang.String SENSOR_SERVICE = "sensor"; + field public static final java.lang.String SHORTCUT_SERVICE = "shortcut"; field public static final java.lang.String STORAGE_SERVICE = "storage"; field public static final java.lang.String TELECOM_SERVICE = "telecom"; field public static final java.lang.String TELEPHONY_SERVICE = "phone"; @@ -9777,13 +9778,19 @@ package android.content.pm { public class LauncherApps { method public java.util.List getActivityList(java.lang.String, android.os.UserHandle); method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int, android.os.UserHandle); + method public android.os.ParcelFileDescriptor getShortcutIconFd(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public int getShortcutIconResId(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public java.util.List getShortcutInfo(java.lang.String, java.util.List, android.os.UserHandle); + method public java.util.List getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); method public boolean isActivityEnabled(android.content.ComponentName, android.os.UserHandle); method public boolean isPackageEnabled(java.lang.String, android.os.UserHandle); + method public void pinShortcuts(java.lang.String, java.util.List, android.os.UserHandle); method public void registerCallback(android.content.pm.LauncherApps.Callback); method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle); method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); + method public void startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle, android.os.UserHandle); method public void unregisterCallback(android.content.pm.LauncherApps.Callback); } @@ -9796,6 +9803,18 @@ package android.content.pm { method public void onPackagesSuspended(java.lang.String[], android.os.UserHandle); method public abstract void onPackagesUnavailable(java.lang.String[], android.os.UserHandle, boolean); method public void onPackagesUnsuspended(java.lang.String[], android.os.UserHandle); + method public void onShortcutsChanged(java.lang.String, java.util.List, android.os.UserHandle); + } + + public static class LauncherApps.ShortcutQuery { + ctor public LauncherApps.ShortcutQuery(); + method public void setActivity(android.content.ComponentName); + method public void setChangedSince(long); + method public void setPackage(java.lang.String); + method public void setQueryFlags(int); + field public static final int FLAG_GET_DYNAMIC = 1; // 0x1 + field public static final int FLAG_GET_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_GET_PINNED = 2; // 0x2 } public class PackageInfo implements android.os.Parcelable { @@ -10355,6 +10374,56 @@ package android.content.pm { field public java.lang.String permission; } + public class ShortcutInfo implements android.os.Parcelable { + method public int describeContents(); + method public android.content.ComponentName getActivityComponent(); + method public android.os.PersistableBundle getExtras(); + method public java.lang.String getId(); + method public android.content.Intent getIntent(); + method public long getLastChangedTimestamp(); + method public java.lang.String getPackageName(); + method public java.lang.String getTitle(); + method public int getWeight(); + method public boolean hasIconFile(); + method public boolean hasIconResource(); + method public boolean isDynamic(); + method public boolean isPinned(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1 + field public static final int CLONE_REMOVE_FOR_LAUNCHER = 3; // 0x3 + field public static final int CLONE_REMOVE_NON_KEY_INFO = 4; // 0x4 + field public static final android.os.Parcelable.Creator CREATOR; + 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_PINNED = 2; // 0x2 + } + + public static class ShortcutInfo.Builder { + ctor public ShortcutInfo.Builder(android.content.Context); + method public android.content.pm.ShortcutInfo build(); + method public android.content.pm.ShortcutInfo.Builder setActivityComponent(android.content.ComponentName); + method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); + method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); + method public android.content.pm.ShortcutInfo.Builder setId(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setIntent(android.content.Intent); + method public android.content.pm.ShortcutInfo.Builder setTitle(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setWeight(int); + } + + public class ShortcutManager { + method public boolean addDynamicShortcut(android.content.pm.ShortcutInfo); + method public void deleteAllDynamicShortcuts(); + method public void deleteDynamicShortcut(java.lang.String); + method public java.util.List getDynamicShortcuts(); + method public int getMaxDynamicShortcutCount(); + method public java.util.List getPinnedShortcuts(); + method public long getRateLimitResetTime(); + method public int getRemainingCallCount(); + method public boolean setDynamicShortcuts(java.util.List); + method public boolean updateShortcuts(java.util.List); + } + public class Signature implements android.os.Parcelable { ctor public Signature(byte[]); ctor public Signature(java.lang.String); @@ -31288,6 +31357,7 @@ 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 f18f4e1cd1c2d..707f489b57684 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -8144,6 +8144,7 @@ package android.content { field public static final java.lang.String RESTRICTIONS_SERVICE = "restrictions"; field public static final java.lang.String SEARCH_SERVICE = "search"; field public static final java.lang.String SENSOR_SERVICE = "sensor"; + field public static final java.lang.String SHORTCUT_SERVICE = "shortcut"; field public static final java.lang.String STORAGE_SERVICE = "storage"; field public static final java.lang.String TELECOM_SERVICE = "telecom"; field public static final java.lang.String TELEPHONY_SERVICE = "phone"; @@ -9452,13 +9453,19 @@ package android.content.pm { public class LauncherApps { method public java.util.List getActivityList(java.lang.String, android.os.UserHandle); method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int, android.os.UserHandle); + method public android.os.ParcelFileDescriptor getShortcutIconFd(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public int getShortcutIconResId(android.content.pm.ShortcutInfo, android.os.UserHandle); + method public java.util.List getShortcutInfo(java.lang.String, java.util.List, android.os.UserHandle); + method public java.util.List getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); method public boolean isActivityEnabled(android.content.ComponentName, android.os.UserHandle); method public boolean isPackageEnabled(java.lang.String, android.os.UserHandle); + method public void pinShortcuts(java.lang.String, java.util.List, android.os.UserHandle); method public void registerCallback(android.content.pm.LauncherApps.Callback); method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle); method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); + method public void startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle, android.os.UserHandle); method public void unregisterCallback(android.content.pm.LauncherApps.Callback); } @@ -9471,6 +9478,18 @@ package android.content.pm { method public void onPackagesSuspended(java.lang.String[], android.os.UserHandle); method public abstract void onPackagesUnavailable(java.lang.String[], android.os.UserHandle, boolean); method public void onPackagesUnsuspended(java.lang.String[], android.os.UserHandle); + method public void onShortcutsChanged(java.lang.String, java.util.List, android.os.UserHandle); + } + + public static class LauncherApps.ShortcutQuery { + ctor public LauncherApps.ShortcutQuery(); + method public void setActivity(android.content.ComponentName); + method public void setChangedSince(long); + method public void setPackage(java.lang.String); + method public void setQueryFlags(int); + field public static final int FLAG_GET_DYNAMIC = 1; // 0x1 + field public static final int FLAG_GET_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_GET_PINNED = 2; // 0x2 } public class PackageInfo implements android.os.Parcelable { @@ -9971,6 +9990,56 @@ package android.content.pm { field public java.lang.String permission; } + public class ShortcutInfo implements android.os.Parcelable { + method public int describeContents(); + method public android.content.ComponentName getActivityComponent(); + method public android.os.PersistableBundle getExtras(); + method public java.lang.String getId(); + method public android.content.Intent getIntent(); + method public long getLastChangedTimestamp(); + method public java.lang.String getPackageName(); + method public java.lang.String getTitle(); + method public int getWeight(); + method public boolean hasIconFile(); + method public boolean hasIconResource(); + method public boolean isDynamic(); + method public boolean isPinned(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1 + field public static final int CLONE_REMOVE_FOR_LAUNCHER = 3; // 0x3 + field public static final int CLONE_REMOVE_NON_KEY_INFO = 4; // 0x4 + field public static final android.os.Parcelable.Creator CREATOR; + 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_PINNED = 2; // 0x2 + } + + public static class ShortcutInfo.Builder { + ctor public ShortcutInfo.Builder(android.content.Context); + method public android.content.pm.ShortcutInfo build(); + method public android.content.pm.ShortcutInfo.Builder setActivityComponent(android.content.ComponentName); + method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); + method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); + method public android.content.pm.ShortcutInfo.Builder setId(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setIntent(android.content.Intent); + method public android.content.pm.ShortcutInfo.Builder setTitle(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setWeight(int); + } + + public class ShortcutManager { + method public boolean addDynamicShortcut(android.content.pm.ShortcutInfo); + method public void deleteAllDynamicShortcuts(); + method public void deleteDynamicShortcut(java.lang.String); + method public java.util.List getDynamicShortcuts(); + method public int getMaxDynamicShortcutCount(); + method public java.util.List getPinnedShortcuts(); + method public long getRateLimitResetTime(); + method public int getRemainingCallCount(); + method public boolean setDynamicShortcuts(java.util.List); + method public boolean updateShortcuts(java.util.List); + } + public class Signature implements android.os.Parcelable { ctor public Signature(byte[]); ctor public Signature(java.lang.String); @@ -29014,6 +29083,7 @@ 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 b1c5fd8c69637..3a5dd30bfda71 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -37,7 +37,9 @@ import android.content.Context; import android.content.IRestrictionsManager; import android.content.RestrictionsManager; import android.content.pm.ILauncherApps; +import android.content.pm.IShortcutService; import android.content.pm.LauncherApps; +import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.hardware.ConsumerIrManager; import android.hardware.ISerialManager; @@ -748,6 +750,15 @@ final class SystemServiceRegistry { Log.i(TAG, "Creating new instance of SoundTriggerManager object."); return new SoundTriggerManager(ctx, ISoundTriggerService.Stub.asInterface(b)); }}); + + 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)); + }}); } /** diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index b935b256b8805..f96ddf0d183a8 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2680,6 +2680,7 @@ public abstract class Context { RADIO_SERVICE, HARDWARE_PROPERTIES_SERVICE, //@hide: SOUND_TRIGGER_SERVICE, + SHORTCUT_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -3575,6 +3576,14 @@ public abstract class Context { */ public static final String HARDWARE_PROPERTIES_SERVICE = "hardwareproperties"; + /** + * TODO Javadoc + * + * @see #getSystemService + * @see android.content.pm.ShortcutManager + */ + public static final String SHORTCUT_SERVICE = "shortcut"; + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index cc266c5527600..da3c873c043e8 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -22,6 +22,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IOnAppsChangedListener; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; import android.graphics.Rect; import android.os.Bundle; import android.os.UserHandle; @@ -42,4 +43,13 @@ interface ILauncherApps { boolean isPackageEnabled(String packageName, in UserHandle user); boolean isActivityEnabled(in ComponentName component, in UserHandle user); ApplicationInfo getApplicationInfo(String packageName, int flags, in UserHandle user); + + ParceledListSlice getShortcuts(String callingPackage, long changedSince, String packageName, + in ComponentName componentName, int flags, in UserHandle user); + ParceledListSlice getShortcutInfo(String callingPackage, String packageName, in List ids, + in UserHandle user); + void pinShortcuts(String callingPackage, String packageName, in List shortcutIds, + in UserHandle user); + void startShortcut(String callingPackage, in ShortcutInfo shortcut, in Rect sourceBounds, + in Bundle startActivityOptions, in UserHandle user); } diff --git a/core/java/android/content/pm/IOnAppsChangedListener.aidl b/core/java/android/content/pm/IOnAppsChangedListener.aidl index 130369661e05a..e6525af311ed3 100644 --- a/core/java/android/content/pm/IOnAppsChangedListener.aidl +++ b/core/java/android/content/pm/IOnAppsChangedListener.aidl @@ -16,6 +16,7 @@ package android.content.pm; +import android.content.pm.ParceledListSlice; import android.os.UserHandle; /** @@ -29,4 +30,5 @@ oneway interface IOnAppsChangedListener { void onPackagesUnavailable(in UserHandle user, in String[] packageNames, boolean replacing); void onPackagesSuspended(in UserHandle user, in String[] packageNames); void onPackagesUnsuspended(in UserHandle user, in String[] packageNames); + void onShortcutChanged(in UserHandle user, String packageName, in ParceledListSlice shortcuts); } diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl new file mode 100644 index 0000000000000..23e671d82f123 --- /dev/null +++ b/core/java/android/content/pm/IShortcutService.aidl @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.content.pm; + +import android.content.pm.ParceledListSlice; +import android.content.pm.ShortcutInfo; + +/** + * {@hide} + */ +interface IShortcutService { + + boolean setDynamicShortcuts(String packageName, in ParceledListSlice shortcutInfoList, + int userId); + + ParceledListSlice getDynamicShortcuts(String packageName, int userId); + + boolean addDynamicShortcut(String packageName, in ShortcutInfo shortcutInfo, int userId); + + void deleteDynamicShortcut(String packageName, in String shortcutId, int userId); + + void deleteAllDynamicShortcuts(String packageName, int userId); + + ParceledListSlice getPinnedShortcuts(String packageName, int userId); + + boolean updateShortcuts(String packageName, in ParceledListSlice shortcuts, int userId); + + int getMaxDynamicShortcutCount(String packageName, int userId); + + int getRemainingCallCount(String packageName, int userId); + + long getRateLimitResetTime(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 e443d5073c549..f05ecd39a44d2 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -16,23 +16,29 @@ package android.content.pm; +import android.Manifest.permission; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.ILauncherApps; -import android.content.pm.IOnAppsChangedListener; import android.content.pm.PackageManager.ApplicationInfoFlags; -import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -148,6 +154,91 @@ public class LauncherApps { */ public void onPackagesUnsuspended(String[] packageNames, UserHandle user) { } + + /** + * Indicates that one or more shortcuts (which may be dynamic and/or pinned) + * have been added, updated or removed. + * + * @param packageName The name of the package that has the shortcuts. + * @param shortcuts all shortcuts from the package (dynamic and/or pinned). + * @param user The UserHandle of the profile that generated the change. + */ + public void onShortcutsChanged(@NonNull String packageName, + @NonNull List shortcuts, @NonNull UserHandle user) { + } + } + + /** + * Represents a query passed to {@link #getShortcuts(ShortcutQuery, UserHandle)}. + */ + public static class ShortcutQuery { + /** + * Include dynamic shortcuts in the result. + */ + public static final int FLAG_GET_DYNAMIC = 1 << 0; + + /** + * Include pinned shortcuts in the result. + */ + public static final int FLAG_GET_PINNED = 1 << 1; + + /** + * Requests "key" fields only. + */ + public static final int FLAG_GET_KEY_FIELDS_ONLY = 1 << 2; + + /** @hide */ + @IntDef(flag = true, + value = { + FLAG_GET_DYNAMIC, + FLAG_GET_PINNED, + FLAG_GET_KEY_FIELDS_ONLY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface QueryFlags {} + + long mChangedSince; + + @Nullable + String mPackage; + + @Nullable + ComponentName mActivity; + + @QueryFlags + int mQueryFlags; + + public ShortcutQuery() { + } + + /** + * If non-zero, returns only shortcuts that have been added or updated since the timestamp, + * which is a milliseconds since the Epoch. + */ + public void setChangedSince(long changedSince) { + mChangedSince = changedSince; + } + + /** + * If non-null, returns only shortcuts from the package. + */ + public void setPackage(@Nullable String packageName) { + mPackage = packageName; + } + + /** + * If non-null, returns only shortcuts associated with the activity. + */ + public void setActivity(@Nullable ComponentName activity) { + mActivity = activity; + } + + /** + * Set query options. + */ + public void setQueryFlags(@QueryFlags int queryFlags) { + mQueryFlags = queryFlags; + } } /** @hide */ @@ -302,6 +393,125 @@ public class LauncherApps { } } + /** + * Returns the IDs of {@link ShortcutInfo}s that match {@code query}. + * + *

Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param query result includes shortcuts matching this query. + * @param user The UserHandle of the profile. + * + * @return the IDs of {@link ShortcutInfo}s that match the query. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + @Nullable + public List getShortcuts(@NonNull ShortcutQuery query, + @NonNull UserHandle user) { + try { + return mService.getShortcuts(mContext.getPackageName(), + query.mChangedSince, query.mPackage, query.mActivity, query.mQueryFlags, user) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns {@link ShortcutInfo}s with the given IDs from a package. + * + *

Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param packageName The target package. + * @param ids IDs of the shortcuts to retrieve. + * @param user The UserHandle of the profile. + * + * @return list of {@link ShortcutInfo} associated with the package. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + @Nullable + public List getShortcutInfo(@NonNull String packageName, + @NonNull List ids, @NonNull UserHandle user) { + try { + return mService.getShortcutInfo(mContext.getPackageName(), packageName, ids, user) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Pin shortcuts on a package. + * + *

This API is NOT cumulative; this will replace all pinned shortcuts for the package. + * However, different launchers may have different set of pinned shortcuts. + * + *

Callers must have the {@link permission#BIND_APPWIDGET} permission. + * + * @param packageName The target package name. + * @param shortcutIds The IDs of the shortcut to be pinned. + * @param user The UserHandle of the profile. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + public void pinShortcuts(@NonNull String packageName, @NonNull List shortcutIds, + @NonNull UserHandle user) { + try { + mService.pinShortcuts(mContext.getPackageName(), packageName, shortcutIds, user); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the icon resource ID, if {@code shortcut} has one + * (i.e. when {@link ShortcutInfo#hasIconResource()} returns {@code true}). + * + *

Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param shortcut The target shortcut. + * @param user The UserHandle of the profile. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + public int getShortcutIconResId(@NonNull ShortcutInfo shortcut, @NonNull UserHandle user) { + throw new RuntimeException("not implemented yet"); + } + + /** + * Return the icon as {@link ParcelFileDescriptor}, when it's stored as a file + * (i.e. when {@link ShortcutInfo#hasIconFile()} returns {@code true}). + * + *

Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param shortcut The target shortcut. + * @param user The UserHandle of the profile. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + public ParcelFileDescriptor getShortcutIconFd( + @NonNull ShortcutInfo shortcut, @NonNull UserHandle user) { + throw new RuntimeException("not implemented yet"); + } + + /** + * Launches a shortcut. + * + *

Callers mut have the {@link permission#BIND_APPWIDGET} permission. + * + * @param shortcut The target shortcut. + * @param sourceBounds The Rect containing the source bounds of the clicked icon. + * @param startActivityOptions Options to pass to startActivity. + * @param user The UserHandle of the profile. + */ + @RequiresPermission(permission.BIND_APPWIDGET) + public void startShortcut(@NonNull ShortcutInfo shortcut, + @Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions, + @NonNull UserHandle user) { + try { + mService.startShortcut(mContext.getPackageName(), shortcut, sourceBounds, + startActivityOptions, user); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } /** * Registers a callback for changes to packages in current and managed profiles. @@ -474,6 +684,20 @@ public class LauncherApps { } } } + + @Override + public void onShortcutChanged(UserHandle user, String packageName, + ParceledListSlice shortcuts) { + if (DEBUG) { + Log.d(TAG, "onShortcutChanged " + user.getIdentifier() + "," + packageName); + } + final List list = shortcuts.getList(); + synchronized (LauncherApps.this) { + for (CallbackMessageHandler callback : mCallbacks) { + callback.postOnShortcutChanged(packageName, user, list); + } + } + } }; private static class CallbackMessageHandler extends Handler { @@ -484,6 +708,7 @@ public class LauncherApps { private static final int MSG_UNAVAILABLE = 5; private static final int MSG_SUSPENDED = 6; private static final int MSG_UNSUSPENDED = 7; + private static final int MSG_SHORTCUT_CHANGED = 8; private LauncherApps.Callback mCallback; @@ -492,6 +717,7 @@ public class LauncherApps { String packageName; boolean replacing; UserHandle user; + List shortcuts; } public CallbackMessageHandler(Looper looper, LauncherApps.Callback callback) { @@ -527,6 +753,9 @@ public class LauncherApps { case MSG_UNSUSPENDED: mCallback.onPackagesUnsuspended(info.packageNames, info.user); break; + case MSG_SHORTCUT_CHANGED: + mCallback.onShortcutsChanged(info.packageName, info.shortcuts, info.user); + break; } } @@ -582,5 +811,14 @@ public class LauncherApps { info.user = user; obtainMessage(MSG_UNSUSPENDED, info).sendToTarget(); } + + public void postOnShortcutChanged(String packageName, UserHandle user, + List shortcuts) { + CallbackInfo info = new CallbackInfo(); + info.packageName = packageName; + info.user = user; + info.shortcuts = shortcuts; + obtainMessage(MSG_SHORTCUT_CHANGED, info).sendToTarget(); + } } } diff --git a/core/java/android/content/pm/ShortcutInfo.aidl b/core/java/android/content/pm/ShortcutInfo.aidl new file mode 100644 index 0000000000000..08e18731aa9b1 --- /dev/null +++ b/core/java/android/content/pm/ShortcutInfo.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.content.pm; + +parcelable ShortcutInfo; diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java new file mode 100644 index 0000000000000..65205636c98e8 --- /dev/null +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -0,0 +1,680 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.content.pm; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.UserHandle; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * TODO Enhance javadoc + * + * Represents a shortcut form an application. + * + * Notes... + * - If an {@link Icon} is of a resource, then we'll just persist the package name and resource ID. + * + * Otherwise, the bitmap will be fetched when it's registered to ShortcutManager, then *shrunk* + * if necessary, and persisted. + * + * We will disallow byte[] icons, because they can easily go over binder size limit. + * + * TODO Move save/load to this class + */ +public class ShortcutInfo implements Parcelable { + /* @hide */ + public static final int FLAG_DYNAMIC = 1 << 0; + + /* @hide */ + public static final int FLAG_PINNED = 1 << 1; + + /* @hide */ + public static final int FLAG_HAS_ICON_RES = 1 << 2; + + /* @hide */ + public static final int FLAG_HAS_ICON_FILE = 1 << 3; + + /** @hide */ + @IntDef(flag = true, + value = { + FLAG_DYNAMIC, + FLAG_PINNED, + FLAG_HAS_ICON_RES, + FLAG_HAS_ICON_FILE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ShortcutFlags {} + + // Cloning options. + + /* @hide */ + private static final int CLONE_REMOVE_ICON = 1 << 0; + + /* @hide */ + private static final int CLONE_REMOVE_INTENT = 1 << 1; + + /* @hide */ + public static final int CLONE_REMOVE_NON_KEY_INFO = 1 << 2; + + /* @hide */ + public static final int CLONE_REMOVE_FOR_CREATOR = CLONE_REMOVE_ICON; + + /* @hide */ + public static final int CLONE_REMOVE_FOR_LAUNCHER = CLONE_REMOVE_ICON | CLONE_REMOVE_INTENT; + + /** @hide */ + @IntDef(flag = true, + value = { + CLONE_REMOVE_ICON, + CLONE_REMOVE_INTENT, + CLONE_REMOVE_NON_KEY_INFO, + CLONE_REMOVE_FOR_CREATOR, + CLONE_REMOVE_FOR_LAUNCHER + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CloneFlags {} + + private final String mId; + + @NonNull + private final String mPackageName; + + @Nullable + private ComponentName mActivityComponent; + + @Nullable + private Icon mIcon; + + @NonNull + private String mTitle; + + @NonNull + private Intent mIntent; + + // Internal use only. + @NonNull + private PersistableBundle mIntentPersistableExtras; + + private int mWeight; + + @Nullable + private PersistableBundle mExtras; + + private long mLastChangedTimestamp; + + // Internal use only. + @ShortcutFlags + private int mFlags; + + // Internal use only. + private int mIconResourceId; + + // Internal use only. + @Nullable + private String mBitmapPath; + + private ShortcutInfo(Builder b) { + mId = Preconditions.checkStringNotEmpty(b.mId, "Shortcut ID must be provided"); + + // Note we can't do other null checks here because SM.updateShortcuts() takes partial + // information. + mPackageName = b.mContext.getPackageName(); + mActivityComponent = b.mActivityComponent; + mIcon = b.mIcon; + mTitle = b.mTitle; + mIntent = b.mIntent; + mWeight = b.mWeight; + mExtras = b.mExtras; + updateTimestamp(); + } + + /** + * Throws if any of the mandatory fields is not set. + * + * @hide + */ + public void enforceMandatoryFields() { + Preconditions.checkStringNotEmpty(mTitle, "Shortcut title must be provided"); + Preconditions.checkNotNull(mIntent, "Shortcut Intent must be provided"); + } + + /** + * Copy constructor. + */ + 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) { + if ((cloneFlags & CLONE_REMOVE_ICON) == 0) { + mIcon = source.mIcon; + } + + mTitle = source.mTitle; + if ((cloneFlags & CLONE_REMOVE_INTENT) == 0) { + mIntent = source.mIntent; + mIntentPersistableExtras = source.mIntentPersistableExtras; + } + mWeight = source.mWeight; + mExtras = source.mExtras; + mIconResourceId = source.mIconResourceId; + mBitmapPath = source.mBitmapPath; + } + } + + /** + * Copy a {@link ShortcutInfo}, optionally removing fields. + * @hide + */ + public ShortcutInfo clone(@CloneFlags int cloneFlags) { + return new ShortcutInfo(this, cloneFlags); + } + + /** + * Copy non-null/zero fields from another {@link ShortcutInfo}. Only "public" information + * will be overwritten. The timestamp will be updated. + * + * - Flags will not change + * - mBitmapPath will not change + * - Current time will be set to timestamp + * + * @hide + */ + public void copyNonNullFieldsFrom(ShortcutInfo source) { + Preconditions.checkState(mId == source.mId, "ID must match"); + Preconditions.checkState(mPackageName.equals(source.mPackageName), + "Package namae must match"); + + if (source.mActivityComponent != null) { + mActivityComponent = source.mActivityComponent; + } + + if (source.mIcon != null) { + mIcon = source.mIcon; + } + if (source.mTitle != null) { + mTitle = source.mTitle; + } + if (source.mIntent != null) { + mIntent = source.mIntent; + mIntentPersistableExtras = source.mIntentPersistableExtras; + } + if (source.mWeight != 0) { + mWeight = source.mWeight; + } + if (source.mExtras != null) { + mExtras = source.mExtras; + } + + updateTimestamp(); + } + + /** + * Builder class for {@link ShortcutInfo} objects. + */ + public static class Builder { + private final Context mContext; + + private String mId; + + private ComponentName mActivityComponent; + + private Icon mIcon; + + private String mTitle; + + private Intent mIntent; + + private int mWeight; + + private PersistableBundle mExtras; + + /** Constructor. */ + public Builder(Context context) { + mContext = context; + } + + /** + * Sets the ID of the shortcut. This is a mandatory field. + */ + @NonNull + public Builder setId(@NonNull String id) { + mId = Preconditions.checkStringNotEmpty(id, "id"); + return this; + } + + /** + * Optionally sets the target activity. + */ + @NonNull + public Builder setActivityComponent(@NonNull ComponentName activityComponent) { + mActivityComponent = Preconditions.checkNotNull(activityComponent, "activityComponent"); + return this; + } + + /** + * 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. + * + * TODO Only allow Bitmap, Resource and URI types. byte[] type can easily go over + * binder size limit. + */ + @NonNull + public Builder setIcon(Icon icon) { + mIcon = icon; + return this; + } + + /** + * Sets the title of a shortcut. This is a mandatory field. + */ + @NonNull + public Builder setTitle(@NonNull String title) { + mTitle = Preconditions.checkStringNotEmpty(title, "title"); + return this; + } + + /** + * Sets the intent of a shortcut. This is a mandatory field. The extras must only contain + * persistable information. (See {@link PersistableBundle}). + */ + @NonNull + public Builder setIntent(@NonNull Intent intent) { + mIntent = Preconditions.checkNotNull(intent, "intent"); + return this; + } + + /** + * Optionally sets the weight of a shortcut, which will be used by Launcher for sorting. + * The larger the weight, the more "important" a shortcut is. + */ + @NonNull + public Builder setWeight(int weight) { + mWeight = weight; + return this; + } + + /** + * Optional values that application can set. + * TODO: reserve keys starting with "android." + */ + @NonNull + public Builder setExtras(@NonNull PersistableBundle extras) { + mExtras = extras; + return this; + } + + /** + * Creates a {@link ShortcutInfo} instance. + */ + @NonNull + public ShortcutInfo build() { + return new ShortcutInfo(this); + } + } + + /** + * Return the ID of the shortcut. + */ + @NonNull + public String getId() { + return mId; + } + + /** + * Return the ID of the shortcut. + */ + @NonNull + public String getPackageName() { + return mPackageName; + } + + /** + * Return the target activity, which may be null, in which case the shortcut is not associated + * with a specific activity. + */ + @Nullable + public ComponentName getActivityComponent() { + return mActivityComponent; + } + + /** + * Icon. + * + * 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. + * + * @hide + */ + @Nullable + public Icon getIcon() { + return mIcon; + } + + /** + * Return the shortcut title. + */ + @NonNull + public String getTitle() { + return mTitle; + } + + /** + * Return the intent. + * TODO Set mIntentPersistableExtras and before returning. + */ + @NonNull + public Intent getIntent() { + return mIntent; + } + + /** @hide */ + @Nullable + public PersistableBundle getIntentPersistableExtras() { + return mIntentPersistableExtras; + } + + /** + * Return the weight of a shortcut, which will be used by Launcher for sorting. + * The larger the weight, the more "important" a shortcut is. + */ + public int getWeight() { + return mWeight; + } + + /** + * Optional values that application can set. + */ + @Nullable + public PersistableBundle getExtras() { + return mExtras; + } + + /** + * Last time when any of the fields was updated. + */ + public long getLastChangedTimestamp() { + return mLastChangedTimestamp; + } + + /** @hide */ + @ShortcutFlags + public int getFlags() { + return mFlags; + } + + /** @hide*/ + public void setFlags(@ShortcutFlags int flags) { + mFlags = flags; + } + + /** @hide*/ + public void addFlags(@ShortcutFlags int flags) { + mFlags |= flags; + } + + /** @hide*/ + public void clearFlags(@ShortcutFlags int flags) { + mFlags &= ~flags; + } + + /** @hide*/ + public boolean hasFlags(@ShortcutFlags int flags) { + return (mFlags & flags) == flags; + } + + /** Return whether a shortcut is dynamic. */ + public boolean isDynamic() { + return hasFlags(FLAG_DYNAMIC); + } + + /** Return whether a shortcut is pinned. */ + public boolean isPinned() { + return hasFlags(FLAG_PINNED); + } + + /** + * Return whether a shortcut's icon is a resource in the owning package. + * + * @see LauncherApps#getShortcutIconResId(ShortcutInfo, UserHandle) + */ + public boolean hasIconResource() { + return hasFlags(FLAG_HAS_ICON_RES); + } + + /** + * Return whether a shortcut's icon is stored as a file. + * + * @see LauncherApps#getShortcutIconFd(ShortcutInfo, UserHandle) + */ + public boolean hasIconFile() { + return hasFlags(FLAG_HAS_ICON_FILE); + } + + /** @hide */ + public void updateTimestamp() { + mLastChangedTimestamp = System.currentTimeMillis(); + } + + /** @hide */ + // VisibleForTesting + public void setTimestamp(long value) { + mLastChangedTimestamp = value; + } + + /** @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; + } + + /** @hide */ + public void setExtras(PersistableBundle extras) { + mExtras = extras; + } + + /** @hide */ + public int getIconResourceId() { + return mIconResourceId; + } + + /** @hide */ + public String getBitmapPath() { + return mBitmapPath; + } + + /** @hide */ + public void setBitmapPath(String bitmapPath) { + mBitmapPath = bitmapPath; + } + + private ShortcutInfo(Parcel source) { + final ClassLoader cl = getClass().getClassLoader(); + + mId = source.readString(); + mPackageName = source.readString(); + mActivityComponent = source.readParcelable(cl); + mIcon = source.readParcelable(cl); + mTitle = source.readString(); + mIntent = source.readParcelable(cl); + mIntentPersistableExtras = source.readParcelable(cl); + mWeight = source.readInt(); + mExtras = source.readParcelable(cl); + mLastChangedTimestamp = source.readLong(); + mFlags = source.readInt(); + mIconResourceId = source.readInt(); + mBitmapPath = source.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + dest.writeString(mPackageName); + dest.writeParcelable(mActivityComponent, flags); + dest.writeParcelable(mIcon, flags); + dest.writeString(mTitle); + dest.writeParcelable(mIntent, flags); + dest.writeParcelable(mIntentPersistableExtras, flags); + dest.writeInt(mWeight); + dest.writeParcelable(mExtras, flags); + dest.writeLong(mLastChangedTimestamp); + dest.writeInt(mFlags); + dest.writeInt(mIconResourceId); + dest.writeString(mBitmapPath); + } + + public static final Creator CREATOR = + new Creator() { + public ShortcutInfo createFromParcel(Parcel source) { + return new ShortcutInfo(source); + } + public ShortcutInfo[] newArray(int size) { + return new ShortcutInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /** + * Return a string representation, intended for logging. Some fields will be retracted. + */ + @Override + public String toString() { + return toStringInner(/* secure =*/ true, /* includeInternalData =*/ false); + } + + /** @hide */ + public String toInsecureString() { + return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true); + } + + private String toStringInner(boolean secure, boolean includeInternalData) { + final StringBuilder sb = new StringBuilder(); + sb.append("ShortcutInfo {"); + + sb.append("id="); + sb.append(secure ? "***" : mId); + + sb.append(", packageName="); + sb.append(mPackageName); + + if (isDynamic()) { + sb.append(", dynamic"); + } + if (isPinned()) { + sb.append(", pinned"); + } + + sb.append(", activity="); + sb.append(mActivityComponent); + + sb.append(", title="); + sb.append(secure ? "***" : mTitle); + + sb.append(", icon="); + sb.append(mIcon); + + sb.append(", weight="); + sb.append(mWeight); + + sb.append(", timestamp="); + sb.append(mLastChangedTimestamp); + + sb.append(", intent="); + sb.append(mIntent); + + sb.append(", intentExtras="); + sb.append(secure ? "***" : mIntentPersistableExtras); + + sb.append(", extras="); + sb.append(mExtras); + + if (includeInternalData) { + sb.append(", flags="); + sb.append(mFlags); + + sb.append(", iconRes="); + sb.append(mIconResourceId); + + sb.append(", bitmapPath="); + sb.append(mBitmapPath); + } + + sb.append("}"); + return sb.toString(); + } + + /** @hide */ + public ShortcutInfo(String id, String packageName, ComponentName activityComponent, + Icon icon, String title, Intent intent, PersistableBundle intentPersistableExtras, + int weight, PersistableBundle extras, long lastChangedTimestamp, + int flags, int iconResId, String bitmapPath) { + mId = id; + mPackageName = packageName; + mActivityComponent = activityComponent; + mIcon = icon; + mTitle = title; + mIntent = intent; + mIntentPersistableExtras = intentPersistableExtras; + mWeight = weight; + mExtras = extras; + mLastChangedTimestamp = lastChangedTimestamp; + mFlags = flags; + mIconResourceId = iconResId; + mBitmapPath = bitmapPath; + } +} diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java new file mode 100644 index 0000000000000..4c51d499ab4a2 --- /dev/null +++ b/core/java/android/content/pm/ShortcutManager.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.content.pm; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.List; + +/** + * TODO Enhance javadoc + * + * {@link ShortcutManager} manages shortcuts created by applications. + * + *

Dynamic shortcuts and pinned shortcuts

+ * + * An application can publish shortcuts with {@link #setDynamicShortcuts(List)} and + * {@link #addDynamicShortcut(ShortcutInfo)}. There can be at most + * {@link #getMaxDynamicShortcutCount()} number of dynamic shortcuts at a time from the same + * application. + * A dynamic shortcut can be deleted with {@link #deleteDynamicShortcut(String)}, and apps + * can also use {@link #deleteAllDynamicShortcuts()} to delete all dynamic shortcuts. + * + *

The shortcuts that are currently published by the above APIs are called "dynamic", because + * they can be removed by the creator application at any time. The user may "pin" dynamic shortcuts + * on Launcher to make "pinned" shortcuts. Pinned shortcuts cannot be removed by the creator + * app. An application can obtain all pinned shortcuts from itself with + * {@link #getPinnedShortcuts()}. Applications should keep the pinned shortcut information + * up-to-date using {@link #updateShortcuts(List)}. + * + *

The number of pinned shortcuts does not affect the number of dynamic shortcuts that can be + * published by an application at a time. + * No matter how many pinned shortcuts that Launcher has for an application, the + * application can still always publish {@link #getMaxDynamicShortcutCount()} number of dynamic + * shortcuts. + * + *

Shortcut IDs

+ * + * Each shortcut must have an ID, which must be unique within each application. When a shortcut is + * published, existing shortcuts with the same ID will be updated. Note this may include a + * pinned shortcut. + * + *

Rate limiting

+ * + * Calls to {@link #setDynamicShortcuts(List)}, {@link #addDynamicShortcut(ShortcutInfo)}, + * and {@link #updateShortcuts(List)} will be + * rate-limited. An application can call these methods at most + * {@link #getRemainingCallCount()} times until the rate-limiting counter is reset, + * which happens at a certain time every day. + * + *

An applications can use {@link #getRateLimitResetTime()} to get the next reset time. + * + *

Backup and Restore

+ * + * Shortcuts will be backed up and restored across devices. This means all information, including + * IDs, must be meaningful on a different device. + * + * TODO: Define a Broadcast to let apps update shortcuts on a restored device. + * + *

APIs for launcher

+ * + * Launcher applications should use {@link LauncherApps} to get shortcuts that are published from + * applications. Launcher applications can also pin shortcuts with + * {@link LauncherApps#pinShortcuts(String, List, UserHandle)}. + */ +public class ShortcutManager { + private static final String TAG = "ShortcutManager"; + + private final Context mContext; + private final IShortcutService mService; + + /** + * @hide + */ + public ShortcutManager(Context context, IShortcutService service) { + mContext = context; + mService = service; + } + + /** + * Publish a list of shortcuts. All existing dynamic shortcuts from the caller application + * will be replaced. + * + *

This API will be rate-limited. + * + * @return {@code true} if the call has succeeded. {@code false} if the call is rate-limited. + * + * @throws IllegalArgumentException if {@code shortcutInfoList} contains more than + * {@link #getMaxDynamicShortcutCount()} shortcuts. + */ + public boolean setDynamicShortcuts(@NonNull List shortcutInfoList) { + try { + return mService.setDynamicShortcuts(mContext.getPackageName(), + new ParceledListSlice(shortcutInfoList), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return all dynamic shortcuts from the caller application. The number of result items + * will not exceed the value returned by {@link #getMaxDynamicShortcutCount()}. + */ + @NonNull + public List getDynamicShortcuts() { + try { + return mService.getDynamicShortcuts(mContext.getPackageName(), injectMyUserId()) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Publish a single dynamic shortcut. If there's already dynamic or pinned shortcuts with + * the same ID, they will all be updated. + * + *

This API will be rate-limited. + * + * @return {@code true} if the call has succeeded. {@code false} if the call is rate-limited. + * + * @throws IllegalArgumentException if the caller application has already published the + * max number of dynamic shortcuts. + */ + public boolean addDynamicShortcut(@NonNull ShortcutInfo shortcutInfo) { + try { + return mService.addDynamicShortcut( + mContext.getPackageName(), shortcutInfo, injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Delete a single dynamic shortcut by ID. + */ + public void deleteDynamicShortcut(@NonNull String shortcutId) { + try { + mService.deleteDynamicShortcut(mContext.getPackageName(), shortcutId, injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Delete all dynamic shortcuts from the caller application. + */ + public void deleteAllDynamicShortcuts() { + try { + mService.deleteAllDynamicShortcuts(mContext.getPackageName(), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return all pinned shortcuts from the caller application. + */ + @NonNull + public List getPinnedShortcuts() { + try { + return mService.getPinnedShortcuts(mContext.getPackageName(), injectMyUserId()) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Update all existing shortcuts with the same IDs. Shortcuts may be pinned and/or dynamic. + * + *

This API will be rate-limited. + * + * @return {@code true} if the call has succeeded. {@code false} if the call is rate-limited. + */ + public boolean updateShortcuts(List shortcutInfoList) { + try { + return mService.updateShortcuts(mContext.getPackageName(), + new ParceledListSlice(shortcutInfoList), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the max number of dynamic shortcuts that each application can have at a time. + */ + public int getMaxDynamicShortcutCount() { + try { + return mService.getMaxDynamicShortcutCount(mContext.getPackageName(), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the number of times the caller application can call the rate-limited APIs + * before the rate limit counter is reset. + * + * @see #getRateLimitResetTime() + */ + public int getRemainingCallCount() { + try { + return mService.getRemainingCallCount(mContext.getPackageName(), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return when the rate limit count will be reset next time, in milliseconds since the epoch. + * + * @see #getRemainingCallCount() + * @see System#currentTimeMillis() + */ + public long getRateLimitResetTime() { + try { + return mService.getRateLimitResetTime(mContext.getPackageName(), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide injection point */ + @VisibleForTesting + protected int injectMyUserId() { + return UserHandle.myUserId(); + } +} diff --git a/core/java/android/content/pm/ShortcutServiceInternal.java b/core/java/android/content/pm/ShortcutServiceInternal.java new file mode 100644 index 0000000000000..8055dd92965e6 --- /dev/null +++ b/core/java/android/content/pm/ShortcutServiceInternal.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.LauncherApps.ShortcutQuery; + +import java.util.List; + +/** + * Entry points used by {@link LauncherApps}. + * + *

No permission / argument checks will be performed inside. + * Callers must check the calling app permission and the calling package name. + * @hide + */ +public abstract class ShortcutServiceInternal { + public interface ShortcutChangeListener { + void onShortcutChanged(@NonNull String packageName, + @NonNull List shortcuts, @UserIdInt int userId); + } + + public abstract List + getShortcuts(@NonNull String callingPackage, long changedSince, + @Nullable String packageName, @Nullable ComponentName componentName, + @ShortcutQuery.QueryFlags int flags, + int userId); + + public abstract List + getShortcutInfo(@NonNull String callingPackage, + @NonNull String packageName, @Nullable List ids, int userId); + + public abstract void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName, + @NonNull List shortcutIds, int userId); + + public abstract Intent createShortcutIntent(@NonNull String callingPackage, + @NonNull ShortcutInfo shortcut, int userId); + + public abstract void addListener(@NonNull ShortcutChangeListener listener); +} diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index ee7bd9acec77d..5c7137341cd39 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -235,6 +235,12 @@ public class BaseBundle { return mParcelledData != null; } + /** @hide */ + ArrayMap getMap() { + unparcel(); + return mMap; + } + /** * Returns the number of mappings contained in this Bundle. * diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java index ea180b216c631..f36bb2989c72c 100644 --- a/core/java/android/os/PersistableBundle.java +++ b/core/java/android/os/PersistableBundle.java @@ -24,9 +24,6 @@ import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; /** * A mapping from String values to various types that can be saved to persistent and later @@ -82,6 +79,18 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa super(b); } + + /** + * Constructs a PersistableBundle from a Bundle. + * + * @param b a Bundle to be copied. + * + * @throws IllegalArgumentException if any element of {@code b} cannot be persisted. + */ + public PersistableBundle(Bundle b) { + this(b.getMap()); + } + /** * Constructs a PersistableBundle containing the mappings passed in. * @@ -101,6 +110,8 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa if (value instanceof ArrayMap) { // Fix up any Maps by replacing them with PersistableBundles. mMap.setValueAt(i, new PersistableBundle((ArrayMap) value)); + } else if (value instanceof Bundle) { + mMap.setValueAt(i, new PersistableBundle(((Bundle) value))); } else if (!isValidType(value)) { throw new IllegalArgumentException("Bad value in PersistableBundle key=" + mMap.keyAt(i) + " value=" + value); diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 8d75f603928e3..902a3d932bae6 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -16,6 +16,8 @@ package com.android.server.pm; +import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; @@ -27,8 +29,12 @@ import android.content.pm.IOnAppsChangedListener; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutServiceInternal; +import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener; import android.content.pm.UserInfo; import android.graphics.Rect; import android.net.Uri; @@ -44,15 +50,16 @@ import android.util.Log; import android.util.Slog; import com.android.internal.content.PackageMonitor; +import com.android.internal.util.Preconditions; +import com.android.server.LocalServices; import com.android.server.SystemService; import java.util.List; /** * Service that manages requests and callbacks for launchers that support - * managed profiles. + * managed profiles. */ - public class LauncherAppsService extends SystemService { private final LauncherAppsImpl mLauncherAppsImpl; @@ -67,21 +74,25 @@ public class LauncherAppsService extends SystemService { publishBinderService(Context.LAUNCHER_APPS_SERVICE, mLauncherAppsImpl); } - class LauncherAppsImpl extends ILauncherApps.Stub { + static class LauncherAppsImpl extends ILauncherApps.Stub { private static final boolean DEBUG = false; private static final String TAG = "LauncherAppsService"; private final Context mContext; private final PackageManager mPm; private final UserManager mUm; + private final ShortcutServiceInternal mShortcutServiceInternal; private final PackageCallbackList mListeners = new PackageCallbackList(); - private MyPackageMonitor mPackageMonitor = new MyPackageMonitor(); + private final MyPackageMonitor mPackageMonitor = new MyPackageMonitor(); public LauncherAppsImpl(Context context) { mContext = context; mPm = mContext.getPackageManager(); mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mShortcutServiceInternal = Preconditions.checkNotNull( + LocalServices.getService(ShortcutServiceInternal.class)); + mShortcutServiceInternal.addListener(mPackageMonitor); } /* @@ -174,6 +185,20 @@ public class LauncherAppsService extends SystemService { } } + private void verifyCallingPackage(String callingPackage) { + int packageUid = -1; + try { + packageUid = mPm.getPackageUid(callingPackage, + PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE + | PackageManager.MATCH_UNINSTALLED_PACKAGES); + } catch (NameNotFoundException e) { + Log.e(TAG, "Package not found: " + callingPackage); + } + if (packageUid != Binder.getCallingUid()) { + throw new SecurityException("Calling package name mismatch"); + } + } + /** * Checks if the user is enabled. */ @@ -264,6 +289,57 @@ public class LauncherAppsService extends SystemService { } } + private void enforceShortcutPermission(UserHandle user) { + ensureInUserProfiles(user, "Cannot start activity for unrelated profile " + user); + // STOPSHIP Implement it + } + + @Override + public ParceledListSlice getShortcuts(String callingPackage, long changedSince, + String packageName, ComponentName componentName, int flags, UserHandle user) + throws RemoteException { + enforceShortcutPermission(user); + verifyCallingPackage(callingPackage); + + return new ParceledListSlice<>( + mShortcutServiceInternal.getShortcuts(callingPackage, changedSince, packageName, + componentName, flags, user.getIdentifier())); + } + + @Override + public ParceledListSlice getShortcutInfo(String callingPackage, String packageName, + List ids, UserHandle user) throws RemoteException { + enforceShortcutPermission(user); + verifyCallingPackage(callingPackage); + + return new ParceledListSlice<>( + mShortcutServiceInternal.getShortcutInfo(callingPackage, packageName, + ids, user.getIdentifier())); + } + + @Override + public void pinShortcuts(String callingPackage, String packageName, List ids, + UserHandle user) throws RemoteException { + enforceShortcutPermission(user); + verifyCallingPackage(callingPackage); + + mShortcutServiceInternal.pinShortcuts(callingPackage, packageName, + ids, user.getIdentifier()); + } + + @Override + public void startShortcut(String callingPackage, ShortcutInfo shortcut, Rect sourceBounds, + Bundle startActivityOptions, UserHandle user) throws RemoteException { + enforceShortcutPermission(user); + verifyCallingPackage(callingPackage); + + final Intent intent = mShortcutServiceInternal.createShortcutIntent(callingPackage, + shortcut, user.getIdentifier()); + // TODO + Slog.e(TAG, "startShortcut() not implemented yet, but the intent is " + intent); + throw new RuntimeException("not implemented yet"); + } + @Override public boolean isActivityEnabled(ComponentName component, UserHandle user) throws RemoteException { @@ -355,7 +431,7 @@ public class LauncherAppsService extends SystemService { } - private class MyPackageMonitor extends PackageMonitor { + private class MyPackageMonitor extends PackageMonitor implements ShortcutChangeListener { /** Checks if user is a profile of or same as listeningUser. * and the user is enabled. */ @@ -390,6 +466,8 @@ public class LauncherAppsService extends SystemService { } } + // TODO Simplify with lambdas. + @Override public void onPackageAdded(String packageName, int uid) { UserHandle user = new UserHandle(getChangingUserId()); @@ -523,6 +601,25 @@ public class LauncherAppsService extends SystemService { super.onPackagesUnsuspended(packages); } + @Override + public void onShortcutChanged(@NonNull String packageName, + @NonNull List shortcuts, @UserIdInt int userId) { + final UserHandle user = UserHandle.of(userId); + + final int n = mListeners.beginBroadcast(); + for (int i = 0; i < n; i++) { + IOnAppsChangedListener listener = mListeners.getBroadcastItem(i); + UserHandle listeningUser = (UserHandle) mListeners.getBroadcastCookie(i); + if (!isEnabledProfileOf(user, listeningUser, "onShortcutChanged")) continue; + try { + listener.onShortcutChanged(user, packageName, + new ParceledListSlice<>(shortcuts)); + } catch (RemoteException re) { + Slog.d(TAG, "Callback failed ", re); + } + } + mListeners.finishBroadcast(); + } } class PackageCallbackList extends RemoteCallbackList { diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java new file mode 100644 index 0000000000000..5382ff1582824 --- /dev/null +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -0,0 +1,1529 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.IShortcutService; +import android.content.pm.LauncherApps; +import android.content.pm.LauncherApps.ShortcutQuery; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ParceledListSlice; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutServiceInternal; +import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener; +import android.graphics.drawable.Icon; +import android.os.Binder; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.PersistableBundle; +import android.os.Process; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCommand; +import android.os.UserHandle; +import android.text.TextUtils; +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.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.Preconditions; +import com.android.server.LocalServices; +import com.android.server.SystemService; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +/** + * TODO: + * - Make save async + * + * - Add Bitmap support + * + * - Implement updateShortcuts + * + * - Listen to PACKAGE_*, remove orphan info, update timestamp for icon res + * + * - 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 + */ +public class ShortcutService extends IShortcutService.Stub { + private 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 SAVE_DELAY_MS = 5000; // in milliseconds. + + @VisibleForTesting + static final String FILENAME_BASE_STATE = "shortcut_service.xml"; + + @VisibleForTesting + static final String DIRECTORY_PER_USER = "shortcut_service"; + + @VisibleForTesting + static final String FILENAME_USER_PACKAGES = "shortcuts.xml"; + + private static final String DIRECTORY_BITMAPS = "bitmaps"; + + private static final String TAG_ROOT = "root"; + private static final String TAG_LAST_RESET_TIME = "last_reset_time"; + private static final String ATTR_VALUE = "value"; + + private final Context mContext; + + private final Object mLock = new Object(); + + private final Handler mHandler; + + @GuardedBy("mLock") + private final ArrayList mListeners = new ArrayList<>(1); + + @GuardedBy("mLock") + private long mRawLastResetTime; + + /** + * All the information relevant to shortcuts from a single package (per-user). + * + * TODO Move the persisting code to this class. + */ + private static class PackageShortcuts { + /** + * All the shortcuts from the package, keyed on IDs. + */ + final private ArrayMap mShortcuts = new ArrayMap<>(); + + /** + * # of dynamic shortcuts. + */ + private int mDynamicShortcutCount = 0; + + /** + * # of times the package has called rate-limited APIs. + */ + private int mApiCallCountInner; + + /** + * When {@link #mApiCallCountInner} 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. + */ + @GuardedBy("mLock") + public ArrayMap getShortcuts() { + return mShortcuts; + } + + /** + * Add a shortcut, or update one with the same ID, with taking over existing flags. + * + * It checks the max number of dynamic shortcuts. + */ + @GuardedBy("mLock") + public void updateShortcutWithCapping(@NonNull ShortcutService s, + @NonNull ShortcutInfo newShortcut) { + final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); + + int oldFlags = 0; + int newDynamicCount = mDynamicShortcutCount; + + if (oldShortcut != null) { + oldFlags = oldShortcut.getFlags(); + if (oldShortcut.isDynamic()) { + newDynamicCount--; + } + } + if (newShortcut.isDynamic()) { + newDynamicCount++; + } + // Make sure there's still room. + s.enforceMaxDynamicShortcuts(newDynamicCount); + + // Okay, make it dynamic and add. + newShortcut.addFlags(oldFlags); + + mShortcuts.put(newShortcut.getId(), newShortcut); + mDynamicShortcutCount = newDynamicCount; + } + + @GuardedBy("mLock") + public void deleteAllDynamicShortcuts() { + 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 (removeList != null) { + for (int i = removeList.size() - 1 ; i >= 0; i--) { + mShortcuts.remove(removeList.get(i)); + } + } + mDynamicShortcutCount = 0; + } + + @GuardedBy("mLock") + public void deleteDynamicWithId(@NonNull String shortcutId) { + final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); + + if (oldShortcut == null) { + return; + } + if (oldShortcut.isDynamic()) { + mDynamicShortcutCount--; + } + if (oldShortcut.isPinned()) { + oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC); + } else { + mShortcuts.remove(shortcutId); + } + } + + @GuardedBy("mLock") + public void pinAll(List shortcutIds) { + 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); + } + } + } + + /** + * Number of calls that the caller has made, since the last reset. + */ + @GuardedBy("mLock") + public int getApiCallCount(@NonNull ShortcutService s) { + final long last = s.getLastResetTimeLocked(); + + // If not reset yet, then reset. + if (mLastResetTime < last) { + mApiCallCountInner = 0; + mLastResetTime = last; + } + return mApiCallCountInner; + } + + /** + * If the caller app hasn't been throttled yet, increment {@link #mApiCallCountInner} + * and return true. Otherwise just return false. + */ + @GuardedBy("mLock") + public boolean tryApiCall(@NonNull ShortcutService s) { + if (getApiCallCount(s) >= s.mMaxDailyUpdates) { + return false; + } + mApiCallCountInner++; + return true; + } + + @GuardedBy("mLock") + public void resetRateLimitingForCommandLine() { + mApiCallCountInner = 0; + mLastResetTime = 0; + } + + /** + * Find all shortcuts that match {@code query}. + */ + @GuardedBy("mLock") + public void findAll(@NonNull List result, + @Nullable Predicate query, int cloneFlag) { + for (int i = 0; i < mShortcuts.size(); i++) { + final ShortcutInfo si = mShortcuts.valueAt(i); + if (query == null || query.test(si)) { + result.add(si.clone(cloneFlag)); + } + } + } + } + + /** + * User ID -> package name -> list of ShortcutInfos. + */ + @GuardedBy("mLock") + private final SparseArray> mShortcuts = + new SparseArray<>(); + + /** + * 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; + + public ShortcutService(Context context) { + mContext = Preconditions.checkNotNull(context); + LocalServices.addService(ShortcutServiceInternal.class, new LocalService()); + mHandler = new Handler(BackgroundThread.get().getLooper()); + } + + /** + * System service lifecycle. + */ + public static final class Lifecycle extends SystemService { + final ShortcutService mService; + + public Lifecycle(Context context) { + super(context); + mService = new ShortcutService(context); + } + + @Override + public void onStart() { + publishBinderService(Context.SHORTCUT_SERVICE, mService); + } + + @Override + public void onBootPhase(int phase) { + mService.onBootPhase(phase); + } + + @Override + public void onCleanupUser(int userHandle) { + synchronized (mService.mLock) { + mService.onCleanupUserInner(userHandle); + } + } + + @Override + public void onStartUser(int userId) { + synchronized (mService.mLock) { + mService.onStartUserLocked(userId); + } + } + } + + /** lifecycle event */ + void onBootPhase(int phase) { + if (DEBUG) { + Slog.d(TAG, "onBootPhase: " + phase); + } + switch (phase) { + case SystemService.PHASE_LOCK_SETTINGS_READY: + initialize(); + break; + } + } + + /** lifecycle event */ + void onStartUserLocked(int userId) { + // Preload + getUserShortcutsLocked(userId); + } + + /** lifecycle event */ + void onCleanupUserInner(int userId) { + // Unload + mShortcuts.delete(userId); + } + + /** Return the base state file name */ + private AtomicFile getBaseStateFile() { + final File path = new File(injectSystemDataPath(), FILENAME_BASE_STATE); + path.mkdirs(); + return new AtomicFile(path); + } + + /** + * Init the instance. (load the state file, etc) + */ + private void initialize() { + synchronized (mLock) { + injectLoadConfigurationLocked(); + loadBaseStateLocked(); + } + } + + // Test overrides it to inject different values. + @VisibleForTesting + void injectLoadConfigurationLocked() { + mResetInterval = DEFAULT_RESET_INTERVAL_SEC * 1000L; + mMaxDailyUpdates = DEFAULT_MAX_DAILY_UPDATES; + mMaxDynamicShortcuts = DEFAULT_MAX_SHORTCUTS_PER_APP; + } + + // === Persistings === + + @Nullable + private String parseStringAttribute(XmlPullParser parser, String attribute) { + return parser.getAttributeValue(null, attribute); + } + + private long parseLongAttribute(XmlPullParser parser, String attribute) { + final String value = parseStringAttribute(parser, attribute); + if (TextUtils.isEmpty(value)) { + return 0; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + Slog.e(TAG, "Error parsing long " + value); + return 0; + } + } + + @Nullable + private ComponentName parseComponentNameAttribute(XmlPullParser parser, String attribute) { + final String value = parseStringAttribute(parser, attribute); + if (TextUtils.isEmpty(value)) { + return null; + } + return ComponentName.unflattenFromString(value); + } + + @Nullable + private Intent parseIntentAttribute(XmlPullParser parser, String attribute) { + final String value = parseStringAttribute(parser, attribute); + if (TextUtils.isEmpty(value)) { + return null; + } + try { + return Intent.parseUri(value, /* flags =*/ 0); + } catch (URISyntaxException e) { + Slog.e(TAG, "Error parsing intent", e); + return null; + } + } + + private void writeTagValue(XmlSerializer out, String tag, String value) throws IOException { + if (TextUtils.isEmpty(value)) return; + + out.startTag(null, tag); + out.attribute(null, ATTR_VALUE, value); + out.endTag(null, tag); + } + + private void writeTagValue(XmlSerializer out, String tag, long value) throws IOException { + writeTagValue(out, tag, Long.toString(value)); + } + + private void writeTagExtra(XmlSerializer out, String tag, PersistableBundle bundle) + throws IOException, XmlPullParserException { + if (bundle == null) return; + + out.startTag(null, tag); + bundle.saveToXml(out); + out.endTag(null, tag); + } + + private void writeAttr(XmlSerializer out, String name, String value) throws IOException { + if (TextUtils.isEmpty(value)) return; + + out.attribute(null, name, value); + } + + private void writeAttr(XmlSerializer out, String name, long value) throws IOException { + writeAttr(out, name, String.valueOf(value)); + } + + private void writeAttr(XmlSerializer out, String name, ComponentName comp) throws IOException { + if (comp == null) return; + writeAttr(out, name, comp.flattenToString()); + } + + private void writeAttr(XmlSerializer out, String name, Intent intent) throws IOException { + if (intent == null) return; + + writeAttr(out, name, intent.toUri(/* flags =*/ 0)); + } + + @VisibleForTesting + void saveBaseStateLocked() { + final AtomicFile file = getBaseStateFile(); + if (DEBUG) { + Slog.i(TAG, "Saving to " + file.getBaseFile()); + } + + FileOutputStream outs = null; + try { + outs = file.startWrite(); + + // Write to XML + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(outs, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_ROOT); + + // Body. + writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime); + + // Epilogue. + out.endTag(null, TAG_ROOT); + out.endDocument(); + + // Close. + file.finishWrite(outs); + } catch (IOException e) { + Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); + file.failWrite(outs); + } + } + + private void loadBaseStateLocked() { + mRawLastResetTime = 0; + + final AtomicFile file = getBaseStateFile(); + if (DEBUG) { + Slog.i(TAG, "Loading from " + file.getBaseFile()); + } + try (FileInputStream in = file.openRead()) { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, StandardCharsets.UTF_8.name()); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + // Check the root tag + final String tag = parser.getName(); + if (depth == 1) { + if (!TAG_ROOT.equals(tag)) { + Slog.e(TAG, "Invalid root tag: " + tag); + return; + } + continue; + } + // Assume depth == 2 + switch (tag) { + case TAG_LAST_RESET_TIME: + mRawLastResetTime = parseLongAttribute(parser, ATTR_VALUE); + break; + default: + Slog.e(TAG, "Invalid tag: " + tag); + break; + } + } + } catch (FileNotFoundException e) { + // Use the default + } catch (IOException|XmlPullParserException e) { + Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); + + mRawLastResetTime = 0; + } + // Adjust the last reset time. + getLastResetTimeLocked(); + } + + private void saveUserLocked(@UserIdInt int userId) { + final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES); + if (DEBUG) { + Slog.i(TAG, "Saving to " + path); + } + path.mkdirs(); + final AtomicFile file = new AtomicFile(path); + FileOutputStream outs = null; + try { + outs = file.startWrite(); + + // Write to XML + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(outs, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_ROOT); + + final ArrayMap packages = getUserShortcutsLocked(userId); + + // Body. + for (int i = 0; i < packages.size(); i++) { + final String packageName = packages.keyAt(i); + final PackageShortcuts shortcuts = packages.valueAt(i); + + // TODO Move this to PackageShortcuts. + + out.startTag(null, "package"); + + writeAttr(out, "name", packageName); + writeAttr(out, "dynamic-count", shortcuts.mDynamicShortcutCount); + writeAttr(out, "call-count", shortcuts.mApiCallCountInner); + writeAttr(out, "last-reset", shortcuts.mLastResetTime); + + final int size = shortcuts.getShortcuts().size(); + for (int j = 0; j < size; j++) { + saveShortcut(out, shortcuts.getShortcuts().valueAt(j)); + } + + out.endTag(null, "package"); + } + + // Epilogue. + out.endTag(null, TAG_ROOT); + out.endDocument(); + + // Close. + file.finishWrite(outs); + } catch (IOException|XmlPullParserException e) { + Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); + file.failWrite(outs); + } + } + + private void saveShortcut(XmlSerializer out, ShortcutInfo si) + throws IOException, XmlPullParserException { + out.startTag(null, "shortcut"); + writeAttr(out, "id", si.getId()); + // writeAttr(out, "package", si.getPackageName()); // not needed + writeAttr(out, "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()); + + writeTagExtra(out, "intent-extras", si.getIntentPersistableExtras()); + writeTagExtra(out, "extras", si.getExtras()); + + out.endTag(null, "shortcut"); + } + + private static IOException throwForInvalidTag(int depth, String tag) throws IOException { + throw new IOException(String.format("Invalid tag '%s' found at depth %d", tag, depth)); + } + + @Nullable + private ArrayMap loadUserLocked(@UserIdInt int userId) { + final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES); + if (DEBUG) { + Slog.i(TAG, "Loading from " + path); + } + path.mkdirs(); + final AtomicFile file = new AtomicFile(path); + + final FileInputStream in; + try { + in = file.openRead(); + } catch (FileNotFoundException e) { + if (DEBUG) { + Slog.i(TAG, "Not found " + path); + } + return null; + } + final ArrayMap ret = new ArrayMap(); + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, StandardCharsets.UTF_8.name()); + + String packageName = null; + PackageShortcuts shortcuts = null; + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + + // TODO Move some of this to PackageShortcuts. + + final String tag = parser.getName(); + if (DEBUG_LOAD) { + Slog.d(TAG, String.format("depth=%d type=%d name=%s", + depth, type, tag)); + } + switch (depth) { + case 1: { + if (TAG_ROOT.equals(tag)) { + continue; + } + break; + } + case 2: { + switch (tag) { + case "package": + packageName = parseStringAttribute(parser, "name"); + shortcuts = new PackageShortcuts(); + ret.put(packageName, shortcuts); + + shortcuts.mDynamicShortcutCount = + (int) parseLongAttribute(parser, "dynamic-count"); + shortcuts.mApiCallCountInner = + (int) parseLongAttribute(parser, "call-count"); + shortcuts.mLastResetTime = parseLongAttribute(parser, "last-reset"); + continue; + } + break; + } + case 3: { + switch (tag) { + case "shortcut": + final ShortcutInfo si = parseShortcut(parser, packageName); + shortcuts.mShortcuts.put(si.getId(), si); + continue; + } + break; + } + } + throwForInvalidTag(depth, tag); + } + return ret; + } catch (IOException|XmlPullParserException e) { + Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); + return null; + } finally { + IoUtils.closeQuietly(in); + } + } + + private ShortcutInfo parseShortcut(XmlPullParser parser, String packgeName) + throws IOException, XmlPullParserException { + String id; + ComponentName activityComponent; + Icon icon; + String title; + Intent intent; + PersistableBundle intentPersistableExtras = null; + int weight; + PersistableBundle extras = null; + long lastChangedTimestamp; + int flags; + 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"); + + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + final String tag = parser.getName(); + if (DEBUG_LOAD) { + Slog.d(TAG, String.format(" depth=%d type=%d name=%s", + depth, type, tag)); + } + switch (tag) { + case "intent-extras": + intentPersistableExtras = PersistableBundle.restoreFromXml(parser); + continue; + case "extras": + extras = PersistableBundle.restoreFromXml(parser); + continue; + } + throw throwForInvalidTag(depth, tag); + } + return new ShortcutInfo( + id, packgeName, activityComponent, /* icon =*/ null, title, intent, + intentPersistableExtras, weight, extras, lastChangedTimestamp, flags, + iconRes, bitmapPath); + } + + // TODO Actually make it async. + private void scheduleSaveBaseState() { + synchronized (mLock) { + saveBaseStateLocked(); + } + } + + // TODO Actually make it async. + private void scheduleSaveUser(@UserIdInt int userId) { + synchronized (mLock) { + saveUserLocked(userId); + } + } + + /** Return the last reset time. */ + long getLastResetTimeLocked() { + updateTimes(); + return mRawLastResetTime; + } + + /** Return the next reset time. */ + long getNextResetTimeLocked() { + updateTimes(); + return mRawLastResetTime + mResetInterval; + } + + /** + * Update the last reset time. + */ + private void updateTimes() { + + final long now = injectCurrentTimeMillis(); + + final long prevLastResetTime = mRawLastResetTime; + + if (mRawLastResetTime == 0) { // first launch. + // TODO Randomize?? + mRawLastResetTime = now; + } else if (now < mRawLastResetTime) { + // Clock rewound. + // TODO Randomize?? + mRawLastResetTime = now; + } else { + // TODO Do it properly. + while ((mRawLastResetTime + mResetInterval) <= now) { + mRawLastResetTime += mResetInterval; + } + } + if (prevLastResetTime != mRawLastResetTime) { + scheduleSaveBaseState(); + } + } + + /** Return the per-user state. */ + @GuardedBy("mLock") + @NonNull + private ArrayMap getUserShortcutsLocked(@UserIdInt int userId) { + ArrayMap userPackages = mShortcuts.get(userId); + if (userPackages == null) { + userPackages = loadUserLocked(userId); + if (userPackages == null) { + userPackages = new ArrayMap<>(); + } + mShortcuts.put(userId, userPackages); + } + return userPackages; + } + + /** Return the per-user per-package state. */ + @GuardedBy("mLock") + @NonNull + private PackageShortcuts getPackageShortcutsLocked( + @NonNull String packageName, @UserIdInt int userId) { + final ArrayMap userPackages = getUserShortcutsLocked(userId); + PackageShortcuts shortcuts = userPackages.get(packageName); + if (shortcuts == null) { + shortcuts = new PackageShortcuts(); + userPackages.put(packageName, shortcuts); + } + return shortcuts; + } + + // === Caller validation === + + private boolean isCallerSystem() { + final int callingUid = injectBinderCallingUid(); + return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID); + } + + private boolean isCallerShell() { + final int callingUid = injectBinderCallingUid(); + return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID; + } + + private void enforceSystemOrShell() { + Preconditions.checkState(isCallerSystem() || isCallerShell(), + "Caller must be system or shell"); + } + + private void enforceShell() { + Preconditions.checkState(isCallerShell(), "Caller must be shell"); + } + + private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) { + Preconditions.checkStringNotEmpty(packageName, "packageName"); + + if (isCallerSystem()) { + return; // no check + } + + final int callingUid = injectBinderCallingUid(); + + // Otherwise, make sure the arguments are valid. + 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()) { + return; // Caller is valid. + } + throw new SecurityException("Caller UID= doesn't own " + packageName); + } + + // Test overrides it. + int injectGetPackageUid(String packageName) { + try { + + // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info? + + return mContext.getPackageManager().getPackageUid(packageName, + PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE + | PackageManager.MATCH_UNINSTALLED_PACKAGES); + } catch (NameNotFoundException e) { + return -1; + } + } + + /** + * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}. + */ + void enforceMaxDynamicShortcuts(int numShortcuts) { + if (numShortcuts > mMaxDynamicShortcuts) { + throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded"); + } + } + + /** + * - Sends a notification to LauncherApps + * - Write to file + */ + private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) { + notifyListeners(packageName, userId); + scheduleSaveUser(userId); + } + + private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) { + final ArrayList copy; + final List shortcuts = new ArrayList<>(); + synchronized (mLock) { + copy = new ArrayList<>(mListeners); + + getPackageShortcutsLocked(packageName, userId) + .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); + } + for (int i = copy.size() - 1; i >= 0; i--) { + copy.get(i).onShortcutChanged(packageName, shortcuts, userId); + } + } + + /** + * Clean up / validate an incoming shortcut. + * - Make sure all mandatory fields are set. + * - Make sure the intent's extras are persistable, and them to set + * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras. + * - Clear flags. + */ + private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut) { + Preconditions.checkNotNull(shortcut, "Null shortcut detected"); + if (shortcut.getActivityComponent() != null) { + Preconditions.checkState( + shortcut.getPackageName().equals( + shortcut.getActivityComponent().getPackageName()), + "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)); + } + + // TODO Save the icon + shortcut.setIcon(null); + + shortcut.setFlags(0); + } + + // === APIs === + + @Override + public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + + final List newShortcuts = (List) shortcutInfoList.getList(); + final int size = newShortcuts.size(); + + synchronized (mLock) { + final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); + + // Throttling. + if (!ps.tryApiCall(this)) { + return false; + } + enforceMaxDynamicShortcuts(size); + + // Validate the shortcuts. + for (int i = 0; i < size; i++) { + fixUpIncomingShortcutInfo(newShortcuts.get(i)); + } + + // First, remove all un-pinned; dynamic shortcuts + ps.deleteAllDynamicShortcuts(); + + // Then, add/update all. We need to make sure to take over "pinned" flag. + for (int i = 0; i < size; i++) { + final ShortcutInfo newShortcut = newShortcuts.get(i); + newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); + ps.updateShortcutWithCapping(this, newShortcut); + } + } + userPackageChanged(packageName, userId); + + return true; + } + + @Override + public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + + final List newShortcuts = (List) shortcutInfoList.getList(); + + synchronized (mLock) { + + if (true) { + throw new RuntimeException("not implemented yet"); + } + + // TODO Similar to setDynamicShortcuts, but don't add new ones, and don't change flags. + // Update non-null fields only. + } + userPackageChanged(packageName, userId); + + return true; + } + + @Override + public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + + synchronized (mLock) { + final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); + + // Throttling. + if (!ps.tryApiCall(this)) { + return false; + } + + // Validate the shortcut. + fixUpIncomingShortcutInfo(newShortcut); + + // Add it. + newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); + ps.updateShortcutWithCapping(this, newShortcut); + } + userPackageChanged(packageName, userId); + + return true; + } + + @Override + public void deleteDynamicShortcut(String packageName, String shortcutId, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided"); + + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(shortcutId); + } + userPackageChanged(packageName, userId); + } + + @Override + public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) { + verifyCaller(packageName, userId); + + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(); + } + userPackageChanged(packageName, userId); + } + + @Override + public ParceledListSlice getDynamicShortcuts(String packageName, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + synchronized (mLock) { + return getShortcutsWithQueryLocked( + packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, + ShortcutInfo::isDynamic); + } + } + + @Override + public ParceledListSlice getPinnedShortcuts(String packageName, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + synchronized (mLock) { + return getShortcutsWithQueryLocked( + packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, + ShortcutInfo::isPinned); + } + } + + private ParceledListSlice getShortcutsWithQueryLocked(@NonNull String packageName, + @UserIdInt int userId, int cloneFlags, @NonNull Predicate query) { + + final ArrayList ret = new ArrayList<>(); + + getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags); + + return new ParceledListSlice<>(ret); + } + + @Override + public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId) + throws RemoteException { + verifyCaller(packageName, userId); + + return mMaxDynamicShortcuts; + } + + @Override + public int getRemainingCallCount(String packageName, @UserIdInt int userId) { + verifyCaller(packageName, userId); + + synchronized (mLock) { + return mMaxDailyUpdates + - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this); + } + } + + @Override + public long getRateLimitResetTime(String packageName, @UserIdInt int userId) { + verifyCaller(packageName, userId); + + synchronized (mLock) { + return getNextResetTimeLocked(); + } + } + + /** + * Reset all throttling, for developer options and command line. Only system/shell can call it. + */ + @Override + public void resetThrottling() { + enforceSystemOrShell(); + + resetThrottlingInner(); + } + + @VisibleForTesting + void resetThrottlingInner() { + synchronized (mLock) { + mRawLastResetTime = injectCurrentTimeMillis(); + } + scheduleSaveBaseState(); + } + + /** + * Entry point from {@link LauncherApps}. + */ + private class LocalService extends ShortcutServiceInternal { + @Override + public List getShortcuts( + @NonNull String callingPackage, long changedSince, + @Nullable String packageName, @Nullable ComponentName componentName, + int queryFlags, int userId) { + final ArrayList ret = new ArrayList<>(); + final int cloneFlag = + ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0) + ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER + : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO; + + synchronized (mLock) { + if (packageName != null) { + getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags, + userId, ret, cloneFlag); + } else { + final ArrayMap packages = + getUserShortcutsLocked(userId); + for (int i = 0; i < packages.size(); i++) { + getShortcutsInnerLocked( + packages.keyAt(i), + changedSince, componentName, queryFlags, userId, ret, cloneFlag); + } + } + } + return ret; + } + + private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince, + @Nullable ComponentName componentName, int queryFlags, + int userId, ArrayList ret, int cloneFlag) { + getPackageShortcutsLocked(packageName, userId).findAll(ret, + (ShortcutInfo si) -> { + if (si.getLastChangedTimestamp() < changedSince) { + return false; + } + if (componentName != null + && !componentName.equals(si.getActivityComponent())) { + return false; + } + final boolean matchDynamic = + ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0) + && si.isDynamic(); + final boolean matchPinned = + ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0) + && si.isPinned(); + return matchDynamic || matchPinned; + }, cloneFlag); + } + + @Override + public List getShortcutInfo( + @NonNull String callingPackage, + @NonNull String packageName, @Nullable List ids, int userId) { + // Calling permission must be checked by LauncherAppsImpl. + Preconditions.checkStringNotEmpty(packageName, "packageName"); + + final ArrayList ret = new ArrayList<>(ids.size()); + final ArraySet idSet = new ArraySet<>(ids); + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).findAll(ret, + (ShortcutInfo si) -> idSet.contains(si.getId()), + ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); + } + return ret; + } + + @Override + public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName, + @NonNull List shortcutIds, int userId) { + // Calling permission must be checked by LauncherAppsImpl. + Preconditions.checkStringNotEmpty(packageName, "packageName"); + Preconditions.checkNotNull(shortcutIds, "shortcutIds"); + + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).pinAll(shortcutIds); + } + userPackageChanged(packageName, userId); + } + + @Override + public Intent createShortcutIntent(@NonNull String callingPackage, + @NonNull ShortcutInfo shortcut, int userId) { + // Calling permission must be checked by LauncherAppsImpl. + Preconditions.checkNotNull(shortcut, "shortcut"); + + synchronized (mLock) { + final ShortcutInfo fullShortcut = + getPackageShortcutsLocked(shortcut.getPackageName(), userId) + .getShortcuts().get(shortcut.getId()); + 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; + } + } + } + + @Override + public void addListener(@NonNull ShortcutChangeListener listener) { + synchronized (mLock) { + mListeners.add(Preconditions.checkNotNull(listener)); + } + } + } + + // === Dump === + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump UserManager from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid() + + " without permission " + + android.Manifest.permission.DUMP); + return; + } + dumpInner(pw); + } + + @VisibleForTesting + void dumpInner(PrintWriter pw) { + synchronized (mLock) { + final long now = injectCurrentTimeMillis(); + pw.print("Now: ["); + 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)); + + pw.print(" Next reset: ["); + pw.print(next); + pw.print("] "); + pw.print(formatTime(next)); + pw.println(); + + pw.println(); + + for (int i = 0; i < mShortcuts.size(); i++) { + dumpUserLocked(pw, mShortcuts.keyAt(i)); + } + + } + } + + private void dumpUserLocked(PrintWriter pw, int userId) { + pw.print(" User: "); + pw.print(userId); + pw.println(); + + final ArrayMap packages = mShortcuts.get(userId); + if (packages == null) { + return; + } + for (int j = 0; j < packages.size(); j++) { + dumpPackageLocked(pw, userId, packages.keyAt(j)); + } + pw.println(); + } + + private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) { + final PackageShortcuts shortcuts = mShortcuts.get(userId).get(packageName); + if (shortcuts == null) { + return; + } + + pw.print(" Package: "); + pw.print(packageName); + pw.println(); + + pw.print(" Calls: "); + pw.print(shortcuts.getApiCallCount(this)); + pw.println(); + + // This should be after getApiCallCount(), which may update it. + pw.print(" Last reset: ["); + pw.print(shortcuts.mLastResetTime); + pw.print("] "); + pw.print(formatTime(shortcuts.mLastResetTime)); + pw.println(); + + pw.println(" Shortcuts:"); + final int size = shortcuts.getShortcuts().size(); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(shortcuts.getShortcuts().valueAt(i).toInsecureString()); + } + } + + private static String formatTime(long time) { + Time tobj = new Time(); + tobj.set(time); + return tobj.format("%Y-%m-%d %H:%M:%S"); + } + + // === Shell support === + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ResultReceiver resultReceiver) throws RemoteException { + + enforceShell(); + + (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver); + } + + /** + * Handle "adb shell cmd". + */ + private class MyShellCommand extends ShellCommand { + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + final PrintWriter pw = getOutPrintWriter(); + switch(cmd) { + case "reset-package-throttling": + return handleResetPackageThrottling(); + case "reset-throttling": + return handleResetThrottling(); + default: + return handleDefaultCommands(cmd); + } + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + pw.println("Usage: cmd shortcut COMMAND [options ...]"); + pw.println(); + pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE"); + pw.println(" Reset throttling for a package"); + pw.println(); + pw.println("cmd shortcut reset-throttling"); + pw.println(" Reset throttling for all packages and users"); + pw.println(); + } + + private int handleResetThrottling() { + resetThrottling(); + return 0; + } + + private int handleResetPackageThrottling() { + final PrintWriter pw = getOutPrintWriter(); + + int userId = UserHandle.USER_SYSTEM; + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + default: + pw.println("Error: Unknown option: " + opt); + return 1; + } + } + final String packageName = getNextArgRequired(); + + synchronized (mLock) { + getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine(); + saveUserLocked(userId); + } + + return 0; + } + } + + // === Unit test support === + + // Injection point. + long injectCurrentTimeMillis() { + return System.currentTimeMillis(); + } + + // Injection point. + int injectBinderCallingUid() { + return getCallingUid(); + } + + File injectSystemDataPath() { + return Environment.getDataSystemDirectory(); + } + + File injectUserDataPath(@UserIdInt int userId) { + return new File(Environment.getDataSystemDeDirectory(userId), DIRECTORY_PER_USER); + } + + @VisibleForTesting + SparseArray> getShortcutsForTest() { + return mShortcuts; + } + + @VisibleForTesting + void setMaxDynamicShortcutsForTest(int max) { + mMaxDynamicShortcuts = max; + } + + @VisibleForTesting + void setMaxDailyUpdatesForTest(int max) { + mMaxDailyUpdates = max; + } + + @VisibleForTesting + public void setResetIntervalForTest(long interval) { + mResetInterval = interval; + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java index 68fd0f681aa51..b316cbd964536 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java @@ -494,7 +494,6 @@ class Owners { abstract void writeInner(XmlSerializer out) throws IOException; abstract boolean readInner(XmlPullParser parser, int depth, String tag); - } private class DeviceOwnerReadWriter extends FileReadWriter { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index b8c31e3f063ef..85185208789f9 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -83,6 +83,7 @@ import com.android.server.pm.Installer; import com.android.server.pm.LauncherAppsService; import com.android.server.pm.OtaDexoptService; import com.android.server.pm.PackageManagerService; +import com.android.server.pm.ShortcutService; import com.android.server.pm.UserManagerService; import com.android.server.power.PowerManagerService; import com.android.server.power.ShutdownThread; @@ -726,6 +727,9 @@ 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) { diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 3ae10729f7414..23f186c42ce31 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -106,6 +106,10 @@ + + + + new ShortcutInfo.Builder(mContext).build()); + } + + // TODO Add more tests. +} diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java new file mode 100644 index 0000000000000..a9b6684a874c6 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java @@ -0,0 +1,1206 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps.ShortcutQuery; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.content.pm.ShortcutServiceInternal; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.FileUtils; +import android.os.UserHandle; +import android.test.AndroidTestCase; +import android.test.mock.MockContext; +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 org.junit.Assert; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * Tests for ShortcutService and ShortcutManager. + * + m FrameworksServicesTests && + adb install \ + -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 + */ +public class ShortcutManagerTest extends AndroidTestCase { + private static final String TAG = "ShortcutManagerTest"; + + /** + * Whether to enable dump or not. Should be only true when debugging to avoid bugs where + * dump affecting the behavior. + */ + private static final boolean ENABLE_DUMP = true; // DO NOT SUBMIT WITH true + + /** Context used in the client side */ + private final class ClientContext extends MockContext { + @Override + public String getPackageName() { + return mInjectedClientPackage; + } + } + + /** Context used in the service side */ + private final class ServiceContext extends MockContext { + } + + /** ShortcutService with injection override methods. */ + private final class ShortcutServiceTestable extends ShortcutService { + public ShortcutServiceTestable(Context context) { + super(context); + + } + + @Override + void injectLoadConfigurationLocked() { + setResetIntervalForTest(INTERVAL); + setMaxDynamicShortcutsForTest(MAX_SHORTCUTS); + setMaxDailyUpdatesForTest(MAX_DAILY_UPDATES); + } + + @Override + long injectCurrentTimeMillis() { + return mInjectedCurrentTimeLillis; + } + + @Override + int injectBinderCallingUid() { + return mInjectedCallingUid; + } + + @Override + int injectGetPackageUid(String packageName) { + Integer uid = mInjectedPackageUidMap.get(packageName); + return uid != null ? uid : -1; + } + + @Override + File injectSystemDataPath() { + return new File(mInjectedFilePathRoot, "system"); + } + + @Override + File injectUserDataPath(@UserIdInt int userId) { + return new File(mInjectedFilePathRoot, "user-" + userId); + } + } + + /** ShortcutManager with injection override methods. */ + private final class ShortcutManagerTestable extends ShortcutManager { + public ShortcutManagerTestable(Context context, ShortcutServiceTestable service) { + super(context, service); + } + + @Override + protected int injectMyUserId() { + return UserHandle.getUserId(mInjectedCallingUid); + } + } + + + public static class ShortcutActivity extends Activity { + } + + public static class ShortcutActivity2 extends Activity { + } + + public static class ShortcutActivity3 extends Activity { + } + + private ServiceContext mServiceContext; + private ClientContext mClientContext; + + private ShortcutServiceTestable mService; + private ShortcutManagerTestable mManager; + private ShortcutServiceInternal mInternal; + + private File mInjectedFilePathRoot; + + private long mInjectedCurrentTimeLillis; + + private int mInjectedCallingUid; + private String mInjectedClientPackage; + + private Map mInjectedPackageUidMap; + + private static final String CALLING_PACKAGE_1 = "com.android.test.1"; + private static final int CALLING_UID_1 = 10001; + + private static final String CALLING_PACKAGE_2 = "com.android.test.2"; + private static final int CALLING_UID_2 = 10002; + + private static final String CALLING_PACKAGE_3 = "com.android.test.3"; + private static final int CALLING_UID_3 = 10003; + + private static final String LAUNCHER_1 = "com.android.launcher.1"; + private static final int LAUNCHER_UID_1 = 10011; + + private static final String LAUNCHER_2 = "com.android.launcher.2"; + private static final int LAUNCHER_UID_2 = 10012; + + private static final long START_TIME = 1234560000000L; + + private static final long INTERVAL = 10000; + + private static final int MAX_SHORTCUTS = 5; + + private static final int MAX_DAILY_UPDATES = 3; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mServiceContext = new ServiceContext(); + mClientContext = new ClientContext(); + + // Prepare injection values. + + mInjectedCurrentTimeLillis = START_TIME; + + mInjectedPackageUidMap = new HashMap<>(); + mInjectedPackageUidMap.put(CALLING_PACKAGE_1, CALLING_UID_1); + mInjectedPackageUidMap.put(CALLING_PACKAGE_2, CALLING_UID_2); + mInjectedPackageUidMap.put(CALLING_PACKAGE_3, CALLING_UID_3); + mInjectedPackageUidMap.put(LAUNCHER_1, LAUNCHER_UID_1); + mInjectedPackageUidMap.put(LAUNCHER_2, LAUNCHER_UID_2); + + mInjectedFilePathRoot = new File(getContext().getCacheDir(), "test-files"); + + // Empty the data directory. + if (mInjectedFilePathRoot.exists()) { + Assert.assertTrue("failed to delete dir", + FileUtils.deleteContents(mInjectedFilePathRoot)); + } + mInjectedFilePathRoot.mkdirs(); + + initService(); + setCaller(CALLING_PACKAGE_1); + } + + /** (Re-) init the manager and the service. */ + private void initService() { + LocalServices.removeServiceForTest(ShortcutServiceInternal.class); + + // Instantiate targets. + mService = new ShortcutServiceTestable(mServiceContext); + mManager = new ShortcutManagerTestable(mClientContext, mService); + + mInternal = LocalServices.getService(ShortcutServiceInternal.class); + + // Load the setting file. + mService.onBootPhase(SystemService.PHASE_LOCK_SETTINGS_READY); + } + + /** Replace the current calling package */ + private void setCaller(String packageName) { + mInjectedClientPackage = packageName; + mInjectedCallingUid = Preconditions.checkNotNull(mInjectedPackageUidMap.get(packageName)); + } + + private String getCallingPackage() { + return mInjectedClientPackage; + } + + private int getCallingUserId() { + return UserHandle.getUserId(mInjectedCallingUid); + } + + /** For debugging */ + private void dumpsysOnLogcat() { + if (!ENABLE_DUMP) return; + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final PrintWriter pw = new PrintWriter(out); + mService.dumpInner(pw); + pw.close(); + + Log.e(TAG, "Dumping ShortcutService:"); + for (String line : out.toString().split("\n")) { + Log.e(TAG, line); + } + } + + /** + * For debugging, dump arbitrary file on logcat. + */ + private void dumpFileOnLogcat(String path) { + if (!ENABLE_DUMP) return; + + Log.i(TAG, "Dumping file: " + path); + final StringBuilder sb = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new FileReader(path))) { + String line; + while ((line = br.readLine()) != null) { + Log.i(TAG, line); + } + } catch (Exception e) { + Log.e(TAG, "Couldn't read file", e); + fail("Exception " + e); + } + } + + /** + * For debugging, dump the main state file on logcat. + */ + private void dumpBaseStateFile() { + dumpFileOnLogcat(mInjectedFilePathRoot.getAbsolutePath() + + "/system/" + ShortcutService.FILENAME_BASE_STATE); + } + + /** + * For debugging, dump per-user state file on logcat. + */ + private void dumpUserFile(int userId) { + dumpFileOnLogcat(mInjectedFilePathRoot.getAbsolutePath() + + "/user-" + userId + + "/" + ShortcutService.FILENAME_USER_PACKAGES); + } + + private static Bundle makeBundle(Object... keysAndValues) { + Preconditions.checkState((keysAndValues.length % 2) == 0); + + if (keysAndValues.length == 0) { + return null; + } + final Bundle ret = new Bundle(); + + for (int i = keysAndValues.length - 2; i >= 0; i -= 2) { + final String key = keysAndValues[i].toString(); + final Object value = keysAndValues[i + 1]; + + if (value == null) { + ret.putString(key, null); + } else if (value instanceof Integer) { + ret.putInt(key, (Integer) value); + } else if (value instanceof String) { + ret.putString(key, (String) value); + } else if (value instanceof Bundle) { + ret.putBundle(key, (Bundle) value); + } else { + fail("Type not supported yet: " + value.getClass().getName()); + } + } + return ret; + } + + /** + * Make a shortcut with an ID. + */ + private ShortcutInfo makeShortcut(String id) { + return makeShortcut( + id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + } + + /** + * Make a shortcut with an ID and timestamp. + */ + private ShortcutInfo makeShortcutWithTimestamp(String id, long timestamp) { + final ShortcutInfo s = makeShortcut( + id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + s.setTimestamp(timestamp); + return s; + } + + /** + * Make multiple shortcuts with IDs. + */ + private List makeShortcuts(String... ids) { + final ArrayList ret = new ArrayList(); + for (String id : ids) { + ret.add(makeShortcut(id)); + } + return ret; + } + + /** + * Make a shortcut with details. + */ + private ShortcutInfo makeShortcut(String id, String title, ComponentName activity, + Icon icon, Intent intent, int weight) { + final ShortcutInfo.Builder b = new ShortcutInfo.Builder(mClientContext) + .setId(id) + .setTitle(title) + .setWeight(weight) + .setIntent(intent); + if (icon != null) { + b.setIcon(icon); + } + if (activity != null) { + b.setActivityComponent(activity); + } + final ShortcutInfo s = b.build(); + + s.setTimestamp(mInjectedCurrentTimeLillis); // HACK + + return s; + } + + /** + * Make an intent. + */ + private Intent makeIntent(String action, Class clazz, Object... bundleKeysAndValues) { + final Intent intent = new Intent(action); + intent.setComponent(makeComponent(clazz)); + intent.replaceExtras(makeBundle(bundleKeysAndValues)); + return intent; + } + + /** + * Make an component name, with the client context. + */ + @NonNull + private ComponentName makeComponent(Class clazz) { + return new ComponentName(mClientContext, clazz); + } + + @NonNull + private ShortcutInfo findById(List list, String id) { + for (ShortcutInfo s : list) { + if (s.getId().equals(id)) { + return s; + } + } + fail("Shortcut with id " + id + " not found"); + return null; + } + + private void assertResetTimes(long expectedLastResetTime, long expectedNextResetTime) { + assertEquals(expectedLastResetTime, mService.getLastResetTimeLocked()); + assertEquals(expectedNextResetTime, mService.getNextResetTimeLocked()); + } + + @NonNull + private List assertShortcutIds(@NonNull List actualShortcuts, + String... expectedIds) { + final HashSet expected = new HashSet<>(Arrays.asList(expectedIds)); + final HashSet actual = new HashSet<>(); + for (ShortcutInfo s : actualShortcuts) { + actual.add(s.getId()); + } + + // Compare the sets. + assertEquals(expected, actual); + return actualShortcuts; + } + + @NonNull + private List assertAllHaveIntents( + @NonNull List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertNotNull("ID " + s.getId(), s.getIntent()); + } + return actualShortcuts; + } + + @NonNull + private List assertAllNotHaveIntents( + @NonNull List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertNull("ID " + s.getId(), s.getIntent()); + } + return actualShortcuts; + } + + @NonNull + private List assertAllHaveTitle( + @NonNull List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertNotNull("ID " + s.getId(), s.getTitle()); + } + return actualShortcuts; + } + + @NonNull + private List assertAllNotHaveTitle( + @NonNull List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertNull("ID " + s.getId(), s.getTitle()); + } + return actualShortcuts; + } + + @NonNull + private List assertAllHaveFlags(@NonNull List actualShortcuts, + int shortcutFlags) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), s.hasFlags(shortcutFlags)); + } + return actualShortcuts; + } + + @NonNull + private List assertAllDynamic(@NonNull List actualShortcuts) { + return assertAllHaveFlags(actualShortcuts, ShortcutInfo.FLAG_DYNAMIC); + } + + @NonNull + private List assertAllPinned(@NonNull List actualShortcuts) { + return assertAllHaveFlags(actualShortcuts, ShortcutInfo.FLAG_PINNED); + } + + @NonNull + private List assertAllDynamicOrPinned( + @NonNull List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), s.isDynamic() || s.isPinned()); + } + return actualShortcuts; + } + + /** + * Test for the first launch path, no settings file available. + */ + public void testFirstInitialize() { + assertResetTimes(START_TIME, START_TIME + INTERVAL); + } + + /** + * Test for {@link ShortcutService#updateTimes()} + */ + public void testUpdateAndGetNextResetTimeLocked() { + assertResetTimes(START_TIME, START_TIME + INTERVAL); + + // Advance clock. + mInjectedCurrentTimeLillis += 100; + + // Shouldn't have changed. + assertResetTimes(START_TIME, START_TIME + INTERVAL); + + // Advance clock, almost the reset time. + mInjectedCurrentTimeLillis = START_TIME + INTERVAL - 1; + + // Shouldn't have changed. + assertResetTimes(START_TIME, START_TIME + INTERVAL); + + // Advance clock. + mInjectedCurrentTimeLillis += 1; + + assertResetTimes(START_TIME + INTERVAL, START_TIME + 2 * INTERVAL); + + // Advance further; 4 days since start. + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL + 50; + + assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL); + } + + /** + * Test for the restoration from saved file. + */ + public void testInitializeFromSavedFile() { + + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL + 50; + assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL); + + mService.saveBaseStateLocked(); + + dumpBaseStateFile(); + + // Restore. + initService(); + + assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL); + } + + /** + * Test for the restoration from restored file. + */ + public void testLoadFromBrokenFile() { + // TODO Add various broken cases. + } + + // === Test for app side APIs === + + /** Test for {@link android.content.pm.ShortcutManager#getMaxDynamicShortcutCount()} */ + public void testGetMaxDynamicShortcutCount() { + assertEquals(MAX_SHORTCUTS, mManager.getMaxDynamicShortcutCount()); + } + + /** Test for {@link android.content.pm.ShortcutManager#getRemainingCallCount()} */ + public void testGetRemainingCallCount() { + assertEquals(MAX_DAILY_UPDATES, mManager.getRemainingCallCount()); + } + + /** Test for {@link android.content.pm.ShortcutManager#getRateLimitResetTime()} */ + public void testGetRateLimitResetTime() { + assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime()); + + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL + 50; + + assertEquals(START_TIME + 5 * INTERVAL, mManager.getRateLimitResetTime()); + } + + public void testSetDynamicShortcuts() { + final Icon icon1 = Icon.createWithResource(mContext, R.drawable.icon1); + final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), R.drawable.icon2)); + + final ShortcutInfo si1 = makeShortcut( + "shortcut1", + "Title 1", + makeComponent(ShortcutActivity.class), + icon1, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + + final ShortcutInfo si2 = makeShortcut( + "shortcut2", + "Title 2", + /* activity */ null, + icon2, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), + /* weight */ 12); + final ShortcutInfo si3 = makeShortcut("shortcut3"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2))); + assertEquals(2, mManager.getDynamicShortcuts().size()); + assertEquals(2, mManager.getRemainingCallCount()); + + // TODO: Check fields + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(1, mManager.getDynamicShortcuts().size()); + assertEquals(1, mManager.getRemainingCallCount()); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList())); + assertEquals(0, mManager.getDynamicShortcuts().size()); + assertEquals(0, mManager.getRemainingCallCount()); + + dumpsysOnLogcat(); + + mInjectedCurrentTimeLillis++; // Need to advance the clock for reset to work. + mService.resetThrottlingInner(); + + dumpsysOnLogcat(); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2, si3))); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + // TODO Check max number + } + + public void testAddDynamicShortcuts() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + final ShortcutInfo si2 = makeShortcut("shortcut2"); + final ShortcutInfo si3 = makeShortcut("shortcut3"); + + assertEquals(3, mManager.getRemainingCallCount()); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + assertEquals(1, mManager.getDynamicShortcuts().size()); + + assertTrue(mManager.addDynamicShortcut(si2)); + assertEquals(1, mManager.getRemainingCallCount()); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + // Add with the same ID + assertTrue(mManager.addDynamicShortcut(makeShortcut("shortcut1"))); + assertEquals(0, mManager.getRemainingCallCount()); + assertEquals(2, mManager.getDynamicShortcuts().size()); // Still 2 + + // TODO Check max number + + // TODO Check fields. + } + + public void testDeleteDynamicShortcut() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + final ShortcutInfo si2 = makeShortcut("shortcut2"); + final ShortcutInfo si3 = makeShortcut("shortcut3"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2, si3))); + assertEquals(3, mManager.getDynamicShortcuts().size()); + + assertEquals(2, mManager.getRemainingCallCount()); + + mManager.deleteDynamicShortcut("shortcut1"); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + mManager.deleteDynamicShortcut("shortcut1"); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + mManager.deleteDynamicShortcut("shortcutXXX"); + assertEquals(2, mManager.getDynamicShortcuts().size()); + + mManager.deleteDynamicShortcut("shortcut2"); + assertEquals(1, mManager.getDynamicShortcuts().size()); + + mManager.deleteDynamicShortcut("shortcut3"); + assertEquals(0, mManager.getDynamicShortcuts().size()); + + // Still 2 calls left. + assertEquals(2, mManager.getRemainingCallCount()); + + // TODO Make sure pinned shortcuts won't be deleted. + } + + public void testDeleteAllDynamicShortcuts() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + final ShortcutInfo si2 = makeShortcut("shortcut2"); + final ShortcutInfo si3 = makeShortcut("shortcut3"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2, si3))); + assertEquals(3, mManager.getDynamicShortcuts().size()); + + assertEquals(2, mManager.getRemainingCallCount()); + + mManager.deleteAllDynamicShortcuts(); + assertEquals(0, mManager.getDynamicShortcuts().size()); + assertEquals(2, mManager.getRemainingCallCount()); + + // Note delete shouldn't affect throttling, so... + assertEquals(0, mManager.getDynamicShortcuts().size()); + assertEquals(0, mManager.getDynamicShortcuts().size()); + assertEquals(0, mManager.getDynamicShortcuts().size()); + + // This should still work. + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2, si3))); + assertEquals(3, mManager.getDynamicShortcuts().size()); + + // Still 1 call left + assertEquals(1, mManager.getRemainingCallCount()); + + // TODO Make sure pinned shortcuts won't be deleted. + } + + public void testThrottling() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(1, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Reached the max + + mInjectedCurrentTimeLillis++; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Still throttled + mInjectedCurrentTimeLillis = START_TIME + INTERVAL - 1; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Now it should work. + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(1, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // 4 days later... + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + + // Make sure getRemainingCallCount() itself gets reset withou calling setDynamicShortcuts(). + mInjectedCurrentTimeLillis = START_TIME + 8 * INTERVAL; + assertEquals(3, mManager.getRemainingCallCount()); + } + + public void testThrottling_perPackage() { + final ShortcutInfo si1 = makeShortcut("shortcut1"); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(2, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(1, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Reached the max + + mInjectedCurrentTimeLillis++; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + // Try from a different caller. + mInjectedClientPackage = CALLING_PACKAGE_2; + mInjectedCallingUid = CALLING_UID_2; + + // Need to create a new one wit the updated package name. + final ShortcutInfo si2 = makeShortcut("shortcut1"); + + assertEquals(3, mManager.getRemainingCallCount()); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertEquals(2, mManager.getRemainingCallCount()); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertEquals(1, mManager.getRemainingCallCount()); + + // Back to the original caller, still throttled. + mInjectedClientPackage = CALLING_PACKAGE_1; + mInjectedCallingUid = CALLING_UID_1; + + mInjectedCurrentTimeLillis = START_TIME + INTERVAL - 1; + assertEquals(0, mManager.getRemainingCallCount()); + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertEquals(0, mManager.getRemainingCallCount()); + + // Now it should work. + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedCurrentTimeLillis++; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedCurrentTimeLillis++; + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedCurrentTimeLillis = START_TIME + 4 * INTERVAL; + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1))); + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si1))); + + mInjectedClientPackage = CALLING_PACKAGE_2; + mInjectedCallingUid = CALLING_UID_2; + + assertEquals(3, mManager.getRemainingCallCount()); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si2))); + assertFalse(mManager.setDynamicShortcuts(Arrays.asList(si2))); + } + + // TODO: updateShortcuts() + // TODO: getPinnedShortcuts() + + // === Test for launcher side APIs === + + public void testGetShortcuts() { + + // Set up shortcuts. + + setCaller(CALLING_PACKAGE_1); + final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 5000); + final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 1000); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s1_1, s1_2))); + + setCaller(CALLING_PACKAGE_2); + final ShortcutInfo s2_2 = makeShortcutWithTimestamp("s2", 1500); + final ShortcutInfo s2_3 = makeShortcutWithTimestamp("s3", 3000); + final ShortcutInfo s2_4 = makeShortcutWithTimestamp("s4", 500); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_2, s2_3, s2_4))); + + setCaller(CALLING_PACKAGE_3); + final ShortcutInfo s3_2 = makeShortcutWithTimestamp("s3", 5000); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s3_2))); + + setCaller(LAUNCHER_1); + + // Get dynamic + assertAllDynamic(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_1, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_DYNAMIC, getCallingUserId()), + "s1", "s2")))); + + // Get pinned + assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_1, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_PINNED, getCallingUserId()) + /* none */); + + // Get both, with timestamp + assertAllDynamic(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 1000, CALLING_PACKAGE_2, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_PINNED | ShortcutQuery.FLAG_GET_DYNAMIC, + getCallingUserId()), + "s2", "s3")))); + + // FLAG_GET_KEY_FIELDS_ONLY + assertAllDynamic(assertAllNotHaveTitle(assertAllNotHaveIntents(assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 1000, CALLING_PACKAGE_2, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY, + getCallingUserId()), + "s2", "s3")))); + + // Pin some shortcuts. + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_2, + Arrays.asList("s3", "s4"), getCallingUserId()); + + // Pinned ones only + assertAllPinned(assertAllHaveTitle(assertAllNotHaveIntents(assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 1000, CALLING_PACKAGE_2, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_PINNED, + getCallingUserId()), + "s3")))); + + // All packages. + assertShortcutIds( + mInternal.getShortcuts(getCallingPackage(), + /* time =*/ 5000, /* package= */ null, + /* activity =*/ null, + ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED, + getCallingUserId()), + "s1", "s3"); + + // TODO More tests: pinned but dynamic, filter by activity + } + + public void testGetShortcutInfo() { + // Create shortcuts. + setCaller(CALLING_PACKAGE_1); + final ShortcutInfo s1_1 = makeShortcut( + "s1", + "Title 1", + makeComponent(ShortcutActivity.class), + /* icon =*/ null, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + + final ShortcutInfo s1_2 = makeShortcut( + "s2", + "Title 2", + /* activity */ null, + /* icon =*/ null, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), + /* weight */ 12); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s1_1, s1_2))); + dumpsysOnLogcat(); + + setCaller(CALLING_PACKAGE_2); + final ShortcutInfo s2_1 = makeShortcut( + "s1", + "ABC", + makeComponent(ShortcutActivity2.class), + /* icon =*/ null, + makeIntent(Intent.ACTION_ANSWER, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_1))); + dumpsysOnLogcat(); + + // Pin some. + setCaller(LAUNCHER_1); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s2"), getCallingUserId()); + + dumpsysOnLogcat(); + + // Delete some. + setCaller(CALLING_PACKAGE_1); + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + mManager.deleteDynamicShortcut("s2"); + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + + dumpsysOnLogcat(); + + setCaller(LAUNCHER_1); + List list; + + // Note we don't guarantee the orders. + list = assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents( + mInternal.getShortcutInfo(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s2", "s1", "s3", null), getCallingUserId()))), + "s1", "s2"); + assertEquals("Title 1", findById(list, "s1").getTitle()); + assertEquals("Title 2", findById(list, "s2").getTitle()); + + assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents( + mInternal.getShortcutInfo(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s3"), getCallingUserId()))) + /* none */); + + list = assertShortcutIds(assertAllHaveTitle(assertAllNotHaveIntents( + mInternal.getShortcutInfo(getCallingPackage(), CALLING_PACKAGE_2, + Arrays.asList("s1", "s2", "s3"), getCallingUserId()))), + "s1"); + assertEquals("ABC", findById(list, "s1").getTitle()); + } + + public void testPinShortcutAndGetPinnedShortcuts() { + // Create some shortcuts. + setCaller(CALLING_PACKAGE_1); + final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000); + final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s1_1, s1_2))); + + setCaller(CALLING_PACKAGE_2); + final ShortcutInfo s2_2 = makeShortcutWithTimestamp("s2", 1500); + final ShortcutInfo s2_3 = makeShortcutWithTimestamp("s3", 3000); + final ShortcutInfo s2_4 = makeShortcutWithTimestamp("s4", 500); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_2, s2_3, s2_4))); + + setCaller(CALLING_PACKAGE_3); + final ShortcutInfo s3_2 = makeShortcutWithTimestamp("s2", 1000); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_2))); + + // Pin some. + setCaller(LAUNCHER_1); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s2", "s3"), getCallingUserId()); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_2, + Arrays.asList("s3", "s4", "s5"), getCallingUserId()); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_3, + Arrays.asList("s3"), getCallingUserId()); // Note ID doesn't exist + + // Delete some. + setCaller(CALLING_PACKAGE_1); + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + mManager.deleteDynamicShortcut("s2"); + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + + setCaller(CALLING_PACKAGE_2); + assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4"); + mManager.deleteDynamicShortcut("s3"); + assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4"); + + setCaller(CALLING_PACKAGE_3); + assertShortcutIds(mManager.getPinnedShortcuts() /* none */); + mManager.deleteDynamicShortcut("s2"); + assertShortcutIds(mManager.getPinnedShortcuts() /* none */); + + // Get pinned shortcuts from launcher + setCaller(LAUNCHER_1); + + // CALLING_PACKAGE_1 deleted s2, but it's pinned, so it still exists. + assertShortcutIds(assertAllPinned( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_1, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId())), + "s2"); + + assertShortcutIds(assertAllPinned( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_2, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId())), + "s3", "s4"); + + assertShortcutIds(assertAllPinned( + mInternal.getShortcuts(getCallingPackage(), /* time =*/ 0, CALLING_PACKAGE_3, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED, getCallingUserId())) + /* none */); + } + + public void testCreateShortcutIntent() { + // Create some shortcuts. + setCaller(CALLING_PACKAGE_1); + final ShortcutInfo s1_1 = makeShortcut( + "s1", + "Title 1", + makeComponent(ShortcutActivity.class), + /* icon =*/ null, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + + final ShortcutInfo s1_2 = makeShortcut( + "s2", + "Title 2", + /* activity */ null, + /* icon =*/ null, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), + /* weight */ 12); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s1_1, s1_2))); + + setCaller(CALLING_PACKAGE_2); + final ShortcutInfo s2_1 = makeShortcut( + "s1", + "ABC", + makeComponent(ShortcutActivity.class), + /* icon =*/ null, + makeIntent(Intent.ACTION_ANSWER, ShortcutActivity.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(s2_1))); + + // Pin all. + setCaller(LAUNCHER_1); + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_1, + Arrays.asList("s1", "s2"), getCallingUserId()); + + mInternal.pinShortcuts(getCallingPackage(), CALLING_PACKAGE_2, + Arrays.asList("s1"), getCallingUserId()); + + // Just to make it complicated, delete some. + setCaller(CALLING_PACKAGE_1); + mManager.deleteDynamicShortcut("s2"); + + // intent and check. + setCaller(LAUNCHER_1); + Intent intent; + intent = mInternal.createShortcutIntent(getCallingPackage(), + s1_1.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO), getCallingUserId()); + assertEquals(ShortcutActivity2.class.getName(), intent.getComponent().getClassName()); + + intent = mInternal.createShortcutIntent(getCallingPackage(), + s1_2.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO), getCallingUserId()); + assertEquals(ShortcutActivity3.class.getName(), intent.getComponent().getClassName()); + + intent = mInternal.createShortcutIntent(getCallingPackage(), + s2_1.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO), getCallingUserId()); + assertEquals(ShortcutActivity.class.getName(), intent.getComponent().getClassName()); + + // TODO Check extra, etc + } + + // === Test for persisting === + + public void testSaveAndLoadUser_empty() { + assertTrue(mManager.setDynamicShortcuts(Arrays.asList())); + + Log.i(TAG, "Saved state"); + dumpsysOnLogcat(); + dumpUserFile(0); + + // Restore. + initService(); + + assertEquals(0, mManager.getDynamicShortcuts().size()); + } + + /** + * Try save and load, also stop/start the user. + */ + public void testSaveAndLoadUser() { + // First, create some shortcuts and save. + final Icon icon1 = Icon.createWithResource(mContext, R.drawable.icon1); + final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), R.drawable.icon2)); + + final ShortcutInfo si1 = makeShortcut( + "shortcut1", + "Title 1", + makeComponent(ShortcutActivity.class), + icon1, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, + "key1", "val1", "nest", makeBundle("key", 123)), + /* weight */ 10); + + final ShortcutInfo si2 = makeShortcut( + "shortcut2", + "Title 2", + /* activity */ null, + icon2, + makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), + /* weight */ 12); + + assertTrue(mManager.setDynamicShortcuts(Arrays.asList(si1, si2))); + + assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime()); + assertEquals(2, mManager.getRemainingCallCount()); + + Log.i(TAG, "Saved state"); + dumpsysOnLogcat(); + dumpUserFile(0); + + // Restore. + initService(); + + // Before the load, the map should be empty. + assertEquals(0, mService.getShortcutsForTest().size()); + + // this will pre-load the per-user info. + mService.onStartUserLocked(UserHandle.USER_SYSTEM); + + // Now it's loaded. + assertEquals(1, mService.getShortcutsForTest().size()); + + // Start another user + mService.onStartUserLocked(10); + + // Now the size is 2. + assertEquals(2, mService.getShortcutsForTest().size()); + + Log.i(TAG, "Dumping the new instance"); + + List loaded = mManager.getDynamicShortcuts(); + + Log.i(TAG, "Loaded state"); + dumpsysOnLogcat(); + + assertEquals(2, loaded.size()); + + assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime()); + assertEquals(2, mManager.getRemainingCallCount()); + + // Try stopping the user + mService.onCleanupUserInner(UserHandle.USER_SYSTEM); + + // Now it's unloaded. + assertEquals(1, mService.getShortcutsForTest().size()); + + // TODO Check all other fields + } +} diff --git a/services/tests/servicestests/src/com/android/server/testutis/TestUtils.java b/services/tests/servicestests/src/com/android/server/testutis/TestUtils.java new file mode 100644 index 0000000000000..52e8f37f0b6cb --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/testutis/TestUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.testutis; + +import android.test.MoreAsserts; + +import junit.framework.Assert; + +public class TestUtils { + private TestUtils() { + } + + public static void assertExpectException(Class expectedExceptionType, + Runnable r) { + assertExpectException(expectedExceptionType, null, r); + } + + public static void assertExpectException(Class expectedExceptionType, + String expectedExceptionMessageRegex, Runnable r) { + try { + r.run(); + Assert.fail("Expected exception type " + expectedExceptionType.getClass().getName() + + " was not thrown"); + } catch (Throwable e) { + Assert.assertTrue( + "Expected exception type was " + expectedExceptionType.getClass().getName() + + " but caught " + e.getClass().getName(), + expectedExceptionType.isAssignableFrom(e.getClass())); + if (expectedExceptionMessageRegex != null) { + MoreAsserts.assertContainsRegex(expectedExceptionMessageRegex, e.getMessage()); + } + } + } +}