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

Commit 2b2462f2 authored by Willie Koomson's avatar Willie Koomson Committed by Android (Google) Code Review
Browse files

Merge "Rate limit calls to setWidgetPreview/removeWidgetPreview" into main

parents 024f1046 c8de8c70
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -9608,7 +9608,7 @@ package android.appwidget {
    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 @FlaggedApi("android.appwidget.flags.generated_previews") public boolean 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);
+4 −2
Original line number Diff line number Diff line
@@ -1417,13 +1417,15 @@ public class AppWidgetManager {
     * @see AppWidgetProviderInfo#WIDGET_CATEGORY_HOME_SCREEN
     * @see AppWidgetProviderInfo#WIDGET_CATEGORY_KEYGUARD
     * @see AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX
     *
     * @return true if the call was successful, false if it was rate-limited.
     */
    @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
    public void setWidgetPreview(@NonNull ComponentName provider,
    public boolean setWidgetPreview(@NonNull ComponentName provider,
            @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
            @NonNull RemoteViews preview) {
        try {
            mService.setWidgetPreview(provider, widgetCategories, preview);
            return mService.setWidgetPreview(provider, widgetCategories, preview);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
+1 −2
Original line number Diff line number Diff line
@@ -80,11 +80,10 @@ 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,
    boolean 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);

}
+13 −0
Original line number Diff line number Diff line
@@ -560,6 +560,19 @@ public final class SystemUiDeviceConfigFlags {
     */
    public static final String CURSOR_HOVER_STATES_ENABLED = "cursor_hover_states_enabled";


    /*
     * (long) The reset interval for generated preview API calls.
     */
    public static final String GENERATED_PREVIEW_API_RESET_INTERVAL_MS =
            "generated_preview_api_reset_interval_ms";

    /*
     * (int) The max number of generated preview API calls per reset interval.
     */
    public static final String GENERATED_PREVIEW_API_MAX_CALLS_PER_INTERVAL =
            "generated_preview_api_max_calls_per_interval";

    private SystemUiDeviceConfigFlags() {
    }
}
+144 −5
Original line number Diff line number Diff line
@@ -84,6 +84,7 @@ import android.os.Binder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
@@ -98,6 +99,7 @@ import android.provider.DeviceConfig;
import android.service.appwidget.AppWidgetServiceDumpProto;
import android.service.appwidget.WidgetProto;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.AttributeSet;
@@ -148,6 +150,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -159,6 +162,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.LongSupplier;

class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBackupProvider,
        OnCrossProfileWidgetProvidersChangeListener {
@@ -187,6 +191,13 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
    // used to verify which request has successfully been received by the host.
    private static final AtomicLong UPDATE_COUNTER = new AtomicLong();

    // Default reset interval for generated preview API rate limiting.
    private static final long DEFAULT_GENERATED_PREVIEW_RESET_INTERVAL_MS =
            Duration.ofHours(1).toMillis();
    // Default max API calls per reset interval for generated preview API rate limiting.
    private static final int DEFAULT_GENERATED_PREVIEW_MAX_CALLS_PER_INTERVAL = 2;


    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
@@ -266,6 +277,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
    // Mark widget lifecycle broadcasts as 'interactive'
    private Bundle mInteractiveBroadcast;

    private ApiCounter mGeneratedPreviewsApiCounter;

    AppWidgetServiceImpl(Context context) {
        mContext = context;
    }
@@ -294,6 +307,17 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        mIsCombinedBroadcastEnabled = DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI,
            SystemUiDeviceConfigFlags.COMBINED_BROADCAST_ENABLED, true);

        final long generatedPreviewResetInterval = DeviceConfig.getLong(NAMESPACE_SYSTEMUI,
                SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_RESET_INTERVAL_MS,
                DEFAULT_GENERATED_PREVIEW_RESET_INTERVAL_MS);
        final int generatedPreviewMaxCallsPerInterval = DeviceConfig.getInt(NAMESPACE_SYSTEMUI,
                SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_RESET_INTERVAL_MS,
                DEFAULT_GENERATED_PREVIEW_MAX_CALLS_PER_INTERVAL);
        mGeneratedPreviewsApiCounter = new ApiCounter(generatedPreviewResetInterval,
                generatedPreviewMaxCallsPerInterval);
        DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI,
                new HandlerExecutor(mCallbackHandler), this::handleSystemUiDeviceConfigChange);

        BroadcastOptions opts = BroadcastOptions.makeBasic();
        opts.setBackgroundActivityStartsAllowed(false);
        opts.setInteractive(true);
@@ -2480,6 +2504,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
    private void deleteProviderLocked(Provider provider) {
        deleteWidgetsLocked(provider, UserHandle.USER_ALL);
        mProviders.remove(provider);
        mGeneratedPreviewsApiCounter.remove(provider.id);

        // no need to send the DISABLE broadcast, since the receiver is gone anyway
        cancelBroadcastsLocked(provider);
@@ -4004,7 +4029,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
    }

    @Override
    public void setWidgetPreview(@NonNull ComponentName providerComponent,
    public boolean setWidgetPreview(@NonNull ComponentName providerComponent,
            @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
            @NonNull RemoteViews preview) {
        final int userId = UserHandle.getCallingUserId();
@@ -4026,8 +4051,12 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
                throw new IllegalArgumentException(
                        providerComponent + " is not a valid AppWidget provider");
            }
            if (mGeneratedPreviewsApiCounter.tryApiCall(providerId)) {
                provider.setGeneratedPreviewLocked(widgetCategories, preview);
                scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
                return true;
            }
            return false;
        }
    }

