Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / directory_model.js
index cc93910..23230b3 100644 (file)
@@ -6,7 +6,7 @@
 
 // If directory files changes too often, don't rescan directory more than once
 // per specified interval
-var SIMULTANEOUS_RESCAN_INTERVAL = 1000;
+var SIMULTANEOUS_RESCAN_INTERVAL = 500;
 // Used for operations that require almost instant rescan.
 var SHORT_RESCAN_INTERVAL = 100;
 
@@ -32,6 +32,8 @@ function DirectoryModel(singleSelection, fileFilter, fileWatcher,
   this.scanFailures_ = 0;
   this.changeDirectorySequence_ = 0;
 
+  this.directoryChangeQueue_ = new AsyncUtil.Queue();
+
   this.fileFilter_ = fileFilter;
   this.fileFilter_.addEventListener('changed',
                                     this.onFilterChanged_.bind(this));
@@ -162,15 +164,39 @@ DirectoryModel.prototype.updateSelectionAndPublishEvent_ =
 
 /**
  * Invoked when a change in the directory is detected by the watcher.
+ * @param {Event} event Event object.
  * @private
  */
-DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() {
-  // Clear the metadata cache since something in this directory has changed.
+DirectoryModel.prototype.onWatcherDirectoryChanged_ = function(event) {
   var directoryEntry = this.getCurrentDirEntry();
-  if (!util.isFakeEntry(directoryEntry))
-    this.metadataCache_.clearRecursively(directoryEntry, '*');
 
-  this.rescanSoon();
+  if (event.changedFiles) {
+    var urls = event.changedFiles.map(function(change) { return change.url; });
+    util.URLsToEntries(urls).then(function(result) {
+      // Removes the metadata of invalid entries.
+      if (result.failureUrls.length > 0)
+        this.metadataCache_.clearByUrl(result.failureUrls, '*');
+
+      // Rescans after force-refreshing the metadata of the changed entries.
+      var entries = result.entries;
+      if (entries.length) {
+        this.currentDirContents_.prefetchMetadata(entries, true, function() {
+          this.rescanSoon(false);
+        }.bind(this));
+      } else {
+        this.rescanSoon(false);
+      }
+    }.bind(this)).catch(function(error) {
+      console.error('Error in proceeding the changed event.', error,
+                    'Fallback to force-refresh');
+      this.rescanSoon(true);
+    }.bind(this));
+  } else {
+    // Invokes force refresh if the detailed information isn't provided.
+    // This can occur very frequently (e.g. when copying files into Downlaods)
+    // and rescan is heavy operation, so we keep some interval for each rescan.
+    this.rescanLater(true);
+  }
 };
 
 /**
@@ -178,7 +204,7 @@ DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() {
  * @private
  */
 DirectoryModel.prototype.onFilterChanged_ = function() {
-  this.rescanSoon();
+  this.rescanSoon(false);
 };
 
 /**
@@ -252,17 +278,21 @@ DirectoryModel.prototype.setLeadEntry_ = function(value) {
 
 /**
  * Schedule rescan with short delay.
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ *     one.
  */
-DirectoryModel.prototype.rescanSoon = function() {
-  this.scheduleRescan(SHORT_RESCAN_INTERVAL);
+DirectoryModel.prototype.rescanSoon = function(refresh) {
+  this.scheduleRescan(SHORT_RESCAN_INTERVAL, refresh);
 };
 
 /**
  * Schedule rescan with delay. Designed to handle directory change
  * notification.
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ *     one.
  */
-DirectoryModel.prototype.rescanLater = function() {
-  this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL);
+DirectoryModel.prototype.rescanLater = function(refresh) {
+  this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL, refresh);
 };
 
 /**
@@ -270,16 +300,24 @@ DirectoryModel.prototype.rescanLater = function() {
  * nothing. File operation may cause a few notifications what should cause
  * a single refresh.
  * @param {number} delay Delay in ms after which the rescan will be performed.
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ *     one.
  */
-DirectoryModel.prototype.scheduleRescan = function(delay) {
+DirectoryModel.prototype.scheduleRescan = function(delay, refresh) {
   if (this.rescanTime_) {
     if (this.rescanTime_ <= Date.now() + delay)
       return;
     clearTimeout(this.rescanTimeoutId_);
   }
 
+  var sequence = this.changeDirectorySequence_;
+
   this.rescanTime_ = Date.now() + delay;
-  this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay);
+  this.rescanTimeoutId_ = setTimeout(function() {
+    this.rescanTimeoutId_ = null;
+    if (sequence === this.changeDirectorySequence_)
+      this.rescan(refresh);
+  }.bind(this), delay);
 };
 
 /**
@@ -301,8 +339,11 @@ DirectoryModel.prototype.clearRescanTimeout_ = function() {
  * preserving the select element etc.
  *
  * This should be to scan the contents of current directory (or search).
+ *
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ *     one.
  */
-DirectoryModel.prototype.rescan = function() {
+DirectoryModel.prototype.rescan = function(refresh) {
   this.clearRescanTimeout_();
   if (this.runningScan_) {
     this.pendingRescan_ = true;
@@ -312,12 +353,17 @@ DirectoryModel.prototype.rescan = function() {
   var dirContents = this.currentDirContents_.clone();
   dirContents.setFileList([]);
 
+  var sequence = this.changeDirectorySequence_;
+
   var successCallback = (function() {
-    this.replaceDirectoryContents_(dirContents);
-    cr.dispatchSimpleEvent(this, 'rescan-completed');
+    if (sequence === this.changeDirectorySequence_) {
+      this.replaceDirectoryContents_(dirContents);
+      cr.dispatchSimpleEvent(this, 'rescan-completed');
+    }
   }).bind(this);
 
   this.scan_(dirContents,
+             refresh,
              successCallback, function() {}, function() {}, function() {});
 };
 
@@ -329,11 +375,12 @@ DirectoryModel.prototype.rescan = function() {
  *
  * @param {DirectoryContentes} newDirContents New DirectoryContents instance to
  *     replace currentDirContents_.
- * @param {function()=} opt_callback Called on success.
+ * @param {function(boolean)} callback Callback with result. True if the scan
+ *     is completed successfully, false if the scan is failed.
  * @private
  */
 DirectoryModel.prototype.clearAndScan_ = function(newDirContents,
-                                                  opt_callback) {
+                                                  callback) {
   if (this.currentDirContents_.isScanning())
     this.currentDirContents_.cancelScan();
   this.currentDirContents_ = newDirContents;
@@ -348,29 +395,53 @@ DirectoryModel.prototype.clearAndScan_ = function(newDirContents,
     this.runningScan_ = null;
   }
 
+  var sequence = this.changeDirectorySequence_;
+  var cancelled = false;
+
   var onDone = function() {
+    if (cancelled)
+      return;
+
     cr.dispatchSimpleEvent(this, 'scan-completed');
-    if (opt_callback)
-      opt_callback();
+    callback(true);
   }.bind(this);
 
   var onFailed = function() {
+    if (cancelled)
+      return;
+
     cr.dispatchSimpleEvent(this, 'scan-failed');
+    callback(false);
   }.bind(this);
 
   var onUpdated = function() {
+    if (cancelled)
+      return;
+
+    if (this.changeDirectorySequence_ !== sequence) {
+      cancelled = true;
+      cr.dispatchSimpleEvent(this, 'scan-cancelled');
+      callback(false);
+      return;
+    }
+
     cr.dispatchSimpleEvent(this, 'scan-updated');
   }.bind(this);
 
   var onCancelled = function() {
+    if (cancelled)
+      return;
+
+    cancelled = true;
     cr.dispatchSimpleEvent(this, 'scan-cancelled');
+    callback(false);
   }.bind(this);
 
   // Clear the table, and start scanning.
   cr.dispatchSimpleEvent(this, 'scan-started');
   var fileList = this.getFileList();
   fileList.splice(0, fileList.length);
-  this.scan_(this.currentDirContents_,
+  this.scan_(this.currentDirContents_, false,
              onDone, onFailed, onUpdated, onCancelled);
 };
 
@@ -380,6 +451,8 @@ DirectoryModel.prototype.clearAndScan_ = function(newDirContents,
  *
  * @param {DirectoryContents} dirContents DirectoryContents instance on which
  *     the scan will be run.
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ *     one.
  * @param {function()} successCallback Callback on success.
  * @param {function()} failureCallback Callback on failure.
  * @param {function()} updatedCallback Callback on update. Only on the last
@@ -389,6 +462,7 @@ DirectoryModel.prototype.clearAndScan_ = function(newDirContents,
  */
 DirectoryModel.prototype.scan_ = function(
     dirContents,
+    refresh,
     successCallback, failureCallback, updatedCallback, cancelledCallback) {
   var self = this;
 
@@ -399,7 +473,7 @@ DirectoryModel.prototype.scan_ = function(
    */
   var maybeRunPendingRescan = function() {
     if (this.pendingRescan_) {
-      this.rescanSoon();
+      this.rescanSoon(refresh);
       this.pendingRescan_ = false;
       return true;
     }
@@ -434,7 +508,7 @@ DirectoryModel.prototype.scan_ = function(
       return;
 
     if (this.scanFailures_ <= 1)
-      this.rescanLater();
+      this.rescanLater(refresh);
   }.bind(this);
 
   this.runningScan_ = dirContents;
@@ -443,7 +517,7 @@ DirectoryModel.prototype.scan_ = function(
   dirContents.addEventListener('scan-updated', updatedCallback);
   dirContents.addEventListener('scan-failed', onFailure);
   dirContents.addEventListener('scan-cancelled', cancelledCallback);
-  dirContents.scan();
+  dirContents.scan(refresh);
 };
 
 /**
@@ -460,6 +534,7 @@ DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) {
     var leadIndex = this.fileListSelection_.leadIndex;
     var leadEntry = this.getLeadEntry_();
 
+    this.currentDirContents_.dispose();
     this.currentDirContents_ = dirContents;
     dirContents.replaceContextFileList();
 
@@ -486,9 +561,9 @@ DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) {
 /**
  * Callback when an entry is changed.
  * @param {util.EntryChangedKind} kind How the entry is changed.
- * @param {Entry} entry The changed entry.
+ * @param {Array.<Entry>} entries The changed entries.
  */
-DirectoryModel.prototype.onEntryChanged = function(kind, entry) {
+DirectoryModel.prototype.onEntriesChanged = function(kind, entries) {
   // TODO(hidehiko): We should update directory model even the search result
   // is shown.
   var rootType = this.getCurrentRootType();
@@ -501,35 +576,38 @@ DirectoryModel.prototype.onEntryChanged = function(kind, entry) {
 
   switch (kind) {
     case util.EntryChangedKind.CREATED:
-      // Refresh the cache.
-      this.metadataCache_.clear([entry], '*');
-      entry.getParent(function(parentEntry) {
-        if (!util.isSameEntry(this.getCurrentDirEntry(), parentEntry)) {
-          // Do nothing if current directory changed during async operations.
-          return;
-        }
-        this.currentDirContents_.prefetchMetadata([entry], function() {
-          if (!util.isSameEntry(this.getCurrentDirEntry(), parentEntry)) {
-            // Do nothing if current directory changed during async operations.
-            return;
-          }
-
-          var index = this.findIndexByEntry_(entry);
+      var parentPromises = [];
+      for (var i = 0; i < entries.length; i++) {
+        parentPromises.push(new Promise(function(resolve, reject) {
+          entries[i].getParent(resolve, reject);
+        }));
+      }
+      Promise.all(parentPromises).then(function(parents) {
+        var entriesToAdd = [];
+        for (var i = 0; i < parents.length; i++) {
+          if (!util.isSameEntry(parents[i], this.getCurrentDirEntry()))
+            continue;
+          var index = this.findIndexByEntry_(entries[i]);
           if (index >= 0) {
             this.getFileList().replaceItem(
-                this.getFileList().item(index), entry);
+                this.getFileList().item(index), entries[i]);
           } else {
-            this.getFileList().push(entry);
+            entriesToAdd.push(entries[i]);
           }
-        }.bind(this));
-      }.bind(this));
+        }
+        this.getFileList().push.apply(this.getFileList(), entriesToAdd);
+      }.bind(this)).catch(function(error) {
+        console.error(error.stack || error);
+      });
       break;
 
     case util.EntryChangedKind.DELETED:
       // This is the delete event.
-      var index = this.findIndexByEntry_(entry);
-      if (index >= 0)
-        this.getFileList().splice(index, 1);
+      for (var i = 0; i < entries.length; i++) {
+        var index = this.findIndexByEntry_(entries[i]);
+        if (index >= 0)
+          this.getFileList().splice(index, 1);
+      }
       break;
 
     default:
@@ -565,7 +643,7 @@ DirectoryModel.prototype.findIndexByEntry_ = function(entry) {
  */
 DirectoryModel.prototype.onRenameEntry = function(
     oldEntry, newEntry, opt_callback) {
-  this.currentDirContents_.prefetchMetadata([newEntry], function() {
+  this.currentDirContents_.prefetchMetadata([newEntry], true, function() {
     // If the current directory is the old entry, then quietly change to the
     // new one.
     if (util.isSameEntry(oldEntry, this.getCurrentDirEntry()))
@@ -602,8 +680,7 @@ DirectoryModel.prototype.createDirectory = function(name,
     return;
   }
 
-  var tracker = this.createDirectoryChangeTracker();
-  tracker.start();
+  var sequence = this.changeDirectorySequence_;
 
   new Promise(entry.getDirectory.bind(
       entry, name, {create: true, exclusive: true})).
@@ -612,17 +689,16 @@ DirectoryModel.prototype.createDirectory = function(name,
         // Refresh the cache.
         this.metadataCache_.clear([newEntry], '*');
         return new Promise(function(onFulfilled, onRejected) {
-          this.metadataCache_.get([newEntry],
-                                  'filesystem',
-                                  onFulfilled.bind(null, newEntry));
+          this.metadataCache_.getOne(newEntry,
+                                     'filesystem',
+                                     onFulfilled.bind(null, newEntry));
         }.bind(this));
       }.bind(this)).
 
       then(function(newEntry) {
         // Do not change anything or call the callback if current
         // directory changed.
-        tracker.stop();
-        if (tracker.hasChanged) {
+        if (this.changeDirectorySequence_ !== sequence) {
           abortCallback();
           return;
         }
@@ -641,18 +717,21 @@ DirectoryModel.prototype.createDirectory = function(name,
           successCallback(newEntry);
         }
       }.bind(this), function(reason) {
-        tracker.stop();
         errorCallback(reason);
       });
 };
 
 /**
- * Change the current directory to the directory represented by
+ * Changes the current directory to the directory represented by
  * a DirectoryEntry or a fake entry.
  *
  * Dispatches the 'directory-changed' event when the directory is successfully
  * changed.
  *
+ * Note : if this is called from UI, please consider to use DirectoryModel.
+ * activateDirectoryEntry instead of this, which is higher-level function and
+ * cares about the selection.
+ *
  * @param {DirectoryEntry|Object} dirEntry The entry of the new directory to
  *     be opened.
  * @param {function()=} opt_callback Executed if the directory loads
@@ -664,26 +743,38 @@ DirectoryModel.prototype.changeDirectoryEntry = function(
   this.changeDirectorySequence_++;
   this.clearSearch_();
 
-  var promise = new Promise(
-      function(onFulfilled, onRejected) {
-        this.fileWatcher_.changeWatchedDirectory(dirEntry, onFulfilled);
-      }.bind(this)).
-
-      then(function(sequence) {
-        return new Promise(function(onFulfilled, onRejected) {
-          if (this.changeDirectorySequence_ !== sequence)
+  this.directoryChangeQueue_.run(function(sequence, queueTaskCallback) {
+    this.fileWatcher_.changeWatchedDirectory(
+        dirEntry,
+        function() {
+          if (this.changeDirectorySequence_ !== sequence) {
+            queueTaskCallback();
             return;
+          }
 
           var newDirectoryContents = this.createDirectoryContents_(
               this.currentFileListContext_, dirEntry, '');
-          if (!newDirectoryContents)
+          if (!newDirectoryContents) {
+            queueTaskCallback();
             return;
+          }
 
-          var previousDirEntry = this.currentDirContents_.getDirectoryEntry();
-          this.clearAndScan_(newDirectoryContents, opt_callback);
-
-          // For tests that open the dialog to empty directories, everything is
-          // loaded at this point.
+          var previousDirEntry =
+              this.currentDirContents_.getDirectoryEntry();
+          this.clearAndScan_(
+              newDirectoryContents,
+              function(result) {
+                // Calls the callback of the method when successful.
+                if (result && opt_callback)
+                  opt_callback();
+
+                // Notify that the current task of this.directoryChangeQueue_
+                // is completed.
+                setTimeout(queueTaskCallback);
+              });
+
+          // For tests that open the dialog to empty directories, everything
+          // is loaded at this point.
           util.testSendMessage('directory-change-complete');
 
           var event = new Event('directory-changed');
@@ -691,7 +782,32 @@ DirectoryModel.prototype.changeDirectoryEntry = function(
           event.newDirEntry = dirEntry;
           this.dispatchEvent(event);
         }.bind(this));
-      }.bind(this, this.changeDirectorySequence_));
+  }.bind(this, this.changeDirectorySequence_));
+};
+
+/**
+ * Activates the given directory.
+ * This method:
+ *  - Changes the current directory, if the given directory is the current
+ *    directory.
+ *  - Clears the selection, if the given directory is the current directory.
+ *
+ * @param {DirectoryEntry|Object} dirEntry The entry of the new directory to
+ *     be opened.
+ * @param {function()=} opt_callback Executed if the directory loads
+ *     successfully.
+ */
+DirectoryModel.prototype.activateDirectoryEntry = function(
+    dirEntry, opt_callback) {
+  var currentDirectoryEntry = this.getCurrentDirEntry();
+  if (currentDirectoryEntry &&
+      util.isSameEntry(dirEntry, currentDirectoryEntry)) {
+    // On activating the current directory, clear the selection on the filelist.
+    this.clearSelection();
+  } else {
+    // Otherwise, changes the current directory.
+    this.changeDirectoryEntry(dirEntry, opt_callback);
+  }
 };
 
 /**
@@ -874,25 +990,39 @@ DirectoryModel.prototype.search = function(query,
     return;
   }
 
-  if (!(query || '').trimLeft()) {
-    if (this.isSearching()) {
-      var newDirContents = this.createDirectoryContents_(
-          this.currentFileListContext_,
-          currentDirEntry);
-      this.clearAndScan_(newDirContents);
+  this.changeDirectorySequence_++;
+  this.directoryChangeQueue_.run(function(sequence, callback) {
+    if (this.changeDirectorySequence_ !== sequence) {
+      callback();
+      return;
     }
-    return;
-  }
 
-  var newDirContents = this.createDirectoryContents_(
-      this.currentFileListContext_, currentDirEntry, query);
-  if (!newDirContents)
-    return;
+    if (!(query || '').trimLeft()) {
+      if (this.isSearching()) {
+        var newDirContents = this.createDirectoryContents_(
+            this.currentFileListContext_,
+            currentDirEntry);
+        this.clearAndScan_(newDirContents,
+                           callback);
+      } else {
+        callback();
+      }
+      return;
+    }
+
+    var newDirContents = this.createDirectoryContents_(
+        this.currentFileListContext_, currentDirEntry, query);
+    if (!newDirContents) {
+      callback();
+      return;
+    }
 
-  this.onSearchCompleted_ = onSearchRescan;
-  this.onClearSearch_ = onClearSearch;
-  this.addEventListener('scan-completed', this.onSearchCompleted_);
-  this.clearAndScan_(newDirContents);
+    this.onSearchCompleted_ = onSearchRescan;
+    this.onClearSearch_ = onClearSearch;
+    this.addEventListener('scan-completed', this.onSearchCompleted_);
+    this.clearAndScan_(newDirContents,
+                       callback);
+  }.bind(this, this.changeDirectorySequence_));
 };
 
 /**