Add get/setWidgetPreview API in AppWidgetManager
This change adds an API to AppWidgetManager that allows providers
to store generated RemoteViews in AppWidgetService, where they
can be retrieved by hosts. Currently these previews are not
persisted between reboots.
Bug: 308041327
Test: adb shell device_config put app_widgets \
android.appwidget.flags.generated_previews true
Test: CtsAppWidgetTestCases
Change-Id: Ib536b4ad20b75245780933dde18e4f14b4ee0ae3
diff --git a/core/api/current.txt b/core/api/current.txt
index 5abb92b..ab0016c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9482,12 +9482,15 @@
method @NonNull public java.util.List<android.appwidget.AppWidgetProviderInfo> getInstalledProvidersForPackage(@NonNull String, @Nullable android.os.UserHandle);
method @NonNull public java.util.List<android.appwidget.AppWidgetProviderInfo> getInstalledProvidersForProfile(@Nullable android.os.UserHandle);
method public static android.appwidget.AppWidgetManager getInstance(android.content.Context);
+ method @FlaggedApi("android.appwidget.flags.generated_previews") @Nullable public android.widget.RemoteViews getWidgetPreview(@NonNull android.content.ComponentName, @Nullable android.os.UserHandle, int);
method public boolean isRequestPinAppWidgetSupported();
method @Deprecated public void notifyAppWidgetViewDataChanged(int[], int);
method @Deprecated public void notifyAppWidgetViewDataChanged(int, int);
method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews);
method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews);
+ method @FlaggedApi("android.appwidget.flags.generated_previews") public void removeWidgetPreview(@NonNull android.content.ComponentName, int);
method public boolean requestPinAppWidget(@NonNull android.content.ComponentName, @Nullable android.os.Bundle, @Nullable android.app.PendingIntent);
+ method @FlaggedApi("android.appwidget.flags.generated_previews") public void setWidgetPreview(@NonNull android.content.ComponentName, int, @NonNull android.widget.RemoteViews);
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);
@@ -9561,6 +9564,7 @@
field public int autoAdvanceViewId;
field public android.content.ComponentName configure;
field @IdRes public int descriptionRes;
+ field @FlaggedApi("android.appwidget.flags.generated_previews") public int generatedPreviewCategories;
field public int icon;
field public int initialKeyguardLayout;
field public int initialLayout;
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index 4cf9fca..6204edc 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -19,6 +19,7 @@
import static android.appwidget.flags.Flags.remoteAdapterConversion;
import android.annotation.BroadcastBehavior;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresFeature;
@@ -30,6 +31,7 @@
import android.annotation.UserIdInt;
import android.app.IServiceConnection;
import android.app.PendingIntent;
+import android.appwidget.flags.Flags;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
@@ -1415,6 +1417,89 @@
}
}
+ /**
+ * Set a preview for this widget. This preview will be used instead of the provider's {@link
+ * AppWidgetProviderInfo#previewLayout previewLayout} or {@link
+ * AppWidgetProviderInfo#previewImage previewImage} for previewing the widget in the widget
+ * picker and pin app widget flow.
+ *
+ * @param provider The {@link ComponentName} for the {@link android.content.BroadcastReceiver
+ * BroadcastReceiver} provider for the AppWidget you intend to provide a preview for.
+ * @param widgetCategories The categories that this preview should be used for. This can be a
+ * single category or combination of categories. If multiple categories are specified,
+ * then this preview will be used for each of those categories. For example, if you
+ * set a preview for WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD, the preview will
+ * be used when picking widgets for the home screen and keyguard.
+ *
+ * <p>Note: You should only use the widget categories that the provider supports, as defined
+ * in {@link AppWidgetProviderInfo#widgetCategory}.
+ * @param preview This preview will be used for previewing the provider when picking widgets for
+ * the selected categories.
+ *
+ * @see AppWidgetProviderInfo#WIDGET_CATEGORY_HOME_SCREEN
+ * @see AppWidgetProviderInfo#WIDGET_CATEGORY_KEYGUARD
+ * @see AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX
+ */
+ @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
+ public void setWidgetPreview(@NonNull ComponentName provider,
+ @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
+ @NonNull RemoteViews preview) {
+ try {
+ mService.setWidgetPreview(provider, widgetCategories, preview);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the RemoteViews previews for this widget.
+ *
+ * @param provider The {@link ComponentName} for the {@link android.content.BroadcastReceiver
+ * BroadcastReceiver} provider for the AppWidget you intend to get a preview for.
+ * @param profile The profile in which the provider resides. Passing null is equivalent
+ * to querying for only the calling user.
+ * @param widgetCategory The widget category for which you want to display previews. This should
+ * be a single category. If a combination of categories is provided, this function will
+ * return a preview that matches at least one of the categories.
+ *
+ * @return The widget preview for the selected category, if available.
+ * @see AppWidgetProviderInfo#generatedPreviewCategories
+ */
+ @Nullable
+ @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
+ public RemoteViews getWidgetPreview(@NonNull ComponentName provider,
+ @Nullable UserHandle profile, @AppWidgetProviderInfo.CategoryFlags int widgetCategory) {
+ try {
+ if (profile == null) {
+ profile = mContext.getUser();
+ }
+ return mService.getWidgetPreview(mPackageName, provider, profile.getIdentifier(),
+ widgetCategory);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove this provider's preview for the specified widget categories. If the provider does not
+ * have a preview for the specified widget category, this is a no-op.
+ *
+ * @param provider The AppWidgetProvider to remove previews for.
+ * @param widgetCategories The categories of the preview to remove. For example, removing the
+ * preview for WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD will remove the
+ * previews for both categories.
+ */
+ @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
+ public void removeWidgetPreview(@NonNull ComponentName provider,
+ @AppWidgetProviderInfo.CategoryFlags int widgetCategories) {
+ try {
+ mService.removeWidgetPreview(provider, widgetCategories);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+
@UiThread
private static @NonNull Executor createUpdateExecutorIfNull() {
if (sUpdateExecutor == null) {
diff --git a/core/java/android/appwidget/AppWidgetProviderInfo.java b/core/java/android/appwidget/AppWidgetProviderInfo.java
index e56e53a..1a80cac2 100644
--- a/core/java/android/appwidget/AppWidgetProviderInfo.java
+++ b/core/java/android/appwidget/AppWidgetProviderInfo.java
@@ -16,6 +16,10 @@
package android.appwidget;
+import static android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS;
+import static android.appwidget.flags.Flags.generatedPreviews;
+
+import android.annotation.FlaggedApi;
import android.annotation.IdRes;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -358,6 +362,20 @@
/** @hide */
public boolean isExtendedFromAppWidgetProvider;
+ /**
+ * Flags indicating the widget categories for which generated previews are available.
+ * These correspond to the previews set by this provider with
+ * {@link AppWidgetManager#setWidgetPreview}.
+ *
+ * @see #WIDGET_CATEGORY_HOME_SCREEN
+ * @see #WIDGET_CATEGORY_KEYGUARD
+ * @see #WIDGET_CATEGORY_SEARCHBOX
+ * @see AppWidgetManager#getWidgetPreview
+ */
+ @FlaggedApi(FLAG_GENERATED_PREVIEWS)
+ @SuppressLint("MutableBareField")
+ public int generatedPreviewCategories;
+
public AppWidgetProviderInfo() {
}
@@ -391,6 +409,9 @@
this.widgetFeatures = in.readInt();
this.descriptionRes = in.readInt();
this.isExtendedFromAppWidgetProvider = in.readBoolean();
+ if (generatedPreviews()) {
+ generatedPreviewCategories = in.readInt();
+ }
}
/**
@@ -515,6 +536,9 @@
out.writeInt(this.widgetFeatures);
out.writeInt(this.descriptionRes);
out.writeBoolean(this.isExtendedFromAppWidgetProvider);
+ if (generatedPreviews()) {
+ out.writeInt(this.generatedPreviewCategories);
+ }
}
@Override
@@ -545,6 +569,9 @@
that.widgetFeatures = this.widgetFeatures;
that.descriptionRes = this.descriptionRes;
that.isExtendedFromAppWidgetProvider = this.isExtendedFromAppWidgetProvider;
+ if (generatedPreviews()) {
+ that.generatedPreviewCategories = this.generatedPreviewCategories;
+ }
return that;
}
diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
index 4fe9aea..85bdbb9 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -80,5 +80,11 @@
in Bundle extras, in IntentSender resultIntent);
boolean isRequestPinAppWidgetSupported();
oneway void noteAppWidgetTapped(in String callingPackage, in int appWidgetId);
+ void setWidgetPreview(in ComponentName providerComponent, in int widgetCategories,
+ in RemoteViews preview);
+ @nullable RemoteViews getWidgetPreview(in String callingPackage,
+ in ComponentName providerComponent, in int profileId, in int widgetCategory);
+ void removeWidgetPreview(in ComponentName providerComponent, in int widgetCategories);
+
}
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index cab2d74..5407af7 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -362,6 +362,7 @@
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ packageFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
packageFilter.addDataScheme("package");
mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL,
packageFilter, null, mCallbackHandler);
@@ -402,6 +403,7 @@
boolean added = false;
boolean changed = false;
boolean componentsModified = false;
+ int clearedUid = -1;
final String pkgList[];
switch (action) {
@@ -416,6 +418,10 @@
case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:
pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
break;
+ case Intent.ACTION_PACKAGE_DATA_CLEARED:
+ pkgList = null;
+ clearedUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ break;
default: {
Uri uri = intent.getData();
if (uri == null) {
@@ -430,7 +436,7 @@
changed = Intent.ACTION_PACKAGE_CHANGED.equals(action);
}
}
- if (pkgList == null || pkgList.length == 0) {
+ if ((pkgList == null || pkgList.length == 0) && clearedUid == -1) {
return;
}
@@ -461,6 +467,8 @@
}
}
}
+ } else if (clearedUid != -1) {
+ componentsModified |= clearPreviewsForUidLocked(clearedUid);
} else {
// If the package is being updated, we'll receive a PACKAGE_ADDED
// shortly, otherwise it is removed permanently.
@@ -486,6 +494,19 @@
}
}
+ @GuardedBy("mLock")
+ private boolean clearPreviewsForUidLocked(int clearedUid) {
+ boolean changed = false;
+ final int providerCount = mProviders.size();
+ for (int i = 0; i < providerCount; i++) {
+ Provider provider = mProviders.get(i);
+ if (provider.id.uid == clearedUid) {
+ changed |= provider.clearGeneratedPreviewsLocked();
+ }
+ }
+ return changed;
+ }
+
/**
* Reload all widgets' masked state for the given user and its associated profiles, including
* due to user not being available and package suspension.
@@ -3904,6 +3925,124 @@
}
}
+ @Override
+ @Nullable
+ public RemoteViews getWidgetPreview(@NonNull String callingPackage,
+ @NonNull ComponentName providerComponent, int profileId,
+ @AppWidgetProviderInfo.CategoryFlags int widgetCategory) {
+ final int callingUserId = UserHandle.getCallingUserId();
+ if (DEBUG) {
+ Slog.i(TAG, "getWidgetPreview() " + callingUserId);
+ }
+ mSecurityPolicy.enforceCallFromPackage(callingPackage);
+ ensureWidgetCategoryCombinationIsValid(widgetCategory);
+
+ synchronized (mLock) {
+ ensureGroupStateLoadedLocked(profileId);
+ final int providerCount = mProviders.size();
+ for (int i = 0; i < providerCount; i++) {
+ Provider provider = mProviders.get(i);
+ final ComponentName componentName = provider.id.componentName;
+ if (provider.zombie || !providerComponent.equals(componentName)) {
+ continue;
+ }
+
+ final AppWidgetProviderInfo info = provider.getInfoLocked(mContext);
+ final int providerProfileId = info.getProfile().getIdentifier();
+ if (providerProfileId != profileId) {
+ continue;
+ }
+
+ // Allow access to this provider if it is from the calling package or the caller has
+ // BIND_APPWIDGET permission.
+ final int callingUid = Binder.getCallingUid();
+ final String providerPackageName = componentName.getPackageName();
+ final boolean providerIsInCallerProfile =
+ mSecurityPolicy.isProviderInCallerOrInProfileAndWhitelListed(
+ providerPackageName, providerProfileId);
+ final boolean shouldFilterAppAccess = mPackageManagerInternal.filterAppAccess(
+ providerPackageName, callingUid, providerProfileId);
+ final boolean providerIsInCallerPackage =
+ mSecurityPolicy.isProviderInPackageForUid(provider, callingUid,
+ callingPackage);
+ final boolean hasBindAppWidgetPermission =
+ mSecurityPolicy.hasCallerBindPermissionOrBindWhiteListedLocked(
+ callingPackage);
+ if (providerIsInCallerProfile && !shouldFilterAppAccess
+ && (providerIsInCallerPackage || hasBindAppWidgetPermission)) {
+ return provider.getGeneratedPreviewLocked(widgetCategory);
+ }
+ }
+ }
+ throw new IllegalArgumentException(
+ providerComponent + " is not a valid AppWidget provider");
+ }
+
+ @Override
+ public void setWidgetPreview(@NonNull ComponentName providerComponent,
+ @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
+ @NonNull RemoteViews preview) {
+ final int userId = UserHandle.getCallingUserId();
+ if (DEBUG) {
+ Slog.i(TAG, "setWidgetPreview() " + userId);
+ }
+
+ // Make sure callers only set previews for their own package.
+ mSecurityPolicy.enforceCallFromPackage(providerComponent.getPackageName());
+
+ ensureWidgetCategoryCombinationIsValid(widgetCategories);
+
+ synchronized (mLock) {
+ ensureGroupStateLoadedLocked(userId);
+
+ final ProviderId providerId = new ProviderId(Binder.getCallingUid(), providerComponent);
+ final Provider provider = lookupProviderLocked(providerId);
+ if (provider == null) {
+ throw new IllegalArgumentException(
+ providerComponent + " is not a valid AppWidget provider");
+ }
+ provider.setGeneratedPreviewLocked(widgetCategories, preview);
+ scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+ }
+ }
+
+ @Override
+ public void removeWidgetPreview(@NonNull ComponentName providerComponent,
+ @AppWidgetProviderInfo.CategoryFlags int widgetCategories) {
+ final int userId = UserHandle.getCallingUserId();
+ if (DEBUG) {
+ Slog.i(TAG, "removeWidgetPreview() " + userId);
+ }
+
+ // Make sure callers only remove previews for their own package.
+ mSecurityPolicy.enforceCallFromPackage(providerComponent.getPackageName());
+
+ ensureWidgetCategoryCombinationIsValid(widgetCategories);
+ synchronized (mLock) {
+ ensureGroupStateLoadedLocked(userId);
+
+ final ProviderId providerId = new ProviderId(Binder.getCallingUid(), providerComponent);
+ final Provider provider = lookupProviderLocked(providerId);
+ if (provider == null) {
+ throw new IllegalArgumentException(
+ providerComponent + " is not a valid AppWidget provider");
+ }
+ final boolean changed = provider.removeGeneratedPreviewLocked(widgetCategories);
+ if (changed) scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+ }
+ }
+
+ private static void ensureWidgetCategoryCombinationIsValid(int widgetCategories) {
+ int validCategories = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+ | AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
+ | AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX;
+ int invalid = ~validCategories;
+ if ((widgetCategories & invalid) != 0) {
+ throw new IllegalArgumentException(widgetCategories
+ + " is not a valid widget category combination");
+ }
+ }
+
private final class CallbackHandler extends Handler {
public static final int MSG_NOTIFY_UPDATE_APP_WIDGET = 1;
public static final int MSG_NOTIFY_PROVIDER_CHANGED = 2;
@@ -4201,6 +4340,12 @@
ArrayList<Widget> widgets = new ArrayList<>();
PendingIntent broadcast;
String infoTag;
+ SparseArray<RemoteViews> generatedPreviews = new SparseArray<>(3);
+ private static final int[] WIDGET_CATEGORY_FLAGS = new int[]{
+ AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN,
+ AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD,
+ AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX,
+ };
boolean zombie; // if we're in safe mode, don't prune this just because nobody references it
@@ -4234,7 +4379,7 @@
return false;
}
- @GuardedBy("AppWidgetServiceImpl.mLock")
+ @GuardedBy("this.mLock")
public AppWidgetProviderInfo getInfoLocked(Context context) {
if (!mInfoParsed) {
// parse
@@ -4250,6 +4395,7 @@
}
if (newInfo != null) {
info = newInfo;
+ updateGeneratedPreviewCategoriesLocked();
}
}
mInfoParsed = true;
@@ -4279,6 +4425,62 @@
mInfoParsed = true;
}
+ @GuardedBy("this.mLock")
+ @Nullable
+ public RemoteViews getGeneratedPreviewLocked(
+ @AppWidgetProviderInfo.CategoryFlags int widgetCategories) {
+ for (int i = 0; i < generatedPreviews.size(); i++) {
+ if ((widgetCategories & generatedPreviews.keyAt(i)) != 0) {
+ return generatedPreviews.valueAt(i);
+ }
+ }
+ return null;
+ }
+
+ @GuardedBy("this.mLock")
+ public void setGeneratedPreviewLocked(
+ @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
+ @NonNull RemoteViews preview) {
+ for (int flag : WIDGET_CATEGORY_FLAGS) {
+ if ((widgetCategories & flag) != 0) {
+ generatedPreviews.put(flag, preview);
+ }
+ }
+ updateGeneratedPreviewCategoriesLocked();
+ }
+
+ @GuardedBy("this.mLock")
+ public boolean removeGeneratedPreviewLocked(int widgetCategories) {
+ boolean changed = false;
+ for (int flag : WIDGET_CATEGORY_FLAGS) {
+ if ((widgetCategories & flag) != 0) {
+ changed |= generatedPreviews.removeReturnOld(flag) != null;
+ }
+ }
+ if (changed) {
+ updateGeneratedPreviewCategoriesLocked();
+ }
+ return changed;
+ }
+
+ @GuardedBy("this.mLock")
+ public boolean clearGeneratedPreviewsLocked() {
+ if (generatedPreviews.size() > 0) {
+ generatedPreviews.clear();
+ updateGeneratedPreviewCategoriesLocked();
+ return true;
+ }
+ return false;
+ }
+
+ @GuardedBy("this.mLock")
+ private void updateGeneratedPreviewCategoriesLocked() {
+ info.generatedPreviewCategories = 0;
+ for (int i = 0; i < generatedPreviews.size(); i++) {
+ info.generatedPreviewCategories |= generatedPreviews.keyAt(i);
+ }
+ }
+
@Override
public String toString() {
return "Provider{" + id + (zombie ? " Z" : "") + '}';