Index: chrome/android/java_staging/src/org/chromium/chrome/browser/omaha/ExponentialBackoffScheduler.java |
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/omaha/ExponentialBackoffScheduler.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/omaha/ExponentialBackoffScheduler.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..d36d8760dbfdd7c361efb02ab05de37a2ebaff9e |
--- /dev/null |
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/omaha/ExponentialBackoffScheduler.java |
@@ -0,0 +1,197 @@ |
+// 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 android.util.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 = "ExponentialBackoffScheduler"; |
+ |
+ 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.v(TAG, "now(" + new Date(getCurrentTime()) + ") refiringAt(" |
+ + new Date(timestamp) + ")"); |
+ am.set(AlarmManager.RTC, timestamp, retryPIntent); |
+ } |
+ |
+ /** |
+ * 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; |
+ } |
+} |