OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.omaha; |
| 6 |
| 7 import android.app.AlarmManager; |
| 8 import android.app.IntentService; |
| 9 import android.app.PendingIntent; |
| 10 import android.content.Context; |
| 11 import android.content.Intent; |
| 12 import android.content.SharedPreferences; |
| 13 import android.content.pm.ApplicationInfo; |
| 14 import android.os.Looper; |
| 15 import android.util.Log; |
| 16 |
| 17 import org.chromium.base.ApiCompatibilityUtils; |
| 18 import org.chromium.base.ApplicationStatus; |
| 19 import org.chromium.base.VisibleForTesting; |
| 20 import org.chromium.base.annotations.SuppressFBWarnings; |
| 21 import org.chromium.chrome.browser.ChromeMobileApplication; |
| 22 |
| 23 import java.io.BufferedOutputStream; |
| 24 import java.io.BufferedReader; |
| 25 import java.io.IOException; |
| 26 import java.io.InputStreamReader; |
| 27 import java.io.OutputStream; |
| 28 import java.io.OutputStreamWriter; |
| 29 import java.net.HttpURLConnection; |
| 30 import java.net.MalformedURLException; |
| 31 import java.net.URL; |
| 32 import java.util.Map; |
| 33 import java.util.UUID; |
| 34 |
| 35 /** |
| 36 * Keeps tabs on the current state of Chrome, tracking if and when a request sho
uld be sent to the |
| 37 * Omaha Server. |
| 38 * |
| 39 * A hook in ChromeActivity's doDeferredResume() initializes the service. Furth
er attempts to |
| 40 * reschedule events will be scheduled by the class itself. |
| 41 * |
| 42 * Each request to the server will perform an update check and ping the server. |
| 43 * We use a repeating alarm to schedule the XML requests to be generated 5 hours
apart. |
| 44 * If Chrome isn't running when the alarm is fired, the request generation will
be stalled until |
| 45 * the next time Chrome runs. |
| 46 * |
| 47 * mevissen suggested being conservative with our timers for sending requests. |
| 48 * POST attempts that fail to be acknowledged by the server are re-attempted, wi
th at least |
| 49 * one hour between each attempt. |
| 50 * |
| 51 * Status is saved directly to the the disk after every operation. Unit tests t
esting the code |
| 52 * paths without using Intents may need to call restoreState() manually as it is
not automatically |
| 53 * handled in onCreate(). |
| 54 * |
| 55 * Implementation notes: |
| 56 * http://docs.google.com/a/google.com/document/d/1scTCovqASf5ktkOeVj8wFRkWTCeDY
w2LrOBNn05CDB0/edit |
| 57 */ |
| 58 public class OmahaClient extends IntentService { |
| 59 private static final String TAG = "OmahaClient"; |
| 60 |
| 61 // Intent actions. |
| 62 private static final String ACTION_INITIALIZE = |
| 63 "org.chromium.chrome.browser.omaha.ACTION_INITIALIZE"; |
| 64 private static final String ACTION_REGISTER_REQUEST = |
| 65 "org.chromium.chrome.browser.omaha.ACTION_REGISTER_REQUEST"; |
| 66 private static final String ACTION_POST_REQUEST = |
| 67 "org.chromium.chrome.browser.omaha.ACTION_POST_REQUEST"; |
| 68 |
| 69 // Strings for extras. |
| 70 private static final String EXTRA_FORCE_ACTION = "forceAction"; |
| 71 |
| 72 // Delays between events. |
| 73 private static final long MS_PER_HOUR = 3600000; |
| 74 private static final long MS_POST_BASE_DELAY = MS_PER_HOUR; |
| 75 private static final long MS_POST_MAX_DELAY = 5 * MS_PER_HOUR; |
| 76 private static final long MS_BETWEEN_REQUESTS = 5 * MS_PER_HOUR; |
| 77 private static final int MS_CONNECTION_TIMEOUT = 60000; |
| 78 |
| 79 // Flags for retrieving the OmahaClient's state after it's written to disk. |
| 80 // The PREF_PACKAGE doesn't match the current OmahaClient package for histor
ical reasons. |
| 81 @VisibleForTesting |
| 82 static final String PREF_PACKAGE = "com.google.android.apps.chrome.omaha"; |
| 83 @VisibleForTesting |
| 84 static final String PREF_PERSISTED_REQUEST_ID = "persistedRequestID"; |
| 85 @VisibleForTesting |
| 86 static final String PREF_TIMESTAMP_OF_REQUEST = "timestampOfRequest"; |
| 87 @VisibleForTesting |
| 88 static final String PREF_INSTALL_SOURCE = "installSource"; |
| 89 private static final String PREF_SEND_INSTALL_EVENT = "sendInstallEvent"; |
| 90 private static final String PREF_TIMESTAMP_OF_INSTALL = "timestampOfInstall"
; |
| 91 private static final String PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT = |
| 92 "timestampForNextPostAttempt"; |
| 93 private static final String PREF_TIMESTAMP_FOR_NEW_REQUEST = "timestampForNe
wRequest"; |
| 94 |
| 95 // Strings indicating how the Chrome APK arrived on the user's device. These
values MUST NOT |
| 96 // be changed without updating the corresponding Omaha server strings. |
| 97 static final String INSTALL_SOURCE_SYSTEM = "system_image"; |
| 98 static final String INSTALL_SOURCE_ORGANIC = "organic"; |
| 99 |
| 100 // Lock object used to synchronize all calls that modify or read sIsFreshIns
tallOrDataCleared. |
| 101 private static final Object sIsFreshInstallLock = new Object(); |
| 102 |
| 103 @VisibleForTesting |
| 104 static final String PREF_LATEST_VERSION = "latestVersion"; |
| 105 @VisibleForTesting |
| 106 static final String PREF_MARKET_URL = "marketURL"; |
| 107 |
| 108 private static final long INVALID_TIMESTAMP = -1; |
| 109 @VisibleForTesting |
| 110 static final String INVALID_REQUEST_ID = "invalid"; |
| 111 |
| 112 // Static fields |
| 113 private static boolean sEnableCommunication = true; |
| 114 private static boolean sEnableUpdateDetection = true; |
| 115 private static VersionNumberGetter sVersionNumberGetter = null; |
| 116 private static MarketURLGetter sMarketURLGetter = null; |
| 117 private static Boolean sIsFreshInstallOrDataCleared = null; |
| 118 |
| 119 // Member fields not persisted to disk. |
| 120 private boolean mStateHasBeenRestored; |
| 121 private Context mApplicationContext; |
| 122 private ExponentialBackoffScheduler mBackoffScheduler; |
| 123 private RequestGenerator mGenerator; |
| 124 |
| 125 // State saved written to and read from disk. |
| 126 private RequestData mCurrentRequest; |
| 127 private long mTimestampOfInstall; |
| 128 private long mTimestampForNextPostAttempt; |
| 129 private long mTimestampForNewRequest; |
| 130 private String mLatestVersion; |
| 131 private String mMarketURL; |
| 132 private String mInstallSource; |
| 133 protected boolean mSendInstallEvent; |
| 134 |
| 135 public OmahaClient() { |
| 136 super(TAG); |
| 137 setIntentRedelivery(true); |
| 138 } |
| 139 |
| 140 @Override |
| 141 public void onCreate() { |
| 142 super.onCreate(); |
| 143 mApplicationContext = getApplicationContext(); |
| 144 mBackoffScheduler = createBackoffScheduler(PREF_PACKAGE, mApplicationCon
text, |
| 145 MS_POST_BASE_DELAY, MS_POST_MAX_DELAY); |
| 146 mGenerator = createRequestGenerator(mApplicationContext); |
| 147 } |
| 148 |
| 149 /** |
| 150 * Sets whether Chrome should be communicating with the Omaha server. |
| 151 * The alternative to using a static field within OmahaClient is using a mem
ber variable in |
| 152 * the ChromeTabbedActivity. The problem is that it is difficult to set the
variable before |
| 153 * ChromeTabbedActivity is started. |
| 154 */ |
| 155 @VisibleForTesting |
| 156 public static void setEnableCommunication(boolean state) { |
| 157 sEnableCommunication = state; |
| 158 } |
| 159 |
| 160 /** |
| 161 * If false, OmahaClient will never report that a newer version is available
. |
| 162 */ |
| 163 @VisibleForTesting |
| 164 public static void setEnableUpdateDetection(boolean state) { |
| 165 sEnableUpdateDetection = state; |
| 166 } |
| 167 |
| 168 @VisibleForTesting |
| 169 long getTimestampForNextPostAttempt() { |
| 170 return mTimestampForNextPostAttempt; |
| 171 } |
| 172 |
| 173 @VisibleForTesting |
| 174 long getTimestampForNewRequest() { |
| 175 return mTimestampForNewRequest; |
| 176 } |
| 177 |
| 178 @VisibleForTesting |
| 179 int getCumulativeFailedAttempts() { |
| 180 return mBackoffScheduler.getNumFailedAttempts(); |
| 181 } |
| 182 |
| 183 /** |
| 184 * Creates the scheduler used to space out POST attempts. |
| 185 */ |
| 186 @VisibleForTesting |
| 187 ExponentialBackoffScheduler createBackoffScheduler(String prefPackage, Conte
xt context, |
| 188 long base, long max) { |
| 189 return new ExponentialBackoffScheduler(prefPackage, context, base, max); |
| 190 } |
| 191 |
| 192 /** |
| 193 * Creates the request generator used to create Omaha XML. |
| 194 */ |
| 195 @VisibleForTesting |
| 196 RequestGenerator createRequestGenerator(Context context) { |
| 197 return ((ChromeMobileApplication) getApplicationContext()).createOmahaRe
questGenerator(); |
| 198 } |
| 199 |
| 200 /** |
| 201 * Handles an action on a thread separate from the UI thread. |
| 202 * @param intent Intent fired by some part of Chrome. |
| 203 */ |
| 204 @Override |
| 205 public void onHandleIntent(Intent intent) { |
| 206 assert Looper.myLooper() != Looper.getMainLooper(); |
| 207 |
| 208 if (!sEnableCommunication) { |
| 209 Log.v(TAG, "Disabled. Ignoring intent."); |
| 210 return; |
| 211 } |
| 212 |
| 213 if (mGenerator == null) { |
| 214 Log.e(TAG, "No request generator set. Ignoring intent."); |
| 215 return; |
| 216 } |
| 217 |
| 218 if (!mStateHasBeenRestored) { |
| 219 restoreState(); |
| 220 } |
| 221 |
| 222 if (ACTION_INITIALIZE.equals(intent.getAction())) { |
| 223 handleInitialize(); |
| 224 } else if (ACTION_REGISTER_REQUEST.equals(intent.getAction())) { |
| 225 handleRegisterRequest(intent); |
| 226 } else if (ACTION_POST_REQUEST.equals(intent.getAction())) { |
| 227 handlePostRequestIntent(intent); |
| 228 } else { |
| 229 Log.e(TAG, "Got unknown action from intent: " + intent.getAction()); |
| 230 } |
| 231 } |
| 232 |
| 233 public static Intent createInitializeIntent(Context context) { |
| 234 Intent intent = new Intent(context, OmahaClient.class); |
| 235 intent.setAction(ACTION_INITIALIZE); |
| 236 return intent; |
| 237 } |
| 238 |
| 239 /** |
| 240 * Start a recurring alarm to fire request generation intents. |
| 241 */ |
| 242 private void handleInitialize() { |
| 243 scheduleRepeatingAlarm(); |
| 244 |
| 245 // If a request exists, fire a POST intent to restart its timer. |
| 246 if (hasRequest()) { |
| 247 Intent postIntent = createPostRequestIntent(mApplicationContext, fal
se); |
| 248 startService(postIntent); |
| 249 } |
| 250 } |
| 251 |
| 252 public static Intent createRegisterRequestIntent(Context context, boolean fo
rce) { |
| 253 Intent intent = new Intent(context, OmahaClient.class); |
| 254 intent.setAction(ACTION_REGISTER_REQUEST); |
| 255 intent.putExtra(EXTRA_FORCE_ACTION, force); |
| 256 return intent; |
| 257 } |
| 258 |
| 259 /** |
| 260 * Determines if a new request should be generated. New requests are only g
enerated if enough |
| 261 * time has passed between now and the last time a request was generated. |
| 262 */ |
| 263 private void handleRegisterRequest(Intent intent) { |
| 264 boolean force = intent.getBooleanExtra(EXTRA_FORCE_ACTION, false); |
| 265 if (!isChromeBeingUsed() && !force) { |
| 266 cancelRepeatingAlarm(); |
| 267 return; |
| 268 } |
| 269 |
| 270 // If the current request is too old, generate a new one. |
| 271 long currentTimestamp = mBackoffScheduler.getCurrentTime(); |
| 272 boolean isTooOld = hasRequest() |
| 273 && mCurrentRequest.getAgeInMilliseconds(currentTimestamp) >= MS_
BETWEEN_REQUESTS; |
| 274 boolean isOverdue = !hasRequest() && currentTimestamp >= mTimestampForNe
wRequest; |
| 275 if (isTooOld || isOverdue || force) { |
| 276 registerNewRequest(currentTimestamp); |
| 277 } |
| 278 |
| 279 // Create an intent to send the request. If we're forcing a registratio
n, force the POST, |
| 280 // as well. |
| 281 if (hasRequest()) { |
| 282 Intent postIntent = createPostRequestIntent(mApplicationContext, for
ce); |
| 283 startService(postIntent); |
| 284 } |
| 285 } |
| 286 |
| 287 public static Intent createPostRequestIntent(Context context, boolean force)
{ |
| 288 Intent intent = new Intent(context, OmahaClient.class); |
| 289 intent.setAction(ACTION_POST_REQUEST); |
| 290 intent.putExtra(EXTRA_FORCE_ACTION, force); |
| 291 return intent; |
| 292 } |
| 293 |
| 294 /** |
| 295 * Sends the request it is holding. |
| 296 */ |
| 297 @VisibleForTesting |
| 298 private void handlePostRequestIntent(Intent intent) { |
| 299 if (!hasRequest()) { |
| 300 return; |
| 301 } |
| 302 |
| 303 boolean force = intent.getBooleanExtra(EXTRA_FORCE_ACTION, false); |
| 304 |
| 305 // If enough time has passed since the last attempt, try sending a reque
st. |
| 306 long currentTimestamp = mBackoffScheduler.getCurrentTime(); |
| 307 if (currentTimestamp >= mTimestampForNextPostAttempt || force) { |
| 308 // All requests made during the same session should have the same ID
. |
| 309 String sessionID = generateRandomUUID(); |
| 310 boolean sendingInstallRequest = mSendInstallEvent; |
| 311 boolean succeeded = generateAndPostRequest(currentTimestamp, session
ID); |
| 312 |
| 313 if (succeeded && sendingInstallRequest) { |
| 314 // Only the first request ever generated should contain an insta
ll event. |
| 315 mSendInstallEvent = false; |
| 316 |
| 317 // Create and immediately send another request for a ping and up
date check. |
| 318 registerNewRequest(currentTimestamp); |
| 319 succeeded = generateAndPostRequest(currentTimestamp, sessionID); |
| 320 } |
| 321 |
| 322 if (force) { |
| 323 if (succeeded) { |
| 324 Log.v(TAG, "Requests successfully sent to Omaha server."); |
| 325 } else { |
| 326 Log.e(TAG, "Requests failed to reach Omaha server."); |
| 327 } |
| 328 } |
| 329 } else { |
| 330 // Set an alarm to POST at the proper time. Previous alarms are des
troyed. |
| 331 Intent postIntent = createPostRequestIntent(mApplicationContext, fal
se); |
| 332 mBackoffScheduler.createAlarm(postIntent, mTimestampForNextPostAttem
pt); |
| 333 } |
| 334 |
| 335 // Write everything back out again to save our state. |
| 336 saveState(); |
| 337 } |
| 338 |
| 339 private boolean generateAndPostRequest(long currentTimestamp, String session
ID) { |
| 340 try { |
| 341 // Generate the XML for the current request. |
| 342 long installAgeInDays = RequestGenerator.installAge(currentTimestamp
, |
| 343 mTimestampOfInstall, mCurrentRequest.isSendInstallEvent()); |
| 344 String version = getVersionNumberGetter().getCurrentlyUsedVersion(mA
pplicationContext); |
| 345 String xml = |
| 346 mGenerator.generateXML(sessionID, version, installAgeInDays,
mCurrentRequest); |
| 347 |
| 348 // Send the request to the server & wait for a response. |
| 349 String response = postRequest(currentTimestamp, xml); |
| 350 parseServerResponse(response); |
| 351 |
| 352 // If we've gotten this far, we've successfully sent a request. |
| 353 mCurrentRequest = null; |
| 354 mTimestampForNextPostAttempt = currentTimestamp + MS_POST_BASE_DELAY
; |
| 355 mBackoffScheduler.resetFailedAttempts(); |
| 356 Log.i(TAG, "Request to Server Successful. Timestamp for next request
:" |
| 357 + String.valueOf(mTimestampForNextPostAttempt)); |
| 358 |
| 359 return true; |
| 360 } catch (RequestFailureException e) { |
| 361 // Set the alarm to try again later. |
| 362 Log.e(TAG, "Failed to contact server: ", e); |
| 363 Intent postIntent = createPostRequestIntent(mApplicationContext, fal
se); |
| 364 mTimestampForNextPostAttempt = mBackoffScheduler.createAlarm(postInt
ent); |
| 365 mBackoffScheduler.increaseFailedAttempts(); |
| 366 return false; |
| 367 } |
| 368 } |
| 369 |
| 370 /** |
| 371 * Sets a repeating alarm that fires request registration Intents. |
| 372 * Setting the alarm overwrites whatever alarm is already there, and rebooti
ng |
| 373 * clears whatever alarms are currently set. |
| 374 */ |
| 375 private void scheduleRepeatingAlarm() { |
| 376 Intent registerIntent = createRegisterRequestIntent(mApplicationContext,
false); |
| 377 PendingIntent pIntent = |
| 378 PendingIntent.getService(mApplicationContext, 0, registerIntent,
0); |
| 379 AlarmManager am = |
| 380 (AlarmManager) mApplicationContext.getSystemService(Context.ALAR
M_SERVICE); |
| 381 setAlarm(am, pIntent, AlarmManager.RTC, mTimestampForNewRequest); |
| 382 } |
| 383 |
| 384 /** |
| 385 * Sets up a timer to fire after each interval. |
| 386 * Override to prevent a real alarm from being set. |
| 387 */ |
| 388 @VisibleForTesting |
| 389 protected void setAlarm(AlarmManager am, PendingIntent operation, int alarmT
ype, |
| 390 long triggerAtTime) { |
| 391 am.setRepeating(AlarmManager.RTC, triggerAtTime, MS_BETWEEN_REQUESTS, op
eration); |
| 392 } |
| 393 |
| 394 /** |
| 395 * Cancels the alarm that launches this service. It will be replaced when C
hrome next resumes. |
| 396 */ |
| 397 private void cancelRepeatingAlarm() { |
| 398 Intent requestIntent = createRegisterRequestIntent(mApplicationContext,
false); |
| 399 PendingIntent pendingIntent = PendingIntent.getService(mApplicationConte
xt, 0, |
| 400 requestIntent, PendingIntent.FLAG_NO_CREATE); |
| 401 // Setting FLAG_NO_CREATE forces Android to return an already existing P
endingIntent. |
| 402 // Here it would be the one that was used to create the existing alarm (
if it exists). |
| 403 // If the pendingIntent is null, it is likely that no alarm was created. |
| 404 if (pendingIntent != null) { |
| 405 AlarmManager am = |
| 406 (AlarmManager) mApplicationContext.getSystemService(Context.
ALARM_SERVICE); |
| 407 am.cancel(pendingIntent); |
| 408 pendingIntent.cancel(); |
| 409 } |
| 410 } |
| 411 |
| 412 /** |
| 413 * Determine whether or not Chrome is currently being used actively. |
| 414 */ |
| 415 @VisibleForTesting |
| 416 protected boolean isChromeBeingUsed() { |
| 417 boolean isChromeVisible = ApplicationStatus.hasVisibleActivities(); |
| 418 boolean isScreenOn = ApiCompatibilityUtils.isInteractive(mApplicationCon
text); |
| 419 return isChromeVisible && isScreenOn; |
| 420 } |
| 421 |
| 422 /** |
| 423 * Registers a new request with the current timestamp. Internal timestamps
are reset to start |
| 424 * fresh. |
| 425 * @param currentTimestamp Current time. |
| 426 */ |
| 427 @VisibleForTesting |
| 428 void registerNewRequest(long currentTimestamp) { |
| 429 mCurrentRequest = createRequestData(currentTimestamp, null); |
| 430 mBackoffScheduler.resetFailedAttempts(); |
| 431 mTimestampForNextPostAttempt = currentTimestamp; |
| 432 |
| 433 // Tentatively set the timestamp for a new request. This will be update
d when the server |
| 434 // is successfully contacted. |
| 435 mTimestampForNewRequest = currentTimestamp + MS_BETWEEN_REQUESTS; |
| 436 scheduleRepeatingAlarm(); |
| 437 |
| 438 saveState(); |
| 439 } |
| 440 |
| 441 private RequestData createRequestData(long currentTimestamp, String persiste
dID) { |
| 442 // If we're sending a persisted event, keep trying to send the same requ
est ID. |
| 443 String requestID; |
| 444 if (persistedID == null || INVALID_REQUEST_ID.equals(persistedID)) { |
| 445 requestID = generateRandomUUID(); |
| 446 } else { |
| 447 requestID = persistedID; |
| 448 } |
| 449 return new RequestData(mSendInstallEvent, currentTimestamp, requestID, m
InstallSource); |
| 450 } |
| 451 |
| 452 @VisibleForTesting |
| 453 boolean hasRequest() { |
| 454 return mCurrentRequest != null; |
| 455 } |
| 456 |
| 457 /** |
| 458 * Posts the request to the Omaha server. |
| 459 * @return the XML response as a String. |
| 460 * @throws RequestFailureException if the request fails. |
| 461 */ |
| 462 @VisibleForTesting |
| 463 String postRequest(long timestamp, String xml) throws RequestFailureExceptio
n { |
| 464 String response = null; |
| 465 |
| 466 HttpURLConnection urlConnection = null; |
| 467 try { |
| 468 urlConnection = createConnection(); |
| 469 setUpPostRequest(timestamp, urlConnection, xml); |
| 470 sendRequestToServer(urlConnection, xml); |
| 471 response = readResponseFromServer(urlConnection); |
| 472 } finally { |
| 473 if (urlConnection != null) { |
| 474 urlConnection.disconnect(); |
| 475 } |
| 476 } |
| 477 |
| 478 return response; |
| 479 } |
| 480 |
| 481 /** |
| 482 * Parse the server's response and confirm that we received an OK response. |
| 483 */ |
| 484 private void parseServerResponse(String response) throws RequestFailureExcep
tion { |
| 485 String appId = mGenerator.getAppId(); |
| 486 boolean sentPingAndUpdate = !mSendInstallEvent; |
| 487 ResponseParser parser = |
| 488 new ResponseParser(appId, mSendInstallEvent, sentPingAndUpdate,
sentPingAndUpdate); |
| 489 parser.parseResponse(response); |
| 490 mTimestampForNewRequest = mBackoffScheduler.getCurrentTime() + MS_BETWEE
N_REQUESTS; |
| 491 mLatestVersion = parser.getNewVersion(); |
| 492 mMarketURL = parser.getURL(); |
| 493 scheduleRepeatingAlarm(); |
| 494 } |
| 495 |
| 496 /** |
| 497 * Returns a HttpURLConnection to the server. |
| 498 */ |
| 499 @VisibleForTesting |
| 500 protected HttpURLConnection createConnection() throws RequestFailureExceptio
n { |
| 501 try { |
| 502 URL url = new URL(mGenerator.getServerUrl()); |
| 503 HttpURLConnection connection = (HttpURLConnection) url.openConnectio
n(); |
| 504 connection.setConnectTimeout(MS_CONNECTION_TIMEOUT); |
| 505 connection.setReadTimeout(MS_CONNECTION_TIMEOUT); |
| 506 return connection; |
| 507 } catch (MalformedURLException e) { |
| 508 throw new RequestFailureException("Caught a malformed URL exception.
", e); |
| 509 } catch (IOException e) { |
| 510 throw new RequestFailureException("Failed to open connection to URL"
, e); |
| 511 } |
| 512 } |
| 513 |
| 514 /** |
| 515 * Prepares the HTTP header. |
| 516 */ |
| 517 private void setUpPostRequest(long timestamp, HttpURLConnection urlConnectio
n, String xml) |
| 518 throws RequestFailureException { |
| 519 try { |
| 520 urlConnection.setDoOutput(true); |
| 521 urlConnection.setFixedLengthStreamingMode(xml.getBytes().length); |
| 522 if (mSendInstallEvent && getCumulativeFailedAttempts() > 0) { |
| 523 String age = Long.toString(mCurrentRequest.getAgeInSeconds(times
tamp)); |
| 524 urlConnection.addRequestProperty("X-RequestAge", age); |
| 525 } |
| 526 } catch (IllegalAccessError e) { |
| 527 throw new RequestFailureException("Caught an IllegalAccessError:", e
); |
| 528 } catch (IllegalArgumentException e) { |
| 529 throw new RequestFailureException("Caught an IllegalArgumentExceptio
n:", e); |
| 530 } catch (IllegalStateException e) { |
| 531 throw new RequestFailureException("Caught an IllegalStateException:"
, e); |
| 532 } |
| 533 } |
| 534 |
| 535 /** |
| 536 * Sends the request to the server. |
| 537 */ |
| 538 private void sendRequestToServer(HttpURLConnection urlConnection, String xml
) |
| 539 throws RequestFailureException { |
| 540 try { |
| 541 OutputStream out = new BufferedOutputStream(urlConnection.getOutputS
tream()); |
| 542 OutputStreamWriter writer = new OutputStreamWriter(out); |
| 543 writer.write(xml, 0, xml.length()); |
| 544 writer.close(); |
| 545 checkServerResponseCode(urlConnection); |
| 546 } catch (IOException e) { |
| 547 throw new RequestFailureException("Failed to write request to server
: ", e); |
| 548 } |
| 549 } |
| 550 |
| 551 /** |
| 552 * Reads the response from the Omaha Server. |
| 553 */ |
| 554 private String readResponseFromServer(HttpURLConnection urlConnection) |
| 555 throws RequestFailureException { |
| 556 try { |
| 557 InputStreamReader reader = new InputStreamReader(urlConnection.getIn
putStream()); |
| 558 BufferedReader in = new BufferedReader(reader); |
| 559 try { |
| 560 StringBuilder response = new StringBuilder(); |
| 561 for (String line = in.readLine(); line != null; line = in.readLi
ne()) { |
| 562 response.append(line); |
| 563 } |
| 564 checkServerResponseCode(urlConnection); |
| 565 return response.toString(); |
| 566 } finally { |
| 567 in.close(); |
| 568 } |
| 569 } catch (IOException e) { |
| 570 throw new RequestFailureException("Failed when reading response from
server: ", e); |
| 571 } |
| 572 } |
| 573 |
| 574 /** |
| 575 * Confirms that the Omaha server sent back an "OK" code. |
| 576 */ |
| 577 private void checkServerResponseCode(HttpURLConnection urlConnection) |
| 578 throws RequestFailureException { |
| 579 try { |
| 580 if (urlConnection.getResponseCode() != 200) { |
| 581 throw new RequestFailureException( |
| 582 "Received " + urlConnection.getResponseCode() |
| 583 + " code instead of 200 (OK) from the server. Aborting.
"); |
| 584 } |
| 585 } catch (IOException e) { |
| 586 throw new RequestFailureException("Failed to read response code from
server: ", e); |
| 587 } |
| 588 } |
| 589 |
| 590 /** |
| 591 * Checks if we know about a newer version available than the one we're usin
g. This does not |
| 592 * actually fire any requests over to the server; it just checks the version
we stored the last |
| 593 * time we talked to the Omaha server. |
| 594 * |
| 595 * NOTE: This function incurs I/O, so don't use it on the main thread. |
| 596 */ |
| 597 public static boolean isNewerVersionAvailable(Context applicationContext) { |
| 598 assert Looper.myLooper() != Looper.getMainLooper(); |
| 599 |
| 600 // This may be explicitly enabled for some channels and for unit tests. |
| 601 if (!sEnableUpdateDetection) { |
| 602 return false; |
| 603 } |
| 604 |
| 605 // If the market link is bad, don't show an update to avoid frustrating
users trying to |
| 606 // hit the "Update" button. |
| 607 if ("".equals(getMarketURL(applicationContext))) { |
| 608 return false; |
| 609 } |
| 610 |
| 611 // Compare version numbers. |
| 612 VersionNumberGetter getter = getVersionNumberGetter(); |
| 613 String currentStr = getter.getCurrentlyUsedVersion(applicationContext); |
| 614 String latestStr = |
| 615 getter.getLatestKnownVersion(applicationContext, PREF_PACKAGE, P
REF_LATEST_VERSION); |
| 616 |
| 617 VersionNumber currentVersionNumber = VersionNumber.fromString(currentStr
); |
| 618 VersionNumber latestVersionNumber = VersionNumber.fromString(latestStr); |
| 619 |
| 620 if (currentVersionNumber == null || latestVersionNumber == null) { |
| 621 return false; |
| 622 } |
| 623 |
| 624 return currentVersionNumber.isSmallerThan(latestVersionNumber); |
| 625 } |
| 626 |
| 627 /** |
| 628 * Determine how the Chrome APK arrived on the device. |
| 629 * @param context Context to pull resources from. |
| 630 * @return A String indicating the install source. |
| 631 */ |
| 632 String determineInstallSource(Context context) { |
| 633 boolean isInSystemImage = (getApplicationFlags() & ApplicationInfo.FLAG_
SYSTEM) != 0; |
| 634 return isInSystemImage ? INSTALL_SOURCE_SYSTEM : INSTALL_SOURCE_ORGANIC; |
| 635 } |
| 636 |
| 637 /** |
| 638 * Returns the Application's flags, used to determine if Chrome was installe
d as part of the |
| 639 * system image. |
| 640 * @return The Application's flags. |
| 641 */ |
| 642 @VisibleForTesting |
| 643 public int getApplicationFlags() { |
| 644 return mApplicationContext.getApplicationInfo().flags; |
| 645 } |
| 646 |
| 647 /** |
| 648 * Reads the data back from the file it was saved to. Uses SharedPreference
s to handle I/O. |
| 649 * Sanity checks are performed on the timestamps to guard against clock chan
ging. |
| 650 */ |
| 651 @VisibleForTesting |
| 652 void restoreState() { |
| 653 boolean mustRewriteState = false; |
| 654 SharedPreferences preferences = |
| 655 mApplicationContext.getSharedPreferences(PREF_PACKAGE, Context.M
ODE_PRIVATE); |
| 656 Map<String, ?> items = preferences.getAll(); |
| 657 |
| 658 // Read out the recorded data. |
| 659 long currentTime = mBackoffScheduler.getCurrentTime(); |
| 660 mTimestampForNewRequest = |
| 661 getLongFromMap(items, PREF_TIMESTAMP_FOR_NEW_REQUEST, currentTim
e); |
| 662 mTimestampForNextPostAttempt = |
| 663 getLongFromMap(items, PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT, curr
entTime); |
| 664 |
| 665 long requestTimestamp = getLongFromMap(items, PREF_TIMESTAMP_OF_REQUEST,
INVALID_TIMESTAMP); |
| 666 |
| 667 // If the preference doesn't exist, it's likely that we haven't sent an
install event. |
| 668 mSendInstallEvent = getBooleanFromMap(items, PREF_SEND_INSTALL_EVENT, tr
ue); |
| 669 |
| 670 // Restore the install source. |
| 671 String defaultInstallSource = determineInstallSource(mApplicationContext
); |
| 672 mInstallSource = getStringFromMap(items, PREF_INSTALL_SOURCE, defaultIns
tallSource); |
| 673 |
| 674 // If we're not sending an install event, don't bother restoring the req
uest ID: |
| 675 // the server does not expect to have persisted request IDs for pings or
update checks. |
| 676 String persistedRequestId = mSendInstallEvent |
| 677 ? getStringFromMap(items, PREF_PERSISTED_REQUEST_ID, INVALID_REQ
UEST_ID) |
| 678 : INVALID_REQUEST_ID; |
| 679 |
| 680 mCurrentRequest = requestTimestamp == INVALID_TIMESTAMP |
| 681 ? null : createRequestData(requestTimestamp, persistedRequestId)
; |
| 682 |
| 683 mLatestVersion = getStringFromMap(items, PREF_LATEST_VERSION, ""); |
| 684 mMarketURL = getStringFromMap(items, PREF_MARKET_URL, ""); |
| 685 |
| 686 // If we don't have a timestamp for when we installed Chrome, then set i
t to now. |
| 687 mTimestampOfInstall = getLongFromMap(items, PREF_TIMESTAMP_OF_INSTALL, c
urrentTime); |
| 688 |
| 689 // Confirm that the timestamp for the next request is less than the base
delay. |
| 690 long delayToNewRequest = mTimestampForNewRequest - currentTime; |
| 691 if (delayToNewRequest > MS_BETWEEN_REQUESTS) { |
| 692 Log.w(TAG, "Delay to next request (" + delayToNewRequest |
| 693 + ") is longer than expected. Resetting to now."); |
| 694 mTimestampForNewRequest = currentTime; |
| 695 mustRewriteState = true; |
| 696 } |
| 697 |
| 698 // Confirm that the timestamp for the next POST is less than the current
delay. |
| 699 long delayToNextPost = mTimestampForNextPostAttempt - currentTime; |
| 700 if (delayToNextPost > mBackoffScheduler.getGeneratedDelay()) { |
| 701 Log.w(TAG, "Delay to next post attempt (" + delayToNextPost |
| 702 + ") is greater than expected (" + mBackoffScheduler.getGene
ratedDelay() |
| 703 + "). Resetting to now."); |
| 704 mTimestampForNextPostAttempt = currentTime; |
| 705 mustRewriteState = true; |
| 706 } |
| 707 |
| 708 if (mustRewriteState) { |
| 709 saveState(); |
| 710 } |
| 711 |
| 712 mStateHasBeenRestored = true; |
| 713 } |
| 714 |
| 715 /** |
| 716 * Writes out the current state to a file. |
| 717 */ |
| 718 private void saveState() { |
| 719 SharedPreferences prefs = |
| 720 mApplicationContext.getSharedPreferences(PREF_PACKAGE, Context.M
ODE_PRIVATE); |
| 721 SharedPreferences.Editor editor = prefs.edit(); |
| 722 editor.putBoolean(PREF_SEND_INSTALL_EVENT, mSendInstallEvent); |
| 723 setIsFreshInstallOrDataHasBeenCleared(mApplicationContext); |
| 724 editor.putLong(PREF_TIMESTAMP_OF_INSTALL, mTimestampOfInstall); |
| 725 editor.putLong(PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT, mTimestampForNextPo
stAttempt); |
| 726 editor.putLong(PREF_TIMESTAMP_FOR_NEW_REQUEST, mTimestampForNewRequest); |
| 727 editor.putLong(PREF_TIMESTAMP_OF_REQUEST, |
| 728 hasRequest() ? mCurrentRequest.getCreationTimestamp() : INVALID_
TIMESTAMP); |
| 729 editor.putString(PREF_PERSISTED_REQUEST_ID, |
| 730 hasRequest() ? mCurrentRequest.getRequestID() : INVALID_REQUEST_
ID); |
| 731 editor.putString(PREF_LATEST_VERSION, mLatestVersion == null ? "" : mLat
estVersion); |
| 732 editor.putString(PREF_MARKET_URL, mMarketURL == null ? "" : mMarketURL); |
| 733 |
| 734 if (mInstallSource != null) editor.putString(PREF_INSTALL_SOURCE, mInsta
llSource); |
| 735 |
| 736 editor.apply(); |
| 737 } |
| 738 |
| 739 /** |
| 740 * Generates a random UUID. |
| 741 */ |
| 742 @VisibleForTesting |
| 743 protected String generateRandomUUID() { |
| 744 return UUID.randomUUID().toString(); |
| 745 } |
| 746 |
| 747 /** |
| 748 * Sets the VersionNumberGetter used to get version numbers. Set a new one
to override what |
| 749 * version numbers are returned. |
| 750 */ |
| 751 @VisibleForTesting |
| 752 static void setVersionNumberGetterForTests(VersionNumberGetter getter) { |
| 753 sVersionNumberGetter = getter; |
| 754 } |
| 755 |
| 756 @SuppressFBWarnings("LI_LAZY_INIT_STATIC") |
| 757 @VisibleForTesting |
| 758 static VersionNumberGetter getVersionNumberGetter() { |
| 759 if (sVersionNumberGetter == null) { |
| 760 sVersionNumberGetter = new VersionNumberGetter(); |
| 761 } |
| 762 return sVersionNumberGetter; |
| 763 } |
| 764 |
| 765 /** |
| 766 * Sets the MarketURLGetter used to get version numbers. Set a new one to o
verride what |
| 767 * URL is returned. |
| 768 */ |
| 769 @VisibleForTesting |
| 770 static void setMarketURLGetterForTests(MarketURLGetter getter) { |
| 771 sMarketURLGetter = getter; |
| 772 } |
| 773 |
| 774 /** |
| 775 * Returns the stub used to grab the market URL for Chrome. |
| 776 */ |
| 777 @SuppressFBWarnings("LI_LAZY_INIT_STATIC") |
| 778 public static String getMarketURL(Context context) { |
| 779 if (sMarketURLGetter == null) { |
| 780 sMarketURLGetter = new MarketURLGetter(); |
| 781 } |
| 782 return sMarketURLGetter.getMarketURL(context, PREF_PACKAGE, PREF_MARKET_
URL); |
| 783 } |
| 784 |
| 785 /** |
| 786 * Pulls a long from the shared preferences map. |
| 787 */ |
| 788 private static long getLongFromMap(final Map<String, ?> items, String key, l
ong defaultValue) { |
| 789 Long value = (Long) items.get(key); |
| 790 return value != null ? value : defaultValue; |
| 791 } |
| 792 |
| 793 /** |
| 794 * Pulls a string from the shared preferences map. |
| 795 */ |
| 796 private static String getStringFromMap(final Map<String, ?> items, String ke
y, |
| 797 String defaultValue) { |
| 798 String value = (String) items.get(key); |
| 799 return value != null ? value : defaultValue; |
| 800 } |
| 801 |
| 802 /** |
| 803 * Pulls a boolean from the shared preferences map. |
| 804 */ |
| 805 private static boolean getBooleanFromMap(final Map<String, ?> items, String
key, |
| 806 boolean defaultValue) { |
| 807 Boolean value = (Boolean) items.get(key); |
| 808 return value != null ? value : defaultValue; |
| 809 } |
| 810 |
| 811 /** |
| 812 * @return Whether it is either a fresh install or data has been cleared. |
| 813 * PREF_TIMESTAMP_OF_INSTALL is set within the first few seconds after a fre
sh install. |
| 814 * sIsFreshInstallOrDataCleared will be set to true if PREF_TIMESTAMP_OF_INS
TALL has not |
| 815 * been previously set. Else, it will be set to false. sIsFreshInstallOrData
Cleared is |
| 816 * guarded by sLock. |
| 817 * @param applicationContext The current application Context. |
| 818 */ |
| 819 public static boolean isFreshInstallOrDataHasBeenCleared(Context application
Context) { |
| 820 return setIsFreshInstallOrDataHasBeenCleared(applicationContext); |
| 821 } |
| 822 |
| 823 private static boolean setIsFreshInstallOrDataHasBeenCleared(Context applica
tionContext) { |
| 824 synchronized (sIsFreshInstallLock) { |
| 825 if (sIsFreshInstallOrDataCleared == null) { |
| 826 SharedPreferences prefs = applicationContext.getSharedPreference
s( |
| 827 PREF_PACKAGE, Context.MODE_PRIVATE); |
| 828 sIsFreshInstallOrDataCleared = (prefs.getLong(PREF_TIMESTAMP_OF_
INSTALL, -1) == -1); |
| 829 } |
| 830 return sIsFreshInstallOrDataCleared; |
| 831 } |
| 832 } |
| 833 } |
OLD | NEW |