Bumping manifests a=b2g-bump
[gecko.git] / b2g / components / UpdatePrompt.js
blobfee312fb749f8d2013830920e2f11f484bca9e60
1 /* -*- indent-tabs-mode: nil; js-indent-level: 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;
64 XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
65                                   "resource://gre/modules/SystemAppProxy.jsm");
67 function UpdateCheckListener(updatePrompt) {
68   this._updatePrompt = updatePrompt;
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   }
120 function UpdatePrompt() {
121   this.wrappedJSObject = this;
122   this._updateCheckListener = new UpdateCheckListener(this);
123   Services.obs.addObserver(this, "update-check-start", false);
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       // Battery not okay, do not wait for idle to re-prompt
316       case "low-battery":
317         break;
318       case "wait":
319         // Wait until the user is idle before prompting to apply the update
320         this.waitForIdle();
321         break;
322       case "restart":
323         this.finishUpdate();
324         this._update = null;
325         break;
326     }
327   },
329   downloadUpdate: function UP_downloadUpdate(aUpdate) {
330     if (!aUpdate) {
331       aUpdate = Services.um.activeUpdate;
332       if (!aUpdate) {
333         log("No active update found to download");
334         return;
335       }
336     }
338     let status = Services.aus.downloadUpdate(aUpdate, true);
339     if (status == STATE_DOWNLOADING) {
340       Services.aus.addDownloadListener(this);
341       return;
342     }
344     // If the update has already been downloaded and applied, then
345     // Services.aus.downloadUpdate will return immediately and not
346     // call showUpdateDownloaded, so we detect this.
347     if (aUpdate.state == "applied" && aUpdate.errorCode == 0) {
348       this.showUpdateDownloaded(aUpdate, true);
349       return;
350     }
352     log("Error downloading update " + aUpdate.name + ": " + aUpdate.errorCode);
353     let errorCode = aUpdate.errorCode >>> 0;
354     if (errorCode == Cr.NS_ERROR_FILE_TOO_BIG) {
355       aUpdate.statusText = "file-too-big";
356     }
357     this.showUpdateError(aUpdate);
358   },
360   handleDownloadCancel: function UP_handleDownloadCancel() {
361     log("Pausing download");
362     Services.aus.pauseDownload();
363   },
365   finishUpdate: function UP_finishUpdate() {
366     if (!this._update.isOSUpdate) {
367       // Standard gecko+gaia updates will just need to restart the process
368       this.restartProcess();
369       return;
370     }
372     try {
373       Services.aus.applyOsUpdate(this._update);
374     }
375     catch (e) {
376       this._update.errorCode = Cr.NS_ERROR_FAILURE;
377       this.showUpdateError(this._update);
378     }
379   },
381   restartProcess: function UP_restartProcess() {
382     log("Update downloaded, restarting to apply it");
384     let callbackAfterSet = function() {
385 #ifndef MOZ_WIDGET_GONK
386       let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
387                        .getService(Ci.nsIAppStartup);
388       appStartup.quit(appStartup.eForceQuit | appStartup.eRestart);
389 #else
390       // NB: on Gonk, we rely on the system process manager to restart us.
391       let pmService = Cc["@mozilla.org/power/powermanagerservice;1"]
392                       .getService(Ci.nsIPowerManagerService);
393       pmService.restart();
394 #endif
395     }
397     if (useSettings()) {
398       // Save current os version in deviceinfo.previous_os
399       let lock = Services.settings.createLock({
400         handle: callbackAfterSet,
401         handleAbort: function(error) {
402           log("Abort callback when trying to set previous_os: " + error);
403           callbackAfterSet();
404         }
405       });
406       lock.get("deviceinfo.os", {
407         handle: function(name, value) {
408           log("Set previous_os to: " + value);
409           lock.set("deviceinfo.previous_os", value, null, null);
410         }
411       });
412     }
413   },
415   forceUpdateCheck: function UP_forceUpdateCheck() {
416     log("Forcing update check");
418     let checker = Cc["@mozilla.org/updates/update-checker;1"]
419                     .createInstance(Ci.nsIUpdateChecker);
420     checker.checkForUpdates(this._updateCheckListener, true);
421   },
423   handleEvent: function UP_handleEvent(evt) {
424     if (evt.type !== "mozContentEvent") {
425       return;
426     }
428     let detail = evt.detail;
429     if (!detail) {
430       return;
431     }
433     switch (detail.type) {
434       case "force-update-check":
435         this.forceUpdateCheck();
436         break;
437       case "update-available-result":
438         this.handleAvailableResult(detail);
439         // If we started the apply prompt timer, this means that we're waiting
440         // for the user to press Later or Install Now. In this situation we
441         // don't want to clear this._update, becuase handleApplyPromptResult
442         // needs it.
443         if (this._applyPromptTimer == null && !this._waitingForIdle) {
444           this._update = null;
445         }
446         break;
447       case "update-download-cancel":
448         this.handleDownloadCancel();
449         break;
450       case "update-prompt-apply-result":
451         this.handleApplyPromptResult(detail);
452         break;
453     }
454   },
456   // nsIObserver
458   observe: function UP_observe(aSubject, aTopic, aData) {
459     switch (aTopic) {
460       case "idle":
461         this._waitingForIdle = false;
462         this.showApplyPrompt(this._update);
463         // Fall through
464       case "quit-application":
465         Services.idle.removeIdleObserver(this, this.applyIdleTimeout / 1000);
466         Services.obs.removeObserver(this, "quit-application");
467         break;
468       case "update-check-start":
469         WebappsUpdater.updateApps();
470         break;
471     }
472   },
474   // nsITimerCallback
476   notify: function UP_notify(aTimer) {
477     if (aTimer == this._applyPromptTimer) {
478       log("Timed out waiting for result, restarting");
479       this._applyPromptTimer = null;
480       this.finishUpdate();
481       this._update = null;
482       return;
483     }
484     if (aTimer == this._watchdogTimer) {
485       log("Download watchdog fired");
486       this._watchdogTimer = null;
487       this._autoRestartDownload = true;
488       Services.aus.pauseDownload();
489       return;
490     }
491   },
493   createTimer: function UP_createTimer(aTimeoutMs) {
494     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
495     timer.initWithCallback(this, aTimeoutMs, timer.TYPE_ONE_SHOT);
496     return timer;
497   },
499   // nsIRequestObserver
501   _startedSent: false,
503   _watchdogTimer: null,
505   _autoRestartDownload: false,
506   _autoRestartCount: 0,
508   startWatchdogTimer: function UP_startWatchdogTimer() {
509     let watchdogTimeout = 120000;  // 120 seconds
510     try {
511       watchdogTimeout = Services.prefs.getIntPref(PREF_DOWNLOAD_WATCHDOG_TIMEOUT);
512     } catch (e) {
513       // This means that the preference doesn't exist. watchdogTimeout will
514       // retain its default assigned above.
515     }
516     if (watchdogTimeout <= 0) {
517       // 0 implies don't bother using the watchdog timer at all.
518       this._watchdogTimer = null;
519       return;
520     }
521     if (this._watchdogTimer) {
522       this._watchdogTimer.cancel();
523     } else {
524       this._watchdogTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
525     }
526     this._watchdogTimer.initWithCallback(this, watchdogTimeout,
527                                          Ci.nsITimer.TYPE_ONE_SHOT);
528   },
530   stopWatchdogTimer: function UP_stopWatchdogTimer() {
531     if (this._watchdogTimer) {
532       this._watchdogTimer.cancel();
533       this._watchdogTimer = null;
534     }
535   },
537   touchWatchdogTimer: function UP_touchWatchdogTimer() {
538     this.startWatchdogTimer();
539   },
541   onStartRequest: function UP_onStartRequest(aRequest, aContext) {
542     // Wait until onProgress to send the update-download-started event, in case
543     // this request turns out to fail for some reason
544     this._startedSent = false;
545     this.startWatchdogTimer();
546   },
548   onStopRequest: function UP_onStopRequest(aRequest, aContext, aStatusCode) {
549     this.stopWatchdogTimer();
550     Services.aus.removeDownloadListener(this);
551     let paused = !Components.isSuccessCode(aStatusCode);
552     if (!paused) {
553       // The download was successful, no need to restart
554       this._autoRestartDownload = false;
555     }
556     if (this._autoRestartDownload) {
557       this._autoRestartDownload = false;
558       let watchdogMaxRetries = Services.prefs.getIntPref(PREF_DOWNLOAD_WATCHDOG_MAX_RETRIES);
559       this._autoRestartCount++;
560       if (this._autoRestartCount > watchdogMaxRetries) {
561         log("Download - retry count exceeded - error");
562         // We exceeded the max retries. Treat the download like an error,
563         // which will give the user a chance to restart manually later.
564         this._autoRestartCount = 0;
565         if (Services.um.activeUpdate) {
566           this.showUpdateError(Services.um.activeUpdate);
567         }
568         return;
569       }
570       log("Download - restarting download - attempt " + this._autoRestartCount);
571       this.downloadUpdate(null);
572       return;
573     }
574     this._autoRestartCount = 0;
575     this.sendChromeEvent("update-download-stopped", {
576       paused: paused
577     });
578   },
580   // nsIProgressEventSink
582   onProgress: function UP_onProgress(aRequest, aContext, aProgress,
583                                      aProgressMax) {
584     if (aProgress == aProgressMax) {
585       // The update.mar validation done by onStopRequest may take
586       // a while before the onStopRequest callback is made, so stop
587       // the timer now.
588       this.stopWatchdogTimer();
589     } else {
590       this.touchWatchdogTimer();
591     }
592     if (!this._startedSent) {
593       this.sendChromeEvent("update-download-started", {
594         total: aProgressMax
595       });
596       this._startedSent = true;
597     }
599     this.sendChromeEvent("update-download-progress", {
600       progress: aProgress,
601       total: aProgressMax
602     });
603   },
605   onStatus: function UP_onStatus(aRequest, aUpdate, aStatus, aStatusArg) { }
608 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UpdatePrompt]);