OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.omaha; |
| 6 |
| 7 import android.app.AlarmManager; |
| 8 import android.app.PendingIntent; |
| 9 import android.content.Context; |
| 10 import android.content.Intent; |
| 11 import android.content.SharedPreferences; |
| 12 import android.util.Log; |
| 13 |
| 14 import org.chromium.base.VisibleForTesting; |
| 15 |
| 16 import java.util.Date; |
| 17 import java.util.Random; |
| 18 |
| 19 import javax.annotation.concurrent.NotThreadSafe; |
| 20 |
| 21 /** |
| 22 * Manages a timer that implements exponential backoff for failed attempts. |
| 23 * |
| 24 * The first timer will fire after BASE_MILLISECONDS. On a failure, the timer i
s changed to |
| 25 * (randomInteger[0, 2^failures) + 1) * BASE_MILLISECONDS. MAX_MILLISECONDS is
used to ensure that |
| 26 * you aren't waiting years for a timer to fire. |
| 27 * |
| 28 * The state is stored in shared preferences to ensure that they are kept after
the device sleeps. |
| 29 * Because multiple ExponentialBackoffSchedulers can be used by different compon
ents, |
| 30 * the owning class must set the preference name. |
| 31 * |
| 32 * Timestamps are recorded in RTC to avoid situations where the phone is reboote
d, messing up |
| 33 * any timestamps generated using elapsedRealtime(). |
| 34 * |
| 35 * This class is not thread-safe because any two different classes could be acce
ssing the same |
| 36 * SharedPreferences. |
| 37 * |
| 38 * TODO(dfalcantara): Consider making this an AlarmManagerHelper class to manage
general alarms. |
| 39 */ |
| 40 @NotThreadSafe |
| 41 public class ExponentialBackoffScheduler { |
| 42 private static final String TAG = "ExponentialBackoffScheduler"; |
| 43 |
| 44 private static final String PREFERENCE_DELAY = "delay"; |
| 45 private static final String PREFERENCE_FAILED_ATTEMPTS = "backoffFailedAttem
pts"; |
| 46 |
| 47 private static Random sRandom = new Random(); |
| 48 |
| 49 private static final int MAX_EXPONENT = 10; |
| 50 |
| 51 private final long mBaseMilliseconds; |
| 52 private final long mMaxMilliseconds; |
| 53 private final Context mContext; |
| 54 private final String mPreferencePackage; |
| 55 |
| 56 /** |
| 57 * Creates a new scheduler. |
| 58 * @param packageName The name under which to store its state in SharedPrefe
rences. |
| 59 * @param context The application's context. |
| 60 * @param baseMilliseconds Used to calculate random backoff times. |
| 61 * @param maxMilliseconds The absolute maximum delay allowed. |
| 62 */ |
| 63 public ExponentialBackoffScheduler(String packageName, Context context, long
baseMilliseconds, |
| 64 long maxMilliseconds) { |
| 65 mPreferencePackage = packageName; |
| 66 mContext = context; |
| 67 mBaseMilliseconds = baseMilliseconds; |
| 68 mMaxMilliseconds = maxMilliseconds; |
| 69 } |
| 70 |
| 71 /** |
| 72 * Creates an alarm to fire the specified intent after a random delay. |
| 73 * @param intent The intent to fire. |
| 74 * @return the timestamp of the scheduled intent |
| 75 */ |
| 76 public long createAlarm(Intent intent) { |
| 77 long delay = generateRandomDelay(); |
| 78 long timestamp = delay + getCurrentTime(); |
| 79 return createAlarm(intent, timestamp); |
| 80 } |
| 81 |
| 82 /** |
| 83 * Creates an alarm to fire the specified intent at the specified time. |
| 84 * @param intent The intent to fire. |
| 85 * @return the timestamp of the scheduled intent |
| 86 */ |
| 87 public long createAlarm(Intent intent, long timestamp) { |
| 88 PendingIntent retryPIntent = PendingIntent.getService(mContext, 0, inten
t, 0); |
| 89 AlarmManager am = (AlarmManager) mContext.getSystemService(Context.ALARM
_SERVICE); |
| 90 setAlarm(am, timestamp, retryPIntent); |
| 91 return timestamp; |
| 92 } |
| 93 |
| 94 /** |
| 95 * Attempts to cancel any alarms set using the given Intent. |
| 96 * @param scheduledIntent Intent that may have been previously scheduled. |
| 97 * @return whether or not an alarm was canceled. |
| 98 */ |
| 99 public boolean cancelAlarm(Intent scheduledIntent) { |
| 100 PendingIntent pendingIntent = PendingIntent.getService(mContext, 0, sche
duledIntent, |
| 101 PendingIntent.FLAG_NO_CREATE); |
| 102 if (pendingIntent != null) { |
| 103 AlarmManager am = (AlarmManager) mContext.getSystemService(Context.A
LARM_SERVICE); |
| 104 am.cancel(pendingIntent); |
| 105 pendingIntent.cancel(); |
| 106 return true; |
| 107 } else { |
| 108 return false; |
| 109 } |
| 110 } |
| 111 |
| 112 public int getNumFailedAttempts() { |
| 113 SharedPreferences preferences = getSharedPreferences(); |
| 114 return preferences.getInt(PREFERENCE_FAILED_ATTEMPTS, 0); |
| 115 } |
| 116 |
| 117 public void increaseFailedAttempts() { |
| 118 SharedPreferences preferences = getSharedPreferences(); |
| 119 int numFailedAttempts = getNumFailedAttempts() + 1; |
| 120 preferences.edit() |
| 121 .putInt(PREFERENCE_FAILED_ATTEMPTS, numFailedAttempts) |
| 122 .apply(); |
| 123 } |
| 124 |
| 125 public void resetFailedAttempts() { |
| 126 SharedPreferences preferences = getSharedPreferences(); |
| 127 preferences.edit() |
| 128 .putInt(PREFERENCE_FAILED_ATTEMPTS, 0) |
| 129 .apply(); |
| 130 } |
| 131 |
| 132 /** |
| 133 * Returns a timestamp representing now, according to the backoff scheduler. |
| 134 */ |
| 135 public long getCurrentTime() { |
| 136 return System.currentTimeMillis(); |
| 137 } |
| 138 |
| 139 /** |
| 140 * Returns the delay used to generate the last alarm. If no previous alarm
was generated, |
| 141 * return the base delay. |
| 142 */ |
| 143 public long getGeneratedDelay() { |
| 144 SharedPreferences preferences = getSharedPreferences(); |
| 145 return preferences.getLong(PREFERENCE_DELAY, mBaseMilliseconds); |
| 146 } |
| 147 |
| 148 /** |
| 149 * Sets an alarm in the alarm manager. |
| 150 */ |
| 151 @VisibleForTesting |
| 152 protected void setAlarm(AlarmManager am, long timestamp, PendingIntent retry
PIntent) { |
| 153 Log.v(TAG, "now(" + new Date(getCurrentTime()) + ") refiringAt(" |
| 154 + new Date(timestamp) + ")"); |
| 155 am.set(AlarmManager.RTC, timestamp, retryPIntent); |
| 156 } |
| 157 |
| 158 /** |
| 159 * Determines the amount of time to wait for the current delay, then saves i
t. |
| 160 * @return the number of milliseconds to wait. |
| 161 */ |
| 162 private long generateRandomDelay() { |
| 163 long delay; |
| 164 int numFailedAttempts = getNumFailedAttempts(); |
| 165 if (numFailedAttempts == 0) { |
| 166 delay = Math.min(mBaseMilliseconds, mMaxMilliseconds); |
| 167 } else { |
| 168 int backoffCoefficient = computeConstrainedBackoffCoefficient(numFai
ledAttempts); |
| 169 delay = Math.min(backoffCoefficient * mBaseMilliseconds, mMaxMillise
conds); |
| 170 } |
| 171 |
| 172 // Save the delay for sanity checks. |
| 173 SharedPreferences preferences = getSharedPreferences(); |
| 174 preferences.edit() |
| 175 .putLong(PREFERENCE_DELAY, delay) |
| 176 .apply(); |
| 177 return delay; |
| 178 } |
| 179 |
| 180 /** |
| 181 * Calculates a random coefficient based on the number of cumulative failed
attempts. |
| 182 * @param numFailedAttempts Number of cumulative failed attempts |
| 183 * @return A random number between 1 and 2^N, where N is the smallest value
of MAX_EXPONENT and |
| 184 * numFailedAttempts |
| 185 */ |
| 186 private int computeConstrainedBackoffCoefficient(int numFailedAttempts) { |
| 187 int n = Math.min(MAX_EXPONENT, numFailedAttempts); |
| 188 int twoToThePowerOfN = 1 << n; |
| 189 return sRandom.nextInt(twoToThePowerOfN) + 1; |
| 190 } |
| 191 |
| 192 private SharedPreferences getSharedPreferences() { |
| 193 SharedPreferences preferences = |
| 194 mContext.getSharedPreferences(mPreferencePackage, Context.MODE_P
RIVATE); |
| 195 return preferences; |
| 196 } |
| 197 } |
OLD | NEW |