@Override
  public void onReceive(Context context, Intent intent) {
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    long lastPrecacheTimeMs = prefs.getLong(PREF_PRECACHE_LAST_TIME, 0L);
    if (lastPrecacheTimeMs > getElapsedRealtimeOnSystem()) {
      // System.elapsedRealtime() counts milliseconds since boot, so if the device has been
      // rebooted since the last time precaching was performed, reset lastPrecacheTimeMs to 0.
      lastPrecacheTimeMs = 0L;
    }

    // Do nothing if precaching is disabled.
    if (!isPrecachingEnabled(context)) return;

    boolean isPowerConnected = mDeviceState.isPowerConnected(context);
    boolean isWifiAvailable = mDeviceState.isWifiAvailable(context);
    boolean isInteractive = mDeviceState.isInteractive(context);
    boolean areConditionsGoodForPrecaching = isPowerConnected && isWifiAvailable && !isInteractive;
    long timeSinceLastPrecacheMs = getElapsedRealtimeOnSystem() - lastPrecacheTimeMs;
    boolean hasEnoughTimePassedSinceLastPrecache =
        timeSinceLastPrecacheMs >= WAIT_UNTIL_NEXT_PRECACHE_MS;

    // Only start precaching when an alarm action is received. This is to prevent situations
    // such as power being connected, precaching starting, then precaching being immediately
    // canceled because the screen turns on in response to power being connected.
    if (ACTION_ALARM.equals(intent.getAction())
        && areConditionsGoodForPrecaching
        && hasEnoughTimePassedSinceLastPrecache) {
      // Store a pref indicating that precaching is starting now.
      Editor editor = prefs.edit();
      editor.putLong(PREF_PRECACHE_LAST_TIME, getElapsedRealtimeOnSystem());
      editor.apply();

      setAlarm(context, Math.max(INTERACTIVE_STATE_POLLING_PERIOD_MS, WAIT_UNTIL_NEXT_PRECACHE_MS));

      acquireWakeLockAndStartService(context);
    } else {
      if (isPowerConnected && isWifiAvailable) {
        // If we're just waiting for non-interactivity (e.g., the screen to be off), or for
        // enough time to pass after Wi-Fi or power has been connected, then set an alarm
        // for the next time to check the device state. We can't receive SCREEN_ON/OFF
        // intents as is done for detecting changes in power and connectivity, because
        // SCREEN_ON/OFF intents are only delivered to BroadcastReceivers that are
        // registered dynamically in code, but the PrecacheServiceLauncher is registered in
        // the Android manifest.
        setAlarm(
            context,
            Math.max(
                INTERACTIVE_STATE_POLLING_PERIOD_MS,
                WAIT_UNTIL_NEXT_PRECACHE_MS - timeSinceLastPrecacheMs));
      } else {
        // If the device doesn't have connected power or doesn't have Wi-Fi, then there's no
        // point in setting an alarm.
        cancelAlarm(context);
      }
    }
  }
/**
 * BroadcastReceiver that determines when conditions are right for precaching, and starts the {@link
 * PrecacheService} if they are. Conditions are right for precaching when the device is connected to
 * power, Wi-Fi, interactivity (e.g., the screen) is off, and at least |WAIT_UNTIL_NEXT_PRECACHE_MS|
 * have passed since the last time precaching was done.
 */
public class PrecacheServiceLauncher extends BroadcastReceiver {
  private static final String TAG = "cr.Precache";

  @VisibleForTesting
  static final String PREF_IS_PRECACHING_ENABLED = "precache.is_precaching_enabled";

  @VisibleForTesting static final String PREF_PRECACHE_LAST_TIME = "precache.last_time";

  @VisibleForTesting
  static final String ACTION_ALARM =
      "org.chromium.chrome.browser.precache.PrecacheServiceLauncher.ALARM";

  private static final int INTERACTIVE_STATE_POLLING_PERIOD_MS = 15 * 60 * 1000; // 15 minutes.
  static final int WAIT_UNTIL_NEXT_PRECACHE_MS = 4 * 60 * 60 * 1000; // 4 hours.

  private static WakeLock sWakeLock = null;

  private DeviceState mDeviceState = DeviceState.getInstance();

  @VisibleForTesting
  void setDeviceState(DeviceState deviceState) {
    mDeviceState = deviceState;
  }

  /**
   * Set whether or not precaching is enabled. If precaching is enabled, this receiver will start
   * the PrecacheService when it receives an intent. If precaching is disabled, any running
   * PrecacheService will be stopped, and this receiver will do nothing when it receives an intent.
   *
   * @param context The Context to use.
   * @param enabled Whether or not precaching is enabled.
   */
  public static void setIsPrecachingEnabled(Context context, boolean enabled) {
    Log.v(TAG, "setIsPrecachingEnabled(%s)", enabled);
    Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
    editor.putBoolean(PREF_IS_PRECACHING_ENABLED, enabled);
    editor.apply();

    if (!enabled) {
      // Stop any running PrecacheService. If PrecacheService is not running, then this does
      // nothing.
      Intent serviceIntent = new Intent(null, null, context, PrecacheService.class);
      context.stopService(serviceIntent);
    }
  }

  @VisibleForTesting
  static boolean isPrecachingEnabled(Context context) {
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    return prefs.getBoolean(PREF_IS_PRECACHING_ENABLED, false);
  }

