Loading core/api/current.txt +1 −1 Original line number Diff line number Diff line Loading @@ -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); core/java/android/appwidget/AppWidgetManager.java +4 −2 Original line number Diff line number Diff line Loading @@ -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(); } Loading core/java/com/android/internal/appwidget/IAppWidgetService.aidl +1 −2 Original line number Diff line number Diff line Loading @@ -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); } core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +13 −0 Original line number Diff line number Diff line Loading @@ -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() { } } services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +144 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading @@ -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 { Loading Loading @@ -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) { Loading Loading @@ -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; } Loading Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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(); Loading @@ -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; } } Loading Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -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 Loading
core/api/current.txt +1 −1 Original line number Diff line number Diff line Loading @@ -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);
core/java/android/appwidget/AppWidgetManager.java +4 −2 Original line number Diff line number Diff line Loading @@ -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(); } Loading
core/java/com/android/internal/appwidget/IAppWidgetService.aidl +1 −2 Original line number Diff line number Diff line Loading @@ -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); }
core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +13 −0 Original line number Diff line number Diff line Loading @@ -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() { } }
services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +144 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading @@ -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 { Loading Loading @@ -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) { Loading Loading @@ -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; } Loading Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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(); Loading @@ -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; } } Loading Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -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