cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / foreground / js / directory_contents.js
blob919af1c248df660152c47bdff9228e7dbbda9ecb
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 'use strict';
7 /**
8  * Scanner of the entries.
9  * @constructor
10  */
11 function ContentScanner() {
12   this.cancelled_ = false;
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) {
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;
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;
50 /**
51  * Extends ContentScanner.
52  */
53 DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype;
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_ || this.entry_ === DirectoryModel.fakeDriveEntry_) {
62     // If entry is not specified or a fake, we cannot read it.
63     errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR));
64     return;
65   }
67   metrics.startInterval('DirectoryScan');
68   var reader = this.entry_.createReader();
69   var readEntries = function() {
70     reader.readEntries(
71         function(entries) {
72           if (this.cancelled_) {
73             errorCallback(util.createFileError(FileError.ABORT_ERR));
74             return;
75           }
77           if (entries.length === 0) {
78             // All entries are read.
79             metrics.recordInterval('DirectoryScan');
80             successCallback();
81             return;
82           }
84           entriesCallback(entries);
85           readEntries();
86         }.bind(this),
87         errorCallback);
88   }.bind(this);
89   readEntries();
92 /**
93  * Scanner of the entries for the search results on Drive File System.
94  * @param {string} query The query string.
95  * @constructor
96  * @extends {ContentScanner}
97  */
98 function DriveSearchContentScanner(query) {
99   ContentScanner.call(this);
100   this.query_ = query;
104  * Extends ContentScanner.
105  */
106 DriveSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
109  * Delay in milliseconds to be used for drive search scan, in order to reduce
110  * the number of server requests while user is typing the query.
111  * @type {number}
112  * @private
113  * @const
114  */
115 DriveSearchContentScanner.SCAN_DELAY_ = 200;
118  * Maximum number of results which is shown on the search.
119  * @type {number}
120  * @private
121  * @const
122  */
123 DriveSearchContentScanner.MAX_RESULTS_ = 100;
126  * Starts to search on Drive File System.
127  * @override
128  */
129 DriveSearchContentScanner.prototype.scan = function(
130     entriesCallback, successCallback, errorCallback) {
131   var numReadEntries = 0;
132   var readEntries = function(nextFeed) {
133     chrome.fileBrowserPrivate.searchDrive(
134         {query: this.query_, nextFeed: nextFeed},
135         function(entries, nextFeed) {
136           if (this.cancelled_) {
137             errorCallback(util.createFileError(FileError.ABORT_ERR));
138             return;
139           }
141           // TODO(tbarzic): Improve error handling.
142           if (!entries) {
143             console.error('Drive search encountered an error.');
144             errorCallback(util.createFileError(
145                 FileError.INVALID_MODIFICATION_ERR));
146             return;
147           }
149           var numRemainingEntries =
150               DriveSearchContentScanner.MAX_RESULTS_ - numReadEntries;
151           if (entries.length >= numRemainingEntries) {
152             // The limit is hit, so quit the scan here.
153             entries = entries.slice(0, numRemainingEntries);
154             nextFeed = '';
155           }
157           numReadEntries += entries.length;
158           if (entries.length > 0)
159             entriesCallback(entries);
161           if (nextFeed === '')
162             successCallback();
163           else
164             readEntries(nextFeed);
165         }.bind(this));
166   }.bind(this);
168   // Let's give another search a chance to cancel us before we begin.
169   setTimeout(
170       function() {
171         // Check cancelled state before read the entries.
172         if (this.cancelled_) {
173           errorCallback(util.createFileError(FileError.ABORT_ERR));
174           return;
175         }
176         readEntries('');
177       }.bind(this),
178       DriveSearchContentScanner.SCAN_DELAY_);
182  * Scanner of the entries of the file name search on the directory tree, whose
183  * root is entry.
184  * @param {DirectoryEntry} entry The root of the search target directory tree.
185  * @param {string} query The query of the search.
186  * @constructor
187  * @extends {ContentScanner}
188  */
189 function LocalSearchContentScanner(entry, query) {
190   ContentScanner.call(this);
191   this.entry_ = entry;
192   this.query_ = query.toLowerCase();
196  * Extedns ContentScanner.
197  */
198 LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
201  * Starts the file name search.
202  * @override
203  */
204 LocalSearchContentScanner.prototype.scan = function(
205     entriesCallback, successCallback, errorCallback) {
206   var numRunningTasks = 0;
207   var error = null;
208   var maybeRunCallback = function() {
209     if (numRunningTasks === 0) {
210       if (this.cancelled_)
211         errorCallback(util.createFileError(FileError.ABORT_ERR));
212       else if (error)
213         errorCallback(error);
214       else
215         successCallback();
216     }
217   }.bind(this);
219   var processEntry = function(entry) {
220     numRunningTasks++;
221     var onError = function(fileError) {
222       if (!error)
223         error = fileError;
224       numRunningTasks--;
225       maybeRunCallback();
226     };
228     var onSuccess = function(entries) {
229       if (this.cancelled_ || error || entries.length === 0) {
230         numRunningTasks--;
231         maybeRunCallback();
232         return;
233       }
235       // Filters by the query, and if found, run entriesCallback.
236       var foundEntries = entries.filter(function(entry) {
237         return entry.name.toLowerCase().indexOf(this.query_) >= 0;
238       }.bind(this));
239       if (foundEntries.length > 0)
240         entriesCallback(foundEntries);
242       // Start to process sub directories.
243       for (var i = 0; i < entries.length; i++) {
244         if (entries[i].isDirectory)
245           processEntry(entries[i]);
246       }
248       // Read remaining entries.
249       reader.readEntries(onSuccess, onError);
250     }.bind(this);
252     var reader = entry.createReader();
253     reader.readEntries(onSuccess, onError);
254   }.bind(this);
256   processEntry(this.entry_);
260  * Scanner of the entries for the metadata search on Drive File System.
261  * @param {string} query The query of the search.
262  * @param {DriveMetadataSearchContentScanner.SearchType} searchType The option
263  *     of the search.
264  * @constructor
265  * @extends {ContentScanner}
266  */
267 function DriveMetadataSearchContentScanner(query, searchType) {
268   ContentScanner.call(this);
269   this.query_ = query;
270   this.searchType_ = searchType;
274  * Extends ContentScanner.
275  */
276 DriveMetadataSearchContentScanner.prototype.__proto__ =
277     ContentScanner.prototype;
280  * The search types on the Drive File System.
281  * @enum {string}
282  */
283 DriveMetadataSearchContentScanner.SearchType = Object.freeze({
284   SEARCH_ALL: 'ALL',
285   SEARCH_SHARED_WITH_ME: 'SHARED_WITH_ME',
286   SEARCH_RECENT_FILES: 'EXCLUDE_DIRECTORIES',
287   SEARCH_OFFLINE: 'OFFLINE'
291  * Starts to metadata-search on Drive File System.
292  * @override
293  */
294 DriveMetadataSearchContentScanner.prototype.scan = function(
295     entriesCallback, successCallback, errorCallback) {
296   chrome.fileBrowserPrivate.searchDriveMetadata(
297       {query: this.query_, types: this.searchType_, maxResults: 500},
298       function(results) {
299         if (this.cancelled_) {
300           errorCallback(util.createFileError(FileError.ABORT_ERR));
301           return;
302         }
304         if (!results) {
305           console.error('Drive search encountered an error.');
306           errorCallback(util.createFileError(
307               FileError.INVALID_MODIFICATION_ERR));
308           return;
309         }
311         var entries = results.map(function(result) { return result.entry; });
312         if (entries.length > 0)
313           entriesCallback(entries);
314         successCallback();
315       }.bind(this));
319  * This class manages filters and determines a file should be shown or not.
320  * When filters are changed, a 'changed' event is fired.
322  * @param {MetadataCache} metadataCache Metadata cache service.
323  * @param {boolean} showHidden If files starting with '.' 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;
334   /**
335    * @type Object.<string, Function>
336    * @private
337    */
338   this.filters_ = {};
339   this.setFilterHidden(!showHidden);
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));
349  * FileFilter extends cr.EventTarget.
350  */
351 FileFilter.prototype = {__proto__: cr.EventTarget.prototype};
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');
364  * @param {string} name Filter identifier.
365  */
366 FileFilter.prototype.removeFilter = function(name) {
367   delete this.filters_[name];
368   cr.dispatchSimpleEvent(this, 'changed');
372  * @param {boolean} value If do not show hidden files.
373  */
374 FileFilter.prototype.setFilterHidden = function(value) {
375   if (value) {
376     this.addFilter(
377         'hidden',
378         function(entry) { return entry.name.substr(0, 1) !== '.'; }
379     );
380   } else {
381     this.removeFilter('hidden');
382   }
386  * @return {boolean} If the files with names starting with "." are not shown.
387  */
388 FileFilter.prototype.isFilterHiddenOn = function() {
389   return 'hidden' in this.filters_;
393  * @param {Entry} entry File entry.
394  * @return {boolean} True if the file should be shown, false otherwise.
395  */
396 FileFilter.prototype.filter = function(entry) {
397   for (var name in this.filters_) {
398     if (!this.filters_[name](entry))
399       return false;
400   }
401   return true;
405  * A context of DirectoryContents.
406  * TODO(yoshiki): remove this. crbug.com/224869.
408  * @param {FileFilter} fileFilter The file-filter context.
409  * @param {MetadataCache} metadataCache Metadata cache service.
410  * @constructor
411  */
412 function FileListContext(fileFilter, metadataCache) {
413   /**
414    * @type {cr.ui.ArrayDataModel}
415    */
416   this.fileList = new cr.ui.ArrayDataModel([]);
418   /**
419    * @type {MetadataCache}
420    */
421   this.metadataCache = metadataCache;
423   /**
424    * @type {FileFilter}
425    */
426   this.fileFilter = fileFilter;
430  * This class is responsible for scanning directory (or search results),
431  * and filling the fileList. Different descendants handle various types of
432  * directory contents shown: basic directory, drive search results, local search
433  * results.
434  * TODO(hidehiko): Remove EventTarget from this.
436  * @param {FileListContext} context The file list context.
437  * @param {boolean} isSearch True for search directory contents, otherwise
438  *     false.
439  * @param {DirectoryEntry} directoryEntry The entry of the current directory.
440  * @param {DirectoryEntry} lastNonSearchDirectoryEntry The entry of the last
441  *     non-search directory.
442  * @param {function():ContentScanner} scannerFactory The factory to create
443  *     ContentScanner instance.
444  * @constructor
445  * @extends {cr.EventTarget}
446  */
447 function DirectoryContents(context, isSearch, directoryEntry,
448                            lastNonSearchDirectoryEntry,
449                            scannerFactory) {
450   this.context_ = context;
451   this.fileList_ = context.fileList;
453   this.isSearch_ = isSearch;
454   this.directoryEntry_ = directoryEntry;
455   this.lastNonSearchDirectoryEntry_ = lastNonSearchDirectoryEntry;
457   this.scannerFactory_ = scannerFactory;
458   this.scanner_ = null;
459   this.prefetchMetadataQueue_ = new AsyncUtil.Queue();
460   this.scanCancelled_ = false;
461   this.fileList_.prepareSort = this.prepareSort_.bind(this);
465  * DirectoryContents extends cr.EventTarget.
466  */
467 DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype;
470  * Create the copy of the object, but without scan started.
471  * @return {DirectoryContents} Object copy.
472  */
473 DirectoryContents.prototype.clone = function() {
474   return new DirectoryContents(
475       this.context_, this.isSearch_, this.directoryEntry_,
476       this.lastNonSearchDirectoryEntry_, this.scannerFactory_);
480  * Use a given fileList instead of the fileList from the context.
481  * @param {Array|cr.ui.ArrayDataModel} fileList The new file list.
482  */
483 DirectoryContents.prototype.setFileList = function(fileList) {
484   this.fileList_ = fileList;
485   this.fileList_.prepareSort = this.prepareSort_.bind(this);
489  * Use the filelist from the context and replace its contents with the entries
490  * from the current fileList.
491  */
492 DirectoryContents.prototype.replaceContextFileList = function() {
493   if (this.context_.fileList !== this.fileList_) {
494     var spliceArgs = [].slice.call(this.fileList_);
495     var fileList = this.context_.fileList;
496     spliceArgs.unshift(0, fileList.length);
497     fileList.splice.apply(fileList, spliceArgs);
498     this.fileList_ = fileList;
499   }
503  * @return {boolean} If the scan is active.
504  */
505 DirectoryContents.prototype.isScanning = function() {
506   return this.scanner_ || this.prefetchMetadataQueue_.isRunning();
510  * @return {boolean} True if search results (drive or local).
511  */
512 DirectoryContents.prototype.isSearch = function() {
513   return this.isSearch_;
517  * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of
518  *     search -- the top directory from which search is run.
519  */
520 DirectoryContents.prototype.getDirectoryEntry = function() {
521   return this.directoryEntry_;
525  * @return {DirectoryEntry} A DirectoryEntry for the last non search contents.
526  */
527 DirectoryContents.prototype.getLastNonSearchDirectoryEntry = function() {
528   return this.lastNonSearchDirectoryEntry_;
532  * Start directory scan/search operation. Either 'scan-completed' or
533  * 'scan-failed' event will be fired upon completion.
534  */
535 DirectoryContents.prototype.scan = function() {
536   // TODO(hidehiko,mtomasz): this scan method must be called at most once.
537   // Remove such a limitation.
538   this.scanner_ = this.scannerFactory_();
539   this.scanner_.scan(this.onNewEntries_.bind(this),
540                      this.onScanCompleted_.bind(this),
541                      this.onScanError_.bind(this));
545  * Cancels the running scan.
546  */
547 DirectoryContents.prototype.cancelScan = function() {
548   if (this.scanCancelled_)
549     return;
550   this.scanCancelled_ = true;
551   if (this.scanner_)
552     this.scanner_.cancel();
554   this.prefetchMetadataQueue_.cancel();
555   cr.dispatchSimpleEvent(this, 'scan-cancelled');
559  * Called when the scanning by scanner_ is done.
560  * @private
561  */
562 DirectoryContents.prototype.onScanCompleted_ = function() {
563   this.scanner_ = null;
564   if (this.scanCancelled_)
565     return;
567   this.prefetchMetadataQueue_.run(function(callback) {
568     // Call callback first, so isScanning() returns false in the event handlers.
569     callback();
570     cr.dispatchSimpleEvent(this, 'scan-completed');
571   }.bind(this));
575  * Called in case scan has failed. Should send the event.
576  * @private
577  */
578 DirectoryContents.prototype.onScanError_ = function() {
579   this.scanner_ = null;
580   if (this.scanCancelled_)
581     return;
583   this.prefetchMetadataQueue_.run(function(callback) {
584     // Call callback first, so isScanning() returns false in the event handlers.
585     callback();
586     cr.dispatchSimpleEvent(this, 'scan-failed');
587   }.bind(this));
591  * Called when some chunk of entries are read by scanner.
592  * @param {Array.<Entry>} entries The list of the scanned entries.
593  * @private
594  */
595 DirectoryContents.prototype.onNewEntries_ = function(entries) {
596   if (this.scanCancelled_)
597     return;
599   var entriesFiltered = [].filter.call(
600       entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter));
602   // Update the filelist without waiting the metadata.
603   this.fileList_.push.apply(this.fileList_, entriesFiltered);
604   cr.dispatchSimpleEvent(this, 'scan-updated');
606   // Because the prefetchMetadata can be slow, throttling by splitting entries
607   // into smaller chunks to reduce UI latency.
608   // TODO(hidehiko,mtomasz): This should be handled in MetadataCache.
609   var MAX_CHUNK_SIZE = 50;
610   for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) {
611     var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE);
612     this.prefetchMetadataQueue_.run(function(chunk, callback) {
613       this.prefetchMetadata(chunk, function() {
614         if (this.scanCancelled_) {
615           // Do nothing if the scanning is cancelled.
616           callback();
617           return;
618         }
620         cr.dispatchSimpleEvent(this, 'scan-updated');
621         callback();
622       }.bind(this));
623     }.bind(this, chunk));
624   }
628  * Cache necessary data before a sort happens.
630  * This is called by the table code before a sort happens, so that we can
631  * go fetch data for the sort field that we may not have yet.
632  * @param {string} field Sort field.
633  * @param {function(Object)} callback Called when done.
634  * @private
635  */
636 DirectoryContents.prototype.prepareSort_ = function(field, callback) {
637   this.prefetchMetadata(this.fileList_.slice(), callback);
641  * @param {Array.<Entry>} entries Files.
642  * @param {function(Object)} callback Callback on done.
643  */
644 DirectoryContents.prototype.prefetchMetadata = function(entries, callback) {
645   this.context_.metadataCache.get(entries, 'filesystem', callback);
649  * @param {Array.<Entry>} entries Files.
650  * @param {function(Object)} callback Callback on done.
651  */
652 DirectoryContents.prototype.reloadMetadata = function(entries, callback) {
653   this.context_.metadataCache.clear(entries, '*');
654   this.context_.metadataCache.get(entries, 'filesystem', callback);
658  * @param {string} name Directory name.
659  * @param {function(DirectoryEntry)} successCallback Called on success.
660  * @param {function(FileError)} errorCallback On error.
661  */
662 DirectoryContents.prototype.createDirectory = function(
663     name, successCallback, errorCallback) {
664   // TODO(hidehiko): createDirectory should not be the part of
665   // DirectoryContent.
666   if (this.isSearch_ || !this.directoryEntry_) {
667     errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR));
668     return;
669   }
671   var onSuccess = function(newEntry) {
672     this.reloadMetadata([newEntry], function() {
673       successCallback(newEntry);
674     });
675   };
677   this.directoryEntry_.getDirectory(name, {create: true, exclusive: true},
678                                     onSuccess.bind(this), errorCallback);
682  * Creates a DirectoryContents instance to show entries in a directory.
684  * @param {FileListContext} context File list context.
685  * @param {DirectoryEntry} directoryEntry The current directory entry.
686  * @return {DirectoryContents} Created DirectoryContents instance.
687  */
688 DirectoryContents.createForDirectory = function(context, directoryEntry) {
689   return new DirectoryContents(
690       context,
691       false,  // Non search.
692       directoryEntry,
693       directoryEntry,
694       function() {
695         return new DirectoryContentScanner(directoryEntry);
696       });
700  * Creates a DirectoryContents instance to show the result of the search on
701  * Drive File System.
703  * @param {FileListContext} context File list context.
704  * @param {DirectoryEntry} directoryEntry The current directory entry.
705  * @param {DirectoryEntry} previousDirectoryEntry The DirectoryEntry that was
706  *     current before the search.
707  * @param {string} query Search query.
708  * @return {DirectoryContents} Created DirectoryContents instance.
709  */
710 DirectoryContents.createForDriveSearch = function(
711     context, directoryEntry, previousDirectoryEntry, query) {
712   return new DirectoryContents(
713       context,
714       true,  // Search.
715       directoryEntry,
716       previousDirectoryEntry,
717       function() {
718         return new DriveSearchContentScanner(query);
719       });
723  * Creates a DirectoryContents instance to show the result of the search on
724  * Local File System.
726  * @param {FileListContext} context File list context.
727  * @param {DirectoryEntry} directoryEntry The current directory entry.
728  * @param {string} query Search query.
729  * @return {DirectoryContents} Created DirectoryContents instance.
730  */
731 DirectoryContents.createForLocalSearch = function(
732     context, directoryEntry, query) {
733   return new DirectoryContents(
734       context,
735       true,  // Search.
736       directoryEntry,
737       directoryEntry,
738       function() {
739         return new LocalSearchContentScanner(directoryEntry, query);
740       });
744  * Creates a DirectoryContents instance to show the result of metadata search
745  * on Drive File System.
747  * @param {FileListContext} context File list context.
748  * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing
749  *     the set of result entries. This serves as a top directory for the
750  *     search.
751  * @param {DirectoryEntry} driveDirectoryEntry Directory for the actual drive.
752  * @param {string} query Search query.
753  * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of
754  *     the search. The scanner will restricts the entries based on the given
755  *     type.
756  * @return {DirectoryContents} Created DirectoryContents instance.
757  */
758 DirectoryContents.createForDriveMetadataSearch = function(
759     context, fakeDirectoryEntry, driveDirectoryEntry, query, searchType) {
760   return new DirectoryContents(
761       context,
762       true,  // Search
763       fakeDirectoryEntry,
764       driveDirectoryEntry,
765       function() {
766         return new DriveMetadataSearchContentScanner(query, searchType);
767       });