diff --git a/api/current.txt b/api/current.txt index ec16f5a5f56b8..709fc12cf13bd 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6778,6 +6778,7 @@ package android.appwidget { method public void notifyAppWidgetViewDataChanged(int, int); method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews); method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews); + method public boolean requestPinAppWidget(android.content.ComponentName, android.app.PendingIntent); method public void updateAppWidget(int[], android.widget.RemoteViews); method public void updateAppWidget(int, android.widget.RemoteViews); method public void updateAppWidget(android.content.ComponentName, android.widget.RemoteViews); @@ -9761,11 +9762,13 @@ package android.content.pm { method public boolean accept(android.os.Bundle); method public boolean accept(); method public int describeContents(); + method public android.appwidget.AppWidgetProviderInfo getAppWidgetProviderInfo(); method public int getRequestType(); method public android.content.pm.ShortcutInfo getShortcutInfo(); method public boolean isValid(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int REQUEST_TYPE_APPWIDGET = 2; // 0x2 field public static final int REQUEST_TYPE_SHORTCUT = 1; // 0x1 } diff --git a/api/system-current.txt b/api/system-current.txt index 4bba111f93921..64e18ffb0c098 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -7091,6 +7091,7 @@ package android.appwidget { method public void notifyAppWidgetViewDataChanged(int, int); method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews); method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews); + method public boolean requestPinAppWidget(android.content.ComponentName, android.app.PendingIntent); method public void updateAppWidget(int[], android.widget.RemoteViews); method public void updateAppWidget(int, android.widget.RemoteViews); method public void updateAppWidget(android.content.ComponentName, android.widget.RemoteViews); @@ -10171,11 +10172,13 @@ package android.content.pm { method public boolean accept(android.os.Bundle); method public boolean accept(); method public int describeContents(); + method public android.appwidget.AppWidgetProviderInfo getAppWidgetProviderInfo(); method public int getRequestType(); method public android.content.pm.ShortcutInfo getShortcutInfo(); method public boolean isValid(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int REQUEST_TYPE_APPWIDGET = 2; // 0x2 field public static final int REQUEST_TYPE_SHORTCUT = 1; // 0x1 } diff --git a/api/test-current.txt b/api/test-current.txt index d4fb027bcb4de..da4ccb1c130ea 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -6800,6 +6800,7 @@ package android.appwidget { method public void notifyAppWidgetViewDataChanged(int, int); method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews); method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews); + method public boolean requestPinAppWidget(android.content.ComponentName, android.app.PendingIntent); method public void updateAppWidget(int[], android.widget.RemoteViews); method public void updateAppWidget(int, android.widget.RemoteViews); method public void updateAppWidget(android.content.ComponentName, android.widget.RemoteViews); @@ -9789,11 +9790,13 @@ package android.content.pm { method public boolean accept(android.os.Bundle); method public boolean accept(); method public int describeContents(); + method public android.appwidget.AppWidgetProviderInfo getAppWidgetProviderInfo(); method public int getRequestType(); method public android.content.pm.ShortcutInfo getShortcutInfo(); method public boolean isValid(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int REQUEST_TYPE_APPWIDGET = 2; // 0x2 field public static final int REQUEST_TYPE_SHORTCUT = 1; // 0x1 } diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 9f654c25e2c5f..3189681585599 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -16,11 +16,15 @@ package android.appwidget; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentSender; import android.content.pm.ParceledListSlice; +import android.content.pm.ShortcutInfo; import android.os.Bundle; import android.os.IBinder; import android.os.Process; @@ -1079,4 +1083,41 @@ public class AppWidgetManager { info.minResizeHeight = TypedValue.complexToDimensionPixelSize(info.minResizeHeight, mDisplayMetrics); } + + /** + * Request to pin an app widget on the current launcher. It's up to the launcher to accept this + * request (optionally showing a user confirmation). If the request is accepted, the caller will + * get a confirmation with extra {@link #EXTRA_APPWIDGET_ID}. + * + *

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

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

When an app calls this API when a previous request is still waiting for a response, + * the previous request will be canceled. + * + * @param provider The {@link ComponentName} for the {@link + * android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget. + * @param successCallback If not null, this intent will be sent when the widget is created. + * + * @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 android.content.pm.ShortcutManager#isRequestPinShortcutSupported() + * @see android.content.pm.ShortcutManager#requestPinShortcut(ShortcutInfo, IntentSender) + * + * @throws IllegalStateException The caller doesn't have a foreground activity or a foreground + * service or when the user is locked. + */ + public boolean requestPinAppWidget(@NonNull ComponentName provider, + @Nullable PendingIntent successCallback) { + try { + return mService.requestPinAppWidget(mPackageName, provider, + successCallback == null ? null : successCallback.getIntentSender()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 5f4bc00a28f09..4b5b995cd2184 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.TestApi; +import android.appwidget.AppWidgetProviderInfo; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; @@ -1138,21 +1139,35 @@ public class LauncherApps { /** This is a request to pin shortcut. */ public static final int REQUEST_TYPE_SHORTCUT = 1; + /** This is a request to pin app widget. */ + public static final int REQUEST_TYPE_APPWIDGET = 2; + @IntDef(value = {REQUEST_TYPE_SHORTCUT}) @Retention(RetentionPolicy.SOURCE) public @interface RequestType {} private final int mRequestType; private final ShortcutInfo mShortcutInfo; + private final AppWidgetProviderInfo mAppWidgetInfo; private final IPinItemRequest mInner; /** * @hide */ - public PinItemRequest(@RequestType int requestType, ShortcutInfo shortcutInfo, - IPinItemRequest inner) { - mRequestType = requestType; + public PinItemRequest(ShortcutInfo shortcutInfo, IPinItemRequest inner) { + mRequestType = REQUEST_TYPE_SHORTCUT; mShortcutInfo = shortcutInfo; + mAppWidgetInfo = null; + mInner = inner; + } + + /** + * @hide + */ + public PinItemRequest(AppWidgetProviderInfo appWidgetInfo, IPinItemRequest inner) { + mRequestType = REQUEST_TYPE_APPWIDGET; + mShortcutInfo = null; + mAppWidgetInfo = appWidgetInfo; mInner = inner; } @@ -1174,6 +1189,15 @@ public class LauncherApps { return mShortcutInfo; } + /** + * {@link AppWidgetProviderInfo} sent by the requesting app. Always non-null for a + * {@link #REQUEST_TYPE_APPWIDGET} request. + */ + @Nullable + public AppWidgetProviderInfo getAppWidgetProviderInfo() { + return mAppWidgetInfo; + } + /** * Return {@code TRUE} if a request is valid -- i.e. {@link #accept(Bundle)} has not been * called, and it has not been canceled. @@ -1208,14 +1232,22 @@ public class LauncherApps { final ClassLoader cl = getClass().getClassLoader(); mRequestType = source.readInt(); - mShortcutInfo = source.readParcelable(cl); + mShortcutInfo = mRequestType == REQUEST_TYPE_SHORTCUT ? + (ShortcutInfo) source.readParcelable(cl) : null; + mAppWidgetInfo = mRequestType == REQUEST_TYPE_APPWIDGET ? + (AppWidgetProviderInfo) source.readParcelable(cl) : null; mInner = IPinItemRequest.Stub.asInterface(source.readStrongBinder()); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mRequestType); - dest.writeParcelable(mShortcutInfo, flags); + if (mRequestType == REQUEST_TYPE_SHORTCUT) { + dest.writeParcelable(mShortcutInfo, flags); + } + if (mRequestType == REQUEST_TYPE_APPWIDGET) { + dest.writeParcelable(mAppWidgetInfo, flags); + } dest.writeStrongBinder(mInner.asBinder()); } diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index c8f00b8e53c6e..c8fb3d174fe07 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -867,7 +867,7 @@ public class ShortcutManager { * * @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. + * service or when the user is locked. */ public boolean requestPinShortcut(@NonNull ShortcutInfo shortcut, @Nullable IntentSender resultIntent) { diff --git a/core/java/android/content/pm/ShortcutServiceInternal.java b/core/java/android/content/pm/ShortcutServiceInternal.java index af5610570f70e..4773c730406ff 100644 --- a/core/java/android/content/pm/ShortcutServiceInternal.java +++ b/core/java/android/content/pm/ShortcutServiceInternal.java @@ -19,8 +19,10 @@ package android.content.pm; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Intent; +import android.content.IntentSender; import android.content.pm.LauncherApps.ShortcutQuery; import android.os.ParcelFileDescriptor; @@ -68,4 +70,8 @@ public abstract class ShortcutServiceInternal { public abstract boolean hasShortcutHostPermission(int launcherUserId, @NonNull String callingPackage); + + public abstract boolean requestPinAppWidget(@NonNull String callingPackage, + @NonNull AppWidgetProviderInfo appWidget, @Nullable IntentSender resultIntent, + int userId); } diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl index 951a45a7b4376..a1eac363a6e8e 100644 --- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl +++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl @@ -67,5 +67,7 @@ interface IAppWidgetService { void unbindRemoteViewsService(String callingPackage, int appWidgetId, in Intent intent); int[] getAppWidgetIds(in ComponentName providerComponent); boolean isBoundWidgetPackage(String packageName, int userId); + boolean requestPinAppWidget(String packageName, in ComponentName providerComponent, + in IntentSender resultIntent); } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 6404604ca0970..87eb380de15ea 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -48,6 +48,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.ShortcutServiceInternal; import android.content.pm.UserInfo; import android.content.res.Resources; import android.content.res.TypedArray; @@ -1541,6 +1542,36 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + @Override + public boolean requestPinAppWidget(String callingPackage, ComponentName componentName, + IntentSender resultSender) { + final int callingUid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(callingUid); + + if (DEBUG) { + Slog.i(TAG, "requestPinAppWidget() " + userId); + } + + final AppWidgetProviderInfo info; + + synchronized (mLock) { + ensureGroupStateLoadedLocked(userId); + + // Look for the widget associated with the caller. + Provider provider = lookupProviderLocked(new ProviderId(callingUid, componentName)); + if (provider == null || provider.zombie) { + return false; + } + info = provider.info; + if ((info.widgetCategory & AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN) == 0) { + return false; + } + } + + return LocalServices.getService(ShortcutServiceInternal.class) + .requestPinAppWidget(callingPackage, info, resultSender, userId); + } + @Override public ParceledListSlice getInstalledProvidersForProfile(int categoryFilter, int profileId) { diff --git a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java index cdb69ce813a7f..e815f0ae18fd0 100644 --- a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java +++ b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java @@ -16,6 +16,8 @@ package com.android.server.pm; import android.annotation.Nullable; +import android.app.PendingIntent; +import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Intent; import android.content.IntentSender; @@ -46,37 +48,17 @@ class ShortcutRequestPinProcessor { /** * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. */ - private static class PinShortcutRequestInner extends IPinItemRequest.Stub { - private final ShortcutRequestPinProcessor mProcessor; - /** Original shortcut passed by the app. */ - public final ShortcutInfo shortcutOriginal; - - /** - * Cloned shortcut that's passed to the launcher. The notable difference from - * {@link #shortcutOriginal} is it must not have the intent. - */ - public final ShortcutInfo shortcutForLauncher; - + private static class PinItemRequestInner extends IPinItemRequest.Stub { + protected final ShortcutRequestPinProcessor mProcessor; 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 shortcutOriginal, ShortcutInfo shortcutForLauncher, - IntentSender resultIntent, - String launcherPackage, int launcherUserId, boolean preExisting) { + private PinItemRequestInner(ShortcutRequestPinProcessor processor, + IntentSender resultIntent) { mProcessor = processor; - this.shortcutOriginal = shortcutOriginal; - this.shortcutForLauncher = shortcutForLauncher; mResultIntent = resultIntent; - this.launcherPackage = launcherPackage; - this.launcherUserId = launcherUserId; - this.preExisting = preExisting; } @Override @@ -95,9 +77,11 @@ class ShortcutRequestPinProcessor { public boolean accept(Bundle options) { // Make sure the options are unparcellable by the FW. (e.g. not containing unknown // classes.) + Intent extras = null; if (options != null) { try { options.size(); + extras = new Intent().putExtras(options); } catch (RuntimeException e) { throw new IllegalArgumentException("options cannot be unparceled", e); } @@ -108,20 +92,58 @@ class ShortcutRequestPinProcessor { } mAccepted = true; } - if (DEBUG) { - Slog.d(TAG, "Launcher accepted shortcut. ID=" + shortcutOriginal.getId() - + " package=" + shortcutOriginal.getPackage() - + " options=" + options); - } // Pin it and send the result intent. - if (mProcessor.directPinShortcut(this)) { - mProcessor.sendResultIntent(mResultIntent); + if (tryAccept()) { + mProcessor.sendResultIntent(mResultIntent, extras); return true; } else { return false; } } + + protected boolean tryAccept() { + return true; + } + } + + /** + * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. + */ + private static class PinShortcutRequestInner extends PinItemRequestInner { + /** Original shortcut passed by the app. */ + public final ShortcutInfo shortcutOriginal; + + /** + * Cloned shortcut that's passed to the launcher. The notable difference from + * {@link #shortcutOriginal} is it must not have the intent. + */ + public final ShortcutInfo shortcutForLauncher; + + public final String launcherPackage; + public final int launcherUserId; + public final boolean preExisting; + + private PinShortcutRequestInner(ShortcutRequestPinProcessor processor, + ShortcutInfo shortcutOriginal, ShortcutInfo shortcutForLauncher, + IntentSender resultIntent, + String launcherPackage, int launcherUserId, boolean preExisting) { + super(processor, resultIntent); + this.shortcutOriginal = shortcutOriginal; + this.shortcutForLauncher = shortcutForLauncher; + this.launcherPackage = launcherPackage; + this.launcherUserId = launcherUserId; + this.preExisting = preExisting; + } + + @Override + protected boolean tryAccept() { + if (DEBUG) { + Slog.d(TAG, "Launcher accepted shortcut. ID=" + shortcutOriginal.getId() + + " package=" + shortcutOriginal.getPackage()); + } + return mProcessor.directPinShortcut(this); + } } public ShortcutRequestPinProcessor(ShortcutService service, Object lock) { @@ -134,15 +156,19 @@ class ShortcutRequestPinProcessor { } /** - * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}. + * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)} and + * {@link android.appwidget.AppWidgetManager#requestPinAppWidget}. + * One of {@param inShortcut} and {@param inAppWidget} is always non-null and the other is + * always null. */ - public boolean requestPinShortcutLocked(ShortcutInfo inShortcut, IntentSender resultIntent) { + public boolean requestPinItemLocked(ShortcutInfo inShortcut, AppWidgetProviderInfo inAppWidget, + int userId, IntentSender resultIntent) { // First, make sure the launcher supports it. // Find the confirmation activity in the default launcher. final Pair confirmActivity = - getRequestPinShortcutConfirmationActivity(inShortcut.getUserId()); + getRequestPinShortcutConfirmationActivity(userId); // If the launcher doesn't support it, just return a rejected result and finish. if (confirmActivity == null) { @@ -150,8 +176,6 @@ class ShortcutRequestPinProcessor { 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 @@ -159,7 +183,25 @@ class ShortcutRequestPinProcessor { mService.throwIfUserLockedL(launcherUserId); // Next, validate the incoming shortcut, etc. + final PinItemRequest request; + if (inShortcut != null) { + request = requestPinShortcutLocked(inShortcut, resultIntent, confirmActivity); + } else { + request = new PinItemRequest(inAppWidget, new PinItemRequestInner(this, resultIntent)); + } + if (request == null) { + sendResultIntent(resultIntent, null); + return true; + } + return startRequestConfirmActivity(confirmActivity.first, launcherUserId, request); + } + + /** + * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}. + */ + private PinItemRequest requestPinShortcutLocked(ShortcutInfo inShortcut, + IntentSender resultIntent, Pair confirmActivity) { final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( inShortcut.getPackage(), inShortcut.getUserId()); @@ -174,6 +216,8 @@ class ShortcutRequestPinProcessor { // This is the shortcut that'll be sent to the launcher. final ShortcutInfo shortcutForLauncher; + final String launcherPackage = confirmActivity.first.getPackageName(); + final int launcherUserId = confirmActivity.second; if (existsAlready) { validateExistingShortcut(existing); @@ -183,8 +227,7 @@ class ShortcutRequestPinProcessor { launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing)) { Log.i(TAG, "Launcher's already pinning shortcut " + existing.getId() + " for package " + existing.getPackage()); - sendResultIntent(resultIntent); - return true; + return null; } // Pass a clone, not the original. @@ -213,10 +256,7 @@ class ShortcutRequestPinProcessor { new PinShortcutRequestInner(this, inShortcut, shortcutForLauncher, resultIntent, launcherPackage, launcherUserId, existsAlready); - final PinItemRequest outer = new PinItemRequest(PinItemRequest.REQUEST_TYPE_SHORTCUT, - shortcutForLauncher, inner); - - return startRequestConfirmActivity(launcherComponent, launcherUserId, outer); + return new PinItemRequest(shortcutForLauncher, inner); } private void validateExistingShortcut(ShortcutInfo shortcutInfo) { @@ -270,11 +310,11 @@ class ShortcutRequestPinProcessor { return (activity == null) ? null : Pair.create(activity, launcherUserId); } - public void sendResultIntent(@Nullable IntentSender intent) { + public void sendResultIntent(@Nullable IntentSender intent, @Nullable Intent extras) { if (DEBUG) { Slog.d(TAG, "Sending result intent."); } - mService.injectSendIntentSender(intent); + mService.injectSendIntentSender(intent, extras); } /** diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 436a53c77580b..86f755694f43c 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -24,6 +24,7 @@ import android.app.ActivityManagerInternal; import android.app.AppGlobals; import android.app.IUidObserver; import android.app.usage.UsageStatsManagerInternal; +import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -1857,21 +1858,32 @@ public class ShortcutService extends IShortcutService.Stub { @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"); + return requestPinItem(packageName, userId, shortcut, null, resultIntent); + } + + /** + * Handles {@link #requestPinShortcut} and {@link ShortcutServiceInternal#requestPinAppWidget}. + * After validating the caller, it passes the request to {@link #mShortcutRequestPinProcessor}. + * Either {@param shortcut} or {@param appWidget} should be non-null. + */ + private boolean requestPinItem(String packageName, int userId, + ShortcutInfo shortcut, AppWidgetProviderInfo appWidget, IntentSender resultIntent) { + verifyCaller(packageName, userId); final boolean ret; synchronized (mLock) { throwIfUserLockedL(userId); Preconditions.checkState(isUidForegroundLocked(injectBinderCallingUid()), - "Calling application must have a foreground activity or a foreground service"); + "Calling application must have a foreground activity or a foreground service"); // TODO Cancel all pending requests from the caller. // Send request to the launcher, if supported. - ret = mShortcutRequestPinProcessor.requestPinShortcutLocked(shortcut, resultIntent); + ret = mShortcutRequestPinProcessor.requestPinItemLocked(shortcut, appWidget, userId, + resultIntent); } verifyStates(); @@ -2591,6 +2603,14 @@ public class ShortcutService extends IShortcutService.Stub { @NonNull String callingPackage) { return ShortcutService.this.hasShortcutHostPermission(callingPackage, launcherUserId); } + + @Override + public boolean requestPinAppWidget(@NonNull String callingPackage, + @NonNull AppWidgetProviderInfo appWidget, @Nullable IntentSender resultIntent, + int userId) { + Preconditions.checkNotNull(appWidget); + return requestPinItem(callingPackage, userId, null, appWidget, resultIntent); + } } final BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -3253,12 +3273,12 @@ public class ShortcutService extends IShortcutService.Stub { } } - void injectSendIntentSender(IntentSender intentSender) { + void injectSendIntentSender(IntentSender intentSender, Intent extras) { if (intentSender == null) { return; } try { - intentSender.sendIntent(mContext, /* code= */ 0, /* intent= */ null, + intentSender.sendIntent(mContext, /* code= */ 0, extras, /* onFinished=*/ null, /* handler= */ null); } catch (SendIntentException e) { Slog.w(TAG, "sendIntent failed().", e); diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk index 86983eb8b6ee5..ae5da78bbab55 100644 --- a/services/tests/servicestests/Android.mk +++ b/services/tests/servicestests/Android.mk @@ -14,6 +14,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_STATIC_JAVA_LIBRARIES := \ frameworks-base-testutils \ services.accessibility \ + services.appwidget \ services.core \ services.devicepolicy \ services.net \ diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 41654676aadd6..1c92e452d03cb 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -164,6 +164,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java new file mode 100644 index 0000000000000..4886a5f573aa4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2017 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.appwidget; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.admin.DevicePolicyManagerInternal; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.pm.ShortcutServiceInternal; +import android.os.Handler; +import android.os.UserHandle; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.server.LocalServices; + +import org.mockito.ArgumentCaptor; + + +/** + * Tests for {@link AppWidgetManager} and {@link AppWidgetServiceImpl}. + * + m FrameworksServicesTests && + adb install \ + -r -g ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk && + adb shell am instrument -e class com.android.server.appwidget.AppWidgetServiceImplTest \ + -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner + */ +@SmallTest +public class AppWidgetServiceImplTest extends InstrumentationTestCase { + + private TestContext mTestContext; + private AppWidgetServiceImpl mService; + private AppWidgetManager mManager; + + private ShortcutServiceInternal mMockShortcutService; + + @Override + protected void setUp() throws Exception { + super.setUp(); + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + LocalServices.removeServiceForTest(ShortcutServiceInternal.class); + + mTestContext = new TestContext(); + mService = new AppWidgetServiceImpl(mTestContext); + mManager = new AppWidgetManager(mTestContext, mService); + + mMockShortcutService = mock(ShortcutServiceInternal.class); + LocalServices.addService(ShortcutServiceInternal.class, mMockShortcutService); + + mService.onStart(); + } + + public void testRequestPinAppWidget_otherProvider() { + ComponentName otherProvider = null; + for (AppWidgetProviderInfo provider : mManager.getInstalledProviders()) { + if (!provider.provider.getPackageName().equals(mTestContext.getPackageName())) { + otherProvider = provider.provider; + break; + } + } + if (otherProvider == null) { + // No other provider found. Ignore this test. + } + assertFalse(mManager.requestPinAppWidget(otherProvider, null)); + } + + public void testRequestPinAppWidget() { + ComponentName provider = new ComponentName(mTestContext, DummyAppWidget.class); + // Set up users. + when(mMockShortcutService.requestPinAppWidget(anyString(), + any(AppWidgetProviderInfo.class), any(IntentSender.class), anyInt())) + .thenReturn(true); + assertTrue(mManager.requestPinAppWidget(provider, null)); + + final ArgumentCaptor providerCaptor = + ArgumentCaptor.forClass(AppWidgetProviderInfo.class); + verify(mMockShortcutService, times(1)).requestPinAppWidget(anyString(), + providerCaptor.capture(), eq(null), anyInt()); + assertEquals(provider, providerCaptor.getValue().provider); + } + + private class TestContext extends ContextWrapper { + + public TestContext() { + super(getInstrumentation().getContext()); + } + + @Override + public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, + IntentFilter filter, String broadcastPermission, Handler scheduler) { + // ignore. + return null; + } + + @Override + public void unregisterReceiver(BroadcastReceiver receiver) { + // ignore. + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/appwidget/DummyAppWidget.java b/services/tests/servicestests/src/com/android/server/appwidget/DummyAppWidget.java new file mode 100644 index 0000000000000..803119f4c5faf --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appwidget/DummyAppWidget.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 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.appwidget; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Dummy widget for testing + */ +public class DummyAppWidget extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + } +} diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 6bc4c19f6c89f..dcaab76b1f535 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -441,7 +441,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { } @Override - void injectSendIntentSender(IntentSender intent) { + void injectSendIntentSender(IntentSender intent, Intent extras) { mContext.sendIntentSender(intent); } diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java new file mode 100644 index 0000000000000..84a7c198cede5 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2017 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.assertExpectException; + +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.appwidget.AppWidgetProviderInfo; +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.os.UserHandle; +import android.test.suitebuilder.annotation.SmallTest; + +import org.mockito.ArgumentCaptor; + +/** + * Tests for {@link android.content.pm.ShortcutServiceInternal#requestPinAppWidget} + * 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.ShortcutManagerTest9 \ + -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner + */ +@SmallTest +public class ShortcutManagerTest9 extends BaseShortcutManagerTest { + + private ShortcutRequestPinProcessor mProcessor; + + @Override + protected void initService() { + super.initService(); + mProcessor = mService.getShortcutRequestPinProcessorForTest(); + } + + @Override + protected void setCaller(String packageName, int userId) { + super.setCaller(packageName, userId); + + // Note during this test, assume all callers are in the foreground by default. + makeCallerForeground(); + } + + private AppWidgetProviderInfo makeProviderInfo(String className) { + AppWidgetProviderInfo info = new AppWidgetProviderInfo(); + info.provider = new ComponentName(CALLING_PACKAGE_3, className); + return info; + } + + 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, String className) { + assertNotNull(actualRequest); + assertEquals(PinItemRequest.REQUEST_TYPE_APPWIDGET, actualRequest.getRequestType()); + assertEquals(className, actualRequest.getAppWidgetProviderInfo().provider.getClassName()); + } + + public void testNotForeground() { + setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0)); + + runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { + makeCallerBackground(); + + assertExpectException(IllegalStateException.class, "foreground activity", () -> { + mInternal.requestPinAppWidget(CALLING_PACKAGE_1, makeProviderInfo("dummy"), + /* resultIntent= */ null, USER_P0); + }); + + verify(mServiceContext, times(0)).sendIntentSender(any(IntentSender.class)); + verify(mServiceContext, times(0)).startActivityAsUser( + any(Intent.class), any(UserHandle.class)); + }); + } + + private void checkRequestPinAppWidget(@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, () -> { + AppWidgetProviderInfo info = makeProviderInfo("c1"); + + assertTrue(mInternal.requestPinAppWidget(CALLING_PACKAGE_1, info, + resultIntent == null ? null : resultIntent.getIntentSender(), USER_P0)); + + verify(mServiceContext, times(0)).sendIntentSender(any(IntentSender.class)); + }); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + // Check the intent passed to startActivityAsUser(). + final ArgumentCaptor intent = ArgumentCaptor.forClass(Intent.class); + + verify(mServiceContext).startActivityAsUser(intent.capture(), eq(HANDLE_USER_0)); + + assertPinItemRequestIntent(intent.getValue(), mInjectedClientPackage); + + // Check the request object. + final PinItemRequest request = mLauncherApps.getPinItemRequest(intent.getValue()); + assertPinItemRequest(request, "c1"); + + // Accept the request. + assertTrue(request.accept()); + }); + + // 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)); + } + } + + public void testRequestPinAppWidget() { + checkRequestPinAppWidget(/* resultIntent=*/ null); + } + + public void testRequestPinAppWidget_withCallback() { + final PendingIntent resultIntent = + PendingIntent.getActivity(getTestContext(), 0, new Intent(), 0); + + checkRequestPinAppWidget(resultIntent); + } +}