@@ -4068,6 +4097,26 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        }
    }

    private void handleSystemUiDeviceConfigChange(DeviceConfig.Properties properties) {
        Set<String> changed = properties.getKeyset();
        synchronized (mLock) {
            if (changed.contains(
                    SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_RESET_INTERVAL_MS)) {
                long resetIntervalMs = properties.getLong(
                        SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_RESET_INTERVAL_MS,
                        /* defaultValue= */ mGeneratedPreviewsApiCounter.getResetIntervalMs());
                mGeneratedPreviewsApiCounter.setResetIntervalMs(resetIntervalMs);
            }
            if (changed.contains(
                    SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_MAX_CALLS_PER_INTERVAL)) {
                int maxCallsPerInterval = properties.getInt(
                        SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_MAX_CALLS_PER_INTERVAL,
                        /* defaultValue= */ mGeneratedPreviewsApiCounter.getMaxCallsPerInterval());
                mGeneratedPreviewsApiCounter.setMaxCallsPerInterval(maxCallsPerInterval);
            }
        }
    }

    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;
@@ -4541,11 +4590,11 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        }
    }

    private static final class ProviderId {
    static final class ProviderId {
        final int uid;
        final ComponentName componentName;

        private ProviderId(int uid, ComponentName componentName) {
        ProviderId(int uid, ComponentName componentName) {
            this.uid = uid;
            this.componentName = componentName;
        }
@@ -4788,6 +4837,96 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
        }
    }

    /**
     * This class keeps track of API calls and implements rate limiting. One instance of this class
     * tracks calls from all providers for one API, or a group of APIs that should share the same
     * rate limit.
     */
    static final class ApiCounter {

        private static final class ApiCallRecord {
            // Number of times the API has been called for this provider.
            public int apiCallCount = 0;
            // The last time (from SystemClock.elapsedRealtime) the api call count was reset.
            public long lastResetTimeMs = 0;

            void reset(long nowMs) {
                apiCallCount = 0;
                lastResetTimeMs = nowMs;
            }
        }

        private final Map<ProviderId, ApiCallRecord> mCallCount = new ArrayMap<>();
        // The interval at which the call count is reset.
        private long mResetIntervalMs;
        // The max number of API calls per interval.
        private int mMaxCallsPerInterval;
        // Returns the current time (monotonic). By default this is SystemClock.elapsedRealtime.
        private LongSupplier mMonotonicClock;

        ApiCounter(long resetIntervalMs, int maxCallsPerInterval) {
            this(resetIntervalMs, maxCallsPerInterval, SystemClock::elapsedRealtime);
        }

        ApiCounter(long resetIntervalMs, int maxCallsPerInterval,
                LongSupplier monotonicClock) {
            mResetIntervalMs = resetIntervalMs;
            mMaxCallsPerInterval = maxCallsPerInterval;
            mMonotonicClock = monotonicClock;
        }

        public void setResetIntervalMs(long resetIntervalMs) {
            mResetIntervalMs = resetIntervalMs;
        }

        public long getResetIntervalMs() {
            return mResetIntervalMs;
        }

        public void setMaxCallsPerInterval(int maxCallsPerInterval) {
            mMaxCallsPerInterval = maxCallsPerInterval;
        }

        public int getMaxCallsPerInterval() {
            return mMaxCallsPerInterval;
        }

        /**
         * Returns true if the API call for the provider should be allowed, false if it should be
         * rate-limited.
         */
        public boolean tryApiCall(@NonNull ProviderId provider) {
            final ApiCallRecord record = getOrCreateRecord(provider);
            final long now = mMonotonicClock.getAsLong();
            final long timeSinceLastResetMs = now - record.lastResetTimeMs;
            // If the last reset was beyond the reset interval, reset now.
            if (timeSinceLastResetMs > mResetIntervalMs) {
                record.reset(now);
            }
            if (record.apiCallCount < mMaxCallsPerInterval) {
                record.apiCallCount++;
                return true;
            }
            return false;
        }

        /**
         * Remove the provider's call record from this counter, when the provider is no longer
         * tracked.
         */
        public void remove(@NonNull ProviderId id) {
            mCallCount.remove(id);
        }

        @NonNull
        private ApiCallRecord getOrCreateRecord(@NonNull ProviderId provider) {
            if (!mCallCount.containsKey(provider)) {
                mCallCount.put(provider, new ApiCallRecord());
            }
            return mCallCount.get(provider);
        }
    }

    private class LoadedWidgetState {
        final Widget widget;
        final int hostTag;
Loading