Merge "ShortcutManager: direct pin shortcut support."
diff --git a/Android.mk b/Android.mk
index 36a35a1..9d1b0cc 100644
--- a/Android.mk
+++ b/Android.mk
@@ -145,6 +145,7 @@
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 \
@@ -157,7 +158,7 @@
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 f259ff8..1863a032 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9695,6 +9695,7 @@
public class LauncherApps {
method public java.util.List<android.content.pm.LauncherActivityInfo> 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<android.content.pm.ShortcutInfo> getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle);
@@ -9710,6 +9711,8 @@
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 {
@@ -9724,6 +9727,21 @@
method public void onShortcutsChanged(java.lang.String, java.util.List<android.content.pm.ShortcutInfo>, 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<android.content.pm.LauncherApps.PinItemRequest> 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);
@@ -10288,9 +10306,11 @@
method public int getMaxShortcutCountPerActivity();
method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
method public boolean isRateLimitingActive();
+ method public boolean isRequestPinShortcutSupported();
method public void removeAllDynamicShortcuts();
method public void removeDynamicShortcuts(java.util.List<java.lang.String>);
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<android.content.pm.ShortcutInfo>);
method public boolean updateShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
}
diff --git a/api/system-current.txt b/api/system-current.txt
index e98ad21..64d0f26 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -10101,6 +10101,7 @@
public class LauncherApps {
method public java.util.List<android.content.pm.LauncherActivityInfo> 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<android.content.pm.ShortcutInfo> getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle);
@@ -10116,6 +10117,8 @@
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 {
@@ -10130,6 +10133,21 @@
method public void onShortcutsChanged(java.lang.String, java.util.List<android.content.pm.ShortcutInfo>, 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<android.content.pm.LauncherApps.PinItemRequest> 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);
@@ -10767,9 +10785,11 @@
method public int getMaxShortcutCountPerActivity();
method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
method public boolean isRateLimitingActive();
+ method public boolean isRequestPinShortcutSupported();
method public void removeAllDynamicShortcuts();
method public void removeDynamicShortcuts(java.util.List<java.lang.String>);
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<android.content.pm.ShortcutInfo>);
method public boolean updateShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
}
diff --git a/api/test-current.txt b/api/test-current.txt
index 6c2c36f..7e5de6a 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -9723,6 +9723,7 @@
public class LauncherApps {
ctor public LauncherApps(android.content.Context);
method public java.util.List<android.content.pm.LauncherActivityInfo> 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<android.content.pm.ShortcutInfo> getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle);
@@ -9738,6 +9739,8 @@
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 {
@@ -9752,6 +9755,21 @@
method public void onShortcutsChanged(java.lang.String, java.util.List<android.content.pm.ShortcutInfo>, 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<android.content.pm.LauncherApps.PinItemRequest> 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);
@@ -10319,9 +10337,11 @@
method public int getMaxShortcutCountPerActivity();
method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
method public boolean isRateLimitingActive();
+ method public boolean isRequestPinShortcutSupported();
method public void removeAllDynamicShortcuts();
method public void removeDynamicShortcuts(java.util.List<java.lang.String>);
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<android.content.pm.ShortcutInfo>);
method public boolean updateShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
}
diff --git a/core/java/android/content/pm/IPinItemRequest.aidl b/core/java/android/content/pm/IPinItemRequest.aidl
new file mode 100644
index 0000000..efe2835
--- /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 1bf2ab0..91df8e8 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 @@
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 @@
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 7fc8044..5f4bc00 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -19,6 +19,8 @@
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.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 @@
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.
+ *
+ * <p>See the {@link ShortcutManager} javadoc for details.
+ *
+ * <p>
+ * 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.
+ *
+ * <p>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 @@
}
}
} 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 @@
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<PinItemRequest> CREATOR =
+ new Creator<PinItemRequest>() {
+ 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 f7c4d59..c8f00b8 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;
/**
+ * <p><strong>TODO Update the overview to how to use the O new features.</strong></p>
+ *
* The ShortcutManager manages an app's <em>shortcuts</em>. 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 @@
*
* @throws IllegalStateException when the user is locked.
*/
- public boolean updateShortcuts(List<ShortcutInfo> shortcutInfoList) {
+ public boolean updateShortcuts(@NonNull List<ShortcutInfo> shortcutInfoList) {
try {
return mService.updateShortcuts(mContext.getPackageName(),
new ParceledListSlice(shortcutInfoList), injectMyUserId());
@@ -815,6 +825,61 @@
}
/**
+ * 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.
+ *
+ * <p>When a request is denied by the user, the caller app will not get any response.
+ *
+ * <p>Only apps with a foreground activity or a foreground service can call it. Otherwise
+ * it'll throw {@link IllegalStateException}.
+ *
+ * <p>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.
* For example, when the user sends an "inline reply" on a notification, the system UI will
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index 2af1bcb..3060840 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 @@
// Nothing to do.
}
+ /**
+ * Pin the given shortcuts, replacing the current pinned ones.
+ */
public void pinShortcuts(@UserIdInt int packageUserId,
@NonNull String packageName, @NonNull List<String> ids) {
final ShortcutPackage packageShortcuts =
@@ -143,11 +147,39 @@
/**
* Return the pinned shortcut IDs for the publisher package.
*/
+ @Nullable
public ArraySet<String> 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<String> 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<String> pinnedSet = getPinnedShortcutIds(packageName, packageUserId);
+ final ArrayList<String> 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 2eb0778..b745062 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 @@
}
}
- private void ensureNotImmutable(@NonNull String id) {
+ public void ensureNotImmutable(@NonNull String id) {
ensureNotImmutable(mShortcuts.get(id));
}
@@ -706,6 +706,7 @@
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 0000000..7928257
--- /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<ComponentName, Integer> 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<ComponentName, Integer> 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 c5c1c0c..424830b 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.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.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 @@
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 @@
private final UsageStatsManagerInternal mUsageStatsManagerInternal;
private final ActivityManagerInternal mActivityManagerInternal;
+ private final ShortcutRequestPinProcessor mShortcutRequestPinProcessor;
+
@GuardedBy("mLock")
final SparseIntArray mUidState = new SparseIntArray();
@@ -336,8 +344,9 @@
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 @@
"checkLauncherActivity",
"isActivityEnabled",
"packageUpdateCheck",
- "asyncPreloadUserDelay"
+ "asyncPreloadUserDelay",
+ "getDefaultLauncher()"
};
final Object mStatLock = new Object();
@@ -417,6 +427,8 @@
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 @@
* - 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 @@
}
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 @@
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<ShortcutInfo> shortcuts) {
-
ComponentName defaultActivity = null;
for (int i = shortcuts.size() - 1; i >= 0; i--) {
final ShortcutInfo si = shortcuts.get(i);
@@ -1834,6 +1855,30 @@
}
@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) {
verifyCaller(packageName, userId);
@@ -2049,6 +2094,16 @@
}
}
+ @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 @@
// 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<ResolveInfo> 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 @@
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 @@
}
}
+ @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<ResolveInfo> 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 @@
if (activity != null) {
baseIntent.setComponent(activity);
}
+ return queryActivities(baseIntent, userId, /* exportedOnly =*/ true);
+ }
- final List<ResolveInfo> resolved =
- mContext.getPackageManager().queryIntentActivitiesAsUser(
- baseIntent, PACKAGE_MATCH_FLAGS, userId);
+ @NonNull
+ List<ResolveInfo> queryActivities(@NonNull Intent intent, int userId,
+ boolean exportedOnly) {
+ final List<ResolveInfo> 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 @@
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 @@
@Nullable
ComponentName injectGetDefaultMainActivity(@NonNull String packageName, int userId) {
final long start = injectElapsedRealtime();
- final long token = injectClearCallingIdentity();
try {
final List<ResolveInfo> 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 @@
*/
boolean injectIsMainActivity(@NonNull ComponentName activity, int userId) {
final long start = injectElapsedRealtime();
- final long token = injectClearCallingIdentity();
try {
- final List<ResolveInfo> resolved =
- queryActivities(getMainActivityIntent(), activity.getPackageName(),
- activity, userId);
+ if (DUMMY_MAIN_ACTIVITY.equals(activity.getClassName())) {
+ return true;
+ }
+ final List<ResolveInfo> 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<ResolveInfo> 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 @@
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<ResolveInfo> 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 @@
}
}
+ /**
+ * 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 @@
}
}
+ @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 99af9e8..cb27af1 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.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.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 @@
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 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 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. */
@@ -304,6 +316,15 @@
}
@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) {
return getInjectedPackageInfo(packageName, userId, getSignatures);
@@ -375,6 +396,12 @@
}
@Override
+ ComponentName injectGetPinConfirmationActivity(@NonNull String launcherPackageName,
+ int launcherUserId) {
+ return mPinConfirmActivityFetcher.apply(launcherPackageName, launcherUserId);
+ }
+
+ @Override
boolean injectIsActivityEnabledAndExported(ComponentName activity, @UserIdInt int userId) {
assertNotNull(activity);
return mEnabledActivityChecker.test(activity, userId);
@@ -413,6 +440,11 @@
}
@Override
+ void injectSendIntentSender(IntentSender intent) {
+ mContext.sendIntentSender(intent);
+ }
+
+ @Override
void wtf(String message, Throwable th) {
// During tests, WTF is fatal.
fail(message + " exception: " + th + "\n" + Log.getStackTraceString(th));
@@ -583,7 +615,7 @@
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 @@
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<String, Integer> mDefaultLauncherChecker =
(callingPackage, userId) ->
LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage)
|| LAUNCHER_3.equals(callingPackage) || LAUNCHER_4.equals(callingPackage);
+ private final Map<Integer, ComponentName> mDefaultLauncher = new ArrayMap<>();
+
protected BiPredicate<ComponentName, Integer> mMainActivityChecker =
(activity, userId) -> true;
protected BiFunction<String, Integer, ComponentName> mMainActivityFetcher =
(packageName, userId) -> new ComponentName(packageName, MAIN_ACTIVITY_CLASS);
+ protected BiFunction<String, Integer, ComponentName> mPinConfirmActivityFetcher =
+ (packageName, userId) -> new ComponentName(packageName, PIN_CONFIRM_ACTIVITY_CLASS);
+
protected BiPredicate<ComponentName, Integer> mEnabledActivityChecker
= (activity, userId) -> true; // all activities are enabled.
@@ -722,6 +759,19 @@
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 @@
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<String, Integer> 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<String, Integer> 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 ba4dbc1..3684ca0 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 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 0000000..de344c2
--- /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<ComponentName, Integer> 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> 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 6e74deb..8ecea71 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 @@
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 @@
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;