Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / directory_contents.js
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 '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.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  */
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.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  */
326 function FileFilter(metadataCache, showHidden) {
327   /**
328    * @type {MetadataCache}
329    * @private
330    */
331   this.metadataCache_ = metadataCache;
332
333   /**
334    * @type Object.<string, Function>
335    * @private
336    */
337   this.filters_ = {};
338   this.setFilterHidden(!showHidden);
339
340   // Do not show entries marked as 'deleted'.
341   this.addFilter('deleted', function(entry) {
342     var internal = this.metadataCache_.getCached(entry, 'internal');
343     return !(internal && internal.deleted);
344   }.bind(this));
345 }
346
347 /*
348  * FileFilter extends cr.EventTarget.
349  */
350 FileFilter.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  */
357 FileFilter.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  */
365 FileFilter.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  */
373 FileFilter.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  */
387 FileFilter.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  */
395 FileFilter.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  */
411 function 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  */
444 function 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.prefetchMetadataQueue_ = new AsyncUtil.Queue();
457   this.scanCancelled_ = false;
458 }
459
460 /**
461  * DirectoryContents extends cr.EventTarget.
462  */
463 DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype;
464
465 /**
466  * Create the copy of the object, but without scan started.
467  * @return {DirectoryContents} Object copy.
468  */
469 DirectoryContents.prototype.clone = function() {
470   return new DirectoryContents(
471       this.context_,
472       this.isSearch_,
473       this.directoryEntry_,
474       this.scannerFactory_);
475 };
476
477 /**
478  * Use a given fileList instead of the fileList from the context.
479  * @param {Array|cr.ui.ArrayDataModel} fileList The new file list.
480  */
481 DirectoryContents.prototype.setFileList = function(fileList) {
482   if (fileList instanceof cr.ui.ArrayDataModel)
483     this.fileList_ = fileList;
484   else
485     this.fileList_ = new cr.ui.ArrayDataModel(fileList);
486   this.context_.metadataCache.setCacheSize(this.fileList_.length);
487 };
488
489 /**
490  * Use the filelist from the context and replace its contents with the entries
491  * from the current fileList.
492  */
493 DirectoryContents.prototype.replaceContextFileList = function() {
494   if (this.context_.fileList !== this.fileList_) {
495     var spliceArgs = this.fileList_.slice();
496     var fileList = this.context_.fileList;
497     spliceArgs.unshift(0, fileList.length);
498     fileList.splice.apply(fileList, spliceArgs);
499     this.fileList_ = fileList;
500     this.context_.metadataCache.setCacheSize(this.fileList_.length);
501   }
502 };
503
504 /**
505  * @return {boolean} If the scan is active.
506  */
507 DirectoryContents.prototype.isScanning = function() {
508   return this.scanner_ || this.prefetchMetadataQueue_.isRunning();
509 };
510
511 /**
512  * @return {boolean} True if search results (drive or local).
513  */
514 DirectoryContents.prototype.isSearch = function() {
515   return this.isSearch_;
516 };
517
518 /**
519  * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of
520  *     search -- the top directory from which search is run.
521  */
522 DirectoryContents.prototype.getDirectoryEntry = function() {
523   return this.directoryEntry_;
524 };
525
526 /**
527  * Start directory scan/search operation. Either 'scan-completed' or
528  * 'scan-failed' event will be fired upon completion.
529  */
530 DirectoryContents.prototype.scan = function() {
531   /**
532    * Invoked when the scanning is completed successfully.
533    * @this {DirectoryContents}
534    */
535   function completionCallback() {
536     this.onScanFinished_();
537     this.onScanCompleted_();
538   }
539
540   /**
541    * Invoked when the scanning is finished but is not completed due to error.
542    * @this {DirectoryContents}
543    */
544   function errorCallback() {
545     this.onScanFinished_();
546     this.onScanError_();
547   }
548
549   // TODO(hidehiko,mtomasz): this scan method must be called at most once.
550   // Remove such a limitation.
551   this.scanner_ = this.scannerFactory_();
552   this.scanner_.scan(this.onNewEntries_.bind(this),
553                      completionCallback.bind(this),
554                      errorCallback.bind(this));
555 };
556
557 /**
558  * Cancels the running scan.
559  */
560 DirectoryContents.prototype.cancelScan = function() {
561   if (this.scanCancelled_)
562     return;
563   this.scanCancelled_ = true;
564   if (this.scanner_)
565     this.scanner_.cancel();
566
567   this.onScanFinished_();
568
569   this.prefetchMetadataQueue_.cancel();
570   cr.dispatchSimpleEvent(this, 'scan-cancelled');
571 };
572
573 /**
574  * Called when the scanning by scanner_ is done, even when the scanning is
575  * succeeded or failed. This is called before completion (or error) callback.
576  *
577  * @private
578  */
579 DirectoryContents.prototype.onScanFinished_ = function() {
580   this.scanner_ = null;
581
582   this.prefetchMetadataQueue_.run(function(callback) {
583     // TODO(yoshiki): Here we should fire the update event of changed
584     // items. Currently we have a method this.fileList_.updateIndex() to
585     // fire an event, but this method takes only 1 argument and invokes sort
586     // one by one. It is obviously time wasting. Instead, we call sort
587     // directory.
588     // In future, we should implement a good method like updateIndexes and
589     // use it here.
590     var status = this.fileList_.sortStatus;
591     if (status)
592       this.fileList_.sort(status.field, status.direction);
593
594     callback();
595   }.bind(this));
596 };
597
598 /**
599  * Called when the scanning by scanner_ is succeeded.
600  * @private
601  */
602 DirectoryContents.prototype.onScanCompleted_ = function() {
603   if (this.scanCancelled_)
604     return;
605
606   this.prefetchMetadataQueue_.run(function(callback) {
607     // Call callback first, so isScanning() returns false in the event handlers.
608     callback();
609
610     cr.dispatchSimpleEvent(this, 'scan-completed');
611   }.bind(this));
612 };
613
614 /**
615  * Called in case scan has failed. Should send the event.
616  * @private
617  */
618 DirectoryContents.prototype.onScanError_ = function() {
619   if (this.scanCancelled_)
620     return;
621
622   this.prefetchMetadataQueue_.run(function(callback) {
623     // Call callback first, so isScanning() returns false in the event handlers.
624     callback();
625     cr.dispatchSimpleEvent(this, 'scan-failed');
626   }.bind(this));
627 };
628
629 /**
630  * Called when some chunk of entries are read by scanner.
631  * @param {Array.<Entry>} entries The list of the scanned entries.
632  * @private
633  */
634 DirectoryContents.prototype.onNewEntries_ = function(entries) {
635   if (this.scanCancelled_)
636     return;
637
638   var entriesFiltered = [].filter.call(
639       entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter));
640
641   // Caching URL to reduce a number of calls of toURL in sort.
642   // This is a temporary solution. We need to fix a root cause of slow toURL.
643   // See crbug.com/370908 for detail.
644   entriesFiltered.forEach(function(entry) { entry.cachedUrl = entry.toURL(); });
645
646   // Update the filelist without waiting the metadata.
647   this.fileList_.push.apply(this.fileList_, entriesFiltered);
648   cr.dispatchSimpleEvent(this, 'scan-updated');
649
650   this.context_.metadataCache.setCacheSize(this.fileList_.length);
651
652   // Because the prefetchMetadata can be slow, throttling by splitting entries
653   // into smaller chunks to reduce UI latency.
654   // TODO(hidehiko,mtomasz): This should be handled in MetadataCache.
655   var MAX_CHUNK_SIZE = 50;
656   for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) {
657     var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE);
658     this.prefetchMetadataQueue_.run(function(chunk, callback) {
659       this.prefetchMetadata(chunk, function() {
660         if (this.scanCancelled_) {
661           // Do nothing if the scanning is cancelled.
662           callback();
663           return;
664         }
665
666         cr.dispatchSimpleEvent(this, 'scan-updated');
667         callback();
668       }.bind(this));
669     }.bind(this, chunk));
670   }
671 };
672
673 /**
674  * @param {Array.<Entry>} entries Files.
675  * @param {function(Object)} callback Callback on done.
676  */
677 DirectoryContents.prototype.prefetchMetadata = function(entries, callback) {
678   this.context_.metadataCache.get(entries, 'filesystem|drive', callback);
679 };
680
681 /**
682  * Creates a DirectoryContents instance to show entries in a directory.
683  *
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       function() {
694         return new DirectoryContentScanner(directoryEntry);
695       });
696 };
697
698 /**
699  * Creates a DirectoryContents instance to show the result of the search on
700  * Drive File System.
701  *
702  * @param {FileListContext} context File list context.
703  * @param {DirectoryEntry} directoryEntry The current directory entry.
704  * @param {string} query Search query.
705  * @return {DirectoryContents} Created DirectoryContents instance.
706  */
707 DirectoryContents.createForDriveSearch = function(
708     context, directoryEntry, query) {
709   return new DirectoryContents(
710       context,
711       true,  // Search.
712       directoryEntry,
713       function() {
714         return new DriveSearchContentScanner(query);
715       });
716 };
717
718 /**
719  * Creates a DirectoryContents instance to show the result of the search on
720  * Local File System.
721  *
722  * @param {FileListContext} context File list context.
723  * @param {DirectoryEntry} directoryEntry The current directory entry.
724  * @param {string} query Search query.
725  * @return {DirectoryContents} Created DirectoryContents instance.
726  */
727 DirectoryContents.createForLocalSearch = function(
728     context, directoryEntry, query) {
729   return new DirectoryContents(
730       context,
731       true,  // Search.
732       directoryEntry,
733       function() {
734         return new LocalSearchContentScanner(directoryEntry, query);
735       });
736 };
737
738 /**
739  * Creates a DirectoryContents instance to show the result of metadata search
740  * on Drive File System.
741  *
742  * @param {FileListContext} context File list context.
743  * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing
744  *     the set of result entries. This serves as a top directory for the
745  *     search.
746  * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of
747  *     the search. The scanner will restricts the entries based on the given
748  *     type.
749  * @return {DirectoryContents} Created DirectoryContents instance.
750  */
751 DirectoryContents.createForDriveMetadataSearch = function(
752     context, fakeDirectoryEntry, searchType) {
753   return new DirectoryContents(
754       context,
755       true,  // Search
756       fakeDirectoryEntry,
757       function() {
758         return new DriveMetadataSearchContentScanner(searchType);
759       });
760 };