Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3f27cd7a authored by Willie Koomson's avatar Willie Koomson
Browse files

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
parent 49f37870
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -9482,12 +9482,15 @@ package android.appwidget {
    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 @@ package android.appwidget {
    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;
+85 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package android.appwidget;
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.UiThread;
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 @@ public class AppWidgetManager {
        }
    }

    /**
     * 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) {
+27 −0
Original line number Diff line number Diff line
@@ -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 @@ public class AppWidgetProviderInfo implements Parcelable {
    /** @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 @@ public class AppWidgetProviderInfo implements Parcelable {
        this.widgetFeatures = in.readInt();
        this.descriptionRes = in.readInt();
        this.isExtendedFromAppWidgetProvider = in.readBoolean();
        if (generatedPreviews()) {
            generatedPreviewCategories = in.readInt();
        }
    }

    /**
@@ -515,6 +536,9 @@ public class AppWidgetProviderInfo implements Parcelable {
        out.writeInt(this.widgetFeatures);
        out.writeInt(this.descriptionRes);
        out.writeBoolean(this.isExtendedFromAppWidgetProvider);
        if (generatedPreviews()) {
            out.writeInt(this.generatedPreviewCategories);
        }
    }

    @Override
@@ -545,6 +569,9 @@ public class AppWidgetProviderInfo implements Parcelable {
        that.widgetFeatures = this.widgetFeatures;
        that.descriptionRes = this.descriptionRes;
        that.isExtendedFromAppWidgetProvider = this.isExtendedFromAppWidgetProvider;
        if (generatedPreviews()) {
            that.generatedPreviewCategories = this.generatedPreviewCategories;
        }
        return that;
    }

+6 −0
Original line number Diff line number Diff line
@@ -80,5 +80,11 @@ interface IAppWidgetService {
            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);

}
+204 −2
Original line number Diff line number Diff line
@@ -362,6 +362,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        boolean added = false;
        boolean changed = false;
        boolean componentsModified = false;
        int clearedUid = -1;

        final String pkgList[];
        switch (action) {
@@ -416,6 +418,10 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
            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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
                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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
                        }
                    }
                }
            } 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        }
    }

    @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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        }
    }

    @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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
            return false;
        }

        @GuardedBy("AppWidgetServiceImpl.mLock")
        @GuardedBy("this.mLock")
        public AppWidgetProviderInfo getInfoLocked(Context context) {
            if (!mInfoParsed) {
                // parse
@@ -4250,6 +4395,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
                    }
                    if (newInfo != null) {
                        info = newInfo;
                        updateGeneratedPreviewCategoriesLocked();
                    }
                }
                mInfoParsed = true;
@@ -4279,6 +4425,62 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
            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" : "") + '}';