// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.omaha;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import java.util.Date;
import java.util.Random;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Manages a timer that implements exponential backoff for failed attempts.
*
* The first timer will fire after BASE_MILLISECONDS. On a failure, the timer is changed to
* (randomInteger[0, 2^failures) + 1) * BASE_MILLISECONDS. MAX_MILLISECONDS is used to ensure that
* you aren't waiting years for a timer to fire.
*
* The state is stored in shared preferences to ensure that they are kept after the device sleeps.
* Because multiple ExponentialBackoffSchedulers can be used by different components,
* the owning class must set the preference name.
*
* Timestamps are recorded in RTC to avoid situations where the phone is rebooted, messing up
* any timestamps generated using elapsedRealtime().
*
* This class is not thread-safe because any two different classes could be accessing the same
* SharedPreferences.
*
* TODO(dfalcantara): Consider making this an AlarmManagerHelper class to manage general alarms.
*/
@NotThreadSafe
public class ExponentialBackoffScheduler {
private static final String TAG = "omaha";
private static final String PREFERENCE_DELAY = "delay";
private static final String PREFERENCE_FAILED_ATTEMPTS = "backoffFailedAttempts";
private static Random sRandom = new Random();
private static final int MAX_EXPONENT = 10;
private final long mBaseMilliseconds;
private final long mMaxMilliseconds;
private final Context mContext;
private final String mPreferencePackage;
/**
* Creates a new scheduler.
* @param packageName The name under which to store its state in SharedPreferences.
* @param context The application's context.
* @param baseMilliseconds Used to calculate random backoff times.
* @param maxMilliseconds The absolute maximum delay allowed.
*/
public ExponentialBackoffScheduler(String packageName, Context context, long baseMilliseconds,
long maxMilliseconds) {
mPreferencePackage = packageName;
mContext = context;
mBaseMilliseconds = baseMilliseconds;
mMaxMilliseconds = maxMilliseconds;
}
/**
* Creates an alarm to fire the specified intent after a random delay.
* @param intent The intent to fire.
* @return the timestamp of the scheduled intent
*/
public long createAlarm(Intent intent) {
long delay = generateRandomDelay();
long timestamp = delay + getCurrentTime();
return createAlarm(intent, timestamp);
}
/**
* Creates an alarm to fire the specified intent at the specified time.
* @param intent The intent to fire.
* @return the timestamp of the scheduled intent
*/
public long createAlarm(Intent intent, long timestamp) {
PendingIntent retryPIntent = PendingIntent.getService(mContext, 0, intent, 0);
AlarmManager am = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
setAlarm(am, timestamp, retryPIntent);
return timestamp;
}
/**
* Attempts to cancel any alarms set using the given Intent.
* @param scheduledIntent Intent that may have been previously scheduled.
* @return whether or not an alarm was canceled.
*/
public boolean cancelAlarm(Intent scheduledIntent) {
PendingIntent pendingIntent = PendingIntent.getService(mContext, 0, scheduledIntent,
PendingIntent.FLAG_NO_CREATE);
if (pendingIntent != null) {
AlarmManager am = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
am.cancel(pendingIntent);
pendingIntent.cancel();
return true;
} else {
return false;
}
}
public int getNumFailedAttempts() {
SharedPreferences preferences = getSharedPreferences();
return preferences.getInt(PREFERENCE_FAILED_ATTEMPTS, 0);
}
public void increaseFailedAttempts() {
SharedPreferences preferences = getSharedPreferences();
int numFailedAttempts = getNumFailedAttempts() + 1;
preferences.edit()
.putInt(PREFERENCE_FAILED_ATTEMPTS, numFailedAttempts)
.apply();
}
public void resetFailedAttempts() {
SharedPreferences preferences = getSharedPreferences();
preferences.edit()
.putInt(PREFERENCE_FAILED_ATTEMPTS, 0)
.apply();
}
/**
* Returns a timestamp representing now, according to the backoff scheduler.
*/
public long getCurrentTime() {
return System.currentTimeMillis();
}
/**
* Returns the delay used to generate the last alarm. If no previous alarm was generated,
* return the base delay.
*/
public long getGeneratedDelay() {
SharedPreferences preferences = getSharedPreferences();
return preferences.getLong(PREFERENCE_DELAY, mBaseMilliseconds);
}
/**
* Sets an alarm in the alarm manager.
*/
@VisibleForTesting
protected void setAlarm(AlarmManager am, long timestamp, PendingIntent retryPIntent) {
Log.d(TAG, "now(" + new Date(getCurrentTime()) + ") refiringAt("
+ new Date(timestamp) + ")");
try {
am.set(AlarmManager.RTC, timestamp, retryPIntent);
} catch (SecurityException e) {
Log.e(TAG, "Failed to set backoff alarm.");
}
}
/**
* Determines the amount of time to wait for the current delay, then saves it.
* @return the number of milliseconds to wait.
*/
private long generateRandomDelay() {
long delay;
int numFailedAttempts = getNumFailedAttempts();
if (numFailedAttempts == 0) {
delay = Math.min(mBaseMilliseconds, mMaxMilliseconds);
} else {
int backoffCoefficient = computeConstrainedBackoffCoefficient(numFailedAttempts);
delay = Math.min(backoffCoefficient * mBaseMilliseconds, mMaxMilliseconds);
}
// Save the delay for sanity checks.
SharedPreferences preferences = getSharedPreferences();
preferences.edit()
.putLong(PREFERENCE_DELAY, delay)
.apply();
return delay;
}
/**
* Calculates a random coefficient based on the number of cumulative failed attempts.
* @param numFailedAttempts Number of cumulative failed attempts
* @return A random number between 1 and 2^N, where N is the smallest value of MAX_EXPONENT and
* numFailedAttempts
*/
private int computeConstrainedBackoffCoefficient(int numFailedAttempts) {
int n = Math.min(MAX_EXPONENT, numFailedAttempts);
int twoToThePowerOfN = 1 << n;
return sRandom.nextInt(twoToThePowerOfN) + 1;
}
private SharedPreferences getSharedPreferences() {
SharedPreferences preferences =
mContext.getSharedPreferences(mPreferencePackage, Context.MODE_PRIVATE);
return preferences;
}
}