chromium/chrome/browser/omaha/android/java/src/org/chromium/chrome/browser/omaha/ExponentialBackoffScheduler.java

// Copyright 2015 The Chromium Authors
// 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.content.Context;
import android.content.SharedPreferences;

import org.chromium.base.ContextUtils;
import org.chromium.base.TimeUtils;

import java.util.Random;

import javax.annotation.concurrent.NotThreadSafe;

/**
 * Manages a timer that implements exponential backoff for failed attempts.
 *
 * <p>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.
 *
 * <p>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.
 *
 * <p>Timestamps are recorded in RTC to avoid situations where the phone is rebooted, messing up any
 * timestamps generated using elapsedRealtime().
 *
 * <p>This class is not thread-safe because any two different classes could be accessing the same
 * SharedPreferences.
 */
@NotThreadSafe
public class ExponentialBackoffScheduler {
    // TODO(crbug.com/40150188): remove and use OmahaBase.TAG when OmahaBase.java is modularized.
    // Used in various org.chromium.chrome.browser.omaha files.
    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 String mPreferencePackage;

    /**
     * Creates a new scheduler.
     * @param packageName The name under which to store its state in SharedPreferences.
     * @param baseMilliseconds Used to calculate random backoff times.
     * @param maxMilliseconds The absolute maximum delay allowed.
     */
    public ExponentialBackoffScheduler(
            String packageName, long baseMilliseconds, long maxMilliseconds) {
        mPreferencePackage = packageName;
        mBaseMilliseconds = baseMilliseconds;
        mMaxMilliseconds = maxMilliseconds;
    }

    /** Calculates when the next event should occur, including delays due to failures. */
    public long calculateNextTimestamp() {
        return generateRandomDelay() + getCurrentTime();
    }

    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 TimeUtils.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);
    }

    /**
     * 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 validation 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 =
                ContextUtils.getApplicationContext()
                        .getSharedPreferences(mPreferencePackage, Context.MODE_PRIVATE);
        return preferences;
    }
}