Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1)

Unified Diff: chrome/android/java_staging/src/org/chromium/chrome/browser/omaha/OmahaClient.java

Issue 1141283003: Upstream oodles of Chrome for Android code into Chromium. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: final patch? Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java_staging/src/org/chromium/chrome/browser/omaha/OmahaClient.java
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/omaha/OmahaClient.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/omaha/OmahaClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d540645ae90d2d202b0bd94ed86dd2fb71f4d3f
--- /dev/null
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/omaha/OmahaClient.java
@@ -0,0 +1,833 @@
+// 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 android.util.Log;
+
+import org.chromium.base.ApiCompatibilityUtils;
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.VisibleForTesting;
+import org.chromium.base.annotations.SuppressFBWarnings;
+import org.chromium.chrome.browser.ChromeMobileApplication;
+
+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 = "OmahaClient";
+
+ // 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";
+
+ // Strings for extras.
+ private static final String EXTRA_FORCE_ACTION = "forceAction";
+
+ // 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 Context mApplicationContext;
+ 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);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mApplicationContext = getApplicationContext();
+ mBackoffScheduler = createBackoffScheduler(PREF_PACKAGE, mApplicationContext,
+ MS_POST_BASE_DELAY, MS_POST_MAX_DELAY);
+ mGenerator = createRequestGenerator(mApplicationContext);
+ }
+
+ /**
+ * 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 mBackoffScheduler.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 ((ChromeMobileApplication) 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 (mGenerator == null) {
+ Log.e(TAG, "No request generator set. Ignoring intent.");
+ 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());
+ }
+ }
+
+ public 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()) {
+ Intent postIntent = createPostRequestIntent(mApplicationContext, false);
+ startService(postIntent);
+ }
+ }
+
+ public static Intent createRegisterRequestIntent(Context context, boolean force) {
+ Intent intent = new Intent(context, OmahaClient.class);
+ intent.setAction(ACTION_REGISTER_REQUEST);
+ intent.putExtra(EXTRA_FORCE_ACTION, force);
+ 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) {
+ boolean force = intent.getBooleanExtra(EXTRA_FORCE_ACTION, false);
+ if (!isChromeBeingUsed() && !force) {
+ cancelRepeatingAlarm();
+ return;
+ }
+
+ // If the current request is too old, generate a new one.
+ long currentTimestamp = mBackoffScheduler.getCurrentTime();
+ boolean isTooOld = hasRequest()
+ && mCurrentRequest.getAgeInMilliseconds(currentTimestamp) >= MS_BETWEEN_REQUESTS;
+ boolean isOverdue = !hasRequest() && currentTimestamp >= mTimestampForNewRequest;
+ if (isTooOld || isOverdue || force) {
+ registerNewRequest(currentTimestamp);
+ }
+
+ // Create an intent to send the request. If we're forcing a registration, force the POST,
+ // as well.
+ if (hasRequest()) {
+ Intent postIntent = createPostRequestIntent(mApplicationContext, force);
+ startService(postIntent);
+ }
+ }
+
+ public static Intent createPostRequestIntent(Context context, boolean force) {
+ Intent intent = new Intent(context, OmahaClient.class);
+ intent.setAction(ACTION_POST_REQUEST);
+ intent.putExtra(EXTRA_FORCE_ACTION, force);
+ return intent;
+ }
+
+ /**
+ * Sends the request it is holding.
+ */
+ @VisibleForTesting
+ private void handlePostRequestIntent(Intent intent) {
+ if (!hasRequest()) {
+ return;
+ }
+
+ boolean force = intent.getBooleanExtra(EXTRA_FORCE_ACTION, false);
+
+ // If enough time has passed since the last attempt, try sending a request.
+ long currentTimestamp = mBackoffScheduler.getCurrentTime();
+ if (currentTimestamp >= mTimestampForNextPostAttempt || force) {
+ // 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);
+ succeeded = generateAndPostRequest(currentTimestamp, sessionID);
+ }
+
+ if (force) {
+ if (succeeded) {
+ Log.v(TAG, "Requests successfully sent to Omaha server.");
+ } else {
+ Log.e(TAG, "Requests failed to reach Omaha server.");
+ }
+ }
+ } else {
+ // Set an alarm to POST at the proper time. Previous alarms are destroyed.
+ Intent postIntent = createPostRequestIntent(mApplicationContext, false);
+ mBackoffScheduler.createAlarm(postIntent, mTimestampForNextPostAttempt);
+ }
+
+ // Write everything back out again to save our state.
+ saveState();
+ }
+
+ private boolean generateAndPostRequest(long currentTimestamp, String sessionID) {
+ try {
+ // Generate the XML for the current request.
+ long installAgeInDays = RequestGenerator.installAge(currentTimestamp,
+ mTimestampOfInstall, mCurrentRequest.isSendInstallEvent());
+ String version = getVersionNumberGetter().getCurrentlyUsedVersion(mApplicationContext);
+ String xml =
+ mGenerator.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;
+ mBackoffScheduler.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(mApplicationContext, false);
+ mTimestampForNextPostAttempt = mBackoffScheduler.createAlarm(postIntent);
+ mBackoffScheduler.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(mApplicationContext, false);
+ PendingIntent pIntent =
+ PendingIntent.getService(mApplicationContext, 0, registerIntent, 0);
+ AlarmManager am =
+ (AlarmManager) mApplicationContext.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) {
+ am.setRepeating(AlarmManager.RTC, triggerAtTime, MS_BETWEEN_REQUESTS, operation);
+ }
+
+ /**
+ * Cancels the alarm that launches this service. It will be replaced when Chrome next resumes.
+ */
+ private void cancelRepeatingAlarm() {
+ Intent requestIntent = createRegisterRequestIntent(mApplicationContext, false);
+ PendingIntent pendingIntent = PendingIntent.getService(mApplicationContext, 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) mApplicationContext.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(mApplicationContext);
+ 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);
+ mBackoffScheduler.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 = mGenerator.getAppId();
+ boolean sentPingAndUpdate = !mSendInstallEvent;
+ ResponseParser parser =
+ new ResponseParser(appId, mSendInstallEvent, sentPingAndUpdate, sentPingAndUpdate);
+ parser.parseResponse(response);
+ mTimestampForNewRequest = mBackoffScheduler.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(mGenerator.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.
+ */
+ public static boolean isNewerVersionAvailable(Context applicationContext) {
+ 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(applicationContext))) {
+ return false;
+ }
+
+ // Compare version numbers.
+ VersionNumberGetter getter = getVersionNumberGetter();
+ String currentStr = getter.getCurrentlyUsedVersion(applicationContext);
+ String latestStr =
+ getter.getLatestKnownVersion(applicationContext, 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);
+ }
+
+ /**
+ * 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(Context context) {
+ 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
+ public int getApplicationFlags() {
+ return mApplicationContext.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 =
+ mApplicationContext.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE);
+ Map<String, ?> items = preferences.getAll();
+
+ // Read out the recorded data.
+ long currentTime = mBackoffScheduler.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(mApplicationContext);
+ 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 > mBackoffScheduler.getGeneratedDelay()) {
+ Log.w(TAG, "Delay to next post attempt (" + delayToNextPost
+ + ") is greater than expected (" + mBackoffScheduler.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 =
+ mApplicationContext.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_SEND_INSTALL_EVENT, mSendInstallEvent);
+ setIsFreshInstallOrDataHasBeenCleared(mApplicationContext);
+ 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 applicationContext The current application Context.
+ */
+ public static boolean isFreshInstallOrDataHasBeenCleared(Context applicationContext) {
+ return setIsFreshInstallOrDataHasBeenCleared(applicationContext);
+ }
+
+ private static boolean setIsFreshInstallOrDataHasBeenCleared(Context applicationContext) {
+ synchronized (sIsFreshInstallLock) {
+ if (sIsFreshInstallOrDataCleared == null) {
+ SharedPreferences prefs = applicationContext.getSharedPreferences(
+ PREF_PACKAGE, Context.MODE_PRIVATE);
+ sIsFreshInstallOrDataCleared = (prefs.getLong(PREF_TIMESTAMP_OF_INSTALL, -1) == -1);
+ }
+ return sIsFreshInstallOrDataCleared;
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698