Bug 1395615 - Implement the "file moved or missing" check for download items in the Library Downloads subview. r?Paolo draft
authorMike de Boer <mdeboer@mozilla.com>
Tue, 26 Sep 2017 17:55:18 +0200
changeset 670543 54dda73aaf6a3a3c4262d88ee8500e325a5e6dc1
parent 670530 719824e45b2ea2f73052e4c026ed99bcfff070d4
child 733264 539e8fa42d592089366f9a7c9b7627c1fccec164
push id81663
push usermdeboer@mozilla.com
push dateTue, 26 Sep 2017 16:01:04 +0000
reviewersPaolo
bugs1395615
milestone58.0a1
Bug 1395615 - Implement the "file moved or missing" check for download items in the Library Downloads subview. r?Paolo MozReview-Commit-ID: 62VJbzJwxVW
browser/components/downloads/DownloadsSubview.jsm
--- a/browser/components/downloads/DownloadsSubview.jsm
+++ b/browser/components/downloads/DownloadsSubview.jsm
@@ -20,16 +20,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource:///modules/DownloadsCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI",
                                   "resource:///modules/DownloadsViewUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 
 let gPanelViewInstances = new WeakMap();
 const kEvents = ["ViewShowing", "ViewHiding", "click", "command"];
+const kRefreshBatchSize = 10;
+const kMaxWaitForIdleMs = 200;
 XPCOMUtils.defineLazyGetter(this, "kButtonLabels", () => {
   return {
     show: DownloadsCommon.strings[AppConstants.platform == "macosx" ? "showMacLabel" : "showLabel"],
     open: DownloadsCommon.strings.openFileLabel,
     retry: DownloadsCommon.strings.retryLabel,
   };
 });
 
@@ -73,16 +75,17 @@ class DownloadsSubview extends Downloads
     this._downloadsData.addView(this);
   }
 
   destructor(event) {
     this.panelview.removeEventListener("click", DownloadsSubview.onClick);
     this.panelview.removeEventListener("ViewHiding", DownloadsSubview.onViewHiding);
     this._downloadsData.removeView(this);
     gPanelViewInstances.delete(this);
+    this.destroyed = true;
   }
 
   /**
    * DataView handler; invoked when a batch of downloads is being passed in -
    * usually when this instance is added as a view in the constructor.
    */
   onDownloadBatchStarting() {
     this.batchFragment = this.document.createDocumentFragment();
@@ -99,18 +102,20 @@ class DownloadsSubview extends Downloads
     let waitForMs = 200;
     if (this.batchFragment.childElementCount) {
       // Prepend the batch fragment.
       this.container.insertBefore(this.batchFragment, this.container.firstChild || null);
       waitForMs = 0;
     }
     // Wait a wee bit to dispatch the event, because another batch may start
     // right away.
-    this._batchTimeout = window.setTimeout(() =>
-      this.panelview.dispatchEvent(new window.CustomEvent("DownloadsLoaded")), waitForMs);
+    this._batchTimeout = window.setTimeout(() => {
+      this._updateStatsFromDisk();
+      this.panelview.dispatchEvent(new window.CustomEvent("DownloadsLoaded"));
+    }, waitForMs);
     this.batchFragment = null;
   }
 
   /**
    * DataView handler; invoked when a new download is added to the list.
    *
    * @param {Download} download
    * @param {DOMNode}  [options.insertBefore]
@@ -145,16 +150,56 @@ class DownloadsSubview extends Downloads
    * DataView handler; invoked when a download is removed.
    *
    * @param {Download} download
    */
   onDownloadRemoved(download) {
     this._viewItemsForDownloads.get(download).element.remove();
   }
 
+  /**
+   * Schedule a refresh of the downloads that were added, which is mainly about
+   * checking whether the target file still exists.
+   * We're doing this during idle time and in chunks.
+   */
+  async _updateStatsFromDisk() {
+    if (this._updatingStats)
+      return;
+
+    this._updatingStats = true;
+
+    try {
+      let idleOptions = { timeout: kMaxWaitForIdleMs };
+      // Start with getting an idle moment to (maybe) refresh the list of downloads.
+      await new Promise(resolve => this.window.requestIdleCallback(resolve), idleOptions);
+      // In the meantime, this instance could have been destroyed, so take note.
+      if (this.destroyed)
+        return;
+
+      let count = 0;
+      for (let button of this.container.childNodes) {
+        if (this.destroyed)
+          return;
+        if (!button._shell)
+          continue;
+
+        await button._shell.refresh();
+
+        // Make sure to request a new idle moment every `kRefreshBatchSize` buttons.
+        if (++count % kRefreshBatchSize === 0) {
+          await new Promise(resolve => this.window.requestIdleCallback(resolve, idleOptions));
+        }
+      }
+    } catch (ex) {
+      Cu.reportError(ex);
+    } finally {
+      this._updatingStats = false;
+    }
+  }
+
   // ----- Static methods. -----
 
   /**
    * Perform all tasks necessary to be able to show a Downloads Subview.
    *
    * @param  {DOMWindow} window  Global window object.
    * @return {Promise}   Will resolve when all tasks are done.
    */
@@ -339,31 +384,43 @@ DownloadsSubview.Button = class extends 
     this.element.classList.add("subviewbutton", "subviewbutton-iconic", "download",
       "download-state");
   }
 
   get browserWindow() {
     return this.element.ownerGlobal;
   }
 
+  async refresh() {
+    if (this._targetFileChecked)
+      return;
+
+    try {
+      await this.download.refresh();
+    } catch (ex) {
+      Cu.reportError(ex);
+    } finally {
+      this._targetFileChecked = true;
+    }
+  }
+
   /**
    * Handle state changes of a download.
    */
   onStateChanged() {
     // Since the state changed, we may need to check the target file again.
     this._targetFileChecked = false;
 
     this._updateState();
   }
 
   /**
    * Handler method; invoked when any state attribute of a download changed.
    */
   onChanged() {
-    // TODO: implement "file moved or missing" check - bug 1395615.
     let newState = DownloadsCommon.stateOfDownload(this.download);
     if (this._downloadState !== newState) {
       this._downloadState = newState;
       this.onStateChanged();
     } else {
       this._updateState();
     }
 
@@ -380,18 +437,25 @@ DownloadsSubview.Button = class extends 
   _updateState() {
     super._updateState();
     this.element.setAttribute("label", this.element.getAttribute("displayName"));
     this.element.setAttribute("tooltiptext", this.element.getAttribute("fullStatus"));
 
     if (this.isCommandEnabled("downloadsCmd_show")) {
       this.element.setAttribute("openLabel", kButtonLabels.open);
       this.element.setAttribute("showLabel", kButtonLabels.show);
+      this.element.removeAttribute("retryLabel");
     } else if (this.isCommandEnabled("downloadsCmd_retry")) {
       this.element.setAttribute("retryLabel", kButtonLabels.retry);
+      this.element.removeAttribute("openLabel");
+      this.element.removeAttribute("showLabel");
+    } else {
+      this.element.removeAttribute("openLabel");
+      this.element.removeAttribute("retryLabel");
+      this.element.removeAttribute("showLabel");
     }
 
     this._updateVisibility();
   }
 
   _updateVisibility() {
     let state = this.element.getAttribute("state");
     // This view only show completed and failed downloads.