Index: chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaBase.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaBase.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaBase.java |
index 5c53bf946ebf1de5e357d281667c48149e782b10..d6c47da5750a58a641ed41b13f97d0e84b651d3b 100644 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaBase.java |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaBase.java |
@@ -5,10 +5,14 @@ |
package org.chromium.chrome.browser.omaha; |
import android.content.Context; |
+import android.content.Intent; |
import android.content.SharedPreferences; |
import android.os.Build; |
+import android.support.annotation.IntDef; |
+import org.chromium.base.Log; |
import org.chromium.base.StreamUtil; |
+import org.chromium.base.VisibleForTesting; |
import org.chromium.chrome.browser.ChromeVersionInfo; |
import java.io.BufferedOutputStream; |
@@ -17,13 +21,39 @@ import java.io.IOException; |
import java.io.InputStreamReader; |
import java.io.OutputStream; |
import java.io.OutputStreamWriter; |
+import java.lang.annotation.Retention; |
+import java.lang.annotation.RetentionPolicy; |
import java.net.HttpURLConnection; |
+import java.net.MalformedURLException; |
+import java.net.URL; |
+import java.util.Date; |
+import java.util.concurrent.TimeUnit; |
/** |
- * Logic used for communicating with the Omaha server. |
- * TODO(dfalcantara): Move everything from OmahaClient over. |
+ * Keeps tabs on the current state of Chrome, tracking if and when a request should be sent to the |
+ * Omaha Server. |
+ * |
+ * When Chrome is brought to the foreground, it will trigger a call to |
+ * {@link OmahaBase#onForegroundSessionStart}, which kicks off a series of scheduled events |
+ * that allow the class to run. A single alarm is used to trigger the whole pipeline when needed. |
+ * - If Chrome isn't running when the alarm is fired, no pings or update checks will be performed. |
+ * - If Chrome doesn't have a pending request to POST, no POST will be performed. |
+ * |
+ * When a fresh install is detected (or the user clears their data), OmahaBase will send an XML |
+ * request saying that a new install was detected, then follow up with an XML request saying that |
+ * the user was active and that we need to check for Chrome updates. |
+ * |
+ * 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 run of the pipeline. |
+ * |
+ * Implementation notes: |
+ * http://docs.google.com/a/google.com/document/d/1scTCovqASf5ktkOeVj8wFRkWTCeDYw2LrOBNn05CDB0/edit |
*/ |
-public abstract class OmahaBase { |
+public class OmahaBase { |
+ private static final String TAG = "omaha"; |
// 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. |
@@ -38,9 +68,52 @@ public abstract class OmahaBase { |
static final String PREF_TIMESTAMP_OF_INSTALL = "timestampOfInstall"; |
static final String PREF_TIMESTAMP_OF_REQUEST = "timestampOfRequest"; |
+ static final int MIN_API_JOB_SCHEDULER = Build.VERSION_CODES.M; |
+ |
/** Whether or not the Omaha server should really be contacted. */ |
private static boolean sIsDisabled; |
+ // Results of {@link #handlePostRequest()}. |
+ @Retention(RetentionPolicy.SOURCE) |
+ @IntDef({POST_RESULT_NO_REQUEST, POST_RESULT_SENT, POST_RESULT_FAILED, POST_RESULT_SCHEDULED}) |
+ @interface PostResult {} |
+ static final int POST_RESULT_NO_REQUEST = 0; |
+ static final int POST_RESULT_SENT = 1; |
+ static final int POST_RESULT_FAILED = 2; |
+ static final int POST_RESULT_SCHEDULED = 3; |
+ |
+ /** Deprecated; kept around to cancel alarms set for OmahaClient pre-M58. */ |
+ private static final String ACTION_REGISTER_REQUEST = |
+ "org.chromium.chrome.browser.omaha.ACTION_REGISTER_REQUEST"; |
+ |
+ // Delays between events. |
+ static final long MS_POST_BASE_DELAY = TimeUnit.HOURS.toMillis(1); |
+ static final long MS_POST_MAX_DELAY = TimeUnit.HOURS.toMillis(5); |
+ static final long MS_BETWEEN_REQUESTS = TimeUnit.HOURS.toMillis(5); |
+ static final int MS_CONNECTION_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(1); |
+ |
+ // 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. |
+ private static final String INSTALL_SOURCE_SYSTEM = "system_image"; |
+ private static final String INSTALL_SOURCE_ORGANIC = "organic"; |
+ |
+ private static final long INVALID_TIMESTAMP = -1; |
+ private static final String INVALID_REQUEST_ID = "invalid"; |
+ |
+ // Member fields not persisted to disk. |
+ private final OmahaDelegate mDelegate; |
+ private boolean mStateHasBeenRestored; |
+ |
+ // 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; |
+ |
/** See {@link #sIsDisabled}. */ |
public static void setIsDisabledForTesting(boolean state) { |
sIsDisabled = state; |
@@ -51,13 +124,342 @@ public abstract class OmahaBase { |
return sIsDisabled; |
} |
+ /** |
+ * Constructs a new OmahaBase. |
+ * @param delegate The {@link OmahaDelegate} used to interact with the system. |
+ */ |
+ OmahaBase(OmahaDelegate delegate) { |
+ mDelegate = delegate; |
+ } |
+ |
+ protected void run() { |
+ if (OmahaBase.isDisabled() || getRequestGenerator() == null) { |
+ Log.v(TAG, "Disabled. Ignoring intent."); |
+ return; |
+ } |
+ |
+ restoreState(getContext()); |
+ |
+ long nextTimestamp = Long.MAX_VALUE; |
+ if (mDelegate.isChromeBeingUsed()) { |
+ handleRegisterActiveRequest(); |
+ nextTimestamp = Math.min(nextTimestamp, mTimestampForNewRequest); |
+ } |
+ |
+ if (hasRequest()) { |
+ int result = handlePostRequest(); |
+ if (result == POST_RESULT_FAILED || result == POST_RESULT_SCHEDULED) { |
+ nextTimestamp = Math.min(nextTimestamp, mTimestampForNextPostAttempt); |
+ } |
+ } |
+ |
+ // TODO(dfalcantara): Prevent Omaha code from repeatedly rescheduling itself immediately in |
+ // case a scheduling error occurs. |
+ if (nextTimestamp != Long.MAX_VALUE && nextTimestamp >= 0) { |
+ long currentTimestamp = mDelegate.getScheduler().getCurrentTime(); |
+ Log.i(TAG, "Attempting to schedule next job for: " + new Date(nextTimestamp)); |
+ mDelegate.scheduleService(currentTimestamp, nextTimestamp); |
+ } |
+ |
+ saveState(getContext()); |
+ } |
+ |
+ /** |
+ * 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 handleRegisterActiveRequest() { |
+ // 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 = currentTimestamp >= mTimestampForNewRequest; |
+ if (isTooOld || isOverdue) { |
+ registerNewRequest(currentTimestamp); |
+ } |
+ } |
+ |
+ /** |
+ * Sends the request it is holding. |
+ */ |
+ private int handlePostRequest() { |
+ if (!hasRequest()) { |
+ mDelegate.onHandlePostRequestDone(POST_RESULT_NO_REQUEST, false); |
+ return POST_RESULT_NO_REQUEST; |
+ } |
+ |
+ // If enough time has passed since the last attempt, try sending a request. |
+ int result; |
+ long currentTimestamp = getBackoffScheduler().getCurrentTime(); |
+ boolean installEventWasSent = false; |
+ if (currentTimestamp >= mTimestampForNextPostAttempt) { |
+ // All requests made during the same session should have the same ID. |
+ String sessionID = mDelegate.generateUUID(); |
+ boolean sendingInstallRequest = mSendInstallEvent; |
+ boolean succeeded = generateAndPostRequest(currentTimestamp, sessionID); |
+ |
+ if (succeeded && sendingInstallRequest) { |
+ // Only the first request ever generated should contain an install event. |
+ mSendInstallEvent = false; |
+ installEventWasSent = true; |
+ |
+ // Create and immediately send another request for a ping and update check. |
+ registerNewRequest(currentTimestamp); |
+ succeeded &= generateAndPostRequest(currentTimestamp, sessionID); |
+ } |
+ |
+ result = succeeded ? POST_RESULT_SENT : POST_RESULT_FAILED; |
+ } else { |
+ result = POST_RESULT_SCHEDULED; |
+ } |
+ |
+ mDelegate.onHandlePostRequestDone(result, installEventWasSent); |
+ return result; |
+ } |
+ |
+ private boolean generateAndPostRequest(long currentTimestamp, String sessionID) { |
+ ExponentialBackoffScheduler scheduler = getBackoffScheduler(); |
+ boolean succeeded = false; |
+ try { |
+ // Generate the XML for the current request. |
+ long installAgeInDays = RequestGenerator.installAge( |
+ currentTimestamp, mTimestampOfInstall, mCurrentRequest.isSendInstallEvent()); |
+ String version = |
+ VersionNumberGetter.getInstance().getCurrentlyUsedVersion(getContext()); |
+ String xml = getRequestGenerator().generateXML( |
+ sessionID, version, installAgeInDays, mCurrentRequest); |
+ |
+ // Send the request to the server & wait for a response. |
+ String response = postRequest(currentTimestamp, xml); |
+ |
+ // Parse out the response. |
+ String appId = getRequestGenerator().getAppId(); |
+ boolean sentPingAndUpdate = !mSendInstallEvent; |
+ ResponseParser parser = new ResponseParser( |
+ appId, mSendInstallEvent, sentPingAndUpdate, sentPingAndUpdate); |
+ parser.parseResponse(response); |
+ mLatestVersion = parser.getNewVersion(); |
+ mMarketURL = parser.getURL(); |
+ |
+ succeeded = true; |
+ } catch (RequestFailureException e) { |
+ Log.e(TAG, "Failed to contact server: ", e); |
+ } |
+ |
+ if (succeeded) { |
+ // If we've gotten this far, we've successfully sent a request. |
+ mCurrentRequest = null; |
+ |
+ scheduler.resetFailedAttempts(); |
+ mTimestampForNewRequest = scheduler.getCurrentTime() + MS_BETWEEN_REQUESTS; |
+ mTimestampForNextPostAttempt = scheduler.calculateNextTimestamp(); |
+ Log.i(TAG, |
+ "Request to Server Successful. Timestamp for next request:" |
+ + mTimestampForNextPostAttempt); |
+ } else { |
+ // Set the alarm to try again later. Failures are incremented after setting the timer |
+ // to allow the first failure to incur the minimum base delay between POSTs. |
+ mTimestampForNextPostAttempt = scheduler.calculateNextTimestamp(); |
+ scheduler.increaseFailedAttempts(); |
+ } |
+ |
+ mDelegate.onGenerateAndPostRequestDone(succeeded); |
+ return succeeded; |
+ } |
+ |
+ /** |
+ * Registers a new request with the current timestamp. Internal timestamps are reset to start |
+ * fresh. |
+ * @param currentTimestamp Current time. |
+ */ |
+ private 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; |
+ |
+ mDelegate.onRegisterNewRequestDone(mTimestampForNewRequest, mTimestampForNextPostAttempt); |
+ } |
+ |
+ 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 = mDelegate.generateUUID(); |
+ } else { |
+ requestID = persistedID; |
+ } |
+ return new RequestData(mSendInstallEvent, currentTimestamp, requestID, mInstallSource); |
+ } |
+ |
+ private 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. |
+ */ |
+ private String postRequest(long timestamp, String xml) throws RequestFailureException { |
+ String response = null; |
+ |
+ HttpURLConnection urlConnection = null; |
+ try { |
+ urlConnection = createConnection(); |
+ |
+ // Prepare the HTTP header. |
+ urlConnection.setDoOutput(true); |
+ urlConnection.setFixedLengthStreamingMode(xml.getBytes().length); |
+ if (mSendInstallEvent && getBackoffScheduler().getNumFailedAttempts() > 0) { |
+ String age = Long.toString(mCurrentRequest.getAgeInSeconds(timestamp)); |
+ urlConnection.addRequestProperty("X-RequestAge", age); |
+ } |
+ |
+ response = OmahaBase.sendRequestToServer(urlConnection, xml); |
+ } 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); |
+ } finally { |
+ if (urlConnection != null) { |
+ urlConnection.disconnect(); |
+ } |
+ } |
+ |
+ return response; |
+ } |
+ |
+ /** |
+ * 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); |
+ } |
+ } |
+ |
+ /** |
+ * 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. |
+ */ |
+ private void restoreState(Context context) { |
+ if (mStateHasBeenRestored) return; |
+ |
+ String installSource = |
+ mDelegate.isInSystemImage() ? INSTALL_SOURCE_SYSTEM : INSTALL_SOURCE_ORGANIC; |
+ ExponentialBackoffScheduler scheduler = getBackoffScheduler(); |
+ long currentTime = scheduler.getCurrentTime(); |
+ |
+ SharedPreferences preferences = OmahaBase.getSharedPreferences(context); |
+ mTimestampForNewRequest = |
+ preferences.getLong(OmahaBase.PREF_TIMESTAMP_FOR_NEW_REQUEST, currentTime); |
+ mTimestampForNextPostAttempt = |
+ preferences.getLong(OmahaBase.PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT, currentTime); |
+ mTimestampOfInstall = preferences.getLong(OmahaBase.PREF_TIMESTAMP_OF_INSTALL, currentTime); |
+ mSendInstallEvent = preferences.getBoolean(OmahaBase.PREF_SEND_INSTALL_EVENT, true); |
+ mInstallSource = preferences.getString(OmahaBase.PREF_INSTALL_SOURCE, installSource); |
+ mLatestVersion = preferences.getString(OmahaBase.PREF_LATEST_VERSION, ""); |
+ mMarketURL = preferences.getString(OmahaBase.PREF_MARKET_URL, ""); |
+ |
+ // 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 |
+ ? preferences.getString(OmahaBase.PREF_PERSISTED_REQUEST_ID, INVALID_REQUEST_ID) |
+ : INVALID_REQUEST_ID; |
+ long requestTimestamp = |
+ preferences.getLong(OmahaBase.PREF_TIMESTAMP_OF_REQUEST, INVALID_TIMESTAMP); |
+ mCurrentRequest = requestTimestamp == INVALID_TIMESTAMP |
+ ? null |
+ : createRequestData(requestTimestamp, persistedRequestId); |
+ |
+ // 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; |
+ } |
+ |
+ // Confirm that the timestamp for the next POST is less than the current delay. |
+ long delayToNextPost = mTimestampForNextPostAttempt - currentTime; |
+ long lastGeneratedDelay = scheduler.getGeneratedDelay(); |
+ if (delayToNextPost > lastGeneratedDelay) { |
+ Log.w(TAG, |
+ "Delay to next post attempt (" + delayToNextPost |
+ + ") is greater than expected (" + lastGeneratedDelay |
+ + "). Resetting to now."); |
+ mTimestampForNextPostAttempt = currentTime; |
+ } |
+ |
+ migrateToNewerChromeVersions(); |
+ mStateHasBeenRestored = true; |
+ } |
+ |
+ /** |
+ * Writes out the current state to a file. |
+ */ |
+ private void saveState(Context context) { |
+ SharedPreferences prefs = OmahaBase.getSharedPreferences(context); |
+ SharedPreferences.Editor editor = prefs.edit(); |
+ editor.putBoolean(OmahaBase.PREF_SEND_INSTALL_EVENT, mSendInstallEvent); |
+ editor.putLong(OmahaBase.PREF_TIMESTAMP_OF_INSTALL, mTimestampOfInstall); |
+ editor.putLong( |
+ OmahaBase.PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT, mTimestampForNextPostAttempt); |
+ editor.putLong(OmahaBase.PREF_TIMESTAMP_FOR_NEW_REQUEST, mTimestampForNewRequest); |
+ editor.putLong(OmahaBase.PREF_TIMESTAMP_OF_REQUEST, |
+ hasRequest() ? mCurrentRequest.getCreationTimestamp() : INVALID_TIMESTAMP); |
+ editor.putString(OmahaBase.PREF_PERSISTED_REQUEST_ID, |
+ hasRequest() ? mCurrentRequest.getRequestID() : INVALID_REQUEST_ID); |
+ editor.putString( |
+ OmahaBase.PREF_LATEST_VERSION, mLatestVersion == null ? "" : mLatestVersion); |
+ editor.putString(OmahaBase.PREF_MARKET_URL, mMarketURL == null ? "" : mMarketURL); |
+ editor.putString(OmahaBase.PREF_INSTALL_SOURCE, mInstallSource); |
+ editor.apply(); |
+ |
+ mDelegate.onSaveStateDone(mTimestampForNewRequest, mTimestampForNextPostAttempt); |
+ } |
+ |
+ private void migrateToNewerChromeVersions() { |
+ // Remove any repeating alarms in favor of the new scheduling setup on M58 and up. |
+ // Seems cheaper to cancel the alarm repeatedly than to store a SharedPreference and never |
+ // do it again. |
+ Intent intent = new Intent(getContext(), OmahaClient.class); |
+ intent.setAction(ACTION_REGISTER_REQUEST); |
+ getBackoffScheduler().cancelAlarm(intent); |
+ } |
+ |
+ private Context getContext() { |
+ return mDelegate.getContext(); |
+ } |
+ |
+ private RequestGenerator getRequestGenerator() { |
+ return mDelegate.getRequestGenerator(); |
+ } |
+ |
+ private ExponentialBackoffScheduler getBackoffScheduler() { |
+ return mDelegate.getScheduler(); |
+ } |
+ |
/** Begin communicating with the Omaha Update Server. */ |
public static void onForegroundSessionStart(Context context) { |
if (!ChromeVersionInfo.isOfficialBuild() || isDisabled()) return; |
- |
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { |
- OmahaClient.startService(context); |
- } |
+ OmahaService.startServiceImmediately(context); |
} |
/** Checks whether Chrome has ever tried contacting Omaha before. */ |
@@ -102,8 +504,7 @@ public abstract class OmahaBase { |
throws RequestFailureException { |
try { |
if (urlConnection.getResponseCode() != 200) { |
- throw new RequestFailureException( |
- "Received " + urlConnection.getResponseCode() |
+ throw new RequestFailureException("Received " + urlConnection.getResponseCode() |
+ " code instead of 200 (OK) from the server. Aborting."); |
} |
} catch (IOException e) { |