ShortcutManager: direct pin shortcut support.

Test: Manual test and all the unit tests:
adb shell am instrument -e class com.android.server.pm.ShortcutManagerTest1 -w com.android.frameworks.servicestests
... to test8

Bug 32908854

Change-Id: I11b81656959cccfb4efa83f08380b915e6eb84a6
This commit is contained in:
Makoto Onuki
2016-12-02 15:48:40 -08:00
parent 5436546589
commit 2d895c3efd
16 changed files with 1336 additions and 106 deletions

View File

@@ -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 \

View File

@@ -9648,6 +9648,7 @@ package android.content.pm {
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);
@@ -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.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);
@@ -10241,9 +10259,11 @@ package android.content.pm {
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>);
}

View File

@@ -10048,6 +10048,7 @@ package android.content.pm {
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);
@@ -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.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);
@@ -10712,9 +10730,11 @@ package android.content.pm {
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>);
}

View File

@@ -9676,6 +9676,7 @@ package android.content.pm {
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);
@@ -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.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);
@@ -10272,9 +10290,11 @@ package android.content.pm {
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>);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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.
*
* <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 @@ 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<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;
}
}
}

View File

@@ -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 @@ public class ShortcutManager {
*
* @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());
@@ -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.
*
* <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.

View File

@@ -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<String> ids) {
final ShortcutPackage packageShortcuts =
@@ -143,11 +147,39 @@ class ShortcutLauncher extends ShortcutPackageItem {
/**
* 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;
}

View File

@@ -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(

View File

@@ -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;
}
}

View File

@@ -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<ShortcutInfo> 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<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 @@ 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<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 @@ public class ShortcutService extends IShortcutService.Stub {
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 @@ 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<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 @@ public class ShortcutService extends IShortcutService.Stub {
*/
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 @@ 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<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 @@ 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.

View File

@@ -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<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 @@ 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<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);

View File

@@ -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 {

View File

@@ -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?
}

View File

@@ -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;