a964a6691a47fa03d8ac42f6775092f15637f996
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / 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 '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_ ||
62       (util.isFakeEntry(this.entry_) &&
63        this.entry_.rootType === RootType.DRIVE)) {
64     // If entry is not specified or a fake, we cannot read it.
65     errorCallback(util.createDOMError(
66         util.FileError.INVALID_MODIFICATION_ERR));
67     return;
68   }
69
70   metrics.startInterval('DirectoryScan');
71   var reader = this.entry_.createReader();
72   var readEntries = function() {
73     reader.readEntries(
74         function(entries) {
75           if (this.cancelled_) {
76             errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
77             return;
78           }
79
80           if (entries.length === 0) {
81             // All entries are read.
82             metrics.recordInterval('DirectoryScan');
83             successCallback();
84             return;
85           }
86
87           entriesCallback(entries);
88           readEntries();
89         }.bind(this),
90         errorCallback);
91   }.bind(this);
92   readEntries();
93 };
94
95 /**
96  * Scanner of the entries for the search results on Drive File System.
97  * @param {string} query The query string.
98  * @constructor
99  * @extends {ContentScanner}
100  */
101 function DriveSearchContentScanner(query) {
102   ContentScanner.call(this);
103   this.query_ = query;
104 }
105
106 /**
107  * Extends ContentScanner.
108  */
109 DriveSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
110
111 /**
112  * Delay in milliseconds to be used for drive search scan, in order to reduce
113  * the number of server requests while user is typing the query.
114  * @type {number}
115  * @private
116  * @const
117  */
118 DriveSearchContentScanner.SCAN_DELAY_ = 200;
119
120 /**
121  * Maximum number of results which is shown on the search.
122  * @type {number}
123  * @private
124  * @const
125  */
126 DriveSearchContentScanner.MAX_RESULTS_ = 100;
127
128 /**
129  * Starts to search on Drive File System.
130  * @override
131  */
132 DriveSearchContentScanner.prototype.scan = function(
133     entriesCallback, successCallback, errorCallback) {
134   var numReadEntries = 0;
135   var readEntries = function(nextFeed) {
136     chrome.fileBrowserPrivate.searchDrive(
137         {query: this.query_, nextFeed: nextFeed},
138         function(entries, nextFeed) {
139           if (this.cancelled_) {
140             errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
141             return;
142           }
143
144           // TODO(tbarzic): Improve error handling.
145           if (!entries) {
146             console.error('Drive search encountered an error.');
147             errorCallback(util.createDOMError(
148                 util.FileError.INVALID_MODIFICATION_ERR));
149             return;
150           }
151
152           var numRemainingEntries =
153               DriveSearchContentScanner.MAX_RESULTS_ - numReadEntries;
154           if (entries.length >= numRemainingEntries) {
155             // The limit is hit, so quit the scan here.
156             entries = entries.slice(0, numRemainingEntries);
157             nextFeed = '';
158           }
159
160           numReadEntries += entries.length;
161           if (entries.length > 0)
162             entriesCallback(entries);
163
164           if (nextFeed === '')
165             successCallback();
166           else
167             readEntries(nextFeed);
168         }.bind(this));
169   }.bind(this);
170
171   // Let's give another search a chance to cancel us before we begin.
172   setTimeout(
173       function() {
174         // Check cancelled state before read the entries.
175         if (this.cancelled_) {
176           errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
177           return;
178         }
179         readEntries('');
180       }.bind(this),
181       DriveSearchContentScanner.SCAN_DELAY_);
182 };
183
184 /**
185  * Scanner of the entries of the file name search on the directory tree, whose
186  * root is entry.
187  * @param {DirectoryEntry} entry The root of the search target directory tree.
188  * @param {string} query The query of the search.
189  * @constructor
190  * @extends {ContentScanner}
191  */
192 function LocalSearchContentScanner(entry, query) {
193   ContentScanner.call(this);
194   this.entry_ = entry;
195   this.query_ = query.toLowerCase();
196 }
197
198 /**
199  * Extedns ContentScanner.
200  */
201 LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
202
203 /**
204  * Starts the file name search.
205  * @override
206  */
207 LocalSearchContentScanner.prototype.scan = function(
208     entriesCallback, successCallback, errorCallback) {
209   var numRunningTasks = 0;
210   var error = null;
211   var maybeRunCallback = function() {
212     if (numRunningTasks === 0) {
213       if (this.cancelled_)
214         errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
215       else if (error)
216         errorCallback(error);
217       else
218         successCallback();
219     }
220   }.bind(this);
221
222   var processEntry = function(entry) {
223     numRunningTasks++;
224     var onError = function(fileError) {
225       if (!error)
226         error = fileError;
227       numRunningTasks--;
228       maybeRunCallback();
229     };
230
231     var onSuccess = function(entries) {
232       if (this.cancelled_ || error || entries.length === 0) {
233         numRunningTasks--;
234         maybeRunCallback();
235         return;
236       }
237
238       // Filters by the query, and if found, run entriesCallback.
239       var foundEntries = entries.filter(function(entry) {
240         return entry.name.toLowerCase().indexOf(this.query_) >= 0;
241       }.bind(this));
242       if (foundEntries.length > 0)
243         entriesCallback(foundEntries);
244
245       // Start to process sub directories.
246       for (var i = 0; i < entries.length; i++) {
247         if (entries[i].isDirectory)
248           processEntry(entries[i]);
249       }
250
251       // Read remaining entries.
252       reader.readEntries(onSuccess, onError);
253     }.bind(this);
254
255     var reader = entry.createReader();
256     reader.readEntries(onSuccess, onError);
257   }.bind(this);
258
259   processEntry(this.entry_);
260 };
261
262 /**
263  * Scanner of the entries for the metadata search on Drive File System.
264  * @param {string} query The query of the search.
265  * @param {DriveMetadataSearchContentScanner.SearchType} searchType The option
266  *     of the search.
267  * @constructor
268  * @extends {ContentScanner}
269  */
270 function DriveMetadataSearchContentScanner(query, searchType) {
271   ContentScanner.call(this);
272   this.query_ = query;
273   this.searchType_ = searchType;
274 }
275
276 /**
277  * Extends ContentScanner.
278  */
279 DriveMetadataSearchContentScanner.prototype.__proto__ =
280     ContentScanner.prototype;
281
282 /**
283  * The search types on the Drive File System.
284  * @enum {string}
285  */
286 DriveMetadataSearchContentScanner.SearchType = Object.freeze({
287   SEARCH_ALL: 'ALL',
288   SEARCH_SHARED_WITH_ME: 'SHARED_WITH_ME',
289   SEARCH_RECENT_FILES: 'EXCLUDE_DIRECTORIES',
290   SEARCH_OFFLINE: 'OFFLINE'
291 });
292
293 /**
294  * Starts to metadata-search on Drive File System.
295  * @override
296  */
297 DriveMetadataSearchContentScanner.prototype.scan = function(
298     entriesCallback, successCallback, errorCallback) {
299   chrome.fileBrowserPrivate.searchDriveMetadata(
300       {query: this.query_, types: this.searchType_, maxResults: 500},
301       function(results) {
302         if (this.cancelled_) {
303           errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
304           return;
305         }
306
307         if (!results) {
308           console.error('Drive search encountered an error.');
309           errorCallback(util.createDOMError(
310               util.FileError.INVALID_MODIFICATION_ERR));
311           return;
312         }
313
314         var entries = results.map(function(result) { return result.entry; });
315         if (entries.length > 0)
316           entriesCallback(entries);
317         successCallback();
318       }.bind(this));
319 };
320
321 /**
322  * This class manages filters and determines a file should be shown or not.
323  * When filters are changed, a 'changed' event is fired.
324  *
325  * @param {MetadataCache} metadataCache Metadata cache service.
326  * @param {boolean} showHidden If files starting with '.' are shown.
327  * @constructor
328  * @extends {cr.EventTarget}
329  */
330 function FileFilter(metadataCache, showHidden) {
331   /**
332    * @type {MetadataCache}
333    * @private
334    */
335   this.metadataCache_ = metadataCache;
336
337   /**
338    * @type Object.<string, Function>
339    * @private
340    */
341   this.filters_ = {};
342   this.setFilterHidden(!showHidden);
343
344   // Do not show entries marked as 'deleted'.
345   this.addFilter('deleted', function(entry) {
346     var internal = this.metadataCache_.getCached(entry, 'internal');
347     return !(internal && internal.deleted);
348   }.bind(this));
349 }
350
351 /*
352  * FileFilter extends cr.EventTarget.
353  */
354 FileFilter.prototype = {__proto__: cr.EventTarget.prototype};
355
356 /**
357  * @param {string} name Filter identifier.
358  * @param {function(Entry)} callback A filter â€” a function receiving an Entry,
359  *     and returning bool.
360  */
361 FileFilter.prototype.addFilter = function(name, callback) {
362   this.filters_[name] = callback;
363   cr.dispatchSimpleEvent(this, 'changed');
364 };
365
366 /**
367  * @param {string} name Filter identifier.
368  */
369 FileFilter.prototype.removeFilter = function(name) {
370   delete this.filters_[name];
371   cr.dispatchSimpleEvent(this, 'changed');
372 };
373
374 /**
375  * @param {boolean} value If do not show hidden files.
376  */
377 FileFilter.prototype.setFilterHidden = function(value) {
378   if (value) {
379     this.addFilter(
380         'hidden',
381         function(entry) { return entry.name.substr(0, 1) !== '.'; }
382     );
383   } else {
384     this.removeFilter('hidden');
385   }
386 };
387
388 /**
389  * @return {boolean} If the files with names starting with "." are not shown.
390  */
391 FileFilter.prototype.isFilterHiddenOn = function() {
392   return 'hidden' in this.filters_;
393 };
394
395 /**
396  * @param {Entry} entry File entry.
397  * @return {boolean} True if the file should be shown, false otherwise.
398  */
399 FileFilter.prototype.filter = function(entry) {
400   for (var name in this.filters_) {
401     if (!this.filters_[name](entry))
402       return false;
403   }
404   return true;
405 };
406
407 /**
408  * A context of DirectoryContents.
409  * TODO(yoshiki): remove this. crbug.com/224869.
410  *
411  * @param {FileFilter} fileFilter The file-filter context.
412  * @param {MetadataCache} metadataCache Metadata cache service.
413  * @constructor
414  */
415 function FileListContext(fileFilter, metadataCache) {
416   /**
417    * @type {cr.ui.ArrayDataModel}
418    */
419   this.fileList = new cr.ui.ArrayDataModel([]);
420
421   /**
422    * @type {MetadataCache}
423    */
424   this.metadataCache = metadataCache;
425
426   /**
427    * @type {FileFilter}
428    */
429   this.fileFilter = fileFilter;
430 }
431
432 /**
433  * This class is responsible for scanning directory (or search results),
434  * and filling the fileList. Different descendants handle various types of
435  * directory contents shown: basic directory, drive search results, local search
436  * results.
437  * TODO(hidehiko): Remove EventTarget from this.
438  *
439  * @param {FileListContext} context The file list context.
440  * @param {boolean} isSearch True for search directory contents, otherwise
441  *     false.
442  * @param {DirectoryEntry} directoryEntry The entry of the current directory.
443  * @param {DirectoryEntry} lastNonSearchDirectoryEntry The entry of the last
444  *     non-search directory.
445  * @param {function():ContentScanner} scannerFactory The factory to create
446  *     ContentScanner instance.
447  * @constructor
448  * @extends {cr.EventTarget}
449  */
450 function DirectoryContents(context, isSearch, directoryEntry,
451                            lastNonSearchDirectoryEntry,
452                            scannerFactory) {
453   this.context_ = context;
454   this.fileList_ = context.fileList;
455
456   this.isSearch_ = isSearch;
457   this.directoryEntry_ = directoryEntry;
458   this.lastNonSearchDirectoryEntry_ = lastNonSearchDirectoryEntry;
459
460   this.scannerFactory_ = scannerFactory;
461   this.scanner_ = null;
462   this.prefetchMetadataQueue_ = new AsyncUtil.Queue();
463   this.scanCancelled_ = false;
464 }
465
466 /**
467  * DirectoryContents extends cr.EventTarget.
468  */
469 DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype;
470
471 /**
472  * Create the copy of the object, but without scan started.
473  * @return {DirectoryContents} Object copy.
474  */
475 DirectoryContents.prototype.clone = function() {
476   return new DirectoryContents(
477       this.context_, this.isSearch_, this.directoryEntry_,
478       this.lastNonSearchDirectoryEntry_, this.scannerFactory_);
479 };
480
481 /**
482  * Use a given fileList instead of the fileList from the context.
483  * @param {Array|cr.ui.ArrayDataModel} fileList The new file list.
484  */
485 DirectoryContents.prototype.setFileList = function(fileList) {
486   if (fileList instanceof cr.ui.ArrayDataModel)
487     this.fileList_ = fileList;
488   else
489     this.fileList_ = new cr.ui.ArrayDataModel(fileList);
490   this.context_.metadataCache.setCacheSize(this.fileList_.length);
491 };
492
493 /**
494  * Use the filelist from the context and replace its contents with the entries
495  * from the current fileList.
496  */
497 DirectoryContents.prototype.replaceContextFileList = function() {
498   if (this.context_.fileList !== this.fileList_) {
499     var spliceArgs = this.fileList_.slice();
500     var fileList = this.context_.fileList;
501     spliceArgs.unshift(0, fileList.length);
502     fileList.splice.apply(fileList, spliceArgs);
503     this.fileList_ = fileList;
504     this.context_.metadataCache.setCacheSize(this.fileList_.length);
505   }
506 };
507
508 /**
509  * @return {boolean} If the scan is active.
510  */
511 DirectoryContents.prototype.isScanning = function() {
512   return this.scanner_ || this.prefetchMetadataQueue_.isRunning();
513 };
514
515 /**
516  * @return {boolean} True if search results (drive or local).
517  */
518 DirectoryContents.prototype.isSearch = function() {
519   return this.isSearch_;
520 };
521
522 /**
523  * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of
524  *     search -- the top directory from which search is run.
525  */
526 DirectoryContents.prototype.getDirectoryEntry = function() {
527   return this.directoryEntry_;
528 };
529
530 /**
531  * @return {DirectoryEntry} A DirectoryEntry for the last non search contents.
532  */
533 DirectoryContents.prototype.getLastNonSearchDirectoryEntry = function() {
534   return this.lastNonSearchDirectoryEntry_;
535 };
536
537 /**
538  * Start directory scan/search operation. Either 'scan-completed' or
539  * 'scan-failed' event will be fired upon completion.
540  */
541 DirectoryContents.prototype.scan = function() {
542   // TODO(hidehiko,mtomasz): this scan method must be called at most once.
543   // Remove such a limitation.
544   this.scanner_ = this.scannerFactory_();
545   this.scanner_.scan(this.onNewEntries_.bind(this),
546                      this.onScanCompleted_.bind(this),
547                      this.onScanError_.bind(this));
548 };
549
550 /**
551  * Cancels the running scan.
552  */
553 DirectoryContents.prototype.cancelScan = function() {
554   if (this.scanCancelled_)
555     return;
556   this.scanCancelled_ = true;
557   if (this.scanner_)
558     this.scanner_.cancel();
559
560   this.prefetchMetadataQueue_.cancel();
561   cr.dispatchSimpleEvent(this, 'scan-cancelled');
562 };
563
564 /**
565  * Called when the scanning by scanner_ is done.
566  * @private
567  */
568 DirectoryContents.prototype.onScanCompleted_ = function() {
569   this.scanner_ = null;
570   if (this.scanCancelled_)
571     return;
572
573   this.prefetchMetadataQueue_.run(function(callback) {
574     // Call callback first, so isScanning() returns false in the event handlers.
575     callback();
576     cr.dispatchSimpleEvent(this, 'scan-completed');
577   }.bind(this));
578 };
579
580 /**
581  * Called in case scan has failed. Should send the event.
582  * @private
583  */
584 DirectoryContents.prototype.onScanError_ = function() {
585   this.scanner_ = null;
586   if (this.scanCancelled_)
587     return;
588
589   this.prefetchMetadataQueue_.run(function(callback) {
590     // Call callback first, so isScanning() returns false in the event handlers.
591     callback();
592     cr.dispatchSimpleEvent(this, 'scan-failed');
593   }.bind(this));
594 };
595
596 /**
597  * Called when some chunk of entries are read by scanner.
598  * @param {Array.<Entry>} entries The list of the scanned entries.
599  * @private
600  */
601 DirectoryContents.prototype.onNewEntries_ = function(entries) {
602   if (this.scanCancelled_)
603     return;
604
605   var entriesFiltered = [].filter.call(
606       entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter));
607
608   // Update the filelist without waiting the metadata.
609   this.fileList_.push.apply(this.fileList_, entriesFiltered);
610   cr.dispatchSimpleEvent(this, 'scan-updated');
611
612   this.context_.metadataCache.setCacheSize(this.fileList_.length);
613
614   // Because the prefetchMetadata can be slow, throttling by splitting entries
615   // into smaller chunks to reduce UI latency.
616   // TODO(hidehiko,mtomasz): This should be handled in MetadataCache.
617   var MAX_CHUNK_SIZE = 50;
618   for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) {
619     var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE);
620     this.prefetchMetadataQueue_.run(function(chunk, callback) {
621       this.prefetchMetadata(chunk, function() {
622         if (this.scanCancelled_) {
623           // Do nothing if the scanning is cancelled.
624           callback();
625           return;
626         }
627
628         // TODO(yoshiki): Here we should fire the update event of changed
629         // items. Currently we have a method this.fileList_.updateIndex() to
630         // fire an event, but this method takes only 1 argument and invokes sort
631         // one by one. It is obviously time wasting. Instead, we call sort
632         // directory.
633         // In future, we should implement a good method like updateIndexes and
634         // use it here.
635         var status = this.fileList_.sortStatus;
636         this.fileList_.sort(status.field, status.direction);
637
638         cr.dispatchSimpleEvent(this, 'scan-updated');
639         callback();
640       }.bind(this));
641     }.bind(this, chunk));
642   }
643 };
644
645 /**
646  * @param {Array.<Entry>} entries Files.
647  * @param {function(Object)} callback Callback on done.
648  */
649 DirectoryContents.prototype.prefetchMetadata = function(entries, callback) {
650   this.context_.metadataCache.get(entries, 'filesystem', callback);
651 };
652
653 /**
654  * @param {Array.<Entry>} entries Files.
655  * @param {function(Object)} callback Callback on done.
656  */
657 DirectoryContents.prototype.reloadMetadata = function(entries, callback) {
658   this.context_.metadataCache.clear(entries, '*');
659   this.context_.metadataCache.get(entries, 'filesystem', callback);
660 };
661
662 /**
663  * @param {string} name Directory name.
664  * @param {function(DirectoryEntry)} successCallback Called on success.
665  * @param {function(FileError)} errorCallback On error.
666  */
667 DirectoryContents.prototype.createDirectory = function(
668     name, successCallback, errorCallback) {
669   // TODO(hidehiko): createDirectory should not be the part of
670   // DirectoryContent.
671   if (this.isSearch_ || !this.directoryEntry_) {
672     errorCallback(util.createDOMError(
673         util.FileError.INVALID_MODIFICATION_ERR));
674     return;
675   }
676
677   var onSuccess = function(newEntry) {
678     this.reloadMetadata([newEntry], function() {
679       successCallback(newEntry);
680     });
681   };
682
683   this.directoryEntry_.getDirectory(name, {create: true, exclusive: true},
684                                     onSuccess.bind(this), errorCallback);
685 };
686
687 /**
688  * Creates a DirectoryContents instance to show entries in a directory.
689  *
690  * @param {FileListContext} context File list context.
691  * @param {DirectoryEntry} directoryEntry The current directory entry.
692  * @return {DirectoryContents} Created DirectoryContents instance.
693  */
694 DirectoryContents.createForDirectory = function(context, directoryEntry) {
695   return new DirectoryContents(
696       context,
697       false,  // Non search.
698       directoryEntry,
699       directoryEntry,
700       function() {
701         return new DirectoryContentScanner(directoryEntry);
702       });
703 };
704
705 /**
706  * Creates a DirectoryContents instance to show the result of the search on
707  * Drive File System.
708  *
709  * @param {FileListContext} context File list context.
710  * @param {DirectoryEntry} directoryEntry The current directory entry.
711  * @param {DirectoryEntry} previousDirectoryEntry The DirectoryEntry that was
712  *     current before the search.
713  * @param {string} query Search query.
714  * @return {DirectoryContents} Created DirectoryContents instance.
715  */
716 DirectoryContents.createForDriveSearch = function(
717     context, directoryEntry, previousDirectoryEntry, query) {
718   return new DirectoryContents(
719       context,
720       true,  // Search.
721       directoryEntry,
722       previousDirectoryEntry,
723       function() {
724         return new DriveSearchContentScanner(query);
725       });
726 };
727
728 /**
729  * Creates a DirectoryContents instance to show the result of the search on
730  * Local File System.
731  *
732  * @param {FileListContext} context File list context.
733  * @param {DirectoryEntry} directoryEntry The current directory entry.
734  * @param {string} query Search query.
735  * @return {DirectoryContents} Created DirectoryContents instance.
736  */
737 DirectoryContents.createForLocalSearch = function(
738     context, directoryEntry, query) {
739   return new DirectoryContents(
740       context,
741       true,  // Search.
742       directoryEntry,
743       directoryEntry,
744       function() {
745         return new LocalSearchContentScanner(directoryEntry, query);
746       });
747 };
748
749 /**
750  * Creates a DirectoryContents instance to show the result of metadata search
751  * on Drive File System.
752  *
753  * @param {FileListContext} context File list context.
754  * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing
755  *     the set of result entries. This serves as a top directory for the
756  *     search.
757  * @param {DirectoryEntry} driveDirectoryEntry Directory for the actual drive.
758  * @param {string} query Search query.
759  * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of
760  *     the search. The scanner will restricts the entries based on the given
761  *     type.
762  * @return {DirectoryContents} Created DirectoryContents instance.
763  */
764 DirectoryContents.createForDriveMetadataSearch = function(
765     context, fakeDirectoryEntry, driveDirectoryEntry, query, searchType) {
766   return new DirectoryContents(
767       context,
768       true,  // Search
769       fakeDirectoryEntry,
770       driveDirectoryEntry,
771       function() {
772         return new DriveMetadataSearchContentScanner(query, searchType);
773       });
774 };