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; |
+ } |
+ } |
+} |