// 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.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.os.Looper;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.ChromeVersionInfo;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.UUID;
/**
* Keeps tabs on the current state of Chrome, tracking if and when a request should be sent to the
* Omaha Server.
*
* A hook in ChromeActivity's doDeferredResume() initializes the service. Further attempts to
* reschedule events will be scheduled by the class itself.
*
* Each request to the server will perform an update check and ping the server.
* We use a repeating alarm to schedule the XML requests to be generated 5 hours apart.
* If Chrome isn't running when the alarm is fired, the request generation will be stalled until
* the next time Chrome runs.
*
* mevissen suggested being conservative with our timers for sending requests.
* POST attempts that fail to be acknowledged by the server are re-attempted, with at least
* one hour between each attempt.
*
* Status is saved directly to the the disk after every operation. Unit tests testing the code
* paths without using Intents may need to call restoreState() manually as it is not automatically
* handled in onCreate().
*
* Implementation notes:
* http://docs.google.com/a/google.com/document/d/1scTCovqASf5ktkOeVj8wFRkWTCeDYw2LrOBNn05CDB0/edit
*/
public class OmahaClient extends IntentService {
private static final String TAG = "omaha";
// Intent actions.
private static final String ACTION_INITIALIZE =
"org.chromium.chrome.browser.omaha.ACTION_INITIALIZE";
private static final String ACTION_REGISTER_REQUEST =
"org.chromium.chrome.browser.omaha.ACTION_REGISTER_REQUEST";
private static final String ACTION_POST_REQUEST =
"org.chromium.chrome.browser.omaha.ACTION_POST_REQUEST";
// Delays between events.
private static final long MS_PER_HOUR = 3600000;
private static final long MS_POST_BASE_DELAY = MS_PER_HOUR;
private static final long MS_POST_MAX_DELAY = 5 * MS_PER_HOUR;
private static final long MS_BETWEEN_REQUESTS = 5 * MS_PER_HOUR;
private static final int MS_CONNECTION_TIMEOUT = 60000;
// Flags for retrieving the OmahaClient's state after it's written to disk.
// The PREF_PACKAGE doesn't match the current OmahaClient package for historical reasons.
@VisibleForTesting
static final String PREF_PACKAGE = "com.google.android.apps.chrome.omaha";
@VisibleForTesting
static final String PREF_PERSISTED_REQUEST_ID = "persistedRequestID";
@VisibleForTesting
static final String PREF_TIMESTAMP_OF_REQUEST = "timestampOfRequest";
@VisibleForTesting
static final String PREF_INSTALL_SOURCE = "installSource";
private static final String PREF_SEND_INSTALL_EVENT = "sendInstallEvent";
private static final String PREF_TIMESTAMP_OF_INSTALL = "timestampOfInstall";
private static final String PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT =
"timestampForNextPostAttempt";
private static final String PREF_TIMESTAMP_FOR_NEW_REQUEST = "timestampForNewRequest";
// Strings indicating how the Chrome APK arrived on the user's device. These values MUST NOT
// be changed without updating the corresponding Omaha server strings.
static final String INSTALL_SOURCE_SYSTEM = "system_image";
static final String INSTALL_SOURCE_ORGANIC = "organic";
// Lock object used to synchronize all calls that modify or read sIsFreshInstallOrDataCleared.
private static final Object sIsFreshInstallLock = new Object();
@VisibleForTesting
static final String PREF_LATEST_VERSION = "latestVersion";
@VisibleForTesting
static final String PREF_MARKET_URL = "marketURL";
private static final long INVALID_TIMESTAMP = -1;
@VisibleForTesting
static final String INVALID_REQUEST_ID = "invalid";
// Static fields
private static boolean sEnableCommunication = true;
private static boolean sEnableUpdateDetection = true;
private static VersionNumberGetter sVersionNumberGetter = null;
private static MarketURLGetter sMarketURLGetter = null;
private static Boolean sIsFreshInstallOrDataCleared = null;
// Member fields not persisted to disk.
private boolean mStateHasBeenRestored;
private ExponentialBackoffScheduler mBackoffScheduler;
private RequestGenerator mGenerator;
// State saved written to and read from disk.
private RequestData mCurrentRequest;
private long mTimestampOfInstall;
private long mTimestampForNextPostAttempt;
private long mTimestampForNewRequest;
private String mLatestVersion;
private String mMarketURL;
private String mInstallSource;
protected boolean mSendInstallEvent;
public OmahaClient() {
super(TAG);
setIntentRedelivery(true);
}
/**
* Sets whether Chrome should be communicating with the Omaha server.
* The alternative to using a static field within OmahaClient is using a member variable in
* the ChromeTabbedActivity. The problem is that it is difficult to set the variable before
* ChromeTabbedActivity is started.
*/
@VisibleForTesting
public static void setEnableCommunication(boolean state) {
sEnableCommunication = state;
}
/**
* If false, OmahaClient will never report that a newer version is available.
*/
@VisibleForTesting
public static void setEnableUpdateDetection(boolean state) {
sEnableUpdateDetection = state;
}
@VisibleForTesting
long getTimestampForNextPostAttempt() {
return mTimestampForNextPostAttempt;
}
@VisibleForTesting
long getTimestampForNewRequest() {
return mTimestampForNewRequest;
}
@VisibleForTesting
int getCumulativeFailedAttempts() {
return getBackoffScheduler().getNumFailedAttempts();
}
/**
* Creates the scheduler used to space out POST attempts.
*/
@VisibleForTesting
ExponentialBackoffScheduler createBackoffScheduler(String prefPackage, Context context,
long base, long max) {
return new ExponentialBackoffScheduler(prefPackage, context, base, max);
}
/**
* Creates the request generator used to create Omaha XML.
*/
@VisibleForTesting
RequestGenerator createRequestGenerator(Context context) {
return ((ChromeApplication) context.getApplicationContext()).createOmahaRequestGenerator();
}
/**
* Handles an action on a thread separate from the UI thread.
* @param intent Intent fired by some part of Chrome.
*/
@Override
public void onHandleIntent(Intent intent) {
assert Looper.myLooper() != Looper.getMainLooper();
if (!sEnableCommunication) {
Log.v(TAG, "Disabled. Ignoring intent.");
return;
}
if (getRequestGenerator() == null) {
return;
}
if (!mStateHasBeenRestored) {
restoreState();
}
if (ACTION_INITIALIZE.equals(intent.getAction())) {
handleInitialize();
} else if (ACTION_REGISTER_REQUEST.equals(intent.getAction())) {
handleRegisterRequest(intent);
} else if (ACTION_POST_REQUEST.equals(intent.getAction())) {
handlePostRequestIntent(intent);
} else {
Log.e(TAG, "Got unknown action from intent: " + intent.getAction());
}
}
/**
* Begin communicating with the Omaha Update Server.
*/
public static void onForegroundSessionStart(Context context) {
if (!ChromeVersionInfo.isOfficialBuild()) return;
Intent omahaIntent = createInitializeIntent(context);
context.startService(omahaIntent);
}
static Intent createInitializeIntent(Context context) {
Intent intent = new Intent(context, OmahaClient.class);
intent.setAction(ACTION_INITIALIZE);
return intent;
}
/**
* Start a recurring alarm to fire request generation intents.
*/
private void handleInitialize() {
scheduleRepeatingAlarm();
// If a request exists, fire a POST intent to restart its timer.
if (hasRequest()) startService(createPostRequestIntent(this));
}
/**
* Returns an Intent for registering a new request to send to the server.
*/
static Intent createRegisterRequestIntent(Context context) {
Intent intent = new Intent(context, OmahaClient.class);
intent.setAction(ACTION_REGISTER_REQUEST);
return intent;
}
/**
* Determines if a new request should be generated. New requests are only generated if enough
* time has passed between now and the last time a request was generated.
*/
private void handleRegisterRequest(Intent intent) {
if (!isChromeBeingUsed()) {
cancelRepeatingAlarm();
return;
}
// If the current request is too old, generate a new one.
long currentTimestamp = getBackoffScheduler().getCurrentTime();
boolean isTooOld = hasRequest()
&& mCurrentRequest.getAgeInMilliseconds(currentTimestamp) >= MS_BETWEEN_REQUESTS;
boolean isOverdue = !hasRequest() && currentTimestamp >= mTimestampForNewRequest;
if (isTooOld || isOverdue) {
registerNewRequest(currentTimestamp);
}
// Create an intent to send the request.
if (hasRequest()) {
startService(createPostRequestIntent(this));
}
}
/**
* Returns an Intent for POSTing the current request to the Omaha server.
*/
static Intent createPostRequestIntent(Context context) {
Intent intent = new Intent(context, OmahaClient.class);
intent.setAction(ACTION_POST_REQUEST);
return intent;
}
/**
* Sends the request it is holding.
*/
@VisibleForTesting
private void handlePostRequestIntent(Intent intent) {
if (!hasRequest()) {
return;
}
// If enough time has passed since the last attempt, try sending a request.
long currentTimestamp = getBackoffScheduler().getCurrentTime();
if (currentTimestamp >= mTimestampForNextPostAttempt) {
// All requests made during the same session should have the same ID.
String sessionID = generateRandomUUID();
boolean sendingInstallRequest = mSendInstallEvent;
boolean succeeded = generateAndPostRequest(currentTimestamp, sessionID);
if (succeeded && sendingInstallRequest) {
// Only the first request ever generated should contain an install event.
mSendInstallEvent = false;
// Create and immediately send another request for a ping and update check.
registerNewRequest(currentTimestamp);
generateAndPostRequest(currentTimestamp, sessionID);
}
} else {
// Set an alarm to POST at the proper time. Previous alarms are destroyed.
Intent postIntent = createPostRequestIntent(this);
getBackoffScheduler().createAlarm(postIntent, mTimestampForNextPostAttempt);
}
// Write everything back out again to save our state.
saveState();
}
private boolean generateAndPostRequest(long currentTimestamp, String sessionID) {
ExponentialBackoffScheduler scheduler = getBackoffScheduler();
try {
// Generate the XML for the current request.
long installAgeInDays = RequestGenerator.installAge(currentTimestamp,
mTimestampOfInstall, mCurrentRequest.isSendInstallEvent());
String version = getVersionNumberGetter().getCurrentlyUsedVersion(this);
String xml = getRequestGenerator().generateXML(
sessionID, version, installAgeInDays, mCurrentRequest);
// Send the request to the server & wait for a response.
String response = postRequest(currentTimestamp, xml);
parseServerResponse(response);
// If we've gotten this far, we've successfully sent a request.
mCurrentRequest = null;
mTimestampForNextPostAttempt = currentTimestamp + MS_POST_BASE_DELAY;
scheduler.resetFailedAttempts();
Log.i(TAG, "Request to Server Successful. Timestamp for next request:"
+ String.valueOf(mTimestampForNextPostAttempt));
return true;
} catch (RequestFailureException e) {
// Set the alarm to try again later.
Log.e(TAG, "Failed to contact server: ", e);
Intent postIntent = createPostRequestIntent(this);
mTimestampForNextPostAttempt = scheduler.createAlarm(postIntent);
scheduler.increaseFailedAttempts();
return false;
}
}
/**
* Sets a repeating alarm that fires request registration Intents.
* Setting the alarm overwrites whatever alarm is already there, and rebooting
* clears whatever alarms are currently set.
*/
private void scheduleRepeatingAlarm() {
Intent registerIntent = createRegisterRequestIntent(this);
PendingIntent pIntent = PendingIntent.getService(this, 0, registerIntent, 0);
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
setAlarm(am, pIntent, AlarmManager.RTC, mTimestampForNewRequest);
}
/**
* Sets up a timer to fire after each interval.
* Override to prevent a real alarm from being set.
*/
@VisibleForTesting
protected void setAlarm(AlarmManager am, PendingIntent operation, int alarmType,
long triggerAtTime) {
try {
am.setRepeating(AlarmManager.RTC, triggerAtTime, MS_BETWEEN_REQUESTS, operation);
} catch (SecurityException e) {
Log.e(TAG, "Failed to set repeating alarm.");
}
}
/**
* Cancels the alarm that launches this service. It will be replaced when Chrome next resumes.
*/
private void cancelRepeatingAlarm() {
Intent requestIntent = createRegisterRequestIntent(this);
PendingIntent pendingIntent =
PendingIntent.getService(this, 0, requestIntent, PendingIntent.FLAG_NO_CREATE);
// Setting FLAG_NO_CREATE forces Android to return an already existing PendingIntent.
// Here it would be the one that was used to create the existing alarm (if it exists).
// If the pendingIntent is null, it is likely that no alarm was created.
if (pendingIntent != null) {
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
am.cancel(pendingIntent);
pendingIntent.cancel();
}
}
/**
* Determine whether or not Chrome is currently being used actively.
*/
@VisibleForTesting
protected boolean isChromeBeingUsed() {
boolean isChromeVisible = ApplicationStatus.hasVisibleActivities();
boolean isScreenOn = ApiCompatibilityUtils.isInteractive(this);
return isChromeVisible && isScreenOn;
}
/**
* Registers a new request with the current timestamp. Internal timestamps are reset to start
* fresh.
* @param currentTimestamp Current time.
*/
@VisibleForTesting
void registerNewRequest(long currentTimestamp) {
mCurrentRequest = createRequestData(currentTimestamp, null);
getBackoffScheduler().resetFailedAttempts();
mTimestampForNextPostAttempt = currentTimestamp;
// Tentatively set the timestamp for a new request. This will be updated when the server
// is successfully contacted.
mTimestampForNewRequest = currentTimestamp + MS_BETWEEN_REQUESTS;
scheduleRepeatingAlarm();
saveState();
}
private RequestData createRequestData(long currentTimestamp, String persistedID) {
// If we're sending a persisted event, keep trying to send the same request ID.
String requestID;
if (persistedID == null || INVALID_REQUEST_ID.equals(persistedID)) {
requestID = generateRandomUUID();
} else {
requestID = persistedID;
}
return new RequestData(mSendInstallEvent, currentTimestamp, requestID, mInstallSource);
}
@VisibleForTesting
boolean hasRequest() {
return mCurrentRequest != null;
}
/**
* Posts the request to the Omaha server.
* @return the XML response as a String.
* @throws RequestFailureException if the request fails.
*/
@VisibleForTesting
String postRequest(long timestamp, String xml) throws RequestFailureException {
String response = null;
HttpURLConnection urlConnection = null;
try {
urlConnection = createConnection();
setUpPostRequest(timestamp, urlConnection, xml);
sendRequestToServer(urlConnection, xml);
response = readResponseFromServer(urlConnection);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
return response;
}
/**
* Parse the server's response and confirm that we received an OK response.
*/
private void parseServerResponse(String response) throws RequestFailureException {
String appId = getRequestGenerator().getAppId();
boolean sentPingAndUpdate = !mSendInstallEvent;
ResponseParser parser =
new ResponseParser(appId, mSendInstallEvent, sentPingAndUpdate, sentPingAndUpdate);
parser.parseResponse(response);
mTimestampForNewRequest = getBackoffScheduler().getCurrentTime() + MS_BETWEEN_REQUESTS;
mLatestVersion = parser.getNewVersion();
mMarketURL = parser.getURL();
scheduleRepeatingAlarm();
}
/**
* Returns a HttpURLConnection to the server.
*/
@VisibleForTesting
protected HttpURLConnection createConnection() throws RequestFailureException {
try {
URL url = new URL(getRequestGenerator().getServerUrl());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(MS_CONNECTION_TIMEOUT);
connection.setReadTimeout(MS_CONNECTION_TIMEOUT);
return connection;
} catch (MalformedURLException e) {
throw new RequestFailureException("Caught a malformed URL exception.", e);
} catch (IOException e) {
throw new RequestFailureException("Failed to open connection to URL", e);
}
}
/**
* Prepares the HTTP header.
*/
private void setUpPostRequest(long timestamp, HttpURLConnection urlConnection, String xml)
throws RequestFailureException {
try {
urlConnection.setDoOutput(true);
urlConnection.setFixedLengthStreamingMode(xml.getBytes().length);
if (mSendInstallEvent && getCumulativeFailedAttempts() > 0) {
String age = Long.toString(mCurrentRequest.getAgeInSeconds(timestamp));
urlConnection.addRequestProperty("X-RequestAge", age);
}
} catch (IllegalAccessError e) {
throw new RequestFailureException("Caught an IllegalAccessError:", e);
} catch (IllegalArgumentException e) {
throw new RequestFailureException("Caught an IllegalArgumentException:", e);
} catch (IllegalStateException e) {
throw new RequestFailureException("Caught an IllegalStateException:", e);
}
}
/**
* Sends the request to the server.
*/
private void sendRequestToServer(HttpURLConnection urlConnection, String xml)
throws RequestFailureException {
try {
OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
OutputStreamWriter writer = new OutputStreamWriter(out);
writer.write(xml, 0, xml.length());
writer.close();
checkServerResponseCode(urlConnection);
} catch (IOException e) {
throw new RequestFailureException("Failed to write request to server: ", e);
}
}
/**
* Reads the response from the Omaha Server.
*/
private String readResponseFromServer(HttpURLConnection urlConnection)
throws RequestFailureException {
try {
InputStreamReader reader = new InputStreamReader(urlConnection.getInputStream());
BufferedReader in = new BufferedReader(reader);
try {
StringBuilder response = new StringBuilder();
for (String line = in.readLine(); line != null; line = in.readLine()) {
response.append(line);
}
checkServerResponseCode(urlConnection);
return response.toString();
} finally {
in.close();
}
} catch (IOException e) {
throw new RequestFailureException("Failed when reading response from server: ", e);
}
}
/**
* Confirms that the Omaha server sent back an "OK" code.
*/
private void checkServerResponseCode(HttpURLConnection urlConnection)
throws RequestFailureException {
try {
if (urlConnection.getResponseCode() != 200) {
throw new RequestFailureException(
"Received " + urlConnection.getResponseCode()
+ " code instead of 200 (OK) from the server. Aborting.");
}
} catch (IOException e) {
throw new RequestFailureException("Failed to read response code from server: ", e);
}
}
/**
* Checks if we know about a newer version available than the one we're using. This does not
* actually fire any requests over to the server; it just checks the version we stored the last
* time we talked to the Omaha server.
*
* NOTE: This function incurs I/O, so don't use it on the main thread.
*/
static boolean isNewerVersionAvailable(Context context) {
assert Looper.myLooper() != Looper.getMainLooper();
// This may be explicitly enabled for some channels and for unit tests.
if (!sEnableUpdateDetection) {
return false;
}
// If the market link is bad, don't show an update to avoid frustrating users trying to
// hit the "Update" button.
if ("".equals(getMarketURL(context))) {
return false;
}
// Compare version numbers.
VersionNumberGetter getter = getVersionNumberGetter();
String currentStr = getter.getCurrentlyUsedVersion(context);
String latestStr = getter.getLatestKnownVersion(context, PREF_PACKAGE, PREF_LATEST_VERSION);
VersionNumber currentVersionNumber = VersionNumber.fromString(currentStr);
VersionNumber latestVersionNumber = VersionNumber.fromString(latestStr);
if (currentVersionNumber == null || latestVersionNumber == null) {
return false;
}
return currentVersionNumber.isSmallerThan(latestVersionNumber);
}
/**
* Retrieves the latest version we know about from disk.
* This function incurs I/O, so make sure you don't use it from the main thread.
*
* @return A string representing the latest version.
*/
static String getLatestVersionNumberString(Context context) {
assert Looper.myLooper() != Looper.getMainLooper();
VersionNumberGetter getter = getVersionNumberGetter();
return getter.getLatestKnownVersion(context, PREF_PACKAGE, PREF_LATEST_VERSION);
}
/**
* Determine how the Chrome APK arrived on the device.
* @param context Context to pull resources from.
* @return A String indicating the install source.
*/
String determineInstallSource() {
boolean isInSystemImage = (getApplicationFlags() & ApplicationInfo.FLAG_SYSTEM) != 0;
return isInSystemImage ? INSTALL_SOURCE_SYSTEM : INSTALL_SOURCE_ORGANIC;
}
/**
* Returns the Application's flags, used to determine if Chrome was installed as part of the
* system image.
* @return The Application's flags.
*/
@VisibleForTesting
int getApplicationFlags() {
return getApplicationInfo().flags;
}
/**
* Reads the data back from the file it was saved to. Uses SharedPreferences to handle I/O.
* Sanity checks are performed on the timestamps to guard against clock changing.
*/
@VisibleForTesting
void restoreState() {
boolean mustRewriteState = false;
SharedPreferences preferences = getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE);
Map<String, ?> items = preferences.getAll();
// Read out the recorded data.
long currentTime = getBackoffScheduler().getCurrentTime();
mTimestampForNewRequest =
getLongFromMap(items, PREF_TIMESTAMP_FOR_NEW_REQUEST, currentTime);
mTimestampForNextPostAttempt =
getLongFromMap(items, PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT, currentTime);
long requestTimestamp = getLongFromMap(items, PREF_TIMESTAMP_OF_REQUEST, INVALID_TIMESTAMP);
// If the preference doesn't exist, it's likely that we haven't sent an install event.
mSendInstallEvent = getBooleanFromMap(items, PREF_SEND_INSTALL_EVENT, true);
// Restore the install source.
String defaultInstallSource = determineInstallSource();
mInstallSource = getStringFromMap(items, PREF_INSTALL_SOURCE, defaultInstallSource);
// If we're not sending an install event, don't bother restoring the request ID:
// the server does not expect to have persisted request IDs for pings or update checks.
String persistedRequestId = mSendInstallEvent
? getStringFromMap(items, PREF_PERSISTED_REQUEST_ID, INVALID_REQUEST_ID)
: INVALID_REQUEST_ID;
mCurrentRequest = requestTimestamp == INVALID_TIMESTAMP
? null : createRequestData(requestTimestamp, persistedRequestId);
mLatestVersion = getStringFromMap(items, PREF_LATEST_VERSION, "");
mMarketURL = getStringFromMap(items, PREF_MARKET_URL, "");
// If we don't have a timestamp for when we installed Chrome, then set it to now.
mTimestampOfInstall = getLongFromMap(items, PREF_TIMESTAMP_OF_INSTALL, currentTime);
// Confirm that the timestamp for the next request is less than the base delay.
long delayToNewRequest = mTimestampForNewRequest - currentTime;
if (delayToNewRequest > MS_BETWEEN_REQUESTS) {
Log.w(TAG, "Delay to next request (" + delayToNewRequest
+ ") is longer than expected. Resetting to now.");
mTimestampForNewRequest = currentTime;
mustRewriteState = true;
}
// Confirm that the timestamp for the next POST is less than the current delay.
long delayToNextPost = mTimestampForNextPostAttempt - currentTime;
if (delayToNextPost > getBackoffScheduler().getGeneratedDelay()) {
Log.w(TAG, "Delay to next post attempt (" + delayToNextPost
+ ") is greater than expected (" + getBackoffScheduler().getGeneratedDelay()
+ "). Resetting to now.");
mTimestampForNextPostAttempt = currentTime;
mustRewriteState = true;
}
if (mustRewriteState) {
saveState();
}
mStateHasBeenRestored = true;
}
/**
* Writes out the current state to a file.
*/
private void saveState() {
SharedPreferences prefs = getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(PREF_SEND_INSTALL_EVENT, mSendInstallEvent);
setIsFreshInstallOrDataHasBeenCleared(this);
editor.putLong(PREF_TIMESTAMP_OF_INSTALL, mTimestampOfInstall);
editor.putLong(PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT, mTimestampForNextPostAttempt);
editor.putLong(PREF_TIMESTAMP_FOR_NEW_REQUEST, mTimestampForNewRequest);
editor.putLong(PREF_TIMESTAMP_OF_REQUEST,
hasRequest() ? mCurrentRequest.getCreationTimestamp() : INVALID_TIMESTAMP);
editor.putString(PREF_PERSISTED_REQUEST_ID,
hasRequest() ? mCurrentRequest.getRequestID() : INVALID_REQUEST_ID);
editor.putString(PREF_LATEST_VERSION, mLatestVersion == null ? "" : mLatestVersion);
editor.putString(PREF_MARKET_URL, mMarketURL == null ? "" : mMarketURL);
if (mInstallSource != null) editor.putString(PREF_INSTALL_SOURCE, mInstallSource);
editor.apply();
}
/**
* Generates a random UUID.
*/
@VisibleForTesting
protected String generateRandomUUID() {
return UUID.randomUUID().toString();
}
/**
* Sets the VersionNumberGetter used to get version numbers. Set a new one to override what
* version numbers are returned.
*/
@VisibleForTesting
static void setVersionNumberGetterForTests(VersionNumberGetter getter) {
sVersionNumberGetter = getter;
}
@SuppressFBWarnings("LI_LAZY_INIT_STATIC")
@VisibleForTesting
static VersionNumberGetter getVersionNumberGetter() {
if (sVersionNumberGetter == null) {
sVersionNumberGetter = new VersionNumberGetter();
}
return sVersionNumberGetter;
}
/**
* Sets the MarketURLGetter used to get version numbers. Set a new one to override what
* URL is returned.
*/
@VisibleForTesting
static void setMarketURLGetterForTests(MarketURLGetter getter) {
sMarketURLGetter = getter;
}
/**
* Returns the stub used to grab the market URL for Chrome.
*/
@SuppressFBWarnings("LI_LAZY_INIT_STATIC")
public static String getMarketURL(Context context) {
if (sMarketURLGetter == null) {
sMarketURLGetter = new MarketURLGetter();
}
return sMarketURLGetter.getMarketURL(context, PREF_PACKAGE, PREF_MARKET_URL);
}
/**
* Pulls a long from the shared preferences map.
*/
private static long getLongFromMap(final Map<String, ?> items, String key, long defaultValue) {
Long value = (Long) items.get(key);
return value != null ? value : defaultValue;
}
/**
* Pulls a string from the shared preferences map.
*/
private static String getStringFromMap(final Map<String, ?> items, String key,
String defaultValue) {
String value = (String) items.get(key);
return value != null ? value : defaultValue;
}
/**
* Pulls a boolean from the shared preferences map.
*/
private static boolean getBooleanFromMap(final Map<String, ?> items, String key,
boolean defaultValue) {
Boolean value = (Boolean) items.get(key);
return value != null ? value : defaultValue;
}
/**
* @return Whether it is either a fresh install or data has been cleared.
* PREF_TIMESTAMP_OF_INSTALL is set within the first few seconds after a fresh install.
* sIsFreshInstallOrDataCleared will be set to true if PREF_TIMESTAMP_OF_INSTALL has not
* been previously set. Else, it will be set to false. sIsFreshInstallOrDataCleared is
* guarded by sLock.
* @param context The current Context.
*/
public static boolean isFreshInstallOrDataHasBeenCleared(Context context) {
return setIsFreshInstallOrDataHasBeenCleared(context);
}
private static boolean setIsFreshInstallOrDataHasBeenCleared(Context context) {
synchronized (sIsFreshInstallLock) {
if (sIsFreshInstallOrDataCleared == null) {
SharedPreferences prefs = context.getSharedPreferences(
PREF_PACKAGE, Context.MODE_PRIVATE);
sIsFreshInstallOrDataCleared = (prefs.getLong(PREF_TIMESTAMP_OF_INSTALL, -1) == -1);
}
return sIsFreshInstallOrDataCleared;
}
}
protected final RequestGenerator getRequestGenerator() {
if (mGenerator == null) mGenerator = createRequestGenerator(this);
return mGenerator;
}
protected final ExponentialBackoffScheduler getBackoffScheduler() {
if (mBackoffScheduler == null) {
mBackoffScheduler = createBackoffScheduler(
PREF_PACKAGE, this, MS_POST_BASE_DELAY, MS_POST_MAX_DELAY);
}
return mBackoffScheduler;
}
}