Bug 740722: Wait until the user is idle before prompting to apply an update in B2G. r=fabrice
authorMarshall Culpepper <marshall.law@gmail.com>
Tue, 02 Oct 2012 13:33:49 -0500
changeset 109033 d66ee1da0348370bf38983babde9ee8c27cd69de
parent 109032 16bc4aa2f52f134e1f759c87333c294c97e71411
child 109034 af956af641bc7a21c48514e2ccba3a8cd6045a57
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewersfabrice
bugs740722
milestone18.0a1
Bug 740722: Wait until the user is idle before prompting to apply an update in B2G. r=fabrice
b2g/app/b2g.js
b2g/components/UpdatePrompt.js
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -445,18 +445,18 @@ pref("marionette.defaultPrefs.port", 282
 #endif
 
 #ifdef MOZ_UPDATER
 // When we're applying updates, we can't let anything hang us on
 // quit+restart.  The user has no recourse.
 pref("shutdown.watchdog.timeoutSecs", 5);
 // Timeout before the update prompt automatically installs the update
 pref("b2g.update.apply-prompt-timeout", 60000); // milliseconds
-// Optional timeout the user can wait before getting another update prompt
-pref("b2g.update.apply-wait-timeout", 1800000); // milliseconds
+// Amount of time to wait after the user is idle before prompting to apply an update
+pref("b2g.update.apply-idle-timeout", 600000); // milliseconds
 // Amount of time the updater waits for the process to exit cleanly before
 // forcefully exiting the process
 pref("b2g.update.self-destruct-timeout", 5000); // milliseconds
 
 pref("app.update.enabled", true);
 pref("app.update.auto", false);
 pref("app.update.silent", false);
 pref("app.update.mode", 0);
--- a/b2g/components/UpdatePrompt.js
+++ b/b2g/components/UpdatePrompt.js
@@ -15,36 +15,43 @@ Cu.import("resource://gre/modules/Servic
 const VERBOSE = 1;
 let log =
   VERBOSE ?
   function log_dump(msg) { dump("UpdatePrompt: "+ msg +"\n"); } :
   function log_noop(msg) { };
 
 const APPLY_PROMPT_TIMEOUT =
       Services.prefs.getIntPref("b2g.update.apply-prompt-timeout");
-const APPLY_WAIT_TIMEOUT =
-      Services.prefs.getIntPref("b2g.update.apply-wait-timeout");
+const APPLY_IDLE_TIMEOUT =
+      Services.prefs.getIntPref("b2g.update.apply-idle-timeout");
 const SELF_DESTRUCT_TIMEOUT =
       Services.prefs.getIntPref("b2g.update.self-destruct-timeout");
 
+const APPLY_IDLE_TIMEOUT_SECONDS = APPLY_IDLE_TIMEOUT / 1000;
+
+
 XPCOMUtils.defineLazyServiceGetter(Services, "aus",
                                    "@mozilla.org/updates/update-service;1",
                                    "nsIApplicationUpdateService");
 
+XPCOMUtils.defineLazyServiceGetter(Services, "idle",
+                                   "@mozilla.org/widget/idleservice;1",
+                                   "nsIIdleService");
 function UpdatePrompt() { }
 
 UpdatePrompt.prototype = {
   classID: Components.ID("{88b3eb21-d072-4e3b-886d-f89d8c49fe59}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePrompt,
                                          Ci.nsIRequestObserver,
-                                         Ci.nsIProgressEventSink]),
+                                         Ci.nsIProgressEventSink,
+                                         Ci.nsIObserver]),
 
   _update: null,
   _applyPromptTimer: null,
-  _applyWaitTimer: null,
+  _waitingForIdle: false,
 
   // nsIUpdatePrompt
 
   // FIXME/bug 737601: we should have users opt-in to downloading
   // updates when on a billed pipe.  Initially, opt-in for 3g, but
   // that doesn't cover all cases.
   checkForUpdates: function UP_checkForUpdates() { },
 
