Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / directory_contents.js
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.
4
5 /**
6  * Scanner of the entries.
7  * @constructor
8  */
9 function ContentScanner() {
10   this.cancelled_ = false;
11 }
12
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) {
27 };
28
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;
35 };
36
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;
46 }
47
48 /**
49  * Extends ContentScanner.
50  */
51 DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype;
52
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   }
65
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           }
75
76           if (entries.length === 0) {
77             // All entries are read.
78             metrics.recordInterval('DirectoryScan');
79             successCallback();
80             return;
81           }
82
83           entriesCallback(entries);
84           readEntries();
85         }.bind(this),
86         errorCallback);
87   }.bind(this);
88   readEntries();
89 };
90
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;
100 }
101
102 /**
103  * Extends ContentScanner.
104  */
105 DriveSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
106
107 /**
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;
115
116 /**
117  * Maximum number of results which is shown on the search.
118  * @type {number}
119  * @private
120  * @const
121  */
122 DriveSearchContentScanner.MAX_RESULTS_ = 100;
123
124 /**
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           }
139
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           }
147
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           }
155
156           numReadEntries += entries.length;
157           if (entries.length > 0)
158             entriesCallback(entries);
159
160           if (nextFeed === '')
161             successCallback();
162           else
163             readEntries(nextFeed);
164         }.bind(this));
165   }.bind(this);
166
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_);
178 };
179
180 /**
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();
192 }
193
194 /**
195  * Extends ContentScanner.
196  */
197 LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
198
199 /**
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);
217
218   var processEntry = function(entry) {
219     numRunningTasks++;
220     var onError = function(fileError) {
221       if (!error)
222         error = fileError;
223       numRunningTasks--;
224       maybeRunCallback();
225     };
226
227     var onSuccess = function(entries) {
228       if (this.cancelled_ || error || entries.length === 0) {
229         numRunningTasks--;
230         maybeRunCallback();
231         return;
232       }
233
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);
240
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       }
246
247       // Read remaining entries.
248       reader.readEntries(onSuccess, onError);
249     }.bind(this);
250
251     var reader = entry.createReader();
252     reader.readEntries(onSuccess, onError);
253   }.bind(this);
254
255   processEntry(this.entry_);
256 };
257
258 /**
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;
268 }
269
270 /**
271  * Extends ContentScanner.
272  */
273 DriveMetadataSearchContentScanner.prototype.__proto__ =
274     ContentScanner.prototype;
275
276 /**
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'
285 };
286 Object.freeze(DriveMetadataSearchContentScanner.SearchType);
287
288 /**
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         }
301
302         if (!results) {
303           console.error('Drive search encountered an error.');
304           errorCallback(util.createDOMError(
305               util.FileError.INVALID_MODIFICATION_ERR));
306           return;
307         }
308
309         var entries = results.map(function(result) { return result.entry; });
310         if (entries.length > 0)
311           entriesCallback(entries);
312         successCallback();
313       }.bind(this));
314 };
315
316 /**
317  * This class manages filters and determines a file should be shown or not.
318  * When filters are changed, a 'changed' event is fired.
319  *
320  * @param {MetadataCache} metadataCache Metadata cache service.
321  * @param {boolean} showHidden If files starting with '.' or ending with
322  *     '.crdownlaod' are shown.
323  * @constructor
324  * @extends {cr.EventTarget}
325  */
326 function FileFilter(metadataCache, showHidden) {
327   /**
328    * @type {MetadataCache}
329    * @private
330    */
331   this.metadataCache_ = metadataCache;
332
333   /**
334    * @type {Object.<string, Function>}
335    * @private
336    */
337   this.filters_ = {};
338   this.setFilterHidden(!showHidden);
339
340   // Do not show entries marked as 'deleted'.
341   this.addFilter('deleted', function(entry) {
342     var internal = /** @type {{deleted}} */
343         (this.metadataCache_.getCached(entry, 'internal'));
344     return !(internal && internal.deleted);
345   }.bind(this));
346 }
347
348 /*
349  * FileFilter extends cr.EventTarget.
350  */
351 FileFilter.prototype = {__proto__: cr.EventTarget.prototype};
352
353 /**
354  * @param {string} name Filter identifier.
355  * @param {function(Entry)} callback A filter â€” a function receiving an Entry,
356  *     and returning bool.
357  */
358 FileFilter.prototype.addFilter = function(name, callback) {
359   this.filters_[name] = callback;
360   cr.dispatchSimpleEvent(this, 'changed');
361 };
362
363 /**
364  * @param {string} name Filter identifier.
365  */
366 FileFilter.prototype.removeFilter = function(name) {
367   delete this.filters_[name];
368   cr.dispatchSimpleEvent(this, 'changed');
369 };
370
371 /**
372  * @param {boolean} value If do not show hidden files.
373  */
374 FileFilter.prototype.setFilterHidden = function(value) {
375   var regexpCrdownloadExtension = /\.crdownload$/i;
376   if (value) {
377     this.addFilter(
378         'hidden',
379         function(entry) {
380           return entry.name.substr(0, 1) !== '.' &&
381                  !regexpCrdownloadExtension.test(entry.name);
382         }
383     );
384   } else {
385     this.removeFilter('hidden');
386   }
387 };
388
389 /**
390  * @return {boolean} If the files with names starting with "." are not shown.
391  */
392 FileFilter.prototype.isFilterHiddenOn = function() {
393   return 'hidden' in this.filters_;
394 };
395
396 /**
397  * @param {Entry} entry File entry.
398  * @return {boolean} True if the file should be shown, false otherwise.
399  */
400 FileFilter.prototype.filter = function(entry) {
401   for (var name in this.filters_) {
402     if (!this.filters_[name](entry))
403       return false;
404   }
405   return true;
406 };
407
408 /**
409  * File list.
410  * @param {MetadataCache} metadataCache Metadata cache.
411  * @constructor
412  * @extends {cr.ui.ArrayDataModel}
413  */
414 function FileListModel(metadataCache) {
415   cr.ui.ArrayDataModel.call(this, []);
416
417   /**
418    * Metadata cache.
419    * @type {MetadataCache}
420    * @private
421    */
422   this.metadataCache_ = metadataCache;
423
424   // Initialize compare functions.
425   this.setCompareFunction('name',
426       /** @type {function(*, *): number} */ (util.compareName));
427   this.setCompareFunction('modificationTime',
428       /** @type {function(*, *): number} */ (this.compareMtime_.bind(this)));
429   this.setCompareFunction('size',
430       /** @type {function(*, *): number} */ (this.compareSize_.bind(this)));
431   this.setCompareFunction('type',
432       /** @type {function(*, *): number} */ (this.compareType_.bind(this)));
433 }
434
435 FileListModel.prototype = {
436   __proto__: cr.ui.ArrayDataModel.prototype
437 };
438
439 /**
440  * Compare by mtime first, then by name.
441  * @param {Entry} a First entry.
442  * @param {Entry} b Second entry.
443  * @return {number} Compare result.
444  * @private
445  */
446 FileListModel.prototype.compareMtime_ = function(a, b) {
447   var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
448   var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0;
449
450   var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
451   var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0;
452
453   if (aTime > bTime)
454     return 1;
455
456   if (aTime < bTime)
457     return -1;
458
459   return util.compareName(a, b);
460 };
461
462 /**
463  * Compare by size first, then by name.
464  * @param {Entry} a First entry.
465  * @param {Entry} b Second entry.
466  * @return {number} Compare result.
467  * @private
468  */
469 FileListModel.prototype.compareSize_ = function(a, b) {
470   var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
471   var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0;
472
473   var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
474   var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0;
475
476   return aSize !== bSize ? aSize - bSize : util.compareName(a, b);
477 };
478
479 /**
480  * Compare by type first, then by subtype and then by name.
481  * @param {Entry} a First entry.
482  * @param {Entry} b Second entry.
483  * @return {number} Compare result.
484  * @private
485  */
486 FileListModel.prototype.compareType_ = function(a, b) {
487   // Directories precede files.
488   if (a.isDirectory !== b.isDirectory)
489     return Number(b.isDirectory) - Number(a.isDirectory);
490
491   var aType = FileType.typeToString(FileType.getType(a));
492   var bType = FileType.typeToString(FileType.getType(b));
493
494   var result = util.collator.compare(aType, bType);
495   return result !== 0 ? result : util.compareName(a, b);
496 };
497
498 /**
499  * A context of DirectoryContents.
500  * TODO(yoshiki): remove this. crbug.com/224869.
501  *
502  * @param {FileFilter} fileFilter The file-filter context.
503  * @param {MetadataCache} metadataCache Metadata cache service.
504  * @constructor
505  */
506 function FileListContext(fileFilter, metadataCache) {
507   /**
508    * @type {FileListModel}
509    */
510   this.fileList = new FileListModel(metadataCache);
511
512   /**
513    * @type {MetadataCache}
514    */
515   this.metadataCache = metadataCache;
516
517   /**
518    * @type {FileFilter}
519    */
520   this.fileFilter = fileFilter;
521 }
522
523 /**
524  * This class is responsible for scanning directory (or search results),
525  * and filling the fileList. Different descendants handle various types of
526  * directory contents shown: basic directory, drive search results, local search
527  * results.
528  * TODO(hidehiko): Remove EventTarget from this.
529  *
530  * @param {FileListContext} context The file list context.
531  * @param {boolean} isSearch True for search directory contents, otherwise
532  *     false.
533  * @param {DirectoryEntry} directoryEntry The entry of the current directory.
534  * @param {function():ContentScanner} scannerFactory The factory to create
535  *     ContentScanner instance.
536  * @constructor
537  * @extends {cr.EventTarget}
538  */
539 function DirectoryContents(context,
540                            isSearch,
541                            directoryEntry,
542                            scannerFactory) {
543   this.context_ = context;
544   this.fileList_ = context.fileList;
545
546   this.isSearch_ = isSearch;
547   this.directoryEntry_ = directoryEntry;
548
549   this.scannerFactory_ = scannerFactory;
550   this.scanner_ = null;
551   this.processNewEntriesQueue_ = new AsyncUtil.Queue();
552   this.scanCancelled_ = false;
553
554   this.lastSpaceInMetadataCache_ = 0;
555 }
556
557 /**
558  * DirectoryContents extends cr.EventTarget.
559  */
560 DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype;
561
562 /**
563  * Create the copy of the object, but without scan started.
564  * @return {DirectoryContents} Object copy.
565  */
566 DirectoryContents.prototype.clone = function() {
567   return new DirectoryContents(
568       this.context_,
569       this.isSearch_,
570       this.directoryEntry_,
571       this.scannerFactory_);
572 };
573
574 /**
575  * Disposes the reserved metadata cache.
576  */
577 DirectoryContents.prototype.dispose = function() {
578   this.context_.metadataCache.resizeBy(-this.lastSpaceInMetadataCache_);
579   // Though the lastSpaceInMetadataCache_ is not supposed to be referred after
580   // dispose(), keep it synced with requested cache size just in case.
581   this.lastSpaceInMetadataCache_ = 0;
582 };
583
584 /**
585  * Make a space for current directory size in the metadata cache.
586  *
587  * @param {number} size The cache size to be set.
588  * @private
589  */
590 DirectoryContents.prototype.makeSpaceInMetadataCache_ = function(size) {
591   this.context_.metadataCache.resizeBy(size - this.lastSpaceInMetadataCache_);
592   this.lastSpaceInMetadataCache_ = size;
593 };
594
595 /**
596  * Use a given fileList instead of the fileList from the context.
597  * @param {(!Array|!cr.ui.ArrayDataModel)} fileList The new file list.
598  */
599 DirectoryContents.prototype.setFileList = function(fileList) {
600   if (fileList instanceof cr.ui.ArrayDataModel)
601     this.fileList_ = fileList;
602   else
603     this.fileList_ = new cr.ui.ArrayDataModel(fileList);
604   this.makeSpaceInMetadataCache_(this.fileList_.length);
605 };
606
607 /**
608  * Use the filelist from the context and replace its contents with the entries
609  * from the current fileList.
610  */
611 DirectoryContents.prototype.replaceContextFileList = function() {
612   if (this.context_.fileList !== this.fileList_) {
613     var spliceArgs = this.fileList_.slice();
614     var fileList = this.context_.fileList;
615     spliceArgs.unshift(0, fileList.length);
616     fileList.splice.apply(fileList, spliceArgs);
617     this.fileList_ = fileList;
618     this.makeSpaceInMetadataCache_(this.fileList_.length);
619   }
620 };
621
622 /**
623  * @return {boolean} If the scan is active.
624  */
625 DirectoryContents.prototype.isScanning = function() {
626   return this.scanner_ || this.processNewEntriesQueue_.isRunning();
627 };
628
629 /**
630  * @return {boolean} True if search results (drive or local).
631  */
632 DirectoryContents.prototype.isSearch = function() {
633   return this.isSearch_;
634 };
635
636 /**
637  * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of
638  *     search -- the top directory from which search is run.
639  */
640 DirectoryContents.prototype.getDirectoryEntry = function() {
641   return this.directoryEntry_;
642 };
643
644 /**
645  * Start directory scan/search operation. Either 'scan-completed' or
646  * 'scan-failed' event will be fired upon completion.
647  *
648  * @param {boolean} refresh True to refresh metadata, or false to use cached
649  *     one.
650  */
651 DirectoryContents.prototype.scan = function(refresh) {
652   /**
653    * Invoked when the scanning is completed successfully.
654    * @this {DirectoryContents}
655    */
656   function completionCallback() {
657     this.onScanFinished_();
658     this.onScanCompleted_();
659   }
660
661   /**
662    * Invoked when the scanning is finished but is not completed due to error.
663    * @this {DirectoryContents}
664    */
665   function errorCallback() {
666     this.onScanFinished_();
667     this.onScanError_();
668   }
669
670   // TODO(hidehiko,mtomasz): this scan method must be called at most once.
671   // Remove such a limitation.
672   this.scanner_ = this.scannerFactory_();
673   this.scanner_.scan(this.onNewEntries_.bind(this, refresh),
674                      completionCallback.bind(this),
675                      errorCallback.bind(this));
676 };
677
678 /**
679  * Adds/removes/updates items of file list.
680  * @param {Array.<Entry>} updatedEntries Entries of updated/added files.
681  * @param {Array.<string>} removedUrls URLs of removed files.
682  */
683 DirectoryContents.prototype.update = function(updatedEntries, removedUrls) {
684   var removedMap = {};
685   for (var i = 0; i < removedUrls.length; i++) {
686     removedMap[removedUrls[i]] = true;
687   }
688
689   var updatedMap = {};
690   for (var i = 0; i < updatedEntries.length; i++) {
691     updatedMap[updatedEntries[i].toURL()] = updatedEntries[i];
692   }
693
694   var updatedList = [];
695   for (var i = 0; i < this.fileList_.length; i++) {
696     var url = this.fileList_.item(i).toURL();
697
698     if (url in removedMap) {
699       this.fileList_.splice(i, 1);
700       i--;
701       continue;
702     }
703
704     if (url in updatedMap) {
705       updatedList.push(updatedMap[url]);
706       delete updatedMap[url];
707     }
708   }
709
710   var addedList = [];
711   for (var url in updatedMap) {
712     addedList.push(updatedMap[url]);
713   }
714
715   if (removedUrls.length > 0)
716     this.fileList_.metadataCache_.clearByUrl(removedUrls, '*');
717
718   this.prefetchMetadata(updatedList, true, function() {
719     this.onNewEntries_(true, addedList);
720     this.onScanFinished_();
721     this.onScanCompleted_();
722   }.bind(this));
723 };
724
725 /**
726  * Cancels the running scan.
727  */
728 DirectoryContents.prototype.cancelScan = function() {
729   if (this.scanCancelled_)
730     return;
731   this.scanCancelled_ = true;
732   if (this.scanner_)
733     this.scanner_.cancel();
734
735   this.onScanFinished_();
736
737   this.processNewEntriesQueue_.cancel();
738   cr.dispatchSimpleEvent(this, 'scan-cancelled');
739 };
740
741 /**
742  * Called when the scanning by scanner_ is done, even when the scanning is
743  * succeeded or failed. This is called before completion (or error) callback.
744  *
745  * @private
746  */
747 DirectoryContents.prototype.onScanFinished_ = function() {
748   this.scanner_ = null;
749
750   this.processNewEntriesQueue_.run(function(callback) {
751     // TODO(yoshiki): Here we should fire the update event of changed
752     // items. Currently we have a method this.fileList_.updateIndex() to
753     // fire an event, but this method takes only 1 argument and invokes sort
754     // one by one. It is obviously time wasting. Instead, we call sort
755     // directory.
756     // In future, we should implement a good method like updateIndexes and
757     // use it here.
758     var status = this.fileList_.sortStatus;
759     if (status)
760       this.fileList_.sort(status.field, status.direction);
761
762     callback();
763   }.bind(this));
764 };
765
766 /**
767  * Called when the scanning by scanner_ is succeeded.
768  * @private
769  */
770 DirectoryContents.prototype.onScanCompleted_ = function() {
771   if (this.scanCancelled_)
772     return;
773
774   this.processNewEntriesQueue_.run(function(callback) {
775     // Call callback first, so isScanning() returns false in the event handlers.
776     callback();
777
778     cr.dispatchSimpleEvent(this, 'scan-completed');
779   }.bind(this));
780 };
781
782 /**
783  * Called in case scan has failed. Should send the event.
784  * @private
785  */
786 DirectoryContents.prototype.onScanError_ = function() {
787   if (this.scanCancelled_)
788     return;
789
790   this.processNewEntriesQueue_.run(function(callback) {
791     // Call callback first, so isScanning() returns false in the event handlers.
792     callback();
793     cr.dispatchSimpleEvent(this, 'scan-failed');
794   }.bind(this));
795 };
796
797 /**
798  * Called when some chunk of entries are read by scanner.
799  *
800  * @param {boolean} refresh True to refresh metadata, or false to use cached
801  *     one.
802  * @param {Array.<Entry>} entries The list of the scanned entries.
803  * @private
804  */
805 DirectoryContents.prototype.onNewEntries_ = function(refresh, entries) {
806   if (this.scanCancelled_)
807     return;
808
809   var entriesFiltered = [].filter.call(
810       entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter));
811
812   // Caching URL to reduce a number of calls of toURL in sort.
813   // This is a temporary solution. We need to fix a root cause of slow toURL.
814   // See crbug.com/370908 for detail.
815   entriesFiltered.forEach(function(entry) { entry.cachedUrl = entry.toURL(); });
816
817   if (entriesFiltered.length === 0)
818     return;
819
820   // Enlarge the cache size into the new filelist size.
821   var newListSize = this.fileList_.length + entriesFiltered.length;
822   this.makeSpaceInMetadataCache_(newListSize);
823
824   this.processNewEntriesQueue_.run(function(callbackOuter) {
825     var finish = function() {
826       if (!this.scanCancelled_) {
827         // Just before inserting entries into the file list, check and avoid
828         // duplication.
829         var currentURLs = {};
830         for (var i = 0; i < this.fileList_.length; i++)
831           currentURLs[this.fileList_.item(i).toURL()] = true;
832         entriesFiltered = entriesFiltered.filter(function(entry) {
833           return !currentURLs[entry.toURL()];
834         });
835         // Update the filelist without waiting the metadata.
836         this.fileList_.push.apply(this.fileList_, entriesFiltered);
837         cr.dispatchSimpleEvent(this, 'scan-updated');
838       }
839       callbackOuter();
840     }.bind(this);
841     // Because the prefetchMetadata can be slow, throttling by splitting entries
842     // into smaller chunks to reduce UI latency.
843     // TODO(hidehiko,mtomasz): This should be handled in MetadataCache.
844     var MAX_CHUNK_SIZE = 25;
845     var prefetchMetadataQueue = new AsyncUtil.ConcurrentQueue(4);
846     for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) {
847       if (prefetchMetadataQueue.isCancelled())
848         break;
849
850       var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE);
851       prefetchMetadataQueue.run(function(chunk, callbackInner) {
852         this.prefetchMetadata(chunk, refresh, function() {
853           if (!prefetchMetadataQueue.isCancelled()) {
854             if (this.scanCancelled_)
855               prefetchMetadataQueue.cancel();
856           }
857
858           // Checks if this is the last task.
859           if (prefetchMetadataQueue.getWaitingTasksCount() === 0 &&
860               prefetchMetadataQueue.getRunningTasksCount() === 1) {
861             // |callbackOuter| in |finish| must be called before
862             // |callbackInner|, to prevent double-calling.
863             finish();
864           }
865
866           callbackInner();
867         }.bind(this));
868       }.bind(this, chunk));
869     }
870   }.bind(this));
871 };
872
873 /**
874  * @param {Array.<Entry>} entries Files.
875  * @param {boolean} refresh True to refresh metadata, or false to use cached
876  *     one.
877  * @param {function(Object)} callback Callback on done.
878  */
879 DirectoryContents.prototype.prefetchMetadata =
880     function(entries, refresh, callback) {
881   var TYPES = 'filesystem|external';
882   if (refresh)
883     this.context_.metadataCache.getLatest(entries, TYPES, callback);
884   else
885     this.context_.metadataCache.get(entries, TYPES, callback);
886 };
887
888 /**
889  * Creates a DirectoryContents instance to show entries in a directory.
890  *
891  * @param {FileListContext} context File list context.
892  * @param {DirectoryEntry} directoryEntry The current directory entry.
893  * @return {DirectoryContents} Created DirectoryContents instance.
894  */
895 DirectoryContents.createForDirectory = function(context, directoryEntry) {
896   return new DirectoryContents(
897       context,
898       false,  // Non search.
899       directoryEntry,
900       function() {
901         return new DirectoryContentScanner(directoryEntry);
902       });
903 };
904
905 /**
906  * Creates a DirectoryContents instance to show the result of the search on
907  * Drive File System.
908  *
909  * @param {FileListContext} context File list context.
910  * @param {DirectoryEntry} directoryEntry The current directory entry.
911  * @param {string} query Search query.
912  * @return {DirectoryContents} Created DirectoryContents instance.
913  */
914 DirectoryContents.createForDriveSearch = function(
915     context, directoryEntry, query) {
916   return new DirectoryContents(
917       context,
918       true,  // Search.
919       directoryEntry,
920       function() {
921         return new DriveSearchContentScanner(query);
922       });
923 };
924
925 /**
926  * Creates a DirectoryContents instance to show the result of the search on
927  * Local File System.
928  *
929  * @param {FileListContext} context File list context.
930  * @param {DirectoryEntry} directoryEntry The current directory entry.
931  * @param {string} query Search query.
932  * @return {DirectoryContents} Created DirectoryContents instance.
933  */
934 DirectoryContents.createForLocalSearch = function(
935     context, directoryEntry, query) {
936   return new DirectoryContents(
937       context,
938       true,  // Search.
939       directoryEntry,
940       function() {
941         return new LocalSearchContentScanner(directoryEntry, query);
942       });
943 };
944
945 /**
946  * Creates a DirectoryContents instance to show the result of metadata search
947  * on Drive File System.
948  *
949  * @param {FileListContext} context File list context.
950  * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing
951  *     the set of result entries. This serves as a top directory for the
952  *     search.
953  * @param {!DriveMetadataSearchContentScanner.SearchType} searchType The type of
954  *     the search. The scanner will restricts the entries based on the given
955  *     type.
956  * @return {DirectoryContents} Created DirectoryContents instance.
957  */
958 DirectoryContents.createForDriveMetadataSearch = function(
959     context, fakeDirectoryEntry, searchType) {
960   return new DirectoryContents(
961       context,
962       true,  // Search
963       fakeDirectoryEntry,
964       function() {
965         return new DriveMetadataSearchContentScanner(searchType);
966       });
967 };