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:
@@ -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 \
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
|
||||
26
core/java/android/content/pm/IPinItemRequest.aidl
Normal file
26
core/java/android/content/pm/IPinItemRequest.aidl
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user