Home | History | Annotate | Download | only in 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 'use strict';
      6 
      7 /**
      8  * Scanner of the entries.
      9  * @constructor
     10  */
     11 function ContentScanner() {
     12   this.cancelled_ = false;
     13 }
     14 
     15 /**
     16  * Starts to scan the entries. For example, starts to read the entries in a
     17  * directory, or starts to search with some query on a file system.
     18  * Derived classes must override this method.
     19  *
     20  * @param {function(Array.<Entry>)} entriesCallback Called when some chunk of
     21  *     entries are read. This can be called a couple of times until the
     22  *     completion.
     23  * @param {function()} successCallback Called when the scan is completed
     24  *     successfully.
     25  * @param {function(FileError)} errorCallback Called an error occurs.
     26  */
     27 ContentScanner.prototype.scan = function(
     28     entriesCallback, successCallback, errorCallback) {
     29 };
     30 
     31 /**
     32  * Request cancelling of the running scan. When the cancelling is done,
     33  * an error will be reported from errorCallback passed to scan().
     34  */
     35 ContentScanner.prototype.cancel = function() {
     36   this.cancelled_ = true;
     37 };
     38 
     39 /**
     40  * Scanner of the entries in a directory.
     41  * @param {DirectoryEntry} entry The directory to be read.
     42  * @constructor
     43  * @extends {ContentScanner}
     44  */
     45 function DirectoryContentScanner(entry) {
     46   ContentScanner.call(this);
     47   this.entry_ = entry;
     48 }
     49 
     50 /**
     51  * Extends ContentScanner.
     52  */
     53 DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype;
     54 
     55 /**
     56  * Starts to read the entries in the directory.
     57  * @override
     58  */
     59 DirectoryContentScanner.prototype.scan = function(
     60     entriesCallback, successCallback, errorCallback) {
     61   if (!this.entry_ || util.isFakeEntry(this.entry_)) {
     62     // If entry is not specified or a fake, we cannot read it.
     63     errorCallback(util.createDOMError(
     64         util.FileError.INVALID_MODIFICATION_ERR));
     65     return;
     66   }
     67 
     68   metrics.startInterval('DirectoryScan');
     69   var reader = this.entry_.createReader();
     70   var readEntries = function() {
     71     reader.readEntries(
     72         function(entries) {
     73           if (this.cancelled_) {
     74             errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
     75             return;
     76           }
     77 
     78           if (entries.length === 0) {
     79             // All entries are read.
     80             metrics.recordInterval('DirectoryScan');
     81             successCallback();
     82             return;
     83           }
     84 
     85           entriesCallback(entries);
     86           readEntries();
     87         }.bind(this),
     88         errorCallback);
     89   }.bind(this);
     90   readEntries();
     91 };
     92 
     93 /**
     94  * Scanner of the entries for the search results on Drive File System.
     95  * @param {string} query The query string.
     96  * @constructor
     97  * @extends {ContentScanner}
     98  */
     99 function DriveSearchContentScanner(query) {
    100   ContentScanner.call(this);
    101   this.query_ = query;
    102 }
    103 
    104 /**
    105  * Extends ContentScanner.
    106  */
    107 DriveSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
    108 
    109 /**
    110  * Delay in milliseconds to be used for drive search scan, in order to reduce
    111  * the number of server requests while user is typing the query.
    112  * @type {number}
    113  * @private
    114  * @const
    115  */
    116 DriveSearchContentScanner.SCAN_DELAY_ = 200;
    117 
    118 /**
    119  * Maximum number of results which is shown on the search.
    120  * @type {number}
    121  * @private
    122  * @const
    123  */
    124 DriveSearchContentScanner.MAX_RESULTS_ = 100;
    125 
    126 /**
    127  * Starts to search on Drive File System.
    128  * @override
    129  */
    130 DriveSearchContentScanner.prototype.scan = function(
    131     entriesCallback, successCallback, errorCallback) {
    132   var numReadEntries = 0;
    133   var readEntries = function(nextFeed) {
    134     chrome.fileManagerPrivate.searchDrive(
    135         {query: this.query_, nextFeed: nextFeed},
    136         function(entries, nextFeed) {
    137           if (this.cancelled_) {
    138             errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
    139             return;
    140           }
    141 
    142           // TODO(tbarzic): Improve error handling.
    143           if (!entries) {
    144             console.error('Drive search encountered an error.');
    145             errorCallback(util.createDOMError(
    146                 util.FileError.INVALID_MODIFICATION_ERR));
    147             return;
    148           }
    149 
    150           var numRemainingEntries =
    151               DriveSearchContentScanner.MAX_RESULTS_ - numReadEntries;
    152           if (entries.length >= numRemainingEntries) {
    153             // The limit is hit, so quit the scan here.
    154             entries = entries.slice(0, numRemainingEntries);
    155             nextFeed = '';
    156           }
    157 
    158           numReadEntries += entries.length;
    159           if (entries.length > 0)
    160             entriesCallback(entries);
    161 
    162           if (nextFeed === '')
    163             successCallback();
    164           else
    165             readEntries(nextFeed);
    166         }.bind(this));
    167   }.bind(this);
    168 
    169   // Let's give another search a chance to cancel us before we begin.
    170   setTimeout(
    171       function() {
    172         // Check cancelled state before read the entries.
    173         if (this.cancelled_) {
    174           errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
    175           return;
    176         }
    177         readEntries('');
    178       }.bind(this),
    179       DriveSearchContentScanner.SCAN_DELAY_);
    180 };
    181 
    182 /**
    183  * Scanner of the entries of the file name search on the directory tree, whose
    184  * root is entry.
    185  * @param {DirectoryEntry} entry The root of the search target directory tree.
    186  * @param {string} query The query of the search.
    187  * @constructor
    188  * @extends {ContentScanner}
    189  */
    190 function LocalSearchContentScanner(entry, query) {
    191   ContentScanner.call(this);
    192   this.entry_ = entry;
    193   this.query_ = query.toLowerCase();
    194 }
    195 
    196 /**
    197  * Extends ContentScanner.
    198  */
    199 LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
    200 
    201 /**
    202  * Starts the file name search.
    203  * @override
    204  */
    205 LocalSearchContentScanner.prototype.scan = function(
    206     entriesCallback, successCallback, errorCallback) {
    207   var numRunningTasks = 0;
    208   var error = null;
    209   var maybeRunCallback = function() {
    210     if (numRunningTasks === 0) {
    211       if (this.cancelled_)
    212         errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
    213       else if (error)
    214         errorCallback(error);
    215       else
    216         successCallback();
    217     }
    218   }.bind(this);
    219 
    220   var processEntry = function(entry) {
    221     numRunningTasks++;
    222     var onError = function(fileError) {
    223       if (!error)
    224         error = fileError;
    225       numRunningTasks--;
    226       maybeRunCallback();
    227     };
    228 
    229     var onSuccess = function(entries) {
    230       if (this.cancelled_ || error || entries.length === 0) {
    231         numRunningTasks--;
    232         maybeRunCallback();
    233         return;
    234       }
    235 
    236       // Filters by the query, and if found, run entriesCallback.
    237       var foundEntries = entries.filter(function(entry) {
    238         return entry.name.toLowerCase().indexOf(this.query_) >= 0;
    239       }.bind(this));
    240       if (foundEntries.length > 0)
    241         entriesCallback(foundEntries);
    242 
    243       // Start to process sub directories.
    244       for (var i = 0; i < entries.length; i++) {
    245         if (entries[i].isDirectory)
    246           processEntry(entries[i]);
    247       }
    248 
    249       // Read remaining entries.
    250       reader.readEntries(onSuccess, onError);
    251     }.bind(this);
    252 
    253     var reader = entry.createReader();
    254     reader.readEntries(onSuccess, onError);
    255   }.bind(this);
    256 
    257   processEntry(this.entry_);
    258 };
    259 
    260 /**
    261  * Scanner of the entries for the metadata search on Drive File System.
    262  * @param {DriveMetadataSearchContentScanner.SearchType} searchType The option
    263  *     of the search.
    264  * @constructor
    265  * @extends {ContentScanner}
    266  */
    267 function DriveMetadataSearchContentScanner(searchType) {
    268   ContentScanner.call(this);
    269   this.searchType_ = searchType;
    270 }
    271 
    272 /**
    273  * Extends ContentScanner.
    274  */
    275 DriveMetadataSearchContentScanner.prototype.__proto__ =
    276     ContentScanner.prototype;
    277 
    278 /**
    279  * The search types on the Drive File System.
    280  * @enum {string}
    281  */
    282 DriveMetadataSearchContentScanner.SearchType = Object.freeze({
    283   SEARCH_ALL: 'ALL',
    284   SEARCH_SHARED_WITH_ME: 'SHARED_WITH_ME',
    285   SEARCH_RECENT_FILES: 'EXCLUDE_DIRECTORIES',
    286   SEARCH_OFFLINE: 'OFFLINE'
    287 });
    288 
    289 /**
    290  * Starts to metadata-search on Drive File System.
    291  * @override
    292  */
    293 DriveMetadataSearchContentScanner.prototype.scan = function(
    294     entriesCallback, successCallback, errorCallback) {
    295   chrome.fileManagerPrivate.searchDriveMetadata(
    296       {query: '', types: this.searchType_, maxResults: 500},
    297       function(results) {
    298         if (this.cancelled_) {
    299           errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
    300           return;
    301         }
    302 
    303         if (!results) {
    304           console.error('Drive search encountered an error.');
    305           errorCallback(util.createDOMError(
    306               util.FileError.INVALID_MODIFICATION_ERR));
    307           return;
    308         }
    309 
    310         var entries = results.map(function(result) { return result.entry; });
    311         if (entries.length > 0)
    312           entriesCallback(entries);
    313         successCallback();
    314       }.bind(this));
    315 };
    316 
    317 /**
    318  * This class manages filters and determines a file should be shown or not.
    319  * When filters are changed, a 'changed' event is fired.
    320  *
    321  * @param {MetadataCache} metadataCache Metadata cache service.
    322  * @param {boolean} showHidden If files starting with '.' or ending with
    323  *     '.crdownlaod' are shown.
    324  * @constructor
    325  * @extends {cr.EventTarget}
    326  */
    327 function FileFilter(metadataCache, showHidden) {
    328   /**
    329    * @type {MetadataCache}
    330    * @private
    331    */
    332   this.metadataCache_ = metadataCache;
    333 
    334   /**
    335    * @type {Object.<string, Function>}
    336    * @private
    337    */
    338   this.filters_ = {};
    339   this.setFilterHidden(!showHidden);
    340 
    341   // Do not show entries marked as 'deleted'.
    342   this.addFilter('deleted', function(entry) {
    343     var internal = 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', util.compareName);
    426   this.setCompareFunction('modificationTime', this.compareMtime_.bind(this));
    427   this.setCompareFunction('size', this.compareSize_.bind(this));
    428   this.setCompareFunction('type', this.compareType_.bind(this));
    429 }
    430 
    431 FileListModel.prototype = {
    432   __proto__: cr.ui.ArrayDataModel.prototype
    433 };
    434 
    435 /**
    436  * Compare by mtime first, then by name.
    437  * @param {Entry} a First entry.
    438  * @param {Entry} b Second entry.
    439  * @return {number} Compare result.
    440  * @private
    441  */
    442 FileListModel.prototype.compareMtime_ = function(a, b) {
    443   var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
    444   var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0;
    445 
    446   var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
    447   var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0;
    448 
    449   if (aTime > bTime)
    450     return 1;
    451 
    452   if (aTime < bTime)
    453     return -1;
    454 
    455   return util.compareName(a, b);
    456 };
    457 
    458 /**
    459  * Compare by size first, then 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.compareSize_ = function(a, b) {
    466   var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
    467   var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0;
    468 
    469   var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
    470   var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0;
    471 
    472   return aSize !== bSize ? aSize - bSize : util.compareName(a, b);
    473 };
    474 
    475 /**
    476  * Compare by type first, then by subtype and then by name.
    477  * @param {Entry} a First entry.
    478  * @param {Entry} b Second entry.
    479  * @return {number} Compare result.
    480  * @private
    481  */
    482 FileListModel.prototype.compareType_ = function(a, b) {
    483   // Directories precede files.
    484   if (a.isDirectory !== b.isDirectory)
    485     return Number(b.isDirectory) - Number(a.isDirectory);
    486 
    487   var aType = FileType.typeToString(FileType.getType(a));
    488   var bType = FileType.typeToString(FileType.getType(b));
    489 
    490   var result = util.collator.compare(aType, bType);
    491   return result !== 0 ? result : util.compareName(a, b);
    492 };
    493 
    494 /**
    495  * A context of DirectoryContents.
    496  * TODO(yoshiki): remove this. crbug.com/224869.
    497  *
    498  * @param {FileFilter} fileFilter The file-filter context.
    499  * @param {MetadataCache} metadataCache Metadata cache service.
    500  * @constructor
    501  */
    502 function FileListContext(fileFilter, metadataCache) {
    503   /**
    504    * @type {FileListModel}
    505    */
    506   this.fileList = new FileListModel(metadataCache);
    507 
    508   /**
    509    * @type {MetadataCache}
    510    */
    511   this.metadataCache = metadataCache;
    512 
    513   /**
    514    * @type {FileFilter}
    515    */
    516   this.fileFilter = fileFilter;
    517 }
    518 
    519 /**
    520  * This class is responsible for scanning directory (or search results),
    521  * and filling the fileList. Different descendants handle various types of
    522  * directory contents shown: basic directory, drive search results, local search
    523  * results.
    524  * TODO(hidehiko): Remove EventTarget from this.
    525  *
    526  * @param {FileListContext} context The file list context.
    527  * @param {boolean} isSearch True for search directory contents, otherwise
    528  *     false.
    529  * @param {DirectoryEntry} directoryEntry The entry of the current directory.
    530  * @param {function():ContentScanner} scannerFactory The factory to create
    531  *     ContentScanner instance.
    532  * @constructor
    533  * @extends {cr.EventTarget}
    534  */
    535 function DirectoryContents(context,
    536                            isSearch,
    537                            directoryEntry,
    538                            scannerFactory) {
    539   this.context_ = context;
    540   this.fileList_ = context.fileList;
    541 
    542   this.isSearch_ = isSearch;
    543   this.directoryEntry_ = directoryEntry;
    544 
    545   this.scannerFactory_ = scannerFactory;
    546   this.scanner_ = null;
    547   this.processNewEntriesQueue_ = new AsyncUtil.Queue();
    548   this.scanCancelled_ = false;
    549 
    550   this.lastSpaceInMetadataCache_ = 0;
    551 }
    552 
    553 /**
    554  * DirectoryContents extends cr.EventTarget.
    555  */
    556 DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype;
    557 
    558 /**
    559  * Create the copy of the object, but without scan started.
    560  * @return {DirectoryContents} Object copy.
    561  */
    562 DirectoryContents.prototype.clone = function() {
    563   return new DirectoryContents(
    564       this.context_,
    565       this.isSearch_,
    566       this.directoryEntry_,
    567       this.scannerFactory_);
    568 };
    569 
    570 /**
    571  * Disposes the reserved metadata cache.
    572  */
    573 DirectoryContents.prototype.dispose = function() {
    574   this.context_.metadataCache.resizeBy(-this.lastSpaceInMetadataCache_);
    575   // Though the lastSpaceInMetadataCache_ is not supposed to be referred after
    576   // dispose(), keep it synced with requested cache size just in case.
    577   this.lastSpaceInMetadataCache_ = 0;
    578 };
    579 
    580 /**
    581  * Make a space for current directory size in the metadata cache.
    582  *
    583  * @param {number} size The cache size to be set.
    584  * @private
    585  */
    586 DirectoryContents.prototype.makeSpaceInMetadataCache_ = function(size) {
    587   this.context_.metadataCache.resizeBy(size - this.lastSpaceInMetadataCache_);
    588   this.lastSpaceInMetadataCache_ = size;
    589 };
    590 
    591 /**
    592  * Use a given fileList instead of the fileList from the context.
    593  * @param {Array|cr.ui.ArrayDataModel} fileList The new file list.
    594  */
    595 DirectoryContents.prototype.setFileList = function(fileList) {
    596   if (fileList instanceof cr.ui.ArrayDataModel)
    597     this.fileList_ = fileList;
    598   else
    599     this.fileList_ = new cr.ui.ArrayDataModel(fileList);
    600   this.makeSpaceInMetadataCache_(this.fileList_.length);
    601 };
    602 
    603 /**
    604  * Use the filelist from the context and replace its contents with the entries
    605  * from the current fileList.
    606  */
    607 DirectoryContents.prototype.replaceContextFileList = function() {
    608   if (this.context_.fileList !== this.fileList_) {
    609     var spliceArgs = this.fileList_.slice();
    610     var fileList = this.context_.fileList;
    611     spliceArgs.unshift(0, fileList.length);
    612     fileList.splice.apply(fileList, spliceArgs);
    613     this.fileList_ = fileList;
    614     this.makeSpaceInMetadataCache_(this.fileList_.length);
    615   }
    616 };
    617 
    618 /**
    619  * @return {boolean} If the scan is active.
    620  */
    621 DirectoryContents.prototype.isScanning = function() {
    622   return this.scanner_ || this.processNewEntriesQueue_.isRunning();
    623 };
    624 
    625 /**
    626  * @return {boolean} True if search results (drive or local).
    627  */
    628 DirectoryContents.prototype.isSearch = function() {
    629   return this.isSearch_;
    630 };
    631 
    632 /**
    633  * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of
    634  *     search -- the top directory from which search is run.
    635  */
    636 DirectoryContents.prototype.getDirectoryEntry = function() {
    637   return this.directoryEntry_;
    638 };
    639 
    640 /**
    641  * Start directory scan/search operation. Either 'scan-completed' or
    642  * 'scan-failed' event will be fired upon completion.
    643  *
    644  * @param {boolean} refresh True to refresh metadata, or false to use cached
    645  *     one.
    646  */
    647 DirectoryContents.prototype.scan = function(refresh) {
    648   /**
    649    * Invoked when the scanning is completed successfully.
    650    * @this {DirectoryContents}
    651    */
    652   function completionCallback() {
    653     this.onScanFinished_();
    654     this.onScanCompleted_();
    655   }
    656 
    657   /**
    658    * Invoked when the scanning is finished but is not completed due to error.
    659    * @this {DirectoryContents}
    660    */
    661   function errorCallback() {
    662     this.onScanFinished_();
    663     this.onScanError_();
    664   }
    665 
    666   // TODO(hidehiko,mtomasz): this scan method must be called at most once.
    667   // Remove such a limitation.
    668   this.scanner_ = this.scannerFactory_();
    669   this.scanner_.scan(this.onNewEntries_.bind(this, refresh),
    670                      completionCallback.bind(this),
    671                      errorCallback.bind(this));
    672 };
    673 
    674 /**
    675  * Adds/removes/updates items of file list.
    676  * @param {Array.<Entry>} updatedEntries Entries of updated/added files.
    677  * @param {Array.<string>} removedUrls URLs of removed files.
    678  */
    679 DirectoryContents.prototype.update = function(updatedEntries, removedUrls) {
    680   var removedMap = {};
    681   for (var i = 0; i < removedUrls.length; i++) {
    682     removedMap[removedUrls[i]] = true;
    683   }
    684 
    685   var updatedMap = {};
    686   for (var i = 0; i < updatedEntries.length; i++) {
    687     updatedMap[updatedEntries[i].toURL()] = updatedEntries[i];
    688   }
    689 
    690   var updatedList = [];
    691   for (var i = 0; i < this.fileList_.length; i++) {
    692     var url = this.fileList_.item(i).toURL();
    693 
    694     if (url in removedMap) {
    695       this.fileList_.splice(i, 1);
    696       i--;
    697       continue;
    698     }
    699 
    700     if (url in updatedMap) {
    701       updatedList.push(updatedMap[url]);
    702       delete updatedMap[url];
    703     }
    704   }
    705 
    706   var addedList = [];
    707   for (var url in updatedMap) {
    708     addedList.push(updatedMap[url]);
    709   }
    710 
    711   if (removedUrls.length > 0)
    712     this.fileList_.metadataCache_.clearByUrl(removedUrls, '*');
    713 
    714   this.prefetchMetadata(updatedList, true, function() {
    715     this.onNewEntries_(true, addedList);
    716     this.onScanFinished_();
    717     this.onScanCompleted_();
    718   }.bind(this));
    719 };
    720 
    721 /**
    722  * Cancels the running scan.
    723  */
    724 DirectoryContents.prototype.cancelScan = function() {
    725   if (this.scanCancelled_)
    726     return;
    727   this.scanCancelled_ = true;
    728   if (this.scanner_)
    729     this.scanner_.cancel();
    730 
    731   this.onScanFinished_();
    732 
    733   this.processNewEntriesQueue_.cancel();
    734   cr.dispatchSimpleEvent(this, 'scan-cancelled');
    735 };
    736 
    737 /**
    738  * Called when the scanning by scanner_ is done, even when the scanning is
    739  * succeeded or failed. This is called before completion (or error) callback.
    740  *
    741  * @private
    742  */
    743 DirectoryContents.prototype.onScanFinished_ = function() {
    744   this.scanner_ = null;
    745 
    746   this.processNewEntriesQueue_.run(function(callback) {
    747     // TODO(yoshiki): Here we should fire the update event of changed
    748     // items. Currently we have a method this.fileList_.updateIndex() to
    749     // fire an event, but this method takes only 1 argument and invokes sort
    750     // one by one. It is obviously time wasting. Instead, we call sort
    751     // directory.
    752     // In future, we should implement a good method like updateIndexes and
    753     // use it here.
    754     var status = this.fileList_.sortStatus;
    755     if (status)
    756       this.fileList_.sort(status.field, status.direction);
    757 
    758     callback();
    759   }.bind(this));
    760 };
    761 
    762 /**
    763  * Called when the scanning by scanner_ is succeeded.
    764  * @private
    765  */
    766 DirectoryContents.prototype.onScanCompleted_ = function() {
    767   if (this.scanCancelled_)
    768     return;
    769 
    770   this.processNewEntriesQueue_.run(function(callback) {
    771     // Call callback first, so isScanning() returns false in the event handlers.
    772     callback();
    773 
    774     cr.dispatchSimpleEvent(this, 'scan-completed');
    775   }.bind(this));
    776 };
    777 
    778 /**
    779  * Called in case scan has failed. Should send the event.
    780  * @private
    781  */
    782 DirectoryContents.prototype.onScanError_ = function() {
    783   if (this.scanCancelled_)
    784     return;
    785 
    786   this.processNewEntriesQueue_.run(function(callback) {
    787     // Call callback first, so isScanning() returns false in the event handlers.
    788     callback();
    789     cr.dispatchSimpleEvent(this, 'scan-failed');
    790   }.bind(this));
    791 };
    792 
    793 /**
    794  * Called when some chunk of entries are read by scanner.
    795  *
    796  * @param {boolean} refresh True to refresh metadata, or false to use cached
    797  *     one.
    798  * @param {Array.<Entry>} entries The list of the scanned entries.
    799  * @private
    800  */
    801 DirectoryContents.prototype.onNewEntries_ = function(refresh, entries) {
    802   if (this.scanCancelled_)
    803     return;
    804 
    805   var entriesFiltered = [].filter.call(
    806       entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter));
    807 
    808   // Caching URL to reduce a number of calls of toURL in sort.
    809   // This is a temporary solution. We need to fix a root cause of slow toURL.
    810   // See crbug.com/370908 for detail.
    811   entriesFiltered.forEach(function(entry) { entry.cachedUrl = entry.toURL(); });
    812 
    813   if (entriesFiltered.length === 0)
    814     return;
    815 
    816   // Enlarge the cache size into the new filelist size.
    817   var newListSize = this.fileList_.length + entriesFiltered.length;
    818   this.makeSpaceInMetadataCache_(newListSize);
    819 
    820   this.processNewEntriesQueue_.run(function(callbackOuter) {
    821     var finish = function() {
    822       if (!this.scanCancelled_) {
    823         // Update the filelist without waiting the metadata.
    824         this.fileList_.push.apply(this.fileList_, entriesFiltered);
    825         cr.dispatchSimpleEvent(this, 'scan-updated');
    826       }
    827       callbackOuter();
    828     }.bind(this);
    829     // Because the prefetchMetadata can be slow, throttling by splitting entries
    830     // into smaller chunks to reduce UI latency.
    831     // TODO(hidehiko,mtomasz): This should be handled in MetadataCache.
    832     var MAX_CHUNK_SIZE = 25;
    833     var prefetchMetadataQueue = new AsyncUtil.ConcurrentQueue(4);
    834     for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) {
    835       if (prefetchMetadataQueue.isCancelled())
    836         break;
    837 
    838       var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE);
    839       prefetchMetadataQueue.run(function(chunk, callbackInner) {
    840         this.prefetchMetadata(chunk, refresh, function() {
    841           if (!prefetchMetadataQueue.isCancelled()) {
    842             if (this.scanCancelled_)
    843               prefetchMetadataQueue.cancel();
    844           }
    845 
    846           // Checks if this is the last task.
    847           if (prefetchMetadataQueue.getWaitingTasksCount() === 0 &&
    848               prefetchMetadataQueue.getRunningTasksCount() === 1) {
    849             // |callbackOuter| in |finish| must be called before
    850             // |callbackInner|, to prevent double-calling.
    851             finish();
    852           }
    853 
    854           callbackInner();
    855         }.bind(this));
    856       }.bind(this, chunk));
    857     }
    858   }.bind(this));
    859 };
    860 
    861 /**
    862  * @param {Array.<Entry>} entries Files.
    863  * @param {boolean} refresh True to refresh metadata, or false to use cached
    864  *     one.
    865  * @param {function(Object)} callback Callback on done.
    866  */
    867 DirectoryContents.prototype.prefetchMetadata =
    868     function(entries, refresh, callback) {
    869   var TYPES = 'filesystem|external';
    870   if (refresh)
    871     this.context_.metadataCache.getLatest(entries, TYPES, callback);
    872   else
    873     this.context_.metadataCache.get(entries, TYPES, callback);
    874 };
    875 
    876 /**
    877  * Creates a DirectoryContents instance to show entries in a directory.
    878  *
    879  * @param {FileListContext} context File list context.
    880  * @param {DirectoryEntry} directoryEntry The current directory entry.
    881  * @return {DirectoryContents} Created DirectoryContents instance.
    882  */
    883 DirectoryContents.createForDirectory = function(context, directoryEntry) {
    884   return new DirectoryContents(
    885       context,
    886       false,  // Non search.
    887       directoryEntry,
    888       function() {
    889         return new DirectoryContentScanner(directoryEntry);
    890       });
    891 };
    892 
    893 /**
    894  * Creates a DirectoryContents instance to show the result of the search on
    895  * Drive File System.
    896  *
    897  * @param {FileListContext} context File list context.
    898  * @param {DirectoryEntry} directoryEntry The current directory entry.
    899  * @param {string} query Search query.
    900  * @return {DirectoryContents} Created DirectoryContents instance.
    901  */
    902 DirectoryContents.createForDriveSearch = function(
    903     context, directoryEntry, query) {
    904   return new DirectoryContents(
    905       context,
    906       true,  // Search.
    907       directoryEntry,
    908       function() {
    909         return new DriveSearchContentScanner(query);
    910       });
    911 };
    912 
    913 /**
    914  * Creates a DirectoryContents instance to show the result of the search on
    915  * Local File System.
    916  *
    917  * @param {FileListContext} context File list context.
    918  * @param {DirectoryEntry} directoryEntry The current directory entry.
    919  * @param {string} query Search query.
    920  * @return {DirectoryContents} Created DirectoryContents instance.
    921  */
    922 DirectoryContents.createForLocalSearch = function(
    923     context, directoryEntry, query) {
    924   return new DirectoryContents(
    925       context,
    926       true,  // Search.
    927       directoryEntry,
    928       function() {
    929         return new LocalSearchContentScanner(directoryEntry, query);
    930       });
    931 };
    932 
    933 /**
    934  * Creates a DirectoryContents instance to show the result of metadata search
    935  * on Drive File System.
    936  *
    937  * @param {FileListContext} context File list context.
    938  * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing
    939  *     the set of result entries. This serves as a top directory for the
    940  *     search.
    941  * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of
    942  *     the search. The scanner will restricts the entries based on the given
    943  *     type.
    944  * @return {DirectoryContents} Created DirectoryContents instance.
    945  */
    946 DirectoryContents.createForDriveMetadataSearch = function(
    947     context, fakeDirectoryEntry, searchType) {
    948   return new DirectoryContents(
    949       context,
    950       true,  // Search
    951       fakeDirectoryEntry,
    952       function() {
    953         return new DriveMetadataSearchContentScanner(searchType);
    954       });
    955 };
    956