[Files.app] Use dictionary-way to access Entry.cachedUrl
[chromium-blink-merge.git] / ui / file_manager / file_manager / foreground / js / directory_contents.js
blob95266c38cc09e1265621f7094603612b01f31d90
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * Scanner of the entries.
7  * @constructor
8  */
9 function ContentScanner() {
10   this.cancelled_ = false;
13 /**
14  * Starts to scan the entries. For example, starts to read the entries in a
15  * directory, or starts to search with some query on a file system.
16  * Derived classes must override this method.
17  *
18  * @param {function(Array.<Entry>)} entriesCallback Called when some chunk of
19  *     entries are read. This can be called a couple of times until the
20  *     completion.
21  * @param {function()} successCallback Called when the scan is completed
22  *     successfully.
23  * @param {function(DOMError)} errorCallback Called an error occurs.
24  */
25 ContentScanner.prototype.scan = function(
26     entriesCallback, successCallback, errorCallback) {
29 /**
30  * Request cancelling of the running scan. When the cancelling is done,
31  * an error will be reported from errorCallback passed to scan().
32  */
33 ContentScanner.prototype.cancel = function() {
34   this.cancelled_ = true;
37 /**
38  * Scanner of the entries in a directory.
39  * @param {DirectoryEntry} entry The directory to be read.
40  * @constructor
41  * @extends {ContentScanner}
42  */
43 function DirectoryContentScanner(entry) {
44   ContentScanner.call(this);
45   this.entry_ = entry;
48 /**
49  * Extends ContentScanner.
50  */
51 DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype;
53 /**
54  * Starts to read the entries in the directory.
55  * @override
56  */
57 DirectoryContentScanner.prototype.scan = function(
58     entriesCallback, successCallback, errorCallback) {
59   if (!this.entry_ || util.isFakeEntry(this.entry_)) {
60     // If entry is not specified or a fake, we cannot read it.
61     errorCallback(util.createDOMError(
62         util.FileError.INVALID_MODIFICATION_ERR));
63     return;
64   }
66   metrics.startInterval('DirectoryScan');
67   var reader = this.entry_.createReader();
68   var readEntries = function() {
69     reader.readEntries(
70         function(entries) {
71           if (this.cancelled_) {
72             errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
73             return;
74           }
76           if (entries.length === 0) {
77             // All entries are read.
78             metrics.recordInterval('DirectoryScan');
79             successCallback();
80             return;
81           }
83           entriesCallback(entries);
84           readEntries();
85         }.bind(this),
86         errorCallback);
87   }.bind(this);
88   readEntries();
91 /**
92  * Scanner of the entries for the search results on Drive File System.
93  * @param {string} query The query string.
94  * @constructor
95  * @extends {ContentScanner}
96  */
97 function DriveSearchContentScanner(query) {
98   ContentScanner.call(this);
99   this.query_ = query;
103  * Extends ContentScanner.
104  */
105 DriveSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
108  * Delay in milliseconds to be used for drive search scan, in order to reduce
109  * the number of server requests while user is typing the query.
110  * @type {number}
111  * @private
112  * @const
113  */
114 DriveSearchContentScanner.SCAN_DELAY_ = 200;
117  * Maximum number of results which is shown on the search.
118  * @type {number}
119  * @private
120  * @const
121  */
122 DriveSearchContentScanner.MAX_RESULTS_ = 100;
125  * Starts to search on Drive File System.
126  * @override
127  */
128 DriveSearchContentScanner.prototype.scan = function(
129     entriesCallback, successCallback, errorCallback) {
130   var numReadEntries = 0;
131   var readEntries = function(nextFeed) {
132     chrome.fileManagerPrivate.searchDrive(
133         {query: this.query_, nextFeed: nextFeed},
134         function(entries, nextFeed) {
135           if (this.cancelled_) {
136             errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
137             return;
138           }
140           // TODO(tbarzic): Improve error handling.
141           if (!entries) {
142             console.error('Drive search encountered an error.');
143             errorCallback(util.createDOMError(
144                 util.FileError.INVALID_MODIFICATION_ERR));
145             return;
146           }
148           var numRemainingEntries =
149               DriveSearchContentScanner.MAX_RESULTS_ - numReadEntries;
150           if (entries.length >= numRemainingEntries) {
151             // The limit is hit, so quit the scan here.
152             entries = entries.slice(0, numRemainingEntries);
153             nextFeed = '';
154           }
156           numReadEntries += entries.length;
157           if (entries.length > 0)
158             entriesCallback(entries);
160           if (nextFeed === '')
161             successCallback();
162           else
163             readEntries(nextFeed);
164         }.bind(this));
165   }.bind(this);
167   // Let's give another search a chance to cancel us before we begin.
168   setTimeout(
169       function() {
170         // Check cancelled state before read the entries.
171         if (this.cancelled_) {
172           errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
173           return;
174         }
175         readEntries('');
176       }.bind(this),
177       DriveSearchContentScanner.SCAN_DELAY_);
181  * Scanner of the entries of the file name search on the directory tree, whose
182  * root is entry.
183  * @param {DirectoryEntry} entry The root of the search target directory tree.
184  * @param {string} query The query of the search.
185  * @constructor
186  * @extends {ContentScanner}
187  */
188 function LocalSearchContentScanner(entry, query) {
189   ContentScanner.call(this);
190   this.entry_ = entry;
191   this.query_ = query.toLowerCase();
195  * Extends ContentScanner.
196  */
197 LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
200  * Starts the file name search.
201  * @override
202  */
203 LocalSearchContentScanner.prototype.scan = function(
204     entriesCallback, successCallback, errorCallback) {
205   var numRunningTasks = 0;
206   var error = null;
207   var maybeRunCallback = function() {
208     if (numRunningTasks === 0) {
209       if (this.cancelled_)
210         errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
211       else if (error)
212         errorCallback(error);
213       else
214         successCallback();
215     }
216   }.bind(this);
218   var processEntry = function(entry) {
219     numRunningTasks++;
220     var onError = function(fileError) {
221       if (!error)
222         error = fileError;
223       numRunningTasks--;
224       maybeRunCallback();
225     };
227     var onSuccess = function(entries) {
228       if (this.cancelled_ || error || entries.length === 0) {
229         numRunningTasks--;
230         maybeRunCallback();
231         return;
232       }
234       // Filters by the query, and if found, run entriesCallback.
235       var foundEntries = entries.filter(function(entry) {
236         return entry.name.toLowerCase().indexOf(this.query_) >= 0;
237       }.bind(this));
238       if (foundEntries.length > 0)
239         entriesCallback(foundEntries);
241       // Start to process sub directories.
242       for (var i = 0; i < entries.length; i++) {
243         if (entries[i].isDirectory)
244           processEntry(entries[i]);
245       }
247       // Read remaining entries.
248       reader.readEntries(onSuccess, onError);
249     }.bind(this);
251     var reader = entry.createReader();
252     reader.readEntries(onSuccess, onError);
253   }.bind(this);
255   processEntry(this.entry_);
259  * Scanner of the entries for the metadata search on Drive File System.
260  * @param {!DriveMetadataSearchContentScanner.SearchType} searchType The option
261  *     of the search.
262  * @constructor
263  * @extends {ContentScanner}
264  */
265 function DriveMetadataSearchContentScanner(searchType) {
266   ContentScanner.call(this);
267   this.searchType_ = searchType;
271  * Extends ContentScanner.
272  */
273 DriveMetadataSearchContentScanner.prototype.__proto__ =
274     ContentScanner.prototype;
277  * The search types on the Drive File System.
278  * @enum {string}
279  */
280 DriveMetadataSearchContentScanner.SearchType = {
281   SEARCH_ALL: 'ALL',
282   SEARCH_SHARED_WITH_ME: 'SHARED_WITH_ME',
283   SEARCH_RECENT_FILES: 'EXCLUDE_DIRECTORIES',
284   SEARCH_OFFLINE: 'OFFLINE'
286 Object.freeze(DriveMetadataSearchContentScanner.SearchType);
289  * Starts to metadata-search on Drive File System.
290  * @override
291  */
292 DriveMetadataSearchContentScanner.prototype.scan = function(
293     entriesCallback, successCallback, errorCallback) {
294   chrome.fileManagerPrivate.searchDriveMetadata(
295       {query: '', types: this.searchType_, maxResults: 500},
296       function(results) {
297         if (this.cancelled_) {
298           errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
299           return;
300         }
302         if (!results) {
303           console.error('Drive search encountered an error.');
304           errorCallback(util.createDOMError(
305               util.FileError.INVALID_MODIFICATION_ERR));
306           return;
307         }
309         var entries = results.map(function(result) { return result.entry; });
310         if (entries.length > 0)
311           entriesCallback(entries);
312         successCallback();
313       }.bind(this));
317  * This class manages filters and determines a file should be shown or not.
318  * When filters are changed, a 'changed' event is fired.
320  * @param {boolean} showHidden If files starting with '.' or ending with
321  *     '.crdownlaod' are shown.
322  * @constructor
323  * @extends {cr.EventTarget}
324  */
325 function FileFilter(showHidden) {
326   /**
327    * @type {Object.<string, Function>}
328    * @private
329    */
330   this.filters_ = {};
331   this.setFilterHidden(!showHidden);
335  * FileFilter extends cr.EventTarget.
336  */
337 FileFilter.prototype = {__proto__: cr.EventTarget.prototype};
340  * @param {string} name Filter identifier.
341  * @param {function(Entry)} callback A filter â€” a function receiving an Entry,
342  *     and returning bool.
343  */
344 FileFilter.prototype.addFilter = function(name, callback) {
345   this.filters_[name] = callback;
346   cr.dispatchSimpleEvent(this, 'changed');
350  * @param {string} name Filter identifier.
351  */
352 FileFilter.prototype.removeFilter = function(name) {
353   delete this.filters_[name];
354   cr.dispatchSimpleEvent(this, 'changed');
358  * @param {boolean} value If do not show hidden files.
359  */
360 FileFilter.prototype.setFilterHidden = function(value) {
361   var regexpCrdownloadExtension = /\.crdownload$/i;
362   if (value) {
363     this.addFilter(
364         'hidden',
365         function(entry) {
366           return entry.name.substr(0, 1) !== '.' &&
367                  !regexpCrdownloadExtension.test(entry.name);
368         }
369     );
370   } else {
371     this.removeFilter('hidden');
372   }
376  * @return {boolean} If the files with names starting with "." are not shown.
377  */
378 FileFilter.prototype.isFilterHiddenOn = function() {
379   return 'hidden' in this.filters_;
383  * @param {Entry} entry File entry.
384  * @return {boolean} True if the file should be shown, false otherwise.
385  */
386 FileFilter.prototype.filter = function(entry) {
387   for (var name in this.filters_) {
388     if (!this.filters_[name](entry))
389       return false;
390   }
391   return true;
395  * File list.
396  * @param {!MetadataModel} metadataModel
397  * @constructor
398  * @extends {cr.ui.ArrayDataModel}
399  */
400 function FileListModel(metadataModel) {
401   cr.ui.ArrayDataModel.call(this, []);
403   /**
404    * @private {!MetadataModel}
405    * @const
406    */
407   this.metadataModel_ = metadataModel;
409   // Initialize compare functions.
410   this.setCompareFunction('name',
411       /** @type {function(*, *): number} */ (this.compareName_.bind(this)));
412   this.setCompareFunction('modificationTime',
413       /** @type {function(*, *): number} */ (this.compareMtime_.bind(this)));
414   this.setCompareFunction('size',
415       /** @type {function(*, *): number} */ (this.compareSize_.bind(this)));
416   this.setCompareFunction('type',
417       /** @type {function(*, *): number} */ (this.compareType_.bind(this)));
419   /**
420    * Whether this file list is sorted in descending order.
421    * @type {boolean}
422    * @private
423    */
424   this.isDescendingOrder_ = false;
427 FileListModel.prototype = {
428   __proto__: cr.ui.ArrayDataModel.prototype
432  * Sorts data model according to given field and direction and dispathes
433  * sorted event.
434  * @param {string} field Sort field.
435  * @param {string} direction Sort direction.
436  * @override
437  */
438 FileListModel.prototype.sort = function(field, direction) {
439   this.isDescendingOrder_ = direction === 'desc';
440   cr.ui.ArrayDataModel.prototype.sort.call(this, field, direction);
444  * Called before a sort happens so that you may fetch additional data
445  * required for the sort.
446  * @param {string} field Sort field.
447  * @param {function()} callback The function to invoke when preparation
448  *     is complete.
449  * @override
450  */
451 FileListModel.prototype.prepareSort = function(field, callback) {
452   // Starts the actual sorting immediately as we don't need any preparation to
453   // sort the file list and we want to start actual sorting as soon as possible
454   // after we get the |this.isDescendingOrder_| value in sort().
455   callback();
459  * Compares entries by name.
460  * @param {!Entry} a First entry.
461  * @param {!Entry} b Second entry.
462  * @return {number} Compare result.
463  * @private
464  */
465 FileListModel.prototype.compareName_ = function(a, b) {
466   // Directories always precede files.
467   if (a.isDirectory !== b.isDirectory)
468     return a.isDirectory === this.isDescendingOrder_ ? 1 : -1;
470   return util.compareName(a, b);
474  * Compares entries by mtime first, then by name.
475  * @param {Entry} a First entry.
476  * @param {Entry} b Second entry.
477  * @return {number} Compare result.
478  * @private
479  */
480 FileListModel.prototype.compareMtime_ = function(a, b) {
481   // Directories always precede files.
482   if (a.isDirectory !== b.isDirectory)
483     return a.isDirectory === this.isDescendingOrder_ ? 1 : -1;
485   var properties =
486       this.metadataModel_.getCache([a, b], ['modificationTime']);
487   var aTime = properties[0].modificationTime || 0;
488   var bTime = properties[1].modificationTime || 0;
490   if (aTime > bTime)
491     return 1;
493   if (aTime < bTime)
494     return -1;
496   return util.compareName(a, b);
500  * Compares entries by size first, then by name.
501  * @param {Entry} a First entry.
502  * @param {Entry} b Second entry.
503  * @return {number} Compare result.
504  * @private
505  */
506 FileListModel.prototype.compareSize_ = function(a, b) {
507   // Directories always precede files.
508   if (a.isDirectory !== b.isDirectory)
509     return a.isDirectory === this.isDescendingOrder_ ? 1 : -1;
511   var properties = this.metadataModel_.getCache([a, b], ['size']);
512   var aSize = properties[0].size || 0;
513   var bSize = properties[1].size || 0;
515   return aSize !== bSize ? aSize - bSize : util.compareName(a, b);
519  * Compares entries by type first, then by subtype and then by name.
520  * @param {Entry} a First entry.
521  * @param {Entry} b Second entry.
522  * @return {number} Compare result.
523  * @private
524  */
525 FileListModel.prototype.compareType_ = function(a, b) {
526   // Directories always precede files.
527   if (a.isDirectory !== b.isDirectory)
528     return a.isDirectory === this.isDescendingOrder_ ? 1 : -1;
530   var aType = FileType.typeToString(FileType.getType(a));
531   var bType = FileType.typeToString(FileType.getType(b));
533   var result = util.collator.compare(aType, bType);
534   return result !== 0 ? result : util.compareName(a, b);
538  * A context of DirectoryContents.
539  * TODO(yoshiki): remove this. crbug.com/224869.
541  * @param {FileFilter} fileFilter The file-filter context.
542  * @param {!MetadataModel} metadataModel
543  * @constructor
544  */
545 function FileListContext(fileFilter, metadataModel) {
546   /**
547    * @type {FileListModel}
548    */
549   this.fileList = new FileListModel(metadataModel);
551   /**
552    * @public {!MetadataModel}
553    * @const
554    */
555   this.metadataModel = metadataModel;
557   /**
558    * @type {FileFilter}
559    */
560   this.fileFilter = fileFilter;
562   /**
563    * @public {!Array<string>}
564    * @const
565    */
566   this.prefetchPropertyNames = FileListContext.createPrefetchPropertyNames_();
570  * @return {!Array<string>}
571  * @private
572  */
573 FileListContext.createPrefetchPropertyNames_ = function() {
574   var set = {};
575   for (var i = 0;
576        i < ListContainer.METADATA_PREFETCH_PROPERTY_NAMES.length;
577        i++) {
578     set[ListContainer.METADATA_PREFETCH_PROPERTY_NAMES[i]] = true;
579   }
580   for (var i = 0; i < Command.METADATA_PREFETCH_PROPERTY_NAMES.length; i++) {
581     set[Command.METADATA_PREFETCH_PROPERTY_NAMES[i]] = true;
582   }
583   for (var i = 0;
584        i < FileSelection.METADATA_PREFETCH_PROPERTY_NAMES.length;
585        i++) {
586     set[FileSelection.METADATA_PREFETCH_PROPERTY_NAMES[i]] = true;
587   }
588   return Object.keys(set);
592  * This class is responsible for scanning directory (or search results),
593  * and filling the fileList. Different descendants handle various types of
594  * directory contents shown: basic directory, drive search results, local search
595  * results.
596  * TODO(hidehiko): Remove EventTarget from this.
598  * @param {FileListContext} context The file list context.
599  * @param {boolean} isSearch True for search directory contents, otherwise
600  *     false.
601  * @param {DirectoryEntry} directoryEntry The entry of the current directory.
602  * @param {function():ContentScanner} scannerFactory The factory to create
603  *     ContentScanner instance.
604  * @constructor
605  * @extends {cr.EventTarget}
606  */
607 function DirectoryContents(context,
608                            isSearch,
609                            directoryEntry,
610                            scannerFactory) {
611   this.context_ = context;
612   this.fileList_ = context.fileList;
614   this.isSearch_ = isSearch;
615   this.directoryEntry_ = directoryEntry;
617   this.scannerFactory_ = scannerFactory;
618   this.scanner_ = null;
619   this.processNewEntriesQueue_ = new AsyncUtil.Queue();
620   this.scanCancelled_ = false;
622   /**
623    * Metadata snapshot which is used to know which file is actually changed.
624    * @type {Object}
625    */
626   this.metadataSnapshot_ = null;
630  * DirectoryContents extends cr.EventTarget.
631  */
632 DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype;
635  * Create the copy of the object, but without scan started.
636  * @return {!DirectoryContents} Object copy.
637  */
638 DirectoryContents.prototype.clone = function() {
639   return new DirectoryContents(
640       this.context_,
641       this.isSearch_,
642       this.directoryEntry_,
643       this.scannerFactory_);
647  * Use a given fileList instead of the fileList from the context.
648  * @param {(!Array|!cr.ui.ArrayDataModel)} fileList The new file list.
649  */
650 DirectoryContents.prototype.setFileList = function(fileList) {
651   if (fileList instanceof cr.ui.ArrayDataModel)
652     this.fileList_ = fileList;
653   else
654     this.fileList_ = new cr.ui.ArrayDataModel(fileList);
658  * Creates snapshot of metadata in the directory.
659  * @return {!Object} Metadata snapshot of current directory contents.
660  */
661 DirectoryContents.prototype.createMetadataSnapshot = function() {
662   var snapshot = {};
663   var entries = /** @type {!Array<!Entry>} */ (this.fileList_.slice());
664   var metadata = this.context_.metadataModel.getCache(
665       entries, ['modificationTime']);
666   for (var i = 0; i < entries.length; i++) {
667     snapshot[entries[i].toURL()] = metadata[i];
668   }
669   return snapshot;
673  * Sets metadata snapshot which is used to check changed files.
674  * @param {!Object} metadataSnapshot A metadata snapshot.
675  */
676 DirectoryContents.prototype.setMetadataSnapshot = function(metadataSnapshot) {
677   this.metadataSnapshot_ = metadataSnapshot;
681  * Use the filelist from the context and replace its contents with the entries
682  * from the current fileList. If metadata snapshot is set, this method checks
683  * actually updated files and dispatch change events by calling updateIndexes.
684  */
685 DirectoryContents.prototype.replaceContextFileList = function() {
686   if (this.context_.fileList !== this.fileList_) {
687     // TODO(yawano): While we should update the list with adding or deleting
688     // what actually added and deleted instead of deleting and adding all items,
689     // splice of array data model is expensive since it always runs sort and we
690     // replace the list in this way to reduce the number of splice calls.
691     var spliceArgs = this.fileList_.slice();
692     var fileList = this.context_.fileList;
693     spliceArgs.unshift(0, fileList.length);
694     fileList.splice.apply(fileList, spliceArgs);
695     this.fileList_ = fileList;
697     // Check updated files and dispatch change events.
698     if (this.metadataSnapshot_) {
699       var updatedIndexes = [];
700       var entries = /** @type {!Array<!Entry>} */ (this.fileList_.slice());
701       var newMetadatas = this.context_.metadataModel.getCache(
702           entries, ['modificationTime']);
704       for (var i = 0; i < entries.length; i++) {
705         var url = entries[i].toURL();
706         var newMetadata = newMetadatas[i];
707         // If Files.app fails to obtain both old and new modificationTime,
708         // regard the entry as not updated.
709         if ((this.metadataSnapshot_[url] &&
710              this.metadataSnapshot_[url].modificationTime &&
711              this.metadataSnapshot_[url].modificationTime.getTime()) !==
712             (newMetadata.modificationTime &&
713              newMetadata.modificationTime.getTime())) {
714           updatedIndexes.push(i);
715         }
716       }
718       if (updatedIndexes.length > 0)
719         this.fileList_.updateIndexes(updatedIndexes);
720     }
721   }
725  * @return {boolean} If the scan is active.
726  */
727 DirectoryContents.prototype.isScanning = function() {
728   return this.scanner_ || this.processNewEntriesQueue_.isRunning();
732  * @return {boolean} True if search results (drive or local).
733  */
734 DirectoryContents.prototype.isSearch = function() {
735   return this.isSearch_;
739  * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of
740  *     search -- the top directory from which search is run.
741  */
742 DirectoryContents.prototype.getDirectoryEntry = function() {
743   return this.directoryEntry_;
747  * Start directory scan/search operation. Either 'scan-completed' or
748  * 'scan-failed' event will be fired upon completion.
750  * @param {boolean} refresh True to refresh metadata, or false to use cached
751  *     one.
752  */
753 DirectoryContents.prototype.scan = function(refresh) {
754   /**
755    * Invoked when the scanning is completed successfully.
756    * @this {DirectoryContents}
757    */
758   function completionCallback() {
759     this.onScanFinished_();
760     this.onScanCompleted_();
761   }
763   /**
764    * Invoked when the scanning is finished but is not completed due to error.
765    * @this {DirectoryContents}
766    */
767   function errorCallback() {
768     this.onScanFinished_();
769     this.onScanError_();
770   }
772   // TODO(hidehiko,mtomasz): this scan method must be called at most once.
773   // Remove such a limitation.
774   this.scanner_ = this.scannerFactory_();
775   this.scanner_.scan(this.onNewEntries_.bind(this, refresh),
776                      completionCallback.bind(this),
777                      errorCallback.bind(this));
781  * Adds/removes/updates items of file list.
782  * @param {Array.<Entry>} updatedEntries Entries of updated/added files.
783  * @param {Array.<string>} removedUrls URLs of removed files.
784  */
785 DirectoryContents.prototype.update = function(updatedEntries, removedUrls) {
786   var removedMap = {};
787   for (var i = 0; i < removedUrls.length; i++) {
788     removedMap[removedUrls[i]] = true;
789   }
791   var updatedMap = {};
792   for (var i = 0; i < updatedEntries.length; i++) {
793     updatedMap[updatedEntries[i].toURL()] = updatedEntries[i];
794   }
796   var updatedList = [];
797   var updatedIndexes = [];
798   for (var i = 0; i < this.fileList_.length; i++) {
799     var url = this.fileList_.item(i).toURL();
801     if (url in removedMap) {
802       // Find the maximum range in which all items need to be removed.
803       var begin = i;
804       var end = i + 1;
805       while (end < this.fileList_.length &&
806              this.fileList_.item(end).toURL() in removedMap) {
807         end++;
808       }
809       // Remove the range [begin, end) at once to avoid multiple sorting.
810       this.fileList_.splice(begin, end - begin);
811       i--;
812       continue;
813     }
815     if (url in updatedMap) {
816       updatedList.push(updatedMap[url]);
817       updatedIndexes.push(i);
818       delete updatedMap[url];
819     }
820   }
822   if (updatedIndexes.length > 0)
823     this.fileList_.updateIndexes(updatedIndexes);
825   var addedList = [];
826   for (var url in updatedMap) {
827     addedList.push(updatedMap[url]);
828   }
830   if (removedUrls.length > 0)
831     this.context_.metadataModel.notifyEntriesRemoved(removedUrls);
833   this.prefetchMetadata(updatedList, true, function() {
834     this.onNewEntries_(true, addedList);
835     this.onScanFinished_();
836     this.onScanCompleted_();
837   }.bind(this));
841  * Cancels the running scan.
842  */
843 DirectoryContents.prototype.cancelScan = function() {
844   if (this.scanCancelled_)
845     return;
846   this.scanCancelled_ = true;
847   if (this.scanner_)
848     this.scanner_.cancel();
850   this.onScanFinished_();
852   this.processNewEntriesQueue_.cancel();
853   cr.dispatchSimpleEvent(this, 'scan-cancelled');
857  * Called when the scanning by scanner_ is done, even when the scanning is
858  * succeeded or failed. This is called before completion (or error) callback.
860  * @private
861  */
862 DirectoryContents.prototype.onScanFinished_ = function() {
863   this.scanner_ = null;
867  * Called when the scanning by scanner_ is succeeded.
868  * @private
869  */
870 DirectoryContents.prototype.onScanCompleted_ = function() {
871   if (this.scanCancelled_)
872     return;
874   this.processNewEntriesQueue_.run(function(callback) {
875     // Call callback first, so isScanning() returns false in the event handlers.
876     callback();
878     cr.dispatchSimpleEvent(this, 'scan-completed');
879   }.bind(this));
883  * Called in case scan has failed. Should send the event.
884  * @private
885  */
886 DirectoryContents.prototype.onScanError_ = function() {
887   if (this.scanCancelled_)
888     return;
890   this.processNewEntriesQueue_.run(function(callback) {
891     // Call callback first, so isScanning() returns false in the event handlers.
892     callback();
893     cr.dispatchSimpleEvent(this, 'scan-failed');
894   }.bind(this));
898  * Called when some chunk of entries are read by scanner.
900  * @param {boolean} refresh True to refresh metadata, or false to use cached
901  *     one.
902  * @param {Array.<Entry>} entries The list of the scanned entries.
903  * @private
904  */
905 DirectoryContents.prototype.onNewEntries_ = function(refresh, entries) {
906   if (this.scanCancelled_)
907     return;
909   var entriesFiltered = [].filter.call(
910       entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter));
912   // Caching URL to reduce a number of calls of toURL in sort.
913   // This is a temporary solution. We need to fix a root cause of slow toURL.
914   // See crbug.com/370908 for detail.
915   entriesFiltered.forEach(function(entry) {
916     entry['cachedUrl'] = entry.toURL();
917   });
919   if (entriesFiltered.length === 0)
920     return;
922   // Enlarge the cache size into the new filelist size.
923   var newListSize = this.fileList_.length + entriesFiltered.length;
925   this.processNewEntriesQueue_.run(function(callbackOuter) {
926     var finish = function() {
927       if (!this.scanCancelled_) {
928         // Just before inserting entries into the file list, check and avoid
929         // duplication.
930         var currentURLs = {};
931         for (var i = 0; i < this.fileList_.length; i++)
932           currentURLs[this.fileList_.item(i).toURL()] = true;
933         entriesFiltered = entriesFiltered.filter(function(entry) {
934           return !currentURLs[entry.toURL()];
935         });
936         // Update the filelist without waiting the metadata.
937         this.fileList_.push.apply(this.fileList_, entriesFiltered);
938         cr.dispatchSimpleEvent(this, 'scan-updated');
939       }
940       callbackOuter();
941     }.bind(this);
942     // Because the prefetchMetadata can be slow, throttling by splitting entries
943     // into smaller chunks to reduce UI latency.
944     // TODO(hidehiko,mtomasz): This should be handled in MetadataCache.
945     var MAX_CHUNK_SIZE = 25;
946     var prefetchMetadataQueue = new AsyncUtil.ConcurrentQueue(4);
947     for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) {
948       if (prefetchMetadataQueue.isCancelled())
949         break;
951       var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE);
952       prefetchMetadataQueue.run(function(chunk, callbackInner) {
953         this.prefetchMetadata(chunk, refresh, function() {
954           if (!prefetchMetadataQueue.isCancelled()) {
955             if (this.scanCancelled_)
956               prefetchMetadataQueue.cancel();
957           }
959           // Checks if this is the last task.
960           if (prefetchMetadataQueue.getWaitingTasksCount() === 0 &&
961               prefetchMetadataQueue.getRunningTasksCount() === 1) {
962             // |callbackOuter| in |finish| must be called before
963             // |callbackInner|, to prevent double-calling.
964             finish();
965           }
967           callbackInner();
968         }.bind(this));
969       }.bind(this, chunk));
970     }
971   }.bind(this));
975  * @param {!Array<!Entry>} entries Files.
976  * @param {boolean} refresh True to refresh metadata, or false to use cached
977  *     one.
978  * @param {function(Object)} callback Callback on done.
979  */
980 DirectoryContents.prototype.prefetchMetadata =
981     function(entries, refresh, callback) {
982   if (refresh)
983     this.context_.metadataModel.notifyEntriesChanged(entries);
984   this.context_.metadataModel.get(
985       entries, this.context_.prefetchPropertyNames).then(callback);
989  * Creates a DirectoryContents instance to show entries in a directory.
991  * @param {FileListContext} context File list context.
992  * @param {DirectoryEntry} directoryEntry The current directory entry.
993  * @return {DirectoryContents} Created DirectoryContents instance.
994  */
995 DirectoryContents.createForDirectory = function(context, directoryEntry) {
996   return new DirectoryContents(
997       context,
998       false,  // Non search.
999       directoryEntry,
1000       function() {
1001         return new DirectoryContentScanner(directoryEntry);
1002       });
1006  * Creates a DirectoryContents instance to show the result of the search on
1007  * Drive File System.
1009  * @param {FileListContext} context File list context.
1010  * @param {DirectoryEntry} directoryEntry The current directory entry.
1011  * @param {string} query Search query.
1012  * @return {DirectoryContents} Created DirectoryContents instance.
1013  */
1014 DirectoryContents.createForDriveSearch = function(
1015     context, directoryEntry, query) {
1016   return new DirectoryContents(
1017       context,
1018       true,  // Search.
1019       directoryEntry,
1020       function() {
1021         return new DriveSearchContentScanner(query);
1022       });
1026  * Creates a DirectoryContents instance to show the result of the search on
1027  * Local File System.
1029  * @param {FileListContext} context File list context.
1030  * @param {DirectoryEntry} directoryEntry The current directory entry.
1031  * @param {string} query Search query.
1032  * @return {DirectoryContents} Created DirectoryContents instance.
1033  */
1034 DirectoryContents.createForLocalSearch = function(
1035     context, directoryEntry, query) {
1036   return new DirectoryContents(
1037       context,
1038       true,  // Search.
1039       directoryEntry,
1040       function() {
1041         return new LocalSearchContentScanner(directoryEntry, query);
1042       });
1046  * Creates a DirectoryContents instance to show the result of metadata search
1047  * on Drive File System.
1049  * @param {FileListContext} context File list context.
1050  * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing
1051  *     the set of result entries. This serves as a top directory for the
1052  *     search.
1053  * @param {!DriveMetadataSearchContentScanner.SearchType} searchType The type of
1054  *     the search. The scanner will restricts the entries based on the given
1055  *     type.
1056  * @return {DirectoryContents} Created DirectoryContents instance.
1057  */
1058 DirectoryContents.createForDriveMetadataSearch = function(
1059     context, fakeDirectoryEntry, searchType) {
1060   return new DirectoryContents(
1061       context,
1062       true,  // Search
1063       fakeDirectoryEntry,
1064       function() {
1065         return new DriveMetadataSearchContentScanner(searchType);
1066       });