b2g/components/UpdatePrompt.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

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]);

mercurial