Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: Java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: sw=2 ts=8 et :
3 */
4 /* This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
6 * You can obtain one at http://mozilla.org/MPL/2.0/. */
8 const Cc = Components.classes;
9 const Ci = Components.interfaces;
10 const Cu = Components.utils;
11 const Cr = Components.results;
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
14 Cu.import("resource://gre/modules/Services.jsm");
15 Cu.import("resource://gre/modules/WebappsUpdater.jsm");
17 const VERBOSE = 1;
18 let log =
19 VERBOSE ?
20 function log_dump(msg) { dump("UpdatePrompt: "+ msg +"\n"); } :
21 function log_noop(msg) { };
23 const PREF_APPLY_PROMPT_TIMEOUT = "b2g.update.apply-prompt-timeout";
24 const PREF_APPLY_IDLE_TIMEOUT = "b2g.update.apply-idle-timeout";
25 const PREF_DOWNLOAD_WATCHDOG_TIMEOUT = "b2g.update.download-watchdog-timeout";
26 const PREF_DOWNLOAD_WATCHDOG_MAX_RETRIES = "b2g.update.download-watchdog-max-retries";
28 const NETWORK_ERROR_OFFLINE = 111;
29 const HTTP_ERROR_OFFSET = 1000;
31 const STATE_DOWNLOADING = 'downloading';
33 XPCOMUtils.defineLazyServiceGetter(Services, "aus",
34 "@mozilla.org/updates/update-service;1",
35 "nsIApplicationUpdateService");
37 XPCOMUtils.defineLazyServiceGetter(Services, "um",
38 "@mozilla.org/updates/update-manager;1",
39 "nsIUpdateManager");
41 XPCOMUtils.defineLazyServiceGetter(Services, "idle",
42 "@mozilla.org/widget/idleservice;1",
43 "nsIIdleService");
45 XPCOMUtils.defineLazyServiceGetter(Services, "settings",
46 "@mozilla.org/settingsService;1",
47 "nsISettingsService");
49 XPCOMUtils.defineLazyServiceGetter(Services, 'env',
50 '@mozilla.org/process/environment;1',
51 'nsIEnvironment');
53 function useSettings() {
54 // When we're running in the real phone, then we can use settings.
55 // But when we're running as part of xpcshell, there is no settings database
56 // and trying to use settings in this scenario causes lots of weird
57 // assertions at shutdown time.
58 if (typeof useSettings.result === "undefined") {
59 useSettings.result = !Services.env.get("XPCSHELL_TEST_PROFILE_DIR");
60 }
61 return useSettings.result;
62 }
64 XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
65 "resource://gre/modules/SystemAppProxy.jsm");
67 function UpdateCheckListener(updatePrompt) {
68 this._updatePrompt = updatePrompt;
69 }
71 UpdateCheckListener.prototype = {
72 QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateCheckListener]),
74 _updatePrompt: null,
76 onCheckComplete: function UCL_onCheckComplete(request, updates, updateCount) {
77 if (Services.um.activeUpdate) {
78 // We're actively downloading an update, that's the update the user should
79 // see, even if a newer update is available.
80 this._updatePrompt.setUpdateStatus("active-update");
81 this._updatePrompt.showUpdateAvailable(Services.um.activeUpdate);
82 return;
83 }
85 if (updateCount == 0) {
86 this._updatePrompt.setUpdateStatus("no-updates");
87 return;
88 }
90 let update = Services.aus.selectUpdate(updates, updateCount);
91 if (!update) {
92 this._updatePrompt.setUpdateStatus("already-latest-version");
93 return;
94 }
96 this._updatePrompt.setUpdateStatus("check-complete");
97 this._updatePrompt.showUpdateAvailable(update);
98 },
100 onError: function UCL_onError(request, update) {
101 // nsIUpdate uses a signed integer for errorCode while any platform errors
102 // require all 32 bits.
103 let errorCode = update.errorCode >>> 0;
104 let isNSError = (errorCode >>> 31) == 1;
106 if (errorCode == NETWORK_ERROR_OFFLINE) {
107 this._updatePrompt.setUpdateStatus("retry-when-online");
108 } else if (isNSError) {
109 this._updatePrompt.setUpdateStatus("check-error-" + errorCode);
110 } else if (errorCode > HTTP_ERROR_OFFSET) {
111 let httpErrorCode = errorCode - HTTP_ERROR_OFFSET;
112 this._updatePrompt.setUpdateStatus("check-error-http-" + httpErrorCode);
113 }
115 Services.aus.QueryInterface(Ci.nsIUpdateCheckListener);
116 Services.aus.onError(request, update);
117 }
118 };
120 function UpdatePrompt() {
121 this.wrappedJSObject = this;
122 this._updateCheckListener = new UpdateCheckListener(this);
123 Services.obs.addObserver(this, "update-check-start", false);
124 }
126 UpdatePrompt.prototype = {
127 classID: Components.ID("{88b3eb21-d072-4e3b-886d-f89d8c49fe59}"),
128 QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePrompt,
129 Ci.nsIUpdateCheckListener,
130 Ci.nsIRequestObserver,
131 Ci.nsIProgressEventSink,
132 Ci.nsIObserver]),
133 _xpcom_factory: XPCOMUtils.generateSingletonFactory(UpdatePrompt),
135 _update: null,
136 _applyPromptTimer: null,
137 _waitingForIdle: false,
138 _updateCheckListner: null,
140 get applyPromptTimeout() {
141 return Services.prefs.getIntPref(PREF_APPLY_PROMPT_TIMEOUT);
142 },
144 get applyIdleTimeout() {
145 return Services.prefs.getIntPref(PREF_APPLY_IDLE_TIMEOUT);
146 },
148 handleContentStart: function UP_handleContentStart() {
149 SystemAppProxy.addEventListener("mozContentEvent", this);
150 },
152 // nsIUpdatePrompt
154 // FIXME/bug 737601: we should have users opt-in to downloading
155 // updates when on a billed pipe. Initially, opt-in for 3g, but
156 // that doesn't cover all cases.
157 checkForUpdates: function UP_checkForUpdates() { },
159 showUpdateAvailable: function UP_showUpdateAvailable(aUpdate) {
160 if (!this.sendUpdateEvent("update-available", aUpdate)) {
162 log("Unable to prompt for available update, forcing download");
163 this.downloadUpdate(aUpdate);
164 }
165 },
167 showUpdateDownloaded: function UP_showUpdateDownloaded(aUpdate, aBackground) {
168 // The update has been downloaded and staged. We send the update-downloaded
169 // event right away. After the user has been idle for a while, we send the
170 // update-prompt-restart event, increasing the chances that we can apply the
171 // update quietly without user intervention.
172 this.sendUpdateEvent("update-downloaded", aUpdate);
174 if (Services.idle.idleTime >= this.applyIdleTimeout) {
175 this.showApplyPrompt(aUpdate);
176 return;
177 }
179 let applyIdleTimeoutSeconds = this.applyIdleTimeout / 1000;
180 // We haven't been idle long enough, so register an observer
181 log("Update is ready to apply, registering idle timeout of " +
182 applyIdleTimeoutSeconds + " seconds before prompting.");
184 this._update = aUpdate;
185 this.waitForIdle();
186 },
188 showUpdateError: function UP_showUpdateError(aUpdate) {
189 log("Update error, state: " + aUpdate.state + ", errorCode: " +
190 aUpdate.errorCode);
191 this.sendUpdateEvent("update-error", aUpdate);
192 this.setUpdateStatus(aUpdate.statusText);
193 },
195 showUpdateHistory: function UP_showUpdateHistory(aParent) { },
196 showUpdateInstalled: function UP_showUpdateInstalled() {
197 if (useSettings()) {
198 let lock = Services.settings.createLock();
199 lock.set("deviceinfo.last_updated", Date.now(), null, null);
200 }
201 },
203 // Custom functions
205 waitForIdle: function UP_waitForIdle() {
206 if (this._waitingForIdle) {
207 return;
208 }
210 this._waitingForIdle = true;
211 Services.idle.addIdleObserver(this, this.applyIdleTimeout / 1000);
212 Services.obs.addObserver(this, "quit-application", false);
213 },
215 setUpdateStatus: function UP_setUpdateStatus(aStatus) {
216 if (useSettings()) {
217 log("Setting gecko.updateStatus: " + aStatus);
219 let lock = Services.settings.createLock();
220 lock.set("gecko.updateStatus", aStatus, null);
221 }
222 },
224 showApplyPrompt: function UP_showApplyPrompt(aUpdate) {
225 if (!this.sendUpdateEvent("update-prompt-apply", aUpdate)) {
226 log("Unable to prompt, forcing restart");
227 this.restartProcess();
228 return;
229 }
231 #ifdef MOZ_B2G_RIL
232 let window = Services.wm.getMostRecentWindow("navigator:browser");
233 let pinReq = window.navigator.mozIccManager.getCardLock("pin");
234 pinReq.onsuccess = function(e) {
235 if (e.target.result.enabled) {
236 // The SIM is pin locked. Don't use a fallback timer. This means that
237 // the user has to press Install to apply the update. If we use the
238 // timer, and the timer reboots the phone, then the phone will be
239 // unusable until the SIM is unlocked.
240 log("SIM is pin locked. Not starting fallback timer.");
241 } else {
242 // This means that no pin lock is enabled, so we go ahead and start
243 // the fallback timer.
244 this._applyPromptTimer = this.createTimer(this.applyPromptTimeout);
245 }
246 }.bind(this);
247 pinReq.onerror = function(e) {
248 this._applyPromptTimer = this.createTimer(this.applyPromptTimeout);
249 }.bind(this);
250 #else
251 // Schedule a fallback timeout in case the UI is unable to respond or show
252 // a prompt for some reason.
253 this._applyPromptTimer = this.createTimer(this.applyPromptTimeout);
254 #endif
255 },
257 _copyProperties: ["appVersion", "buildID", "detailsURL", "displayVersion",
258 "errorCode", "isOSUpdate", "platformVersion",
259 "previousAppVersion", "state", "statusText"],
261 sendUpdateEvent: function UP_sendUpdateEvent(aType, aUpdate) {
262 let detail = {};
263 for each (let property in this._copyProperties) {
264 detail[property] = aUpdate[property];
265 }
267 let patch = aUpdate.selectedPatch;
268 if (!patch && aUpdate.patchCount > 0) {
269 // For now we just check the first patch to get size information if a
270 // patch hasn't been selected yet.
271 patch = aUpdate.getPatchAt(0);
272 }
274 if (patch) {
275 detail.size = patch.size;
276 detail.updateType = patch.type;
277 } else {
278 log("Warning: no patches available in update");
279 }
281 this._update = aUpdate;
282 return this.sendChromeEvent(aType, detail);
283 },
285 sendChromeEvent: function UP_sendChromeEvent(aType, aDetail) {
286 let detail = aDetail || {};
287 detail.type = aType;
289 let sent = SystemAppProxy.dispatchEvent(detail);
290 if (!sent) {
291 log("Warning: Couldn't send update event " + aType +
292 ": no content browser. Will send again when content becomes available.");
293 return false;
294 }
295 return true;
296 },
298 handleAvailableResult: function UP_handleAvailableResult(aDetail) {
299 // If the user doesn't choose "download", the updater will implicitly call
300 // showUpdateAvailable again after a certain period of time
301 switch (aDetail.result) {
302 case "download":
303 this.downloadUpdate(this._update);
304 break;
305 }
306 },
308 handleApplyPromptResult: function UP_handleApplyPromptResult(aDetail) {
309 if (this._applyPromptTimer) {
310 this._applyPromptTimer.cancel();
311 this._applyPromptTimer = null;
312 }
314 switch (aDetail.result) {
315 case "wait":
316 // Wait until the user is idle before prompting to apply the update
317 this.waitForIdle();
318 break;
319 case "restart":
320 this.finishUpdate();
321 this._update = null;
322 break;
323 }
324 },
326 downloadUpdate: function UP_downloadUpdate(aUpdate) {
327 if (!aUpdate) {
328 aUpdate = Services.um.activeUpdate;
329 if (!aUpdate) {
330 log("No active update found to download");
331 return;
332 }
333 }
335 let status = Services.aus.downloadUpdate(aUpdate, true);
336 if (status == STATE_DOWNLOADING) {
337 Services.aus.addDownloadListener(this);
338 return;
339 }
341 // If the update has already been downloaded and applied, then
342 // Services.aus.downloadUpdate will return immediately and not
343 // call showUpdateDownloaded, so we detect this.
344 if (aUpdate.state == "applied" && aUpdate.errorCode == 0) {
345 this.showUpdateDownloaded(aUpdate, true);
346 return;
347 }
349 log("Error downloading update " + aUpdate.name + ": " + aUpdate.errorCode);
350 let errorCode = aUpdate.errorCode >>> 0;
351 if (errorCode == Cr.NS_ERROR_FILE_TOO_BIG) {
352 aUpdate.statusText = "file-too-big";
353 }
354 this.showUpdateError(aUpdate);
355 },
357 handleDownloadCancel: function UP_handleDownloadCancel() {
358 log("Pausing download");
359 Services.aus.pauseDownload();
360 },
362 finishUpdate: function UP_finishUpdate() {
363 if (!this._update.isOSUpdate) {
364 // Standard gecko+gaia updates will just need to restart the process
365 this.restartProcess();
366 return;
367 }
369 try {
370 Services.aus.applyOsUpdate(this._update);
371 }
372 catch (e) {
373 this._update.errorCode = Cr.NS_ERROR_FAILURE;
374 this.showUpdateError(this._update);
375 }
376 },
378 restartProcess: function UP_restartProcess() {
379 log("Update downloaded, restarting to apply it");
381 let callbackAfterSet = function() {
382 #ifndef MOZ_WIDGET_GONK
383 let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
384 .getService(Ci.nsIAppStartup);
385 appStartup.quit(appStartup.eForceQuit | appStartup.eRestart);
386 #else
387 // NB: on Gonk, we rely on the system process manager to restart us.
388 let pmService = Cc["@mozilla.org/power/powermanagerservice;1"]
389 .getService(Ci.nsIPowerManagerService);
390 pmService.restart();
391 #endif
392 }
394 if (useSettings()) {
395 // Save current os version in deviceinfo.previous_os
396 let lock = Services.settings.createLock({
397 handle: callbackAfterSet,
398 handleAbort: function(error) {
399 log("Abort callback when trying to set previous_os: " + error);
400 callbackAfterSet();
401 }
402 });
403 lock.get("deviceinfo.os", {
404 handle: function(name, value) {
405 log("Set previous_os to: " + value);
406 lock.set("deviceinfo.previous_os", value, null, null);
407 }
408 });
409 }
410 },
412 forceUpdateCheck: function UP_forceUpdateCheck() {
413 log("Forcing update check");
415 let checker = Cc["@mozilla.org/updates/update-checker;1"]
416 .createInstance(Ci.nsIUpdateChecker);
417 checker.checkForUpdates(this._updateCheckListener, true);
418 },
420 handleEvent: function UP_handleEvent(evt) {
421 if (evt.type !== "mozContentEvent") {
422 return;
423 }
425 let detail = evt.detail;
426 if (!detail) {
427 return;
428 }
430 switch (detail.type) {
431 case "force-update-check":
432 this.forceUpdateCheck();
433 break;
434 case "update-available-result":
435 this.handleAvailableResult(detail);
436 // If we started the apply prompt timer, this means that we're waiting
437 // for the user to press Later or Install Now. In this situation we
438 // don't want to clear this._update, becuase handleApplyPromptResult
439 // needs it.
440 if (this._applyPromptTimer == null && !this._waitingForIdle) {
441 this._update = null;
442 }
443 break;
444 case "update-download-cancel":
445 this.handleDownloadCancel();
446 break;
447 case "update-prompt-apply-result":
448 this.handleApplyPromptResult(detail);
449 break;
450 }
451 },
453 // nsIObserver
455 observe: function UP_observe(aSubject, aTopic, aData) {
456 switch (aTopic) {
457 case "idle":
458 this._waitingForIdle = false;
459 this.showApplyPrompt(this._update);
460 // Fall through
461 case "quit-application":
462 Services.idle.removeIdleObserver(this, this.applyIdleTimeout / 1000);
463 Services.obs.removeObserver(this, "quit-application");
464 break;
465 case "update-check-start":
466 WebappsUpdater.updateApps();
467 break;
468 }
469 },
471 // nsITimerCallback
473 notify: function UP_notify(aTimer) {
474 if (aTimer == this._applyPromptTimer) {
475 log("Timed out waiting for result, restarting");
476 this._applyPromptTimer = null;
477 this.finishUpdate();
478 this._update = null;
479 return;
480 }
481 if (aTimer == this._watchdogTimer) {
482 log("Download watchdog fired");
483 this._watchdogTimer = null;
484 this._autoRestartDownload = true;
485 Services.aus.pauseDownload();
486 return;
487 }
488 },
490 createTimer: function UP_createTimer(aTimeoutMs) {
491 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
492 timer.initWithCallback(this, aTimeoutMs, timer.TYPE_ONE_SHOT);
493 return timer;
494 },
496 // nsIRequestObserver
498 _startedSent: false,
500 _watchdogTimer: null,
502 _autoRestartDownload: false,
503 _autoRestartCount: 0,
505 startWatchdogTimer: function UP_startWatchdogTimer() {
506 let watchdogTimeout = 120000; // 120 seconds
507 try {
508 watchdogTimeout = Services.prefs.getIntPref(PREF_DOWNLOAD_WATCHDOG_TIMEOUT);
509 } catch (e) {
510 // This means that the preference doesn't exist. watchdogTimeout will
511 // retain its default assigned above.
512 }
513 if (watchdogTimeout <= 0) {
514 // 0 implies don't bother using the watchdog timer at all.
515 this._watchdogTimer = null;
516 return;
517 }
518 if (this._watchdogTimer) {
519 this._watchdogTimer.cancel();
520 } else {
521 this._watchdogTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
522 }
523 this._watchdogTimer.initWithCallback(this, watchdogTimeout,
524 Ci.nsITimer.TYPE_ONE_SHOT);
525 },
527 stopWatchdogTimer: function UP_stopWatchdogTimer() {
528 if (this._watchdogTimer) {
529 this._watchdogTimer.cancel();
530 this._watchdogTimer = null;
531 }
532 },
534 touchWatchdogTimer: function UP_touchWatchdogTimer() {
535 this.startWatchdogTimer();
536 },
538 onStartRequest: function UP_onStartRequest(aRequest, aContext) {
539 // Wait until onProgress to send the update-download-started event, in case
540 // this request turns out to fail for some reason
541 this._startedSent = false;
542 this.startWatchdogTimer();
543 },
545 onStopRequest: function UP_onStopRequest(aRequest, aContext, aStatusCode) {
546 this.stopWatchdogTimer();
547 Services.aus.removeDownloadListener(this);
548 let paused = !Components.isSuccessCode(aStatusCode);
549 if (!paused) {
550 // The download was successful, no need to restart
551 this._autoRestartDownload = false;
552 }
553 if (this._autoRestartDownload) {
554 this._autoRestartDownload = false;
555 let watchdogMaxRetries = Services.prefs.getIntPref(PREF_DOWNLOAD_WATCHDOG_MAX_RETRIES);
556 this._autoRestartCount++;
557 if (this._autoRestartCount > watchdogMaxRetries) {
558 log("Download - retry count exceeded - error");
559 // We exceeded the max retries. Treat the download like an error,
560 // which will give the user a chance to restart manually later.
561 this._autoRestartCount = 0;
562 if (Services.um.activeUpdate) {
563 this.showUpdateError(Services.um.activeUpdate);
564 }
565 return;
566 }
567 log("Download - restarting download - attempt " + this._autoRestartCount);
568 this.downloadUpdate(null);
569 return;
570 }
571 this._autoRestartCount = 0;
572 this.sendChromeEvent("update-download-stopped", {
573 paused: paused
574 });
575 },
577 // nsIProgressEventSink
579 onProgress: function UP_onProgress(aRequest, aContext, aProgress,
580 aProgressMax) {
581 if (aProgress == aProgressMax) {
582 // The update.mar validation done by onStopRequest may take
583 // a while before the onStopRequest callback is made, so stop
584 // the timer now.
585 this.stopWatchdogTimer();
586 } else {
587 this.touchWatchdogTimer();
588 }
589 if (!this._startedSent) {
590 this.sendChromeEvent("update-download-started", {
591 total: aProgressMax
592 });
593 this._startedSent = true;
594 }
596 this.sendChromeEvent("update-download-progress", {
597 progress: aProgress,
598 total: aProgressMax
599 });
600 },
602 onStatus: function UP_onStatus(aRequest, aUpdate, aStatus, aStatusArg) { }
603 };
605 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UpdatePrompt]);