diff --git a/Android.mk b/Android.mk index 5d902c2059038..09fc269e3e387 100644 --- a/Android.mk +++ b/Android.mk @@ -144,6 +144,7 @@ LOCAL_SRC_FILES += \ core/java/android/content/ISyncStatusObserver.aidl \ core/java/android/content/pm/ILauncherApps.aidl \ core/java/android/content/pm/IOnAppsChangedListener.aidl \ + core/java/android/content/pm/IOnPermissionsChangeListener.aidl \ core/java/android/content/pm/IOtaDexopt.aidl \ core/java/android/content/pm/IPackageDataObserver.aidl \ core/java/android/content/pm/IPackageDeleteObserver.aidl \ @@ -156,7 +157,7 @@ LOCAL_SRC_FILES += \ core/java/android/content/pm/IPackageManager.aidl \ 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/IPinItemRequest.aidl \ core/java/android/content/pm/IShortcutService.aidl \ core/java/android/content/pm/permission/IRuntimePermissionPresenter.aidl \ core/java/android/database/IContentObserver.aidl \ diff --git a/api/current.txt b/api/current.txt index 304ccf836d8a1..52b12f632d910 100644 --- a/api/current.txt +++ b/api/current.txt @@ -9648,6 +9648,7 @@ package android.content.pm { public class LauncherApps { method public java.util.List getActivityList(java.lang.String, android.os.UserHandle); + method public android.content.pm.LauncherApps.PinItemRequest getPinItemRequest(android.content.Intent); method public android.graphics.drawable.Drawable getShortcutBadgedIconDrawable(android.content.pm.ShortcutInfo, int); method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int); method public java.util.List getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); @@ -9663,6 +9664,8 @@ package android.content.pm { method public void startShortcut(java.lang.String, java.lang.String, android.graphics.Rect, android.os.Bundle, android.os.UserHandle); method public void startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle); method public void unregisterCallback(android.content.pm.LauncherApps.Callback); + field public static final java.lang.String ACTION_CONFIRM_PIN_ITEM = "android.content.pm.action.CONFIRM_PIN_ITEM"; + field public static final java.lang.String EXTRA_PIN_ITEM_REQUEST = "android.content.pm.extra.PIN_ITEM_REQUEST"; } public static abstract class LauncherApps.Callback { @@ -9677,6 +9680,21 @@ package android.content.pm { method public void onShortcutsChanged(java.lang.String, java.util.List, android.os.UserHandle); } + public static final class LauncherApps.PinItemRequest implements android.os.Parcelable { + method public boolean accept(android.os.Bundle); + method public boolean accept(); + method public int describeContents(); + method public int getRequestType(); + method public android.content.pm.ShortcutInfo getShortcutInfo(); + method public boolean isValid(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int REQUEST_TYPE_SHORTCUT = 1; // 0x1 + } + + public static abstract class LauncherApps.PinItemRequest.RequestType implements java.lang.annotation.Annotation { + } + public static class LauncherApps.ShortcutQuery { ctor public LauncherApps.ShortcutQuery(); method public android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName); @@ -10241,9 +10259,11 @@ package android.content.pm { method public int getMaxShortcutCountPerActivity(); method public java.util.List getPinnedShortcuts(); method public boolean isRateLimitingActive(); + method public boolean isRequestPinShortcutSupported(); method public void removeAllDynamicShortcuts(); method public void removeDynamicShortcuts(java.util.List); method public void reportShortcutUsed(java.lang.String); + method public boolean requestPinShortcut(android.content.pm.ShortcutInfo, android.content.IntentSender); method public boolean setDynamicShortcuts(java.util.List); method public boolean updateShortcuts(java.util.List); } diff --git a/api/system-current.txt b/api/system-current.txt index d54e6a9bf05a4..9430f79558d81 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -10048,6 +10048,7 @@ package android.content.pm { public class LauncherApps { method public java.util.List getActivityList(java.lang.String, android.os.UserHandle); + method public android.content.pm.LauncherApps.PinItemRequest getPinItemRequest(android.content.Intent); method public android.graphics.drawable.Drawable getShortcutBadgedIconDrawable(android.content.pm.ShortcutInfo, int); method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int); method public java.util.List getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); @@ -10063,6 +10064,8 @@ package android.content.pm { method public void startShortcut(java.lang.String, java.lang.String, android.graphics.Rect, android.os.Bundle, android.os.UserHandle); method public void startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle); method public void unregisterCallback(android.content.pm.LauncherApps.Callback); + field public static final java.lang.String ACTION_CONFIRM_PIN_ITEM = "android.content.pm.action.CONFIRM_PIN_ITEM"; + field public static final java.lang.String EXTRA_PIN_ITEM_REQUEST = "android.content.pm.extra.PIN_ITEM_REQUEST"; } public static abstract class LauncherApps.Callback { @@ -10077,6 +10080,21 @@ package android.content.pm { method public void onShortcutsChanged(java.lang.String, java.util.List, android.os.UserHandle); } + public static final class LauncherApps.PinItemRequest implements android.os.Parcelable { + method public boolean accept(android.os.Bundle); + method public boolean accept(); + method public int describeContents(); + method public int getRequestType(); + method public android.content.pm.ShortcutInfo getShortcutInfo(); + method public boolean isValid(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int REQUEST_TYPE_SHORTCUT = 1; // 0x1 + } + + public static abstract class LauncherApps.PinItemRequest.RequestType implements java.lang.annotation.Annotation { + } + public static class LauncherApps.ShortcutQuery { ctor public LauncherApps.ShortcutQuery(); method public android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName); @@ -10712,9 +10730,11 @@ package android.content.pm { method public int getMaxShortcutCountPerActivity(); method public java.util.List getPinnedShortcuts(); method public boolean isRateLimitingActive(); + method public boolean isRequestPinShortcutSupported(); method public void removeAllDynamicShortcuts(); method public void removeDynamicShortcuts(java.util.List); method public void reportShortcutUsed(java.lang.String); + method public boolean requestPinShortcut(android.content.pm.ShortcutInfo, android.content.IntentSender); method public boolean setDynamicShortcuts(java.util.List); method public boolean updateShortcuts(java.util.List); } diff --git a/api/test-current.txt b/api/test-current.txt index f2694791cce92..dcac07e4798ea 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -9676,6 +9676,7 @@ package android.content.pm { public class LauncherApps { ctor public LauncherApps(android.content.Context); method public java.util.List getActivityList(java.lang.String, android.os.UserHandle); + method public android.content.pm.LauncherApps.PinItemRequest getPinItemRequest(android.content.Intent); method public android.graphics.drawable.Drawable getShortcutBadgedIconDrawable(android.content.pm.ShortcutInfo, int); method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int); method public java.util.List getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); @@ -9691,6 +9692,8 @@ package android.content.pm { method public void startShortcut(java.lang.String, java.lang.String, android.graphics.Rect, android.os.Bundle, android.os.UserHandle); method public void startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle); method public void unregisterCallback(android.content.pm.LauncherApps.Callback); + field public static final java.lang.String ACTION_CONFIRM_PIN_ITEM = "android.content.pm.action.CONFIRM_PIN_ITEM"; + field public static final java.lang.String EXTRA_PIN_ITEM_REQUEST = "android.content.pm.extra.PIN_ITEM_REQUEST"; } public static abstract class LauncherApps.Callback { @@ -9705,6 +9708,21 @@ package android.content.pm { method public void onShortcutsChanged(java.lang.String, java.util.List, android.os.UserHandle); } + public static final class LauncherApps.PinItemRequest implements android.os.Parcelable { + method public boolean accept(android.os.Bundle); + method public boolean accept(); + method public int describeContents(); + method public int getRequestType(); + method public android.content.pm.ShortcutInfo getShortcutInfo(); + method public boolean isValid(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int REQUEST_TYPE_SHORTCUT = 1; // 0x1 + } + + public static abstract class LauncherApps.PinItemRequest.RequestType implements java.lang.annotation.Annotation { + } + public static class LauncherApps.ShortcutQuery { ctor public LauncherApps.ShortcutQuery(); method public android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName); @@ -10272,9 +10290,11 @@ package android.content.pm { method public int getMaxShortcutCountPerActivity(); method public java.util.List getPinnedShortcuts(); method public boolean isRateLimitingActive(); + method public boolean isRequestPinShortcutSupported(); method public void removeAllDynamicShortcuts(); method public void removeDynamicShortcuts(java.util.List); method public void reportShortcutUsed(java.lang.String); + method public boolean requestPinShortcut(android.content.pm.ShortcutInfo, android.content.IntentSender); method public boolean setDynamicShortcuts(java.util.List); method public boolean updateShortcuts(java.util.List); } diff --git a/core/java/android/content/pm/IPinItemRequest.aidl b/core/java/android/content/pm/IPinItemRequest.aidl new file mode 100644 index 0000000000000..efe283527b010 --- /dev/null +++ b/core/java/android/content/pm/IPinItemRequest.aidl @@ -0,0 +1,26 @@ +/* + * 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.os.Bundle; + +/** + * {@hide} + */ +interface IPinItemRequest { + boolean isValid(); + boolean accept(in Bundle options); +} diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl index 1bf2ab0abbada..91df8e8b84c30 100644 --- a/core/java/android/content/pm/IShortcutService.aidl +++ b/core/java/android/content/pm/IShortcutService.aidl @@ -15,6 +15,7 @@ */ package android.content.pm; +import android.content.IntentSender; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; @@ -41,6 +42,9 @@ interface IShortcutService { boolean updateShortcuts(String packageName, in ParceledListSlice shortcuts, int userId); + boolean requestPinShortcut(String packageName, in ShortcutInfo shortcut, + in IntentSender resultIntent, int userId); + void disableShortcuts(String packageName, in List shortcutIds, CharSequence disabledMessage, int disabledMessageResId, int userId); @@ -63,4 +67,6 @@ interface IShortcutService { byte[] getBackupPayload(int user); void applyRestore(in byte[] payload, int user); + + boolean isRequestPinShortcutSupported(int user); } \ 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 7fc80442be90a..5f4bc00a28f09 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -19,6 +19,8 @@ package android.content.pm; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; import android.annotation.TestApi; import android.content.ActivityNotFoundException; import android.content.ComponentName; @@ -32,11 +34,14 @@ import android.graphics.BitmapFactory; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; 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.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -69,6 +74,36 @@ public class LauncherApps { static final String TAG = "LauncherApps"; static final boolean DEBUG = false; + /** + * Activity Action: For the default launcher to show the confirmation dialog to create + * a pinned shortcut. + * + *

See the {@link ShortcutManager} javadoc for details. + * + *

+ * Use {@link #getPinItemRequest(Intent)} to get a {@link PinItemRequest} object, + * and call {@link PinItemRequest#accept(Bundle)} + * if the user accepts. If the user doesn't accept, no further action is required. + * + * @see #EXTRA_PIN_ITEM_REQUEST + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CONFIRM_PIN_ITEM = + "android.content.pm.action.CONFIRM_PIN_ITEM"; + + /** + * An extra for {@link #ACTION_CONFIRM_PIN_ITEM} containing a + * {@link ShortcutInfo} of the shortcut the publisher app asked to pin. + * + *

A helper function {@link #getPinItemRequest(Intent)} can be used + * instead of using this constant directly. + * + * @see #ACTION_CONFIRM_PIN_ITEM + */ + public static final String EXTRA_PIN_ITEM_REQUEST = + "android.content.pm.extra.PIN_ITEM_REQUEST"; + + private Context mContext; private ILauncherApps mService; private PackageManager mPm; @@ -655,23 +690,41 @@ public class LauncherApps { } } } else if (shortcut.hasIconResource()) { - try { - final int resId = shortcut.getIconResourceId(); - if (resId == 0) { - return null; // Shouldn't happen but just in case. + return loadDrawableResourceFromPackage(shortcut.getPackage(), + shortcut.getIconResourceId(), shortcut.getUserHandle(), density); + } else if (shortcut.getIcon() != null) { + // This happens if a shortcut is pending-approval. + final Icon icon = shortcut.getIcon(); + switch (icon.getType()) { + case Icon.TYPE_RESOURCE: { + return loadDrawableResourceFromPackage(shortcut.getPackage(), + icon.getResId(), shortcut.getUserHandle(), density); } - final ApplicationInfo ai = getApplicationInfo(shortcut.getPackage(), - /* flags =*/ 0, shortcut.getUserHandle()); - final Resources res = mContext.getPackageManager().getResourcesForApplication(ai); - return res.getDrawableForDensity(resId, density); - } catch (NameNotFoundException | Resources.NotFoundException e) { - return null; + case Icon.TYPE_BITMAP: { + return icon.loadDrawable(mContext); + } + default: + return null; // Shouldn't happen though. } } else { return null; // Has no icon. } } + private Drawable loadDrawableResourceFromPackage(String packageName, int resId, + UserHandle user, int density) { + try { + if (resId == 0) { + return null; // Shouldn't happen but just in case. + } + final ApplicationInfo ai = getApplicationInfo(packageName, /* flags =*/ 0, user); + final Resources res = mContext.getPackageManager().getResourcesForApplication(ai); + return res.getDrawableForDensity(resId, density); + } catch (NameNotFoundException | Resources.NotFoundException e) { + return null; + } + } + /** * Returns the shortcut icon with badging appropriate for the profile. * @@ -1064,4 +1117,121 @@ public class LauncherApps { obtainMessage(MSG_SHORTCUT_CHANGED, info).sendToTarget(); } } + + /** + * A helper method to extract a {@link PinItemRequest} set to + * the {@link #EXTRA_PIN_ITEM_REQUEST} extra. + */ + public PinItemRequest getPinItemRequest(Intent intent) { + return intent.getParcelableExtra(EXTRA_PIN_ITEM_REQUEST); + } + + /** + * Represents a "pin shortcut" request made by an app, which is sent with + * an {@link #ACTION_CONFIRM_PIN_ITEM} intent to the default launcher app. + * + * @see #EXTRA_PIN_ITEM_REQUEST + * @see #getPinItemRequest(Intent) + */ + public static final class PinItemRequest implements Parcelable { + + /** This is a request to pin shortcut. */ + public static final int REQUEST_TYPE_SHORTCUT = 1; + + @IntDef(value = {REQUEST_TYPE_SHORTCUT}) + @Retention(RetentionPolicy.SOURCE) + public @interface RequestType {} + + private final int mRequestType; + private final ShortcutInfo mShortcutInfo; + private final IPinItemRequest mInner; + + /** + * @hide + */ + public PinItemRequest(@RequestType int requestType, ShortcutInfo shortcutInfo, + IPinItemRequest inner) { + mRequestType = requestType; + mShortcutInfo = shortcutInfo; + mInner = inner; + } + + /** + * Represents the type of a request. For now {@link #REQUEST_TYPE_SHORTCUT} is the only + * valid type. + */ + @RequestType + public int getRequestType() { + return mRequestType; + } + + /** + * {@link ShortcutInfo} sent by the requesting app. Always non-null for a + * {@link #REQUEST_TYPE_SHORTCUT} request. + */ + @Nullable + public ShortcutInfo getShortcutInfo() { + return mShortcutInfo; + } + + /** + * Return {@code TRUE} if a request is valid -- i.e. {@link #accept(Bundle)} has not been + * called, and it has not been canceled. + */ + public boolean isValid() { + try { + return mInner.isValid(); + } catch (RemoteException e) { + return false; + } + } + + /** + * Called by the receiving launcher app when the user accepts the request. + */ + public boolean accept(@Nullable Bundle options) { + try { + return mInner.accept(options); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Same as as {@link #accept(Bundle)} with no options. + */ + public boolean accept() { + return accept(/* options= */ null); + } + + private PinItemRequest(Parcel source) { + final ClassLoader cl = getClass().getClassLoader(); + + mRequestType = source.readInt(); + mShortcutInfo = source.readParcelable(cl); + mInner = IPinItemRequest.Stub.asInterface(source.readStrongBinder()); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRequestType); + dest.writeParcelable(mShortcutInfo, flags); + dest.writeStrongBinder(mInner.asBinder()); + } + + public static final Creator CREATOR = + new Creator() { + public PinItemRequest createFromParcel(Parcel source) { + return new PinItemRequest(source); + } + public PinItemRequest[] newArray(int size) { + return new PinItemRequest[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + } } diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index f7c4d592b3a92..c8f00b8e53c6e 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -16,21 +16,31 @@ package android.content.pm; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.Activity; import android.app.usage.UsageStatsManager; import android.content.Context; import android.content.Intent; +import android.content.IntentSender; +import android.os.Binder; +import android.os.Parcel; +import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; import java.util.List; /** + *

TODO Update the overview to how to use the O new features.

+ * * The ShortcutManager manages an app's shortcuts. Shortcuts provide users * with quick access to activities other than an app's main activity in the currently-active * launcher. For example, @@ -618,7 +628,7 @@ public class ShortcutManager { * * @throws IllegalStateException when the user is locked. */ - public boolean updateShortcuts(List shortcutInfoList) { + public boolean updateShortcuts(@NonNull List shortcutInfoList) { try { return mService.updateShortcuts(mContext.getPackageName(), new ParceledListSlice(shortcutInfoList), injectMyUserId()); @@ -814,6 +824,61 @@ public class ShortcutManager { } } + /** + * Return {@code TRUE} if the default launcher supports + * {@link #requestPinShortcut(ShortcutInfo, IntentSender)}. + */ + public boolean isRequestPinShortcutSupported() { + try { + return mService.isRequestPinShortcutSupported(injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Request to create a pinned shortcut. The default launcher will receive this request and + * ask the user for approval. If the user approves it, the shortcut will be created and + * {@code resultIntent} will be sent. Otherwise, no responses will be sent to the caller. + * + *

When a request is denied by the user, the caller app will not get any response. + * + *

Only apps with a foreground activity or a foreground service can call it. Otherwise + * it'll throw {@link IllegalStateException}. + * + *

When an app calls this API when a previous request is still waiting for a response, + * the previous request will be canceled. + * + * @param shortcut New shortcut to pin. If an app wants to pin an existing (either dynamic + * or manifest) shortcut, then it only needs to have an ID, and other fields don't have to + * be set, in which case, the target shortcut must be enabled. + * If it's a new shortcut, all the mandatory fields, such as a short label, must be + * set. + * @param resultIntent If not null, this intent will be sent when the shortcut is pinned. + * Use {@link android.app.PendingIntent#getIntentSender()} to create a {@link IntentSender}. + * + * @return {@code TRUE} if the launcher supports this feature. Note the API will return without + * waiting for the user to respond, so getting {@code TRUE} from this API does *not* mean + * the shortcut is pinned. {@code FALSE} if the launcher doesn't support this feature. + * + * @see #isRequestPinShortcutSupported() + * @see IntentSender + * @see android.app.PendingIntent#getIntentSender() + * + * @throws IllegalArgumentException if a shortcut with the same ID exists and is disabled. + * @throws IllegalStateException The caller doesn't have a foreground activity or a foreground + * service. + */ + public boolean requestPinShortcut(@NonNull ShortcutInfo shortcut, + @Nullable IntentSender resultIntent) { + try { + return mService.requestPinShortcut(mContext.getPackageName(), shortcut, + resultIntent, injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Called internally when an app is considered to have come to the foreground * even when technically it's not. This method resets the throttling for this package. diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java index 2af1bcb425eca..30608403ca63f 100644 --- a/services/core/java/com/android/server/pm/ShortcutLauncher.java +++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java @@ -16,6 +16,7 @@ package com.android.server.pm; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.pm.PackageInfo; import android.content.pm.ShortcutInfo; @@ -103,6 +104,9 @@ class ShortcutLauncher extends ShortcutPackageItem { // Nothing to do. } + /** + * Pin the given shortcuts, replacing the current pinned ones. + */ public void pinShortcuts(@UserIdInt int packageUserId, @NonNull String packageName, @NonNull List ids) { final ShortcutPackage packageShortcuts = @@ -143,11 +147,39 @@ class ShortcutLauncher extends ShortcutPackageItem { /** * Return the pinned shortcut IDs for the publisher package. */ + @Nullable public ArraySet getPinnedShortcutIds(@NonNull String packageName, @UserIdInt int packageUserId) { return mPinnedShortcuts.get(PackageWithUser.of(packageUserId, packageName)); } + /** + * Return true if the given shortcut is pinned by this launcher. + */ + public boolean hasPinned(ShortcutInfo shortcut) { + final ArraySet pinned = + getPinnedShortcutIds(shortcut.getPackage(), shortcut.getUserId()); + return (pinned != null) && pinned.contains(shortcut.getId()); + } + + /** + * Additionally pin a shortcut. c.f. {@link #pinShortcuts(int, String, List)} + */ + public void addPinnedShortcut(@NonNull String packageName, @UserIdInt int packageUserId, + String id) { + final ArraySet pinnedSet = getPinnedShortcutIds(packageName, packageUserId); + final ArrayList pinnedList; + if (pinnedSet != null) { + pinnedList = new ArrayList<>(pinnedSet.size() + 1); + pinnedList.addAll(pinnedSet); + } else { + pinnedList = new ArrayList<>(1); + } + pinnedList.add(id); + + pinShortcuts(packageUserId, packageName, pinnedList); + } + boolean cleanUpPackage(String packageName, @UserIdInt int packageUserId) { return mPinnedShortcuts.remove(PackageWithUser.of(packageUserId, packageName)) != null; } diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 2eb077876b989..b74506229ff78 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -179,7 +179,7 @@ class ShortcutPackage extends ShortcutPackageItem { } } - private void ensureNotImmutable(@NonNull String id) { + public void ensureNotImmutable(@NonNull String id) { ensureNotImmutable(mShortcuts.get(id)); } @@ -706,6 +706,7 @@ class ShortcutPackage extends ShortcutPackageItem { for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); + // Disable dynamic shortcuts whose target activity is gone. if (si.isDynamic()) { if (!s.injectIsMainActivity(si.getActivity(), getPackageUserId())) { Slog.w(TAG, String.format( diff --git a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java new file mode 100644 index 0000000000000..792825798ce61 --- /dev/null +++ b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java @@ -0,0 +1,342 @@ +/* + * 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.Nullable; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.IPinItemRequest; +import android.content.pm.LauncherApps; +import android.content.pm.LauncherApps.PinItemRequest; +import android.content.pm.ShortcutInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.util.Log; +import android.util.Pair; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; + +/** + * Handles {@link android.content.pm.ShortcutManager#requestPinShortcut} related tasks. + */ +class ShortcutRequestPinProcessor { + private static final String TAG = ShortcutService.TAG; + private static final boolean DEBUG = ShortcutService.DEBUG; + + private final ShortcutService mService; + private final Object mLock; + + /** + * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. + */ + private static class PinShortcutRequestInner extends IPinItemRequest.Stub { + private final ShortcutRequestPinProcessor mProcessor; + public final ShortcutInfo shortcut; + private final IntentSender mResultIntent; + + public final String launcherPackage; + public final int launcherUserId; + public final boolean preExisting; + + @GuardedBy("this") + private boolean mAccepted; + + private PinShortcutRequestInner(ShortcutRequestPinProcessor processor, + ShortcutInfo shortcut, IntentSender resultIntent, + String launcherPackage, int launcherUserId, boolean preExisting) { + mProcessor = processor; + this.shortcut = shortcut; + mResultIntent = resultIntent; + this.launcherPackage = launcherPackage; + this.launcherUserId = launcherUserId; + this.preExisting = preExisting; + } + + @Override + public boolean isValid() { + // TODO When an app calls requestPinShortcut(), all pending requests should be + // invalidated. + synchronized (this) { + return !mAccepted; + } + } + + /** + * Called when the launcher calls {@link PinItemRequest#accept}. + */ + @Override + public boolean accept(Bundle options) { + // Make sure the options are unparcellable by the FW. (e.g. not containing unknown + // classes.) + if (options != null) { + try { + options.size(); + } catch (RuntimeException e) { + throw new IllegalArgumentException("options cannot be unparceled", e); + } + } + synchronized (this) { + if (mAccepted) { + throw new IllegalStateException("accept() called already"); + } + mAccepted = true; + } + if (DEBUG) { + Slog.d(TAG, "Launcher accepted shortcut. ID=" + shortcut.getId() + + " package=" + shortcut.getPackage() + + " options=" + options); + } + + // Pin it and send the result intent. + if (mProcessor.directPinShortcut(this)) { + mProcessor.sendResultIntent(mResultIntent); + return true; + } else { + return false; + } + } + } + + public ShortcutRequestPinProcessor(ShortcutService service, Object lock) { + mService = service; + mLock = lock; + } + + public boolean isRequestPinnedShortcutSupported(int callingUserId) { + return getRequestPinShortcutConfirmationActivity(callingUserId) != null; + } + + /** + * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}. + */ + public boolean requestPinShortcutLocked(ShortcutInfo inShortcut, IntentSender resultIntent) { + + // First, make sure the launcher supports it. + + // Find the confirmation activity in the default launcher. + final Pair confirmActivity = + getRequestPinShortcutConfirmationActivity(inShortcut.getUserId()); + + // If the launcher doesn't support it, just return a rejected result and finish. + if (confirmActivity == null) { + Log.w(TAG, "Launcher doesn't support requestPinnedShortcut(). Shortcut not created."); + return false; + } + + final ComponentName launcherComponent = confirmActivity.first; + final String launcherPackage = confirmActivity.first.getPackageName(); + final int launcherUserId = confirmActivity.second; + + // Make sure the launcher user is unlocked. (it's always the parent profile, so should + // really be unlocked here though.) + mService.throwIfUserLockedL(launcherUserId); + + // Next, validate the incoming shortcut, etc. + + final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( + inShortcut.getPackage(), inShortcut.getUserId()); + + final ShortcutInfo existing = ps.findShortcutById(inShortcut.getId()); + final boolean existsAlready = existing != null; + + if (DEBUG) { + Slog.d(TAG, "requestPinnedShortcut package=" + inShortcut.getPackage() + + " existsAlready=" + existsAlready + + " shortcut=" + inShortcut.toInsecureString()); + } + + // This is the shortcut that'll be sent to the launcher. + final ShortcutInfo shortcutToSend; + + if (existsAlready) { + validateExistingShortcut(existing); + + // See if it's already pinned. + if (mService.getLauncherShortcutsLocked( + launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing)) { + Log.i(TAG, "Launcher's already pinning shortcut " + existing.getId() + + " for package " + existing.getPackage()); + sendResultIntent(resultIntent); + return true; + } + + // Pass a clone, not the original. + // Note this will remove the intent and icons. + shortcutToSend = existing.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); + shortcutToSend.clearFlags(ShortcutInfo.FLAG_PINNED); + } else { + // It doesn't exist, so it must have all mandatory fields. + mService.validateShortcutForPinRequest(inShortcut); + + // Initialize the ShortcutInfo for pending approval. + inShortcut.resolveResourceStrings(mService.injectGetResourcesForApplicationAsUser( + inShortcut.getPackage(), inShortcut.getUserId())); + if (DEBUG) { + Slog.d(TAG, "resolved shortcut=" + inShortcut.toInsecureString()); + } + // TODO Remove the intent here -- don't pass shortcut intents to the launcher. + shortcutToSend = inShortcut; + } + + // Create a request object. + final PinShortcutRequestInner inner = + new PinShortcutRequestInner(this, shortcutToSend, resultIntent, + launcherPackage, launcherUserId, existsAlready); + + final PinItemRequest outer = new PinItemRequest(PinItemRequest.REQUEST_TYPE_SHORTCUT, + shortcutToSend, inner); + + return startRequestConfirmActivity(launcherComponent, launcherUserId, outer); + } + + private void validateExistingShortcut(ShortcutInfo shortcutInfo) { + // Make sure it's enabled. + // (Because we can't always force enable it automatically as it may be a stale + // manifest shortcut.) + Preconditions.checkState(shortcutInfo.isEnabled(), + "Shortcut ID=" + shortcutInfo + " already exists but disabled."); + + } + + private boolean startRequestConfirmActivity(ComponentName activity, int launcherUserId, + PinItemRequest request) { + // Start the activity. + final Intent confirmIntent = new Intent(LauncherApps.ACTION_CONFIRM_PIN_ITEM); + confirmIntent.setComponent(activity); + confirmIntent.putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request); + confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + final long token = mService.injectClearCallingIdentity(); + try { + mService.mContext.startActivityAsUser( + confirmIntent, UserHandle.of(launcherUserId)); + } catch (RuntimeException e) { // ActivityNotFoundException, etc. + Log.e(TAG, "Unable to start activity " + activity, e); + return false; + } finally { + mService.injectRestoreCallingIdentity(token); + } + return true; + } + + /** + * Find the activity that handles {@link LauncherApps#ACTION_CONFIRM_PIN_ITEM} in the + * default launcher. + */ + @Nullable + @VisibleForTesting + Pair getRequestPinShortcutConfirmationActivity( + int callingUserId) { + // Find the default launcher. + final int launcherUserId = mService.getParentOrSelfUserId(callingUserId); + final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId); + + if (defaultLauncher == null) { + Log.e(TAG, "Default launcher not found."); + return null; + } + final ComponentName activity = mService.injectGetPinConfirmationActivity( + defaultLauncher.getPackageName(), launcherUserId); + return (activity == null) ? null : Pair.create(activity, launcherUserId); + } + + public void sendResultIntent(@Nullable IntentSender intent) { + if (DEBUG) { + Slog.d(TAG, "Sending result intent."); + } + mService.injectSendIntentSender(intent); + } + + /** + * The last step of the "request pin shortcut" flow. Called when the launcher accepted a + * request. + */ + public boolean directPinShortcut(PinShortcutRequestInner request) { + + final ShortcutInfo original = request.shortcut; + final int appUserId = original.getUserId(); + final String appPackageName = original.getPackage(); + final int launcherUserId = request.launcherUserId; + final String launcherPackage = request.launcherPackage; + final String shortcutId = original.getId(); + + synchronized (mLock) { + if (!(mService.isUserUnlockedL(appUserId) + && mService.isUserUnlockedL(request.launcherUserId))) { + Log.w(TAG, "User is locked now."); + return false; + } + + final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( + appPackageName, appUserId); + final ShortcutInfo current = ps.findShortcutById(shortcutId); + + // The shortcut might have been changed, so we need to do the same validation again. + try { + if (current == null) { + // It doesn't exist, so it must have all necessary fields. + mService.validateShortcutForPinRequest(request.shortcut); + } else { + validateExistingShortcut(current); + } + } catch (RuntimeException e) { + Log.w(TAG, "Unable to pin shortcut: " + e.getMessage()); + return false; + } + + // If the shortcut doesn't exist, need to create it. + // First, create it as a dynamic shortcut. + if (current == null) { + if (DEBUG) { + Slog.d(TAG, "Temporarily adding " + shortcutId + " as dynamic"); + } + // Add as a dynamic shortcut. + if (original.getActivity() == null) { + original.setActivity(mService.getDummyMainActivity(appPackageName)); + } + ps.addOrUpdateDynamicShortcut(original); + } + + // Pin the shortcut. + if (DEBUG) { + Slog.d(TAG, "Pinning " + shortcutId); + } + + final ShortcutLauncher launcher = mService.getLauncherShortcutsLocked( + launcherPackage, appUserId, launcherUserId); + launcher.attemptToRestoreIfNeededAndSave(); + launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId); + + if (current == null) { + if (DEBUG) { + Slog.d(TAG, "Removing " + shortcutId + " as dynamic"); + } + ps.deleteDynamicWithId(shortcutId); + } + + ps.adjustRanks(); // Shouldn't be needed, but just in case. + } + + mService.verifyStates(); + mService.packageShortcutsChanged(appPackageName, appUserId); + + return true; + } +} diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index c5c1c0cac43cf..424830bd7919a 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -29,6 +29,8 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.IntentSender.SendIntentException; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; @@ -42,8 +44,10 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; import android.content.pm.ShortcutServiceInternal; import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener; +import android.content.pm.UserInfo; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.Bitmap; @@ -190,6 +194,8 @@ public class ShortcutService extends IShortcutService.Stub { private static final String KEY_LOW_RAM = "lowRam"; private static final String KEY_ICON_SIZE = "iconSize"; + private static final String DUMMY_MAIN_ACTIVITY = "android.__dummy__"; + @VisibleForTesting interface ConfigConstants { /** @@ -298,6 +304,8 @@ public class ShortcutService extends IShortcutService.Stub { private final UsageStatsManagerInternal mUsageStatsManagerInternal; private final ActivityManagerInternal mActivityManagerInternal; + private final ShortcutRequestPinProcessor mShortcutRequestPinProcessor; + @GuardedBy("mLock") final SparseIntArray mUidState = new SparseIntArray(); @@ -336,8 +344,9 @@ public class ShortcutService extends IShortcutService.Stub { int IS_ACTIVITY_ENABLED = 13; int PACKAGE_UPDATE_CHECK = 14; int ASYNC_PRELOAD_USER_DELAY = 15; + int GET_DEFAULT_LAUNCHER = 16; - int COUNT = ASYNC_PRELOAD_USER_DELAY + 1; + int COUNT = GET_DEFAULT_LAUNCHER + 1; } private static final String[] STAT_LABELS = { @@ -356,7 +365,8 @@ public class ShortcutService extends IShortcutService.Stub { "checkLauncherActivity", "isActivityEnabled", "packageUpdateCheck", - "asyncPreloadUserDelay" + "asyncPreloadUserDelay", + "getDefaultLauncher()" }; final Object mStatLock = new Object(); @@ -417,6 +427,8 @@ public class ShortcutService extends IShortcutService.Stub { mActivityManagerInternal = Preconditions.checkNotNull( LocalServices.getService(ActivityManagerInternal.class)); + mShortcutRequestPinProcessor = new ShortcutRequestPinProcessor(this, mLock); + if (onlyForPackageManagerApis) { return; // Don't do anything further. For unit tests only. } @@ -1591,12 +1603,11 @@ public class ShortcutService extends IShortcutService.Stub { * - Make sure the intent's extras are persistable, and them to set * {@link ShortcutInfo#mIntentPersistableExtrases}. Also clear its extras. * - Clear flags. - * - * TODO Detailed unit tests */ - private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) { + private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate, + boolean forPinRequest) { Preconditions.checkNotNull(shortcut, "Null shortcut detected"); - if (shortcut.getActivity() != null) { + if (!forPinRequest && shortcut.getActivity() != null) { Preconditions.checkState( shortcut.getPackage().equals(shortcut.getActivity().getPackageName()), "Cannot publish shortcut: activity " + shortcut.getActivity() + " does not" @@ -1608,10 +1619,13 @@ public class ShortcutService extends IShortcutService.Stub { } if (!forUpdate) { - shortcut.enforceMandatoryFields(/* forPinned= */ false); - Preconditions.checkArgument( - injectIsMainActivity(shortcut.getActivity(), shortcut.getUserId()), - "Cannot publish shortcut: " + shortcut.getActivity() + " is not main activity"); + shortcut.enforceMandatoryFields(/* forPinned= */ forPinRequest); + if (!forPinRequest) { + Preconditions.checkArgument( + injectIsMainActivity(shortcut.getActivity(), shortcut.getUserId()), + "Cannot publish shortcut: " + shortcut.getActivity() + + " is not main activity"); + } } if (shortcut.getIcon() != null) { ShortcutInfo.validateIcon(shortcut.getIcon()); @@ -1620,11 +1634,18 @@ public class ShortcutService extends IShortcutService.Stub { shortcut.replaceFlags(0); } + private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) { + fixUpIncomingShortcutInfo(shortcut, forUpdate, /*forPinRequest=*/ false); + } + + public void validateShortcutForPinRequest(@NonNull ShortcutInfo shortcut) { + fixUpIncomingShortcutInfo(shortcut, /* forUpdate= */ false, /*forPinRequest=*/ true); + } + /** * When a shortcut has no target activity, set the default one from the package. */ private void fillInDefaultActivity(List shortcuts) { - ComponentName defaultActivity = null; for (int i = shortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = shortcuts.get(i); @@ -1833,6 +1854,30 @@ public class ShortcutService extends IShortcutService.Stub { return true; } + @Override + public boolean requestPinShortcut(String packageName, ShortcutInfo shortcut, + IntentSender resultIntent, int userId) { + verifyCaller(packageName, userId); + Preconditions.checkNotNull(shortcut); + Preconditions.checkArgument(shortcut.isEnabled(), "Shortcut must be enabled"); + + final boolean ret; + synchronized (mLock) { + throwIfUserLockedL(userId); + + // TODO Make sure the caller is in the foreground. + + // TODO Cancel all pending request from the same app. + + // Send request to the launcher, if supported. + ret = mShortcutRequestPinProcessor.requestPinShortcutLocked(shortcut, resultIntent); + } + + verifyStates(); + + return ret; + } + @Override public void disableShortcuts(String packageName, List shortcutIds, CharSequence disabledMessage, int disabledMessageResId, @UserIdInt int userId) { @@ -2049,6 +2094,16 @@ public class ShortcutService extends IShortcutService.Stub { } } + @Override + public boolean isRequestPinShortcutSupported(int callingUserId) { + final long token = injectClearCallingIdentity(); + try { + return mShortcutRequestPinProcessor.isRequestPinnedShortcutSupported(callingUserId); + } finally { + injectRestoreCallingIdentity(token); + } + } + /** * Reset all throttling, for developer options and command line. Only system/shell can call * it. @@ -2113,77 +2168,22 @@ public class ShortcutService extends IShortcutService.Stub { // This method is extracted so we can directly call this method from unit tests, // even when hasShortcutPermission() is overridden. @VisibleForTesting - boolean hasShortcutHostPermissionInner(@NonNull String callingPackage, int userId) { + boolean hasShortcutHostPermissionInner(@NonNull String packageName, int userId) { synchronized (mLock) { throwIfUserLockedL(userId); final ShortcutUser user = getUserShortcutsLocked(userId); - // Always trust the in-memory cache. + // Always trust the cached component. final ComponentName cached = user.getCachedLauncher(); if (cached != null) { - if (cached.getPackageName().equals(callingPackage)) { + if (cached.getPackageName().equals(packageName)) { return true; } } // If the cached one doesn't match, then go ahead - final List allHomeCandidates = new ArrayList<>(); - - // Default launcher from package manager. - final long startGetHomeActivitiesAsUser = injectElapsedRealtime(); - final ComponentName defaultLauncher = mPackageManagerInternal - .getHomeActivitiesAsUser(allHomeCandidates, userId); - logDurationStat(Stats.GET_DEFAULT_HOME, startGetHomeActivitiesAsUser); - - ComponentName detected; - if (defaultLauncher != null) { - detected = defaultLauncher; - if (DEBUG) { - Slog.v(TAG, "Default launcher from PM: " + detected); - } - } else { - detected = user.getLastKnownLauncher(); - - if (detected != null) { - if (injectIsActivityEnabledAndExported(detected, userId)) { - if (DEBUG) { - Slog.v(TAG, "Cached launcher: " + detected); - } - } else { - Slog.w(TAG, "Cached launcher " + detected + " no longer exists"); - detected = null; - user.clearLauncher(); - } - } - } - - if (detected == null) { - // If we reach here, that means it's the first check since the user was created, - // and there's already multiple launchers and there's no default set. - // Find the system one with the highest priority. - // (We need to check the priority too because of FallbackHome in Settings.) - // If there's no system launcher yet, then no one can access shortcuts, until - // the user explicitly - final int size = allHomeCandidates.size(); - - int lastPriority = Integer.MIN_VALUE; - for (int i = 0; i < size; i++) { - final ResolveInfo ri = allHomeCandidates.get(i); - if (!ri.activityInfo.applicationInfo.isSystemApp()) { - continue; - } - if (DEBUG) { - Slog.d(TAG, String.format("hasShortcutPermissionInner: pkg=%s prio=%d", - ri.activityInfo.getComponentName(), ri.priority)); - } - if (ri.priority < lastPriority) { - continue; - } - detected = ri.activityInfo.getComponentName(); - lastPriority = ri.priority; - } - } + final ComponentName detected = getDefaultLauncher(userId); // Update the cache. user.setLauncher(detected); @@ -2191,7 +2191,7 @@ public class ShortcutService extends IShortcutService.Stub { if (DEBUG) { Slog.v(TAG, "Detected launcher: " + detected); } - return detected.getPackageName().equals(callingPackage); + return detected.getPackageName().equals(packageName); } else { // Default launcher not found. return false; @@ -2199,6 +2199,80 @@ public class ShortcutService extends IShortcutService.Stub { } } + @Nullable + ComponentName getDefaultLauncher(@UserIdInt int userId) { + final long start = injectElapsedRealtime(); + final long token = injectClearCallingIdentity(); + try { + synchronized (mLock) { + throwIfUserLockedL(userId); + + final ShortcutUser user = getUserShortcutsLocked(userId); + + final List allHomeCandidates = new ArrayList<>(); + + // Default launcher from package manager. + final long startGetHomeActivitiesAsUser = injectElapsedRealtime(); + final ComponentName defaultLauncher = mPackageManagerInternal + .getHomeActivitiesAsUser(allHomeCandidates, userId); + logDurationStat(Stats.GET_DEFAULT_HOME, startGetHomeActivitiesAsUser); + + ComponentName detected = null; + if (defaultLauncher != null) { + detected = defaultLauncher; + if (DEBUG) { + Slog.v(TAG, "Default launcher from PM: " + detected); + } + } else { + detected = user.getLastKnownLauncher(); + + if (detected != null) { + if (injectIsActivityEnabledAndExported(detected, userId)) { + if (DEBUG) { + Slog.v(TAG, "Cached launcher: " + detected); + } + } else { + Slog.w(TAG, "Cached launcher " + detected + " no longer exists"); + detected = null; + user.clearLauncher(); + } + } + } + + if (detected == null) { + // If we reach here, that means it's the first check since the user was created, + // and there's already multiple launchers and there's no default set. + // Find the system one with the highest priority. + // (We need to check the priority too because of FallbackHome in Settings.) + // If there's no system launcher yet, then no one can access shortcuts, until + // the user explicitly + final int size = allHomeCandidates.size(); + + int lastPriority = Integer.MIN_VALUE; + for (int i = 0; i < size; i++) { + final ResolveInfo ri = allHomeCandidates.get(i); + if (!ri.activityInfo.applicationInfo.isSystemApp()) { + continue; + } + if (DEBUG) { + Slog.d(TAG, String.format("hasShortcutPermissionInner: pkg=%s prio=%d", + ri.activityInfo.getComponentName(), ri.priority)); + } + if (ri.priority < lastPriority) { + continue; + } + detected = ri.activityInfo.getComponentName(); + lastPriority = ri.priority; + } + } + return detected; + } + } finally { + injectRestoreCallingIdentity(token); + logDurationStat(Stats.GET_DEFAULT_LAUNCHER, start); + } + } + // === House keeping === private void cleanUpPackageForAllLoadedUsers(String packageName, @UserIdInt int packageUserId, @@ -3034,10 +3108,21 @@ public class ShortcutService extends IShortcutService.Stub { if (activity != null) { baseIntent.setComponent(activity); } + return queryActivities(baseIntent, userId, /* exportedOnly =*/ true); + } - final List resolved = - mContext.getPackageManager().queryIntentActivitiesAsUser( - baseIntent, PACKAGE_MATCH_FLAGS, userId); + @NonNull + List queryActivities(@NonNull Intent intent, int userId, + boolean exportedOnly) { + final List resolved; + final long token = injectClearCallingIdentity(); + try { + resolved = + mContext.getPackageManager().queryIntentActivitiesAsUser( + intent, PACKAGE_MATCH_FLAGS, userId); + } finally { + injectRestoreCallingIdentity(token); + } if (resolved == null || resolved.size() == 0) { return EMPTY_RESOLVE_INFO; } @@ -3045,7 +3130,9 @@ public class ShortcutService extends IShortcutService.Stub { if (!isInstalled(resolved.get(0).activityInfo)) { return EMPTY_RESOLVE_INFO; } - resolved.removeIf(ACTIVITY_NOT_EXPORTED); + if (exportedOnly) { + resolved.removeIf(ACTIVITY_NOT_EXPORTED); + } return resolved; } @@ -3056,14 +3143,11 @@ public class ShortcutService extends IShortcutService.Stub { @Nullable ComponentName injectGetDefaultMainActivity(@NonNull String packageName, int userId) { final long start = injectElapsedRealtime(); - final long token = injectClearCallingIdentity(); try { final List resolved = queryActivities(getMainActivityIntent(), packageName, null, userId); return resolved.size() == 0 ? null : resolved.get(0).activityInfo.getComponentName(); } finally { - injectRestoreCallingIdentity(token); - logDurationStat(Stats.GET_LAUNCHER_ACTIVITY, start); } } @@ -3073,31 +3157,36 @@ public class ShortcutService extends IShortcutService.Stub { */ boolean injectIsMainActivity(@NonNull ComponentName activity, int userId) { final long start = injectElapsedRealtime(); - final long token = injectClearCallingIdentity(); try { - final List resolved = - queryActivities(getMainActivityIntent(), activity.getPackageName(), - activity, userId); + if (DUMMY_MAIN_ACTIVITY.equals(activity.getClassName())) { + return true; + } + final List resolved = queryActivities( + getMainActivityIntent(), activity.getPackageName(), activity, userId); return resolved.size() > 0; } finally { - injectRestoreCallingIdentity(token); - logDurationStat(Stats.CHECK_LAUNCHER_ACTIVITY, start); } } + /** + * Create a dummy "main activity" component name which is used to create a dynamic shortcut + * with no main activity temporarily. + */ + @NonNull + ComponentName getDummyMainActivity(@NonNull String packageName) { + return new ComponentName(packageName, DUMMY_MAIN_ACTIVITY); + } + /** * Return all the enabled, exported and main activities from a package. */ @NonNull List injectGetMainActivities(@NonNull String packageName, int userId) { final long start = injectElapsedRealtime(); - final long token = injectClearCallingIdentity(); try { return queryActivities(getMainActivityIntent(), packageName, null, userId); } finally { - injectRestoreCallingIdentity(token); - logDurationStat(Stats.CHECK_LAUNCHER_ACTIVITY, start); } } @@ -3109,17 +3198,33 @@ public class ShortcutService extends IShortcutService.Stub { boolean injectIsActivityEnabledAndExported( @NonNull ComponentName activity, @UserIdInt int userId) { final long start = injectElapsedRealtime(); - final long token = injectClearCallingIdentity(); try { return queryActivities(new Intent(), activity.getPackageName(), activity, userId) .size() > 0; } finally { - injectRestoreCallingIdentity(token); - logDurationStat(Stats.IS_ACTIVITY_ENABLED, start); } } + /** + * Get the {@link LauncherApps#ACTION_CONFIRM_PIN_ITEM} activity in a given package. + */ + @Nullable + ComponentName injectGetPinConfirmationActivity(@NonNull String launcherPackageName, + int launcherUserId) { + Preconditions.checkNotNull(launcherPackageName); + + final Intent confirmIntent = new Intent(LauncherApps.ACTION_CONFIRM_PIN_ITEM); + confirmIntent.setPackage(launcherPackageName); + + final List candidates = queryActivities( + confirmIntent, launcherUserId, /* exportedOnly =*/ false); + for (ResolveInfo ri : candidates) { + return ri.activityInfo.getComponentName(); + } + return null; + } + boolean injectIsSafeModeEnabled() { final long token = injectClearCallingIdentity(); try { @@ -3133,6 +3238,32 @@ public class ShortcutService extends IShortcutService.Stub { } } + /** + * If {@code userId} is of a managed profile, return the parent user ID. Otherwise return + * itself. + */ + int getParentOrSelfUserId(int userId) { + final long token = injectClearCallingIdentity(); + try { + final UserInfo parent = mUserManager.getProfileParent(userId); + return (parent != null) ? parent.id : userId; + } finally { + injectRestoreCallingIdentity(token); + } + } + + void injectSendIntentSender(IntentSender intentSender) { + if (intentSender == null) { + return; + } + try { + intentSender.sendIntent(mContext, /* code= */ 0, /* intent= */ null, + /* onFinished=*/ null, /* handler= */ null); + } catch (SendIntentException e) { + Slog.w(TAG, "sendIntent failed().", e); + } + } + // === Backup & restore === boolean shouldBackupApp(String packageName, int userId) { @@ -3749,6 +3880,11 @@ public class ShortcutService extends IShortcutService.Stub { } } + @VisibleForTesting + ShortcutRequestPinProcessor getShortcutRequestPinProcessorForTest() { + return mShortcutRequestPinProcessor; + } + /** * Control whether {@link #verifyStates} should be performed. We always perform it during unit * tests. diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 99af9e8ef8e28..cb27af1e3fd69 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -46,6 +46,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ILauncherApps; @@ -74,6 +75,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.test.InstrumentationTestCase; import android.test.mock.MockContext; +import android.util.ArrayMap; import android.util.Log; import android.util.Pair; @@ -123,6 +125,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected static final String[] EMPTY_STRINGS = new String[0]; // Just for readability. protected static final String MAIN_ACTIVITY_CLASS = "MainActivity"; + protected static final String PIN_CONFIRM_ACTIVITY_CLASS = "PinConfirmActivity"; // public for mockito public class BaseContext extends MockContext { @@ -161,6 +164,11 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { public void unregisterReceiver(BroadcastReceiver receiver) { // ignore. } + + @Override + public void startActivityAsUser(Intent intent, UserHandle user) { + // ignore, use spy to intercept it. + } } /** Context used in the client side */ @@ -201,6 +209,10 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { public XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) { return BaseShortcutManagerTest.this.injectXmlMetaData(activityInfo, key); } + + public void sendIntentSender(IntentSender intent) { + // Placeholder for spying. + } } /** ShortcutService with injection override methods. */ @@ -303,6 +315,15 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { return mDefaultLauncherChecker.test(callingPackage, userId); } + @Override + ComponentName getDefaultLauncher(@UserIdInt int userId) { + final ComponentName activity = mDefaultLauncher.get(userId); + if (activity != null) { + return activity; + } + return super.getDefaultLauncher(userId); + } + @Override PackageInfo injectPackageInfoWithUninstalled(String packageName, @UserIdInt int userId, boolean getSignatures) { @@ -374,6 +395,12 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { return mMainActivityFetcher.apply(packageName, userId); } + @Override + ComponentName injectGetPinConfirmationActivity(@NonNull String launcherPackageName, + int launcherUserId) { + return mPinConfirmActivityFetcher.apply(launcherPackageName, launcherUserId); + } + @Override boolean injectIsActivityEnabledAndExported(ComponentName activity, @UserIdInt int userId) { assertNotNull(activity); @@ -412,6 +439,11 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { return mInjectedBuildFingerprint; } + @Override + void injectSendIntentSender(IntentSender intent) { + mContext.sendIntentSender(intent); + } + @Override void wtf(String message, Throwable th) { // During tests, WTF is fatal. @@ -583,7 +615,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected static final UserInfo USER_INFO_0 = withProfileGroupId( new UserInfo(USER_0, "user0", - UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY | UserInfo.FLAG_INITIALIZED), 10); + UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY | UserInfo.FLAG_INITIALIZED), 0); protected static final UserInfo USER_INFO_10 = new UserInfo(USER_10, "user10", UserInfo.FLAG_INITIALIZED); @@ -593,19 +625,24 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected static final UserInfo USER_INFO_P0 = withProfileGroupId( new UserInfo(USER_P0, "userP0", - UserInfo.FLAG_MANAGED_PROFILE), 10); + UserInfo.FLAG_MANAGED_PROFILE), 0); protected BiPredicate mDefaultLauncherChecker = (callingPackage, userId) -> LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage) || LAUNCHER_3.equals(callingPackage) || LAUNCHER_4.equals(callingPackage); + private final Map mDefaultLauncher = new ArrayMap<>(); + protected BiPredicate mMainActivityChecker = (activity, userId) -> true; protected BiFunction mMainActivityFetcher = (packageName, userId) -> new ComponentName(packageName, MAIN_ACTIVITY_CLASS); + protected BiFunction mPinConfirmActivityFetcher = + (packageName, userId) -> new ComponentName(packageName, PIN_CONFIRM_ACTIVITY_CLASS); + protected BiPredicate mEnabledActivityChecker = (activity, userId) -> true; // all activities are enabled. @@ -722,6 +759,19 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { return b(mRunningUsers.get(userId)) && b(mUnlockedUsers.get(userId)); })); + when(mMockUserManager.getProfileParent(anyInt())) + .thenAnswer(new AnswerWithSystemCheck<>(inv -> { + final int userId = (Integer) inv.getArguments()[0]; + final UserInfo ui = mUserInfos.get(userId); + assertNotNull(ui); + if (ui.profileGroupId == UserInfo.NO_PROFILE_GROUP_ID) { + return null; + } + final UserInfo parent = mUserInfos.get(ui.profileGroupId); + assertNotNull(parent); + return parent; + })); + when(mMockActivityManagerInternal.getUidProcessState(anyInt())).thenReturn( ActivityManager.PROCESS_STATE_CACHED_EMPTY); @@ -1098,10 +1148,31 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { return mInjectedClientPackage; } + /** + * This controls {@link ShortcutService#hasShortcutHostPermission(String, int)}, but + * not {@link ShortcutService#getDefaultLauncher(int)}. To control the later, use + * {@link #setDefaultLauncher(int, ComponentName)}. + */ protected void setDefaultLauncherChecker(BiPredicate p) { mDefaultLauncherChecker = p; } + /** + * Set the default launcher. This will update {@link #mDefaultLauncherChecker} set by + * {@link #setDefaultLauncherChecker} too. + */ + protected void setDefaultLauncher(int userId, ComponentName launcherActivity) { + mDefaultLauncher.put(userId, launcherActivity); + + final BiPredicate oldChecker = mDefaultLauncherChecker; + mDefaultLauncherChecker = (checkPackageName, checkUserId) -> { + if ((checkUserId == userId) && (launcherActivity != null)) { + return launcherActivity.getPackageName().equals(checkPackageName); + } + return oldChecker.test(checkPackageName, checkUserId); + }; + } + protected void runWithCaller(String packageName, int userId, Runnable r) { final String previousPackage = mInjectedClientPackage; final int previousUserId = UserHandle.getUserId(mInjectedCallingUid); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest6.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest6.java index ba4dbc1d258ee..3684ca0228fda 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest6.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest6.java @@ -31,7 +31,8 @@ import android.test.suitebuilder.annotation.SmallTest; import java.util.List; /** - * Tests for {@link ShortcutService#hasShortcutHostPermissionInner}. + * Tests for {@link ShortcutService#hasShortcutHostPermissionInner}, which includes + * {@link ShortcutService#getDefaultLauncher}. */ @SmallTest public class ShortcutManagerTest6 extends BaseShortcutManagerTest { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java new file mode 100644 index 0000000000000..de344c2705b16 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java @@ -0,0 +1,291 @@ +/* + * 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 static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertWith; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.LauncherApps; +import android.content.pm.LauncherApps.PinItemRequest; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.os.UserHandle; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Pair; + +import org.mockito.ArgumentCaptor; + +/** + * Tests for {@link ShortcutManager#requestPinShortcut} and relevant APIs. + * + m FrameworksServicesTests && + adb install \ + -r -g ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk && + adb shell am instrument -e class com.android.server.pm.ShortcutManagerTest8 \ + -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner + */ +@SmallTest +public class ShortcutManagerTest8 extends BaseShortcutManagerTest { + private ShortcutRequestPinProcessor mProcessor; + + @Override + protected void initService() { + super.initService(); + mProcessor = mService.getShortcutRequestPinProcessorForTest(); + } + + public void testGetParentOrSelfUserId() { + assertEquals(USER_0, mService.getParentOrSelfUserId(USER_0)); + assertEquals(USER_10, mService.getParentOrSelfUserId(USER_10)); + assertEquals(USER_11, mService.getParentOrSelfUserId(USER_11)); + assertEquals(USER_0, mService.getParentOrSelfUserId(USER_P0)); + } + + public void testIsRequestPinShortcutSupported() { + setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0)); + setDefaultLauncher(USER_10, mMainActivityFetcher.apply(LAUNCHER_2, USER_10)); + + Pair actual; + // User 0 + actual = mProcessor.getRequestPinShortcutConfirmationActivity(USER_0); + + assertEquals(LAUNCHER_1, actual.first.getPackageName()); + assertEquals(PIN_CONFIRM_ACTIVITY_CLASS, actual.first.getClassName()); + assertEquals(USER_0, (int) actual.second); + + // User 10 + actual = mProcessor.getRequestPinShortcutConfirmationActivity(USER_10); + + assertEquals(LAUNCHER_2, actual.first.getPackageName()); + assertEquals(PIN_CONFIRM_ACTIVITY_CLASS, actual.first.getClassName()); + assertEquals(USER_10, (int) actual.second); + + // User P0 -> managed profile, return user-0's launcher. + actual = mProcessor.getRequestPinShortcutConfirmationActivity(USER_P0); + + assertEquals(LAUNCHER_1, actual.first.getPackageName()); + assertEquals(PIN_CONFIRM_ACTIVITY_CLASS, actual.first.getClassName()); + assertEquals(USER_0, (int) actual.second); + + // Check from the public API. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.isRequestPinShortcutSupported()); + }); + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertTrue(mManager.isRequestPinShortcutSupported()); + }); + runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { + assertTrue(mManager.isRequestPinShortcutSupported()); + }); + runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { + assertTrue(mManager.isRequestPinShortcutSupported()); + }); + + // Now, USER_0's launcher no longer has a confirm activity. + mPinConfirmActivityFetcher = (packageName, userId) -> + !LAUNCHER_2.equals(packageName) + ? null : new ComponentName(packageName, PIN_CONFIRM_ACTIVITY_CLASS); + + // User 10 -- still has confirm activity. + actual = mProcessor.getRequestPinShortcutConfirmationActivity(USER_10); + + assertEquals(LAUNCHER_2, actual.first.getPackageName()); + assertEquals(PIN_CONFIRM_ACTIVITY_CLASS, actual.first.getClassName()); + assertEquals(USER_10, (int) actual.second); + + // But user-0 and user p0 no longer has a confirmation activity. + assertNull(mProcessor.getRequestPinShortcutConfirmationActivity(USER_0)); + assertNull(mProcessor.getRequestPinShortcutConfirmationActivity(USER_P0)); + + // Check from the public API. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertFalse(mManager.isRequestPinShortcutSupported()); + }); + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertFalse(mManager.isRequestPinShortcutSupported()); + }); + runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { + assertTrue(mManager.isRequestPinShortcutSupported()); + }); + runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { + assertFalse(mManager.isRequestPinShortcutSupported()); + }); + } + + public void testRequestPinShortcut_notSupported() { + // User-0's launcher has no confirmation activity. + setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0)); + + mPinConfirmActivityFetcher = (packageName, userId) -> + !LAUNCHER_2.equals(packageName) + ? null : new ComponentName(packageName, PIN_CONFIRM_ACTIVITY_CLASS); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + ShortcutInfo s1 = makeShortcut("s1"); + + assertFalse(mManager.requestPinShortcut(s1, + /*PendingIntent=*/ null)); + + verify(mServiceContext, times(0)) + .startActivityAsUser(any(Intent.class), any(UserHandle.class)); + verify(mServiceContext, times(0)) + .sendIntentSender(any(IntentSender.class)); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + ShortcutInfo s1 = makeShortcut("s1"); + + assertFalse(mManager.requestPinShortcut(s1, + /*PendingIntent=*/ null)); + + verify(mServiceContext, times(0)) + .startActivityAsUser(any(Intent.class), any(UserHandle.class)); + verify(mServiceContext, times(0)) + .sendIntentSender(any(IntentSender.class)); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { + ShortcutInfo s1 = makeShortcut("s1"); + + assertFalse(mManager.requestPinShortcut(s1, + /*PendingIntent=*/ null)); + + verify(mServiceContext, times(0)) + .startActivityAsUser(any(Intent.class), any(UserHandle.class)); + verify(mServiceContext, times(0)) + .sendIntentSender(any(IntentSender.class)); + }); + } + + private void assertPinItemRequestIntent(Intent actualIntent, String expectedPackage) { + assertEquals(LauncherApps.ACTION_CONFIRM_PIN_ITEM, actualIntent.getAction()); + assertEquals(expectedPackage, actualIntent.getComponent().getPackageName()); + assertEquals(PIN_CONFIRM_ACTIVITY_CLASS, + actualIntent.getComponent().getClassName()); + assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK, + actualIntent.getFlags()); + } + + private void assertPinItemRequest(PinItemRequest actualRequest) { + assertNotNull(actualRequest); + + assertEquals(PinItemRequest.REQUEST_TYPE_SHORTCUT, actualRequest.getRequestType()); + } + + /** + * Basic flow: + * - Launcher supports the feature. + * - Shortcut doesn't pre-exist. + */ + private void checkRequestPinShortcut(@Nullable PendingIntent resultIntent) { + setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0)); + setDefaultLauncher(USER_10, mMainActivityFetcher.apply(LAUNCHER_2, USER_10)); + + runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { + ShortcutInfo s1 = makeShortcut("s1"); + + assertTrue(mManager.requestPinShortcut(s1, + resultIntent == null ? null : resultIntent.getIntentSender())); + + verify(mServiceContext, times(0)) + .sendIntentSender(any(IntentSender.class)); + + // Shortcut shouldn't be registered yet. + assertWith(getCallerShortcuts()) + .isEmpty(); + }); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + // Check the intent passed to startActivityAsUser(). + final ArgumentCaptor intent = ArgumentCaptor.forClass(Intent.class); + + verify(mServiceContext).startActivityAsUser(intent.capture(), eq(HANDLE_USER_0)); + + assertPinItemRequestIntent(intent.getValue(), mInjectedClientPackage); + + // Check the request object. + final PinItemRequest request = mLauncherApps.getPinItemRequest(intent.getValue()); + + assertPinItemRequest(request); + + assertWith(request.getShortcutInfo()) + .haveIds("s1") + .areAllOrphan(); + + // Can't test icons; need to test on CTS. + + // Accept the request. + request.accept(); + }); + + // Check from the launcher side, including callback + + // This method is always called, even with PI == null. + if (resultIntent == null) { + verify(mServiceContext, times(1)).sendIntentSender(eq(null)); + } else { + verify(mServiceContext, times(1)).sendIntentSender(any(IntentSender.class)); + } + + runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { + assertWith(getCallerShortcuts()) + .haveIds("s1") + .areAllNotDynamic() + .areAllEnabled() + .areAllPinned(); + }); + } + + public void testRequestPinShortcut() { + checkRequestPinShortcut(/* resultIntent=*/ null); + } + + public void testRequestPinShortcut_withCallback() { + final PendingIntent resultIntent = + PendingIntent.getActivity(getTestContext(), 0, new Intent(), 0); + + checkRequestPinShortcut(resultIntent); + } + + // TODO More tests: + // Shortcut exists as a dynamic shortcut. + // Shortcut exists as a manifest shortcut. + // Shortcut exists as a dynamic, already pinned by this launcher + // Shortcut exists as a manifest, already pinned by this launcher + // Shortcut exists as floating, already pinned by this launcher + + // Shortcut exists as a dynamic, already pinned by another launcher + // Shortcut exists as a manifest, already pinned by another launcher + // Shortcut exists as floating, already pinned by another launcher + + // Shortcut exists but disabled (both mutable and immutable) + + // Shortcut exists but removed before accept(). + // Shortcut exists but disabled before accept(). + // Shortcut exists but pinned before accept(). + // Shortcut exists but unpinned before accept(). + + // Cancel previous pending request and release memory? +} diff --git a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java index 6e74deb02e6c9..8ecea7175ae13 100644 --- a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java +++ b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java @@ -729,6 +729,10 @@ public class ShortcutManagerTestUtils { return new ShortcutListAsserter(list); } + public static ShortcutListAsserter assertWith(ShortcutInfo... list) { + return assertWith(list(list)); + } + /** * New style assertion that allows chained calls. */ @@ -886,6 +890,30 @@ public class ShortcutManagerTestUtils { return this; } + public ShortcutListAsserter areAllFloating() { + forAllShortcuts(s -> assertTrue("id=" + s.getId(), + s.isPinned() && !s.isDeclaredInManifest() && !s.isDynamic())); + return this; + } + + public ShortcutListAsserter areAllNotFloating() { + forAllShortcuts(s -> assertTrue("id=" + s.getId(), + !(s.isPinned() && !s.isDeclaredInManifest() && !s.isDynamic()))); + return this; + } + + public ShortcutListAsserter areAllOrphan() { + forAllShortcuts(s -> assertTrue("id=" + s.getId(), + !s.isPinned() && !s.isDeclaredInManifest() && !s.isDynamic())); + return this; + } + + public ShortcutListAsserter areAllNotOrphan() { + forAllShortcuts(s -> assertTrue("id=" + s.getId(), + s.isPinned() || s.isDeclaredInManifest() || s.isDynamic())); + return this; + } + public ShortcutListAsserter areAllWithKeyFieldsOnly() { forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.hasKeyFieldsOnly())); return this;