blob: 337ae5a9fb17ef808a3585dca80935d0bc48f0a1 [file] [log] [blame]
/*
* Copyright (C) 2018 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 androidx.car.moderator;
import static java.util.concurrent.TimeUnit.SECONDS;
import androidx.annotation.MainThread;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Preconditions;
import android.util.Log;
/**
* A class that keeps track of a general number of permitted actions that happen over time and
* determines if a subsequent interaction is allowed. The type of interaction is arbitrary and not
* transparent to this class. Instead, it will refer to these actions as "permits," short for
* "permitted action." It is up to a user of this class to determine the unit of permits.
*
* <p>This class allows for two quick acquires in succession to only consume one permit. This is
* intended behavior to account for the fact that the user can be using many taps to scroll
* quickly. This can fit within the window for which a user does not necessary have their eyes
* off the road for a long period of time, and thus should not be penalized.
*
* <p>This class allows for the maximum number of permits that can be stored,the amount of permits
* that are filled each second, as well as the delay before re-fill to be configured.
*/
public class ContentRateLimiter {
private static final String TAG = "ContentRateLimiter";
/** The maximum number of stored permits. */
private final float mMaxStoredPermits;
/**
* The interval between two unit requests at our stable rate. For example, a stable rate of
* 5 permits per second has a stable interval of 200ms.
*/
private final long mStableIntervalMs;
/**
* The amount of time to wait between when a permit is acquired and when the model starts
* refilling.
*/
private final long mFillDelayMs;
/** Unlimited mode. Once enabled, any number of permits can be acquired and consumed. */
private boolean mUnlimitedModeEnabled;
/**
* Used to do incremental calculations by {@link #getLastCalculatedPermitCount()}, cannot be
* used directly.
*/
private float mLastCalculatedPermitCount;
/** Time in milliseconds when permits can resume incrementing. */
private long mResumeIncrementingMs;
/** Tracks if the model will allow a second permit to be requested in the fill delay. */
private boolean mSecondaryFillDelayPermitAvailable = true;
private final ElapsedTimeProvider mElapsedTimeProvider;
/**
* An interface for a provider of the current time that has passed since boot. This interface
* is meant to abstract the {@link android.os.SystemClock} so that it can be mocked during
* testing.
*/
interface ElapsedTimeProvider {
/** Returns milliseconds since boot, including time spent in sleep. */
long getElapsedRealtime();
}
/**
* Creates a {@code ContentRateLimiter} with the given parameters.
*
* @param acquiredPermitsPerSecond The amount of permits that are acquired each second.
* @param maxStoredPermits The maximum number of permits that can be stored.
* @param fillDelayMs The amount of time to wait between when a permit is acquired and when
* the number of available permits start refilling.
*/
public ContentRateLimiter(float acquiredPermitsPerSecond, float maxStoredPermits,
long fillDelayMs) {
this(acquiredPermitsPerSecond, maxStoredPermits, fillDelayMs,
new SystemClockTimeProvider());
}
// A constructor that allows for the SystemClockTimeProvider to be provided. This is needed for
// testing so that the unit test does not rely on the actual SystemClock.
@VisibleForTesting
ContentRateLimiter(float acquiredPermitsPerSecond, float maxStoredPermits,
long fillDelayMs, ElapsedTimeProvider elapsedTimeProvider) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, String.format("permitsPerSecond: %f maxStoredPermits: %f, fillDelayMs %d",
acquiredPermitsPerSecond, maxStoredPermits, fillDelayMs));
}
Preconditions.checkArgument(acquiredPermitsPerSecond > 0);
Preconditions.checkArgument(maxStoredPermits >= 0);
Preconditions.checkArgument(fillDelayMs >= 0);
mStableIntervalMs = (long) (SECONDS.toMillis(1L) / acquiredPermitsPerSecond);
mMaxStoredPermits = maxStoredPermits;
mLastCalculatedPermitCount = maxStoredPermits;
mFillDelayMs = fillDelayMs;
mElapsedTimeProvider = elapsedTimeProvider;
mResumeIncrementingMs = mElapsedTimeProvider.getElapsedRealtime();
}
/** Gets the current number of stored permits ready to be used. */
@MainThread
public float getAvailablePermits() {
return getLastCalculatedPermitCount();
}
/**
* Sets the current number of stored permits that are ready to be used. If this value exceeds
* the maximum number of stored permits that is passed to the constructor, then the max value
* is used instead.
*/
@MainThread
public void setAvailablePermits(float availablePermits) {
setLastCalculatedPermitCount(availablePermits, mElapsedTimeProvider.getElapsedRealtime());
}
/** Gets the max number of permits allowed to be stored for future usage. */
public float getMaxStoredPermits() {
return mMaxStoredPermits;
}
/**
* Checks if there are enough available permits for a single permit to be acquired.
*
* @return {@code true} if unlimited mode is enabled or enough permits are acquirable at the
* time of this call; {@code false} if there isn't the number of permits requested available
* currently.
*/
@MainThread
public boolean tryAcquire() {
return tryAcquire(1);
}
/**
* Checks whether there are enough available permits to acquire.
*
* @return {@code true} if unlimited mode is enabled or enough permits are acquirable at the
* time of this call; {@code false} if there isn't the number of permits requested available
* currently.
*/
@MainThread
public boolean tryAcquire(int permits) {
// Once unlimited mode is enabled, we can acquire any number of permits we want and don't
// consume the stored permits.
if (mUnlimitedModeEnabled) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unlimited mode is enabled.");
}
return true;
}
float availablePermits = getLastCalculatedPermitCount();
long nowMs = mElapsedTimeProvider.getElapsedRealtime();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, String.format("Requesting: %d, Stored: %f/%f", permits,
mLastCalculatedPermitCount, mMaxStoredPermits));
}
if (availablePermits <= permits) {
// Once locked out, the user is prevented from acquiring any more permits until they
// have waited long enough for a permit to refill. If the user attempts to acquire a
// permit during this time, the countdown timer until a permit is refilled is reset.
setLastCalculatedPermitCount(0, nowMs + mFillDelayMs);
return false;
} else if (nowMs < mResumeIncrementingMs && mSecondaryFillDelayPermitAvailable) {
// If a second permit is requested between the time a first permit was requested and
// the fill delay, allow the second permit to be acquired without decrementing the model
// and set the point where permits can resume incrementing {@link #mFillDelayMs} in the
// future.
setLastCalculatedPermitCount(availablePermits, nowMs + mFillDelayMs);
// Don't allow a third "free" permit to be acquired in the fill delay fringe.
mSecondaryFillDelayPermitAvailable = false;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Used up free secondary permit");
}
return true;
} else {
// Decrement the available permits, and set the point where permits can resume
// incrementing {@link #mFillDelayMs} in the future.
setLastCalculatedPermitCount(availablePermits - permits, nowMs + mFillDelayMs);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, String.format("permits remaining %s, secondary permit available %s",
mLastCalculatedPermitCount,
mSecondaryFillDelayPermitAvailable));
}
mSecondaryFillDelayPermitAvailable = true;
return true;
}
}
/**
* Sets unlimited mode. If enabled, there is no restriction on the number of permits that
* can be acquired and any interaction does not consume stored permits.
*/
public void setUnlimitedMode(boolean enabled) {
mUnlimitedModeEnabled = enabled;
}
/**
* Updates {@link #mLastCalculatedPermitCount} and {@link #mResumeIncrementingMs} based on the
* current time.
*/
private float getLastCalculatedPermitCount() {
long nowMs = mElapsedTimeProvider.getElapsedRealtime();
if (nowMs > mResumeIncrementingMs) {
long deltaMs = nowMs - mResumeIncrementingMs;
float newPermits = deltaMs / (float) mStableIntervalMs;
setLastCalculatedPermitCount(mLastCalculatedPermitCount + newPermits, nowMs);
}
return mLastCalculatedPermitCount;
}
private void setLastCalculatedPermitCount(float newCount, long nextMs) {
mLastCalculatedPermitCount = Math.min(mMaxStoredPermits, newCount);
mResumeIncrementingMs = nextMs;
}
}