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