Bug 1395615 - Implement the "file moved or missing" check for download items in the Library Downloads subview. r=Paolo, a=ritu
authorMike de Boer <mdeboer@mozilla.com>
Tue, 26 Sep 2017 17:55:18 +0200
changeset 432211 adf452ba200a6f4ce3f80208ce5fb2da988e3fb4
parent 432210 2b0fdf04cfbab4f34bb76c17e2a7febe554d8489
child 432212 b7170f0744906ff767d59ef12fb5eb9528ce3d36
push id7907
push userryanvm@gmail.com
push dateThu, 05 Oct 2017 19:59:03 +0000
treeherdermozilla-beta@178de8c0b3e4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersPaolo, ritu
bugs1395615
milestone57.0
Bug 1395615 - Implement the "file moved or missing" check for download items in the Library Downloads subview. r=Paolo, a=ritu 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,
   };
 });
 
@@ -71,16 +73,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();
@@ -97,18 +100,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]
@@ -143,16 +148,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.
    */
@@ -328,31 +373,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();
     }
 
@@ -369,18 +426,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.