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

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

Issue 2664253005: [Omaha] Move most functionality to OmahaBase, add JobService (Closed)
Patch Set: [Omaha] Move most functionality to OmahaBase, add JobService Created 3 years, 10 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
« no previous file with comments | « no previous file | chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaClient.java » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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) {
« no previous file with comments | « no previous file | chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaClient.java » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698