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 */ 11function 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 */ 27ContentScanner.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 */ 35ContentScanner.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 */ 45function DirectoryContentScanner(entry) { 46 ContentScanner.call(this); 47 this.entry_ = entry; 48} 49 50/** 51 * Extends ContentScanner. 52 */ 53DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype; 54 55/** 56 * Starts to read the entries in the directory. 57 * @override 58 */ 59DirectoryContentScanner.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 */ 99function DriveSearchContentScanner(query) { 100 ContentScanner.call(this); 101 this.query_ = query; 102} 103 104/** 105 * Extends ContentScanner. 106 */ 107DriveSearchContentScanner.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 */ 116DriveSearchContentScanner.SCAN_DELAY_ = 200; 117 118/** 119 * Maximum number of results which is shown on the search. 120 * @type {number} 121 * @private 122 * @const 123 */ 124DriveSearchContentScanner.MAX_RESULTS_ = 100; 125 126/** 127 * Starts to search on Drive File System. 128 * @override 129 */ 130DriveSearchContentScanner.prototype.scan = function( 131 entriesCallback, successCallback, errorCallback) { 132 var numReadEntries = 0; 133 var readEntries = function(nextFeed) { 134 chrome.fileBrowserPrivate.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 */ 190function LocalSearchContentScanner(entry, query) { 191 ContentScanner.call(this); 192 this.entry_ = entry; 193 this.query_ = query.toLowerCase(); 194} 195 196/** 197 * Extends ContentScanner. 198 */ 199LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype; 200 201/** 202 * Starts the file name search. 203 * @override 204 */ 205LocalSearchContentScanner.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 */ 267function DriveMetadataSearchContentScanner(searchType) { 268 ContentScanner.call(this); 269 this.searchType_ = searchType; 270} 271 272/** 273 * Extends ContentScanner. 274 */ 275DriveMetadataSearchContentScanner.prototype.__proto__ = 276 ContentScanner.prototype; 277 278/** 279 * The search types on the Drive File System. 280 * @enum {string} 281 */ 282DriveMetadataSearchContentScanner.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 */ 293DriveMetadataSearchContentScanner.prototype.scan = function( 294 entriesCallback, successCallback, errorCallback) { 295 chrome.fileBrowserPrivate.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 '.' are shown. 323 * @constructor 324 * @extends {cr.EventTarget} 325 */ 326function 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 = this.metadataCache_.getCached(entry, 'internal'); 343 return !(internal && internal.deleted); 344 }.bind(this)); 345} 346 347/* 348 * FileFilter extends cr.EventTarget. 349 */ 350FileFilter.prototype = {__proto__: cr.EventTarget.prototype}; 351 352/** 353 * @param {string} name Filter identifier. 354 * @param {function(Entry)} callback A filter — a function receiving an Entry, 355 * and returning bool. 356 */ 357FileFilter.prototype.addFilter = function(name, callback) { 358 this.filters_[name] = callback; 359 cr.dispatchSimpleEvent(this, 'changed'); 360}; 361 362/** 363 * @param {string} name Filter identifier. 364 */ 365FileFilter.prototype.removeFilter = function(name) { 366 delete this.filters_[name]; 367 cr.dispatchSimpleEvent(this, 'changed'); 368}; 369 370/** 371 * @param {boolean} value If do not show hidden files. 372 */ 373FileFilter.prototype.setFilterHidden = function(value) { 374 if (value) { 375 this.addFilter( 376 'hidden', 377 function(entry) { return entry.name.substr(0, 1) !== '.'; } 378 ); 379 } else { 380 this.removeFilter('hidden'); 381 } 382}; 383 384/** 385 * @return {boolean} If the files with names starting with "." are not shown. 386 */ 387FileFilter.prototype.isFilterHiddenOn = function() { 388 return 'hidden' in this.filters_; 389}; 390 391/** 392 * @param {Entry} entry File entry. 393 * @return {boolean} True if the file should be shown, false otherwise. 394 */ 395FileFilter.prototype.filter = function(entry) { 396 for (var name in this.filters_) { 397 if (!this.filters_[name](entry)) 398 return false; 399 } 400 return true; 401}; 402 403/** 404 * A context of DirectoryContents. 405 * TODO(yoshiki): remove this. crbug.com/224869. 406 * 407 * @param {FileFilter} fileFilter The file-filter context. 408 * @param {MetadataCache} metadataCache Metadata cache service. 409 * @constructor 410 */ 411function FileListContext(fileFilter, metadataCache) { 412 /** 413 * @type {cr.ui.ArrayDataModel} 414 */ 415 this.fileList = new cr.ui.ArrayDataModel([]); 416 417 /** 418 * @type {MetadataCache} 419 */ 420 this.metadataCache = metadataCache; 421 422 /** 423 * @type {FileFilter} 424 */ 425 this.fileFilter = fileFilter; 426} 427 428/** 429 * This class is responsible for scanning directory (or search results), 430 * and filling the fileList. Different descendants handle various types of 431 * directory contents shown: basic directory, drive search results, local search 432 * results. 433 * TODO(hidehiko): Remove EventTarget from this. 434 * 435 * @param {FileListContext} context The file list context. 436 * @param {boolean} isSearch True for search directory contents, otherwise 437 * false. 438 * @param {DirectoryEntry} directoryEntry The entry of the current directory. 439 * @param {function():ContentScanner} scannerFactory The factory to create 440 * ContentScanner instance. 441 * @constructor 442 * @extends {cr.EventTarget} 443 */ 444function DirectoryContents(context, 445 isSearch, 446 directoryEntry, 447 scannerFactory) { 448 this.context_ = context; 449 this.fileList_ = context.fileList; 450 451 this.isSearch_ = isSearch; 452 this.directoryEntry_ = directoryEntry; 453 454 this.scannerFactory_ = scannerFactory; 455 this.scanner_ = null; 456 this.processNewEntriesQueue_ = new AsyncUtil.Queue(); 457 this.scanCancelled_ = false; 458 459 this.lastSpaceInMetadataCache_ = 0; 460} 461 462/** 463 * DirectoryContents extends cr.EventTarget. 464 */ 465DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype; 466 467/** 468 * Create the copy of the object, but without scan started. 469 * @return {DirectoryContents} Object copy. 470 */ 471DirectoryContents.prototype.clone = function() { 472 return new DirectoryContents( 473 this.context_, 474 this.isSearch_, 475 this.directoryEntry_, 476 this.scannerFactory_); 477}; 478 479/** 480 * Disposes the reserved metadata cache. 481 */ 482DirectoryContents.prototype.dispose = function() { 483 this.context_.metadataCache.resizeBy(-this.lastSpaceInMetadataCache_); 484}; 485 486/** 487 * Make a space for current directory size in the metadata cache. 488 * 489 * @param {number} size The cache size to be set. 490 * @private 491 */ 492DirectoryContents.prototype.makeSpaceInMetadataCache_ = function(size) { 493 this.context_.metadataCache.resizeBy(size - this.lastSpaceInMetadataCache_); 494 this.lastSpaceInMetadataCache_ = size; 495}; 496 497/** 498 * Use a given fileList instead of the fileList from the context. 499 * @param {Array|cr.ui.ArrayDataModel} fileList The new file list. 500 */ 501DirectoryContents.prototype.setFileList = function(fileList) { 502 if (fileList instanceof cr.ui.ArrayDataModel) 503 this.fileList_ = fileList; 504 else 505 this.fileList_ = new cr.ui.ArrayDataModel(fileList); 506 this.makeSpaceInMetadataCache_(this.fileList_.length); 507}; 508 509/** 510 * Use the filelist from the context and replace its contents with the entries 511 * from the current fileList. 512 */ 513DirectoryContents.prototype.replaceContextFileList = function() { 514 if (this.context_.fileList !== this.fileList_) { 515 var spliceArgs = this.fileList_.slice(); 516 var fileList = this.context_.fileList; 517 spliceArgs.unshift(0, fileList.length); 518 fileList.splice.apply(fileList, spliceArgs); 519 this.fileList_ = fileList; 520 this.makeSpaceInMetadataCache_(this.fileList_.length); 521 } 522}; 523 524/** 525 * @return {boolean} If the scan is active. 526 */ 527DirectoryContents.prototype.isScanning = function() { 528 return this.scanner_ || this.processNewEntriesQueue_.isRunning(); 529}; 530 531/** 532 * @return {boolean} True if search results (drive or local). 533 */ 534DirectoryContents.prototype.isSearch = function() { 535 return this.isSearch_; 536}; 537 538/** 539 * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of 540 * search -- the top directory from which search is run. 541 */ 542DirectoryContents.prototype.getDirectoryEntry = function() { 543 return this.directoryEntry_; 544}; 545 546/** 547 * Start directory scan/search operation. Either 'scan-completed' or 548 * 'scan-failed' event will be fired upon completion. 549 * 550 * @param {boolean} refresh True to refrech metadata, or false to use cached 551 * one. 552 */ 553DirectoryContents.prototype.scan = function(refresh) { 554 /** 555 * Invoked when the scanning is completed successfully. 556 * @this {DirectoryContents} 557 */ 558 function completionCallback() { 559 this.onScanFinished_(); 560 this.onScanCompleted_(); 561 } 562 563 /** 564 * Invoked when the scanning is finished but is not completed due to error. 565 * @this {DirectoryContents} 566 */ 567 function errorCallback() { 568 this.onScanFinished_(); 569 this.onScanError_(); 570 } 571 572 // TODO(hidehiko,mtomasz): this scan method must be called at most once. 573 // Remove such a limitation. 574 this.scanner_ = this.scannerFactory_(); 575 this.scanner_.scan(this.onNewEntries_.bind(this, refresh), 576 completionCallback.bind(this), 577 errorCallback.bind(this)); 578}; 579 580/** 581 * Cancels the running scan. 582 */ 583DirectoryContents.prototype.cancelScan = function() { 584 if (this.scanCancelled_) 585 return; 586 this.scanCancelled_ = true; 587 if (this.scanner_) 588 this.scanner_.cancel(); 589 590 this.onScanFinished_(); 591 592 this.processNewEntriesQueue_.cancel(); 593 cr.dispatchSimpleEvent(this, 'scan-cancelled'); 594}; 595 596/** 597 * Called when the scanning by scanner_ is done, even when the scanning is 598 * succeeded or failed. This is called before completion (or error) callback. 599 * 600 * @private 601 */ 602DirectoryContents.prototype.onScanFinished_ = function() { 603 this.scanner_ = null; 604 605 this.processNewEntriesQueue_.run(function(callback) { 606 // TODO(yoshiki): Here we should fire the update event of changed 607 // items. Currently we have a method this.fileList_.updateIndex() to 608 // fire an event, but this method takes only 1 argument and invokes sort 609 // one by one. It is obviously time wasting. Instead, we call sort 610 // directory. 611 // In future, we should implement a good method like updateIndexes and 612 // use it here. 613 var status = this.fileList_.sortStatus; 614 if (status) 615 this.fileList_.sort(status.field, status.direction); 616 617 callback(); 618 }.bind(this)); 619}; 620 621/** 622 * Called when the scanning by scanner_ is succeeded. 623 * @private 624 */ 625DirectoryContents.prototype.onScanCompleted_ = function() { 626 if (this.scanCancelled_) 627 return; 628 629 this.processNewEntriesQueue_.run(function(callback) { 630 // Call callback first, so isScanning() returns false in the event handlers. 631 callback(); 632 633 cr.dispatchSimpleEvent(this, 'scan-completed'); 634 }.bind(this)); 635}; 636 637/** 638 * Called in case scan has failed. Should send the event. 639 * @private 640 */ 641DirectoryContents.prototype.onScanError_ = function() { 642 if (this.scanCancelled_) 643 return; 644 645 this.processNewEntriesQueue_.run(function(callback) { 646 // Call callback first, so isScanning() returns false in the event handlers. 647 callback(); 648 cr.dispatchSimpleEvent(this, 'scan-failed'); 649 }.bind(this)); 650}; 651 652/** 653 * Called when some chunk of entries are read by scanner. 654 * 655 * @param {boolean} refresh True to refresh metadata, or false to use cached 656 * one. 657 * @param {Array.<Entry>} entries The list of the scanned entries. 658 * @private 659 */ 660DirectoryContents.prototype.onNewEntries_ = function(refresh, entries) { 661 if (this.scanCancelled_) 662 return; 663 664 var entriesFiltered = [].filter.call( 665 entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter)); 666 667 // Caching URL to reduce a number of calls of toURL in sort. 668 // This is a temporary solution. We need to fix a root cause of slow toURL. 669 // See crbug.com/370908 for detail. 670 entriesFiltered.forEach(function(entry) { entry.cachedUrl = entry.toURL(); }); 671 672 if (entriesFiltered.length === 0) 673 return; 674 675 // Enlarge the cache size into the new filelist size. 676 var newListSize = this.fileList_.length + entriesFiltered.length; 677 this.makeSpaceInMetadataCache_(newListSize); 678 679 this.processNewEntriesQueue_.run(function(callbackOuter) { 680 var finish = function() { 681 // Update the filelist without waiting the metadata. 682 this.fileList_.push.apply(this.fileList_, entriesFiltered); 683 cr.dispatchSimpleEvent(this, 'scan-updated'); 684 685 callbackOuter(); 686 }.bind(this); 687 // Because the prefetchMetadata can be slow, throttling by splitting entries 688 // into smaller chunks to reduce UI latency. 689 // TODO(hidehiko,mtomasz): This should be handled in MetadataCache. 690 var MAX_CHUNK_SIZE = 25; 691 var prefetchMetadataQueue = new AsyncUtil.ConcurrentQueue(4); 692 for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) { 693 if (prefetchMetadataQueue.isCancelled()) 694 break; 695 696 var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE); 697 prefetchMetadataQueue.run(function(chunk, callbackInner) { 698 this.prefetchMetadata(chunk, refresh, function() { 699 if (!prefetchMetadataQueue.isCancelled()) { 700 if (this.scanCancelled_) 701 prefetchMetadataQueue.cancel(); 702 } 703 704 // Checks if this is the last task. 705 if (prefetchMetadataQueue.getWaitingTasksCount() === 0 && 706 prefetchMetadataQueue.getRunningTasksCount() === 1) { 707 // |callbackOuter| in |finish| must be called before 708 // |callbackInner|, to prevent double-calling. 709 finish(); 710 } 711 712 callbackInner(); 713 }.bind(this)); 714 }.bind(this, chunk)); 715 } 716 }.bind(this)); 717}; 718 719/** 720 * @param {Array.<Entry>} entries Files. 721 * @param {boolean} refresh True to refresh metadata, or false to use cached 722 * one. 723 * @param {function(Object)} callback Callback on done. 724 */ 725DirectoryContents.prototype.prefetchMetadata = 726 function(entries, refresh, callback) { 727 var TYPES = 'filesystem|drive'; 728 if (refresh) 729 this.context_.metadataCache.getLatest(entries, TYPES, callback); 730 else 731 this.context_.metadataCache.get(entries, TYPES, callback); 732}; 733 734/** 735 * Creates a DirectoryContents instance to show entries in a directory. 736 * 737 * @param {FileListContext} context File list context. 738 * @param {DirectoryEntry} directoryEntry The current directory entry. 739 * @return {DirectoryContents} Created DirectoryContents instance. 740 */ 741DirectoryContents.createForDirectory = function(context, directoryEntry) { 742 return new DirectoryContents( 743 context, 744 false, // Non search. 745 directoryEntry, 746 function() { 747 return new DirectoryContentScanner(directoryEntry); 748 }); 749}; 750 751/** 752 * Creates a DirectoryContents instance to show the result of the search on 753 * Drive File System. 754 * 755 * @param {FileListContext} context File list context. 756 * @param {DirectoryEntry} directoryEntry The current directory entry. 757 * @param {string} query Search query. 758 * @return {DirectoryContents} Created DirectoryContents instance. 759 */ 760DirectoryContents.createForDriveSearch = function( 761 context, directoryEntry, query) { 762 return new DirectoryContents( 763 context, 764 true, // Search. 765 directoryEntry, 766 function() { 767 return new DriveSearchContentScanner(query); 768 }); 769}; 770 771/** 772 * Creates a DirectoryContents instance to show the result of the search on 773 * Local File System. 774 * 775 * @param {FileListContext} context File list context. 776 * @param {DirectoryEntry} directoryEntry The current directory entry. 777 * @param {string} query Search query. 778 * @return {DirectoryContents} Created DirectoryContents instance. 779 */ 780DirectoryContents.createForLocalSearch = function( 781 context, directoryEntry, query) { 782 return new DirectoryContents( 783 context, 784 true, // Search. 785 directoryEntry, 786 function() { 787 return new LocalSearchContentScanner(directoryEntry, query); 788 }); 789}; 790 791/** 792 * Creates a DirectoryContents instance to show the result of metadata search 793 * on Drive File System. 794 * 795 * @param {FileListContext} context File list context. 796 * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing 797 * the set of result entries. This serves as a top directory for the 798 * search. 799 * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of 800 * the search. The scanner will restricts the entries based on the given 801 * type. 802 * @return {DirectoryContents} Created DirectoryContents instance. 803 */ 804DirectoryContents.createForDriveMetadataSearch = function( 805 context, fakeDirectoryEntry, searchType) { 806 return new DirectoryContents( 807 context, 808 true, // Search 809 fakeDirectoryEntry, 810 function() { 811 return new DriveMetadataSearchContentScanner(searchType); 812 }); 813}; 814