  @Override
  public void onReceive(Context context, Intent intent) {
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    long lastPrecacheTimeMs = prefs.getLong(PREF_PRECACHE_LAST_TIME, 0L);
    if (lastPrecacheTimeMs > getElapsedRealtimeOnSystem()) {
      // System.elapsedRealtime() counts milliseconds since boot, so if the device has been
      // rebooted since the last time precaching was performed, reset lastPrecacheTimeMs to 0.
      lastPrecacheTimeMs = 0L;
    }

    // Do nothing if precaching is disabled.
    if (!isPrecachingEnabled(context)) return;

    boolean isPowerConnected = mDeviceState.isPowerConnected(context);
    boolean isWifiAvailable = mDeviceState.isWifiAvailable(context);
    boolean isInteractive = mDeviceState.isInteractive(context);
    boolean areConditionsGoodForPrecaching = isPowerConnected && isWifiAvailable && !isInteractive;
    long timeSinceLastPrecacheMs = getElapsedRealtimeOnSystem() - lastPrecacheTimeMs;
    boolean hasEnoughTimePassedSinceLastPrecache =
        timeSinceLastPrecacheMs >= WAIT_UNTIL_NEXT_PRECACHE_MS;

    // Only start precaching when an alarm action is received. This is to prevent situations
    // such as power being connected, precaching starting, then precaching being immediately
    // canceled because the screen turns on in response to power being connected.
    if (ACTION_ALARM.equals(intent.getAction())
        && areConditionsGoodForPrecaching
        && hasEnoughTimePassedSinceLastPrecache) {
      // Store a pref indicating that precaching is starting now.
      Editor editor = prefs.edit();
      editor.putLong(PREF_PRECACHE_LAST_TIME, getElapsedRealtimeOnSystem());
      editor.apply();

      setAlarm(context, Math.max(INTERACTIVE_STATE_POLLING_PERIOD_MS, WAIT_UNTIL_NEXT_PRECACHE_MS));

      acquireWakeLockAndStartService(context);
    } else {
      if (isPowerConnected && isWifiAvailable) {
        // If we're just waiting for non-interactivity (e.g., the screen to be off), or for
        // enough time to pass after Wi-Fi or power has been connected, then set an alarm
        // for the next time to check the device state. We can't receive SCREEN_ON/OFF
        // intents as is done for detecting changes in power and connectivity, because
        // SCREEN_ON/OFF intents are only delivered to BroadcastReceivers that are
        // registered dynamically in code, but the PrecacheServiceLauncher is registered in
        // the Android manifest.
        setAlarm(
            context,
            Math.max(
                INTERACTIVE_STATE_POLLING_PERIOD_MS,
                WAIT_UNTIL_NEXT_PRECACHE_MS - timeSinceLastPrecacheMs));
      } else {
        // If the device doesn't have connected power or doesn't have Wi-Fi, then there's no
        // point in setting an alarm.
        cancelAlarm(context);
      }
    }
  }

  /** Release the wakelock if it is held. */
  @VisibleForTesting
  protected static void releaseWakeLock() {
    ThreadUtils.assertOnUiThread();
    if (sWakeLock != null && sWakeLock.isHeld()) sWakeLock.release();
  }

  /**
   * Acquire the wakelock and start the PrecacheService.
   *
   * @param context The Context to use to start the service.
   */
  private void acquireWakeLockAndStartService(Context context) {
    acquireWakeLock(context);
    startPrecacheService(context);
  }

  @VisibleForTesting
  protected void startPrecacheService(Context context) {
    Intent serviceIntent =
        new Intent(PrecacheService.ACTION_START_PRECACHE, null, context, PrecacheService.class);
    context.startService(serviceIntent);
  }

  @VisibleForTesting
  protected void acquireWakeLock(Context context) {
    ThreadUtils.assertOnUiThread();
    if (sWakeLock == null) {
      PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
      sWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    }
    sWakeLock.acquire();
  }

  /**
   * Get a PendingIntent for setting an alarm to notify the PrecacheServiceLauncher.
   *
   * @param context The Context to use for the PendingIntent.
   * @return The PendingIntent.
   */
  private static PendingIntent getPendingAlarmIntent(Context context) {
    return PendingIntent.getBroadcast(
        context,
        0,
        new Intent(ACTION_ALARM, null, context, PrecacheServiceLauncher.class),
        PendingIntent.FLAG_UPDATE_CURRENT);
  }

  /**
   * Set an alarm to notify the PrecacheServiceLauncher in the future.
   *
   * @param context The Context to use.
   * @param delayMs Delay in milliseconds before the alarm goes off.
   */
  private void setAlarm(Context context, long delayMs) {
    setAlarmOnSystem(
        context,
        AlarmManager.ELAPSED_REALTIME_WAKEUP,
        getElapsedRealtimeOnSystem() + delayMs,
        getPendingAlarmIntent(context));
  }

  /**
   * Set the alarm on the system using the given parameters. This method can be overridden in tests.
   */
  @VisibleForTesting
  protected void setAlarmOnSystem(
      Context context, int type, long triggerAtMillis, PendingIntent operation) {
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    alarmManager.set(type, triggerAtMillis, operation);
  }

  /**
   * Cancel a previously set alarm, if there is one. This method can be overridden in tests.
   *
   * @param context The Context to use.
   */
  private void cancelAlarm(Context context) {
    cancelAlarmOnSystem(context, getPendingAlarmIntent(context));
  }

  /** Cancel a previously set alarm on the system. This method can be overridden in tests. */
  @VisibleForTesting
  protected void cancelAlarmOnSystem(Context context, PendingIntent operation) {
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    alarmManager.cancel(operation);
  }

  @VisibleForTesting
  protected long getElapsedRealtimeOnSystem() {
    return SystemClock.elapsedRealtime();
  }
}