@@ -53,37 +60,70 @@ UpdatePrompt.prototype = {
                              this.handleAvailableResult)) {
 
       log("Unable to prompt for available update, forcing download");
       this.downloadUpdate(aUpdate);
     }
   },
 
   showUpdateDownloaded: function UP_showUpdateDownloaded(aUpdate, aBackground) {
-    if (!this.sendUpdateEvent("update-downloaded", aUpdate,
-                             this.handleDownloadedResult)) {
-      log("Unable to prompt, forcing restart");
-      this.restartProcess();
+    // The update has been downloaded and staged. We send the update-downloaded
+    // event right away. After the user has been idle for a while, we send the
+    // update-prompt-restart event, increasing the chances that we can apply the
+    // update quietly without user intervention.
+    this.sendUpdateEvent("update-downloaded", aUpdate);
+
+    if (Services.idle.idleTime >= APPLY_IDLE_TIMEOUT) {
+      this.showApplyPrompt(aUpdate);
       return;
     }
 
-    // Schedule a fallback timeout in case the UI is unable to respond or show
-    // a prompt for some reason
-    this._applyPromptTimer = this.createTimer(APPLY_PROMPT_TIMEOUT);
+    // We haven't been idle long enough, so register an observer
+    log("Update is ready to apply, registering idle timeout of " +
+        APPLY_IDLE_TIMEOUT_SECONDS + " seconds before prompting.");
+
+    this._update = aUpdate;
+    this.waitForIdle();
   },
 
   showUpdateError: function UP_showUpdateError(aUpdate) {
     if (aUpdate.state == "failed") {
       log("Failed to download update, errorCode: " + aUpdate.errorCode);
     }
   },
 
   showUpdateHistory: function UP_showUpdateHistory(aParent) { },
   showUpdateInstalled: function UP_showUpdateInstalled() { },
 
+  // Custom functions
+
+  waitForIdle: function UP_waitForIdle() {
+    if (this._waitingForIdle) {
+      return;
+    }
+
+    this._waitingForIdle = true;
+    Services.idle.addIdleObserver(this, APPLY_IDLE_TIMEOUT_SECONDS);
+    Services.obs.addObserver(this, "quit-application", false);
+  },
+
+
+  showApplyPrompt: function UP_showApplyPrompt(aUpdate) {
+    if (!this.sendUpdateEvent("update-prompt-apply", aUpdate,
+                             this.handleApplyPromptResult)) {
+      log("Unable to prompt, forcing restart");
+      this.restartProcess();
+      return;
+    }
+
+    // Schedule a fallback timeout in case the UI is unable to respond or show
+    // a prompt for some reason.
+    this._applyPromptTimer = this.createTimer(APPLY_PROMPT_TIMEOUT);
+  },
+
   sendUpdateEvent: function UP_sendUpdateEvent(aType, aUpdate, aCallback) {
     let detail = {
       displayVersion: aUpdate.displayVersion,
       detailsURL: aUpdate.detailsURL
     };
 
     let patch = aUpdate.selectedPatch;
     if (!patch) {
@@ -150,27 +190,26 @@ UpdatePrompt.prototype = {
     // showUpdateAvailable again after a certain period of time
     switch (aDetail.result) {
       case "download":
         this.downloadUpdate(this._update);
         break;
     }
   },
 
-  handleDownloadedResult: function UP_handleDownloadedResult(aDetail) {
+  handleApplyPromptResult: function UP_handleApplyPromptResult(aDetail) {
     if (this._applyPromptTimer) {
       this._applyPromptTimer.cancel();
       this._applyPromptTimer = null;
     }
 
     switch (aDetail.result) {
       case "wait":
-        // Wait for a fixed period of time, allowing the user to temporarily
-        // postpone applying an update
-        this._applyWaitTimer = this.createTimer(APPLY_WAIT_TIMEOUT);
+        // Wait until the user is idle before prompting to apply the update
+        this.waitForIdle();
         break;
       case "restart":
         this.finishUpdate();
         break;
     }
   },
 
   downloadUpdate: function UP_downloadUpdate(aUpdate) {
@@ -231,24 +270,36 @@ UpdatePrompt.prototype = {
     try {
       recoveryService.installFotaUpdate(aOsUpdatePath);
     } catch(e) {
       log("Error: Couldn't reboot into recovery to apply FOTA update " +
           aOsUpdatePath);
     }
   },
 
+  // nsIObserver
+
+  observe: function UP_observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "idle":
+        this._waitingForIdle = false;
+        this.showApplyPrompt(this._update);
+        // Fall through
+      case "quit-application":
+        Services.idle.removeIdleObserver(this, APPLY_IDLE_TIMEOUT_SECONDS);
+        Services.obs.removeObserver(this, "quit-application");
+        break;
+    }
+  },
+
   notify: function UP_notify(aTimer) {
     if (aTimer == this._applyPromptTimer) {
       log("Timed out waiting for result, restarting");
       this._applyPromptTimer = null;
       this.finishUpdate();
-    } else if (aTimer == this._applyWaitTimer) {
-      this._applyWaitTimer = null;
-      this.showUpdatePrompt();
     }
   },
 
   createTimer: function UP_createTimer(aTimeoutMs) {
     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     timer.initWithCallback(this, aTimeoutMs, timer.TYPE_ONE_SHOT);
     return timer;
   },