OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package org.chromium.chrome.browser.omaha; | 5 package org.chromium.chrome.browser.omaha; |
6 | 6 |
7 import android.app.IntentService; | 7 import android.app.IntentService; |
8 import android.content.Context; | 8 import android.content.Context; |
9 import android.content.Intent; | 9 import android.content.Intent; |
10 import android.content.SharedPreferences; | |
11 import android.os.Build; | |
12 import android.support.annotation.IntDef; | |
13 | |
14 import org.chromium.base.Log; | |
15 import org.chromium.base.ThreadUtils; | |
16 import org.chromium.base.VisibleForTesting; | |
17 | |
18 import java.io.IOException; | |
19 import java.lang.annotation.Retention; | |
20 import java.lang.annotation.RetentionPolicy; | |
21 import java.net.HttpURLConnection; | |
22 import java.net.MalformedURLException; | |
23 import java.net.URL; | |
24 import java.util.concurrent.TimeUnit; | |
25 | 10 |
26 /** | 11 /** |
27 * Keeps tabs on the current state of Chrome, tracking if and when a request sho
uld be sent to the | 12 * Runs the {@link OmahaBase} pipeline as a {@link IntentService}. |
28 * Omaha Server. | |
29 * | |
30 * A hook in ChromeActivity's doDeferredResume() initializes the service. Furth
er attempts to | |
31 * reschedule events will be scheduled by the class itself. | |
32 * | |
33 * Each request to the server will perform an update check and ping the server. | |
34 * We use a repeating alarm to schedule the XML requests to be generated 5 hours
apart. | |
35 * If Chrome isn't running when the alarm is fired, the request generation will
be stalled until | |
36 * the next time Chrome runs. | |
37 * | |
38 * mevissen suggested being conservative with our timers for sending requests. | |
39 * POST attempts that fail to be acknowledged by the server are re-attempted, wi
th at least | |
40 * one hour between each attempt. | |
41 * | |
42 * Status is saved directly to the the disk after every operation. Unit tests t
esting the code | |
43 * paths without using Intents may need to call restoreState() manually as it is
not automatically | |
44 * handled in onCreate(). | |
45 * | |
46 * Implementation notes: | |
47 * http://docs.google.com/a/google.com/document/d/1scTCovqASf5ktkOeVj8wFRkWTCeDY
w2LrOBNn05CDB0/edit | |
48 * | 13 * |
49 * NOTE: This class can never be renamed because the user may have Intents float
ing around that | 14 * NOTE: This class can never be renamed because the user may have Intents float
ing around that |
50 * reference this class specifically. | 15 * reference this class specifically. |
51 */ | 16 */ |
52 public class OmahaClient extends IntentService { | 17 public class OmahaClient extends IntentService { |
53 // Results of {@link #handlePostRequest()}. | |
54 @Retention(RetentionPolicy.SOURCE) | |
55 @IntDef({POST_RESULT_NO_REQUEST, POST_RESULT_SENT, POST_RESULT_FAILED, POST_
RESULT_SCHEDULED}) | |
56 @interface PostResult {} | |
57 static final int POST_RESULT_NO_REQUEST = 0; | |
58 static final int POST_RESULT_SENT = 1; | |
59 static final int POST_RESULT_FAILED = 2; | |
60 static final int POST_RESULT_SCHEDULED = 3; | |
61 | |
62 private static final String TAG = "omaha"; | 18 private static final String TAG = "omaha"; |
63 | 19 |
64 /** Deprecated; kept around to cancel alarms set for OmahaClient pre-M58. */ | |
65 private static final String ACTION_REGISTER_REQUEST = | |
66 "org.chromium.chrome.browser.omaha.ACTION_REGISTER_REQUEST"; | |
67 | |
68 // Delays between events. | |
69 static final long MS_POST_BASE_DELAY = TimeUnit.HOURS.toMillis(1); | |
70 static final long MS_POST_MAX_DELAY = TimeUnit.HOURS.toMillis(5); | |
71 static final long MS_BETWEEN_REQUESTS = TimeUnit.HOURS.toMillis(5); | |
72 static final int MS_CONNECTION_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(1); | |
73 | |
74 // Strings indicating how the Chrome APK arrived on the user's device. These
values MUST NOT | |
75 // be changed without updating the corresponding Omaha server strings. | |
76 static final String INSTALL_SOURCE_SYSTEM = "system_image"; | |
77 static final String INSTALL_SOURCE_ORGANIC = "organic"; | |
78 | |
79 private static final long INVALID_TIMESTAMP = -1; | |
80 @VisibleForTesting | |
81 static final String INVALID_REQUEST_ID = "invalid"; | |
82 | |
83 // Member fields not persisted to disk. | |
84 private boolean mStateHasBeenRestored; | |
85 private OmahaDelegate mDelegate; | |
86 | |
87 // State saved written to and read from disk. | |
88 private RequestData mCurrentRequest; | |
89 private long mTimestampOfInstall; | |
90 private long mTimestampForNextPostAttempt; | |
91 private long mTimestampForNewRequest; | |
92 private String mLatestVersion; | |
93 private String mMarketURL; | |
94 private String mInstallSource; | |
95 protected boolean mSendInstallEvent; | |
96 | |
97 public OmahaClient() { | 20 public OmahaClient() { |
98 super(TAG); | 21 super(TAG); |
99 setIntentRedelivery(true); | 22 setIntentRedelivery(true); |
100 } | 23 } |
101 | 24 |
102 /** | |
103 * Handles an action on a thread separate from the UI thread. | |
104 * @param intent Intent fired by some part of Chrome. | |
105 */ | |
106 @Override | 25 @Override |
107 public void onHandleIntent(Intent intent) { | 26 public void onHandleIntent(Intent intent) { |
108 assert !ThreadUtils.runningOnUiThread(); | 27 OmahaService.getInstance(this).run(); |
109 run(); | |
110 } | 28 } |
111 | 29 |
112 protected void run() { | 30 static Intent createIntent(Context context) { |
113 if (mDelegate == null) mDelegate = new OmahaDelegateImpl(this); | |
114 | |
115 if (OmahaBase.isDisabled() || Build.VERSION.SDK_INT > Build.VERSION_CODE
S.N | |
116 || getRequestGenerator() == null) { | |
117 Log.v(TAG, "Disabled. Ignoring intent."); | |
118 return; | |
119 } | |
120 | |
121 restoreState(getContext()); | |
122 | |
123 long nextTimestamp = Long.MAX_VALUE; | |
124 if (mDelegate.isChromeBeingUsed()) { | |
125 handleRegisterActiveRequest(); | |
126 nextTimestamp = Math.min(nextTimestamp, mTimestampForNewRequest); | |
127 } | |
128 | |
129 if (hasRequest()) { | |
130 int result = handlePostRequest(); | |
131 if (result == POST_RESULT_FAILED || result == POST_RESULT_SCHEDULED)
{ | |
132 nextTimestamp = Math.min(nextTimestamp, mTimestampForNextPostAtt
empt); | |
133 } | |
134 } | |
135 | |
136 // TODO(dfalcantara): Prevent Omaha code from repeatedly rescheduling it
self immediately in | |
137 // case a scheduling error occurs. | |
138 if (nextTimestamp != Long.MAX_VALUE && nextTimestamp >= 0) { | |
139 mDelegate.scheduleService(this, nextTimestamp); | |
140 } | |
141 saveState(getContext()); | |
142 } | |
143 | |
144 /** | |
145 * Begin communicating with the Omaha Update Server. | |
146 */ | |
147 static void startService(Context context) { | |
148 context.startService(createOmahaIntent(context)); | |
149 } | |
150 | |
151 static Intent createOmahaIntent(Context context) { | |
152 return new Intent(context, OmahaClient.class); | 31 return new Intent(context, OmahaClient.class); |
153 } | 32 } |
154 | |
155 /** | |
156 * Determines if a new request should be generated. New requests are only g
enerated if enough | |
157 * time has passed between now and the last time a request was generated. | |
158 */ | |
159 private void handleRegisterActiveRequest() { | |
160 // If the current request is too old, generate a new one. | |
161 long currentTimestamp = getBackoffScheduler().getCurrentTime(); | |
162 boolean isTooOld = hasRequest() | |
163 && mCurrentRequest.getAgeInMilliseconds(currentTimestamp) >= MS_
BETWEEN_REQUESTS; | |
164 boolean isOverdue = currentTimestamp >= mTimestampForNewRequest; | |
165 if (isTooOld || isOverdue) { | |
166 registerNewRequest(currentTimestamp); | |
167 } | |
168 } | |
169 | |
170 /** | |
171 * Sends the request it is holding. | |
172 */ | |
173 private int handlePostRequest() { | |
174 if (!hasRequest()) { | |
175 mDelegate.onHandlePostRequestDone(POST_RESULT_NO_REQUEST, false); | |
176 return POST_RESULT_NO_REQUEST; | |
177 } | |
178 | |
179 // If enough time has passed since the last attempt, try sending a reque
st. | |
180 int result; | |
181 long currentTimestamp = getBackoffScheduler().getCurrentTime(); | |
182 boolean installEventWasSent = false; | |
183 if (currentTimestamp >= mTimestampForNextPostAttempt) { | |
184 // All requests made during the same session should have the same ID
. | |
185 String sessionID = mDelegate.generateUUID(); | |
186 boolean sendingInstallRequest = mSendInstallEvent; | |
187 boolean succeeded = generateAndPostRequest(currentTimestamp, session
ID); | |
188 | |
189 if (succeeded && sendingInstallRequest) { | |
190 // Only the first request ever generated should contain an insta
ll event. | |
191 mSendInstallEvent = false; | |
192 installEventWasSent = true; | |
193 | |
194 // Create and immediately send another request for a ping and up
date check. | |
195 registerNewRequest(currentTimestamp); | |
196 succeeded &= generateAndPostRequest(currentTimestamp, sessionID)
; | |
197 } | |
198 | |
199 result = succeeded ? POST_RESULT_SENT : POST_RESULT_FAILED; | |
200 } else { | |
201 result = POST_RESULT_SCHEDULED; | |
202 } | |
203 | |
204 mDelegate.onHandlePostRequestDone(result, installEventWasSent); | |
205 return result; | |
206 } | |
207 | |
208 private boolean generateAndPostRequest(long currentTimestamp, String session
ID) { | |
209 ExponentialBackoffScheduler scheduler = getBackoffScheduler(); | |
210 boolean succeeded = false; | |
211 try { | |
212 // Generate the XML for the current request. | |
213 long installAgeInDays = RequestGenerator.installAge(currentTimestamp
, | |
214 mTimestampOfInstall, mCurrentRequest.isSendInstallEvent()); | |
215 String version = VersionNumberGetter.getInstance().getCurrentlyUsedV
ersion(this); | |
216 String xml = getRequestGenerator().generateXML( | |
217 sessionID, version, installAgeInDays, mCurrentRequest); | |
218 | |
219 // Send the request to the server & wait for a response. | |
220 String response = postRequest(currentTimestamp, xml); | |
221 | |
222 // Parse out the response. | |
223 String appId = getRequestGenerator().getAppId(); | |
224 boolean sentPingAndUpdate = !mSendInstallEvent; | |
225 ResponseParser parser = new ResponseParser( | |
226 appId, mSendInstallEvent, sentPingAndUpdate, sentPingAndUpda
te); | |
227 parser.parseResponse(response); | |
228 mLatestVersion = parser.getNewVersion(); | |
229 mMarketURL = parser.getURL(); | |
230 | |
231 succeeded = true; | |
232 } catch (RequestFailureException e) { | |
233 Log.e(TAG, "Failed to contact server: ", e); | |
234 } | |
235 | |
236 if (succeeded) { | |
237 // If we've gotten this far, we've successfully sent a request. | |
238 mCurrentRequest = null; | |
239 | |
240 scheduler.resetFailedAttempts(); | |
241 mTimestampForNewRequest = scheduler.getCurrentTime() + MS_BETWEEN_RE
QUESTS; | |
242 mTimestampForNextPostAttempt = scheduler.calculateNextTimestamp(); | |
243 Log.i(TAG, "Request to Server Successful. Timestamp for next request
:" | |
244 + String.valueOf(mTimestampForNextPostAttempt)); | |
245 } else { | |
246 // Set the alarm to try again later. Failures are incremented after
setting the timer | |
247 // to allow the first failure to incur the minimum base delay betwee
n POSTs. | |
248 mTimestampForNextPostAttempt = scheduler.calculateNextTimestamp(); | |
249 scheduler.increaseFailedAttempts(); | |
250 } | |
251 | |
252 mDelegate.onGenerateAndPostRequestDone(succeeded); | |
253 return succeeded; | |
254 } | |
255 | |
256 /** | |
257 * Registers a new request with the current timestamp. Internal timestamps
are reset to start | |
258 * fresh. | |
259 * @param currentTimestamp Current time. | |
260 */ | |
261 private void registerNewRequest(long currentTimestamp) { | |
262 mCurrentRequest = createRequestData(currentTimestamp, null); | |
263 getBackoffScheduler().resetFailedAttempts(); | |
264 mTimestampForNextPostAttempt = currentTimestamp; | |
265 | |
266 // Tentatively set the timestamp for a new request. This will be update
d when the server | |
267 // is successfully contacted. | |
268 mTimestampForNewRequest = currentTimestamp + MS_BETWEEN_REQUESTS; | |
269 | |
270 mDelegate.onRegisterNewRequestDone(mTimestampForNewRequest, mTimestampFo
rNextPostAttempt); | |
271 } | |
272 | |
273 private RequestData createRequestData(long currentTimestamp, String persiste
dID) { | |
274 // If we're sending a persisted event, keep trying to send the same requ
est ID. | |
275 String requestID; | |
276 if (persistedID == null || INVALID_REQUEST_ID.equals(persistedID)) { | |
277 requestID = mDelegate.generateUUID(); | |
278 } else { | |
279 requestID = persistedID; | |
280 } | |
281 return new RequestData(mSendInstallEvent, currentTimestamp, requestID, m
InstallSource); | |
282 } | |
283 | |
284 private boolean hasRequest() { | |
285 return mCurrentRequest != null; | |
286 } | |
287 | |
288 /** | |
289 * Posts the request to the Omaha server. | |
290 * @return the XML response as a String. | |
291 * @throws RequestFailureException if the request fails. | |
292 */ | |
293 private String postRequest(long timestamp, String xml) throws RequestFailure
Exception { | |
294 String response = null; | |
295 | |
296 HttpURLConnection urlConnection = null; | |
297 try { | |
298 urlConnection = createConnection(); | |
299 | |
300 // Prepare the HTTP header. | |
301 urlConnection.setDoOutput(true); | |
302 urlConnection.setFixedLengthStreamingMode(xml.getBytes().length); | |
303 if (mSendInstallEvent && getBackoffScheduler().getNumFailedAttempts(
) > 0) { | |
304 String age = Long.toString(mCurrentRequest.getAgeInSeconds(times
tamp)); | |
305 urlConnection.addRequestProperty("X-RequestAge", age); | |
306 } | |
307 | |
308 response = OmahaBase.sendRequestToServer(urlConnection, xml); | |
309 } catch (IllegalAccessError e) { | |
310 throw new RequestFailureException("Caught an IllegalAccessError:", e
); | |
311 } catch (IllegalArgumentException e) { | |
312 throw new RequestFailureException("Caught an IllegalArgumentExceptio
n:", e); | |
313 } catch (IllegalStateException e) { | |
314 throw new RequestFailureException("Caught an IllegalStateException:"
, e); | |
315 } finally { | |
316 if (urlConnection != null) { | |
317 urlConnection.disconnect(); | |
318 } | |
319 } | |
320 | |
321 return response; | |
322 } | |
323 | |
324 /** | |
325 * Returns a HttpURLConnection to the server. | |
326 */ | |
327 @VisibleForTesting | |
328 protected HttpURLConnection createConnection() throws RequestFailureExceptio
n { | |
329 try { | |
330 URL url = new URL(getRequestGenerator().getServerUrl()); | |
331 HttpURLConnection connection = (HttpURLConnection) url.openConnectio
n(); | |
332 connection.setConnectTimeout(MS_CONNECTION_TIMEOUT); | |
333 connection.setReadTimeout(MS_CONNECTION_TIMEOUT); | |
334 return connection; | |
335 } catch (MalformedURLException e) { | |
336 throw new RequestFailureException("Caught a malformed URL exception.
", e); | |
337 } catch (IOException e) { | |
338 throw new RequestFailureException("Failed to open connection to URL"
, e); | |
339 } | |
340 } | |
341 | |
342 /** | |
343 * Reads the data back from the file it was saved to. Uses SharedPreference
s to handle I/O. | |
344 * Sanity checks are performed on the timestamps to guard against clock chan
ging. | |
345 */ | |
346 @VisibleForTesting | |
347 void restoreState(Context context) { | |
348 if (mStateHasBeenRestored) return; | |
349 | |
350 String installSource = | |
351 mDelegate.isInSystemImage() ? INSTALL_SOURCE_SYSTEM : INSTALL_SO
URCE_ORGANIC; | |
352 ExponentialBackoffScheduler scheduler = getBackoffScheduler(); | |
353 long currentTime = scheduler.getCurrentTime(); | |
354 | |
355 SharedPreferences preferences = OmahaBase.getSharedPreferences(context); | |
356 mTimestampForNewRequest = | |
357 preferences.getLong(OmahaBase.PREF_TIMESTAMP_FOR_NEW_REQUEST, cu
rrentTime); | |
358 mTimestampForNextPostAttempt = | |
359 preferences.getLong(OmahaBase.PREF_TIMESTAMP_FOR_NEXT_POST_ATTEM
PT, currentTime); | |
360 mTimestampOfInstall = preferences.getLong(OmahaBase.PREF_TIMESTAMP_OF_IN
STALL, currentTime); | |
361 mSendInstallEvent = preferences.getBoolean(OmahaBase.PREF_SEND_INSTALL_E
VENT, true); | |
362 mInstallSource = preferences.getString(OmahaBase.PREF_INSTALL_SOURCE, in
stallSource); | |
363 mLatestVersion = preferences.getString(OmahaBase.PREF_LATEST_VERSION, ""
); | |
364 mMarketURL = preferences.getString(OmahaBase.PREF_MARKET_URL, ""); | |
365 | |
366 // If we're not sending an install event, don't bother restoring the req
uest ID: | |
367 // the server does not expect to have persisted request IDs for pings or
update checks. | |
368 String persistedRequestId = mSendInstallEvent | |
369 ? preferences.getString(OmahaBase.PREF_PERSISTED_REQUEST_ID, INV
ALID_REQUEST_ID) | |
370 : INVALID_REQUEST_ID; | |
371 long requestTimestamp = | |
372 preferences.getLong(OmahaBase.PREF_TIMESTAMP_OF_REQUEST, INVALID
_TIMESTAMP); | |
373 mCurrentRequest = requestTimestamp == INVALID_TIMESTAMP | |
374 ? null : createRequestData(requestTimestamp, persistedRequestId)
; | |
375 | |
376 // Confirm that the timestamp for the next request is less than the base
delay. | |
377 long delayToNewRequest = mTimestampForNewRequest - currentTime; | |
378 if (delayToNewRequest > MS_BETWEEN_REQUESTS) { | |
379 Log.w(TAG, "Delay to next request (" + delayToNewRequest | |
380 + ") is longer than expected. Resetting to now."); | |
381 mTimestampForNewRequest = currentTime; | |
382 } | |
383 | |
384 // Confirm that the timestamp for the next POST is less than the current
delay. | |
385 long delayToNextPost = mTimestampForNextPostAttempt - currentTime; | |
386 long lastGeneratedDelay = scheduler.getGeneratedDelay(); | |
387 if (delayToNextPost > lastGeneratedDelay) { | |
388 Log.w(TAG, "Delay to next post attempt (" + delayToNextPost | |
389 + ") is greater than expected (" + lastGeneratedDela
y | |
390 + "). Resetting to now."); | |
391 mTimestampForNextPostAttempt = currentTime; | |
392 } | |
393 | |
394 migrateToNewerChromeVersions(); | |
395 mStateHasBeenRestored = true; | |
396 } | |
397 | |
398 /** | |
399 * Writes out the current state to a file. | |
400 */ | |
401 private void saveState(Context context) { | |
402 SharedPreferences prefs = OmahaBase.getSharedPreferences(context); | |
403 SharedPreferences.Editor editor = prefs.edit(); | |
404 editor.putBoolean(OmahaBase.PREF_SEND_INSTALL_EVENT, mSendInstallEvent); | |
405 editor.putLong(OmahaBase.PREF_TIMESTAMP_OF_INSTALL, mTimestampOfInstall)
; | |
406 editor.putLong( | |
407 OmahaBase.PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT, mTimestampForNex
tPostAttempt); | |
408 editor.putLong(OmahaBase.PREF_TIMESTAMP_FOR_NEW_REQUEST, mTimestampForNe
wRequest); | |
409 editor.putLong(OmahaBase.PREF_TIMESTAMP_OF_REQUEST, | |
410 hasRequest() ? mCurrentRequest.getCreationTimestamp() : INVALID_
TIMESTAMP); | |
411 editor.putString(OmahaBase.PREF_PERSISTED_REQUEST_ID, | |
412 hasRequest() ? mCurrentRequest.getRequestID() : INVALID_REQUEST_
ID); | |
413 editor.putString( | |
414 OmahaBase.PREF_LATEST_VERSION, mLatestVersion == null ? "" : mLa
testVersion); | |
415 editor.putString(OmahaBase.PREF_MARKET_URL, mMarketURL == null ? "" : mM
arketURL); | |
416 editor.putString(OmahaBase.PREF_INSTALL_SOURCE, mInstallSource); | |
417 editor.apply(); | |
418 | |
419 mDelegate.onSaveStateDone(mTimestampForNewRequest, mTimestampForNextPost
Attempt); | |
420 } | |
421 | |
422 private void migrateToNewerChromeVersions() { | |
423 // Remove any repeating alarms in favor of the new scheduling setup on M
58 and up. | |
424 // Seems cheaper to cancel the alarm repeatedly than to store a SharedPr
eference and never | |
425 // do it again. | |
426 Intent intent = new Intent(getContext(), OmahaClient.class); | |
427 intent.setAction(ACTION_REGISTER_REQUEST); | |
428 getBackoffScheduler().cancelAlarm(intent); | |
429 } | |
430 | |
431 Context getContext() { | |
432 return mDelegate.getContext(); | |
433 } | |
434 | |
435 private RequestGenerator getRequestGenerator() { | |
436 return mDelegate.getRequestGenerator(); | |
437 } | |
438 | |
439 private ExponentialBackoffScheduler getBackoffScheduler() { | |
440 return mDelegate.getScheduler(); | |
441 } | |
442 | |
443 void setDelegateForTests(OmahaDelegate delegate) { | |
444 mDelegate = delegate; | |
445 } | |
446 } | 33 } |
OLD | NEW |