cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / foreground / js / directory_model.js
blob0b2ddf14693b7fd305d5023f3cdc92d3d42b66a4
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 // If directory files changes too often, don't rescan directory more than once
8 // per specified interval
9 var SIMULTANEOUS_RESCAN_INTERVAL = 1000;
10 // Used for operations that require almost instant rescan.
11 var SHORT_RESCAN_INTERVAL = 100;
13 /**
14  * Data model of the file manager.
15  *
16  * @param {boolean} singleSelection True if only one file could be selected
17  *                                  at the time.
18  * @param {FileFilter} fileFilter Instance of FileFilter.
19  * @param {FileWatcher} fileWatcher Instance of FileWatcher.
20  * @param {MetadataCache} metadataCache The metadata cache service.
21  * @param {VolumeManagerWrapper} volumeManager The volume manager.
22  * @constructor
23  */
24 function DirectoryModel(singleSelection, fileFilter, fileWatcher,
25                         metadataCache, volumeManager) {
26   this.fileListSelection_ = singleSelection ?
27       new cr.ui.ListSingleSelectionModel() : new cr.ui.ListSelectionModel();
29   this.runningScan_ = null;
30   this.pendingScan_ = null;
31   this.rescanTime_ = null;
32   this.scanFailures_ = 0;
34   this.fileFilter_ = fileFilter;
35   this.fileFilter_.addEventListener('changed',
36                                     this.onFilterChanged_.bind(this));
38   this.currentFileListContext_ = new FileListContext(
39       fileFilter, metadataCache);
40   this.currentDirContents_ =
41       DirectoryContents.createForDirectory(this.currentFileListContext_, null);
43   this.volumeManager_ = volumeManager;
44   this.volumeManager_.volumeInfoList.addEventListener(
45       'splice', this.onVolumeInfoListUpdated_.bind(this));
47   this.fileWatcher_ = fileWatcher;
48   this.fileWatcher_.addEventListener(
49       'watcher-directory-changed',
50       this.onWatcherDirectoryChanged_.bind(this));
53 /**
54  * Fake entry to be used in currentDirEntry_ when current directory is
55  * unmounted DRIVE. TODO(haruki): Support "drive/root" and "drive/other".
56  * @type {Object}
57  * @const
58  * @private
59  */
60 DirectoryModel.fakeDriveEntry_ = {
61   fullPath: RootDirectory.DRIVE + '/' + DriveSubRootDirectory.ROOT,
62   isDirectory: true,
63   rootType: RootType.DRIVE
66 /**
67  * Fake entry representing a psuedo directory, which contains Drive files
68  * available offline. This entry works as a trigger to start a search for
69  * offline files.
70  * @type {Object}
71  * @const
72  * @private
73  */
74 DirectoryModel.fakeDriveOfflineEntry_ = {
75   fullPath: RootDirectory.DRIVE_OFFLINE,
76   isDirectory: true,
77   rootType: RootType.DRIVE_OFFLINE
80 /**
81  * Fake entry representing a pseudo directory, which contains shared-with-me
82  * Drive files. This entry works as a trigger to start a search for
83  * shared-with-me files.
84  * @type {Object}
85  * @const
86  * @private
87  */
88 DirectoryModel.fakeDriveSharedWithMeEntry_ = {
89   fullPath: RootDirectory.DRIVE_SHARED_WITH_ME,
90   isDirectory: true,
91   rootType: RootType.DRIVE_SHARED_WITH_ME
94 /**
95  * Fake entry representing a pseudo directory, which contains Drive files
96  * accessed recently. This entry works as a trigger to start a metadata search
97  * implemented as DirectoryContentsDriveRecent.
98  * DirectoryModel is responsible to start the search when the UI tries to open
99  * this fake entry (e.g. changeDirectory()).
100  * @type {Object}
101  * @const
102  * @private
103  */
104 DirectoryModel.fakeDriveRecentEntry_ = {
105   fullPath: RootDirectory.DRIVE_RECENT,
106   isDirectory: true,
107   rootType: RootType.DRIVE_RECENT
111  * List of fake entries for special searches.
113  * @type {Array.<Object>}
114  * @const
115  */
116 DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES = [
117   DirectoryModel.fakeDriveSharedWithMeEntry_,
118   DirectoryModel.fakeDriveRecentEntry_,
119   DirectoryModel.fakeDriveOfflineEntry_
123  * DirectoryModel extends cr.EventTarget.
124  */
125 DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype;
128  * Disposes the directory model by removing file watchers.
129  */
130 DirectoryModel.prototype.dispose = function() {
131   this.fileWatcher_.dispose();
135  * @return {cr.ui.ArrayDataModel} Files in the current directory.
136  */
137 DirectoryModel.prototype.getFileList = function() {
138   return this.currentFileListContext_.fileList;
142  * Sort the file list.
143  * @param {string} sortField Sort field.
144  * @param {string} sortDirection "asc" or "desc".
145  */
146 DirectoryModel.prototype.sortFileList = function(sortField, sortDirection) {
147   this.getFileList().sort(sortField, sortDirection);
151  * @return {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} Selection
152  * in the fileList.
153  */
154 DirectoryModel.prototype.getFileListSelection = function() {
155   return this.fileListSelection_;
159  * @return {RootType} Root type of current root.
160  */
161 DirectoryModel.prototype.getCurrentRootType = function() {
162   var entry = this.currentDirContents_.getDirectoryEntry();
163   return PathUtil.getRootType(entry ? entry.fullPath : '');
167  * @return {string} Root path.
168  */
169 DirectoryModel.prototype.getCurrentRootPath = function() {
170   var entry = this.currentDirContents_.getDirectoryEntry();
171   return entry ? PathUtil.getRootPath(entry.fullPath) : '';
175  * @return {string} Filesystem URL representing the mountpoint for the current
176  *     contents.
177  */
178 DirectoryModel.prototype.getCurrentMountPointUrl = function() {
179   var rootPath = this.getCurrentRootPath();
180   // Special search roots are just showing a search results from DRIVE.
181   if (PathUtil.getRootType(rootPath) == RootType.DRIVE ||
182       PathUtil.isSpecialSearchRoot(rootPath))
183     return util.makeFilesystemUrl(RootDirectory.DRIVE);
185   return util.makeFilesystemUrl(rootPath);
189  * @return {boolean} on True if offline.
190  */
191 DirectoryModel.prototype.isDriveOffline = function() {
192   var connection = this.volumeManager_.getDriveConnectionState();
193   return connection.type == util.DriveConnectionType.OFFLINE;
197  * TODO(haruki): This actually checks the current root. Fix the method name and
198  * related code.
199  * @return {boolean} True if the root for the current directory is read only.
200  */
201 DirectoryModel.prototype.isReadOnly = function() {
202   return this.isPathReadOnly(this.getCurrentRootPath());
206  * @return {boolean} True if the a scan is active.
207  */
208 DirectoryModel.prototype.isScanning = function() {
209   return this.currentDirContents_.isScanning();
213  * @return {boolean} True if search is in progress.
214  */
215 DirectoryModel.prototype.isSearching = function() {
216   return this.currentDirContents_.isSearch();
220  * @param {string} path Path to check.
221  * @return {boolean} True if the |path| is read only.
222  */
223 DirectoryModel.prototype.isPathReadOnly = function(path) {
224   // TODO(hidehiko): Migrate this into VolumeInfo.
225   switch (PathUtil.getRootType(path)) {
226     case RootType.REMOVABLE:
227       var volumeInfo = this.volumeManager_.getVolumeInfo(path);
228       // Returns true if the volume is actually read only, or if an error
229       // is found during the mounting.
230       // TODO(hidehiko): Remove "error" check here, by removing error'ed volume
231       // info from VolumeManager.
232       return volumeInfo && (volumeInfo.isReadOnly || !!volumeInfo.error);
233     case RootType.ARCHIVE:
234       return true;
235     case RootType.DOWNLOADS:
236       return false;
237     case RootType.DRIVE:
238       // TODO(haruki): Maybe add DRIVE_OFFLINE as well to allow renaming in the
239       // offline tab.
240       return this.isDriveOffline();
241     default:
242       return true;
243   }
247  * Updates the selection by using the updateFunc and publish the change event.
248  * If updateFunc returns true, it force to dispatch the change event even if the
249  * selection index is not changed.
251  * @param {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} selection
252  *     Selection to be updated.
253  * @param {function(): boolean} updateFunc Function updating the selection.
254  * @private
255  */
256 DirectoryModel.prototype.updateSelectionAndPublishEvent_ =
257     function(selection, updateFunc) {
258   // Begin change.
259   selection.beginChange();
261   // If dispatchNeeded is true, we should ensure the change event is
262   // dispatched.
263   var dispatchNeeded = updateFunc();
265   // Check if the change event is dispatched in the endChange function
266   // or not.
267   var eventDispatched = function() { dispatchNeeded = false; };
268   selection.addEventListener('change', eventDispatched);
269   selection.endChange();
270   selection.removeEventListener('change', eventDispatched);
272   // If the change event have been already dispatched, dispatchNeeded is false.
273   if (dispatchNeeded) {
274     var event = new Event('change');
275     // The selection status (selected or not) is not changed because
276     // this event is caused by the change of selected item.
277     event.changes = [];
278     selection.dispatchEvent(event);
279   }
283  * Invoked when a change in the directory is detected by the watcher.
284  * @private
285  */
286 DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() {
287   this.rescanSoon();
291  * Invoked when filters are changed.
292  * @private
293  */
294 DirectoryModel.prototype.onFilterChanged_ = function() {
295   this.rescanSoon();
299  * Returns the filter.
300  * @return {FileFilter} The file filter.
301  */
302 DirectoryModel.prototype.getFileFilter = function() {
303   return this.fileFilter_;
307  * @return {DirectoryEntry} Current directory.
308  */
309 DirectoryModel.prototype.getCurrentDirEntry = function() {
310   return this.currentDirContents_.getDirectoryEntry();
314  * @return {string} URL of the current directory. or null if unavailable.
315  */
316 DirectoryModel.prototype.getCurrentDirectoryURL = function() {
317   var entry = this.currentDirContents_.getDirectoryEntry();
318   if (!entry)
319     return null;
320   if (entry === DirectoryModel.fakeDriveOfflineEntry_)
321     return util.makeFilesystemUrl(entry.fullPath);
322   return entry.toURL();
326  * @return {string} Path for the current directory, or empty string if the
327  *     current directory is not yet set.
328  */
329 DirectoryModel.prototype.getCurrentDirPath = function() {
330   var entry = this.currentDirContents_.getDirectoryEntry();
331   return entry ? entry.fullPath : '';
335  * @return {Array.<string>} File paths of selected files.
336  * @private
337  */
338 DirectoryModel.prototype.getSelectedPaths_ = function() {
339   var indexes = this.fileListSelection_.selectedIndexes;
340   var fileList = this.getFileList();
341   if (fileList) {
342     return indexes.map(function(i) {
343       return fileList.item(i).fullPath;
344     });
345   }
346   return [];
350  * @param {Array.<string>} value List of file paths of selected files.
351  * @private
352  */
353 DirectoryModel.prototype.setSelectedPaths_ = function(value) {
354   var indexes = [];
355   var fileList = this.getFileList();
357   var safeKey = function(key) {
358     // The transformation must:
359     // 1. Never generate a reserved name ('__proto__')
360     // 2. Keep different keys different.
361     return '#' + key;
362   };
364   var hash = {};
366   for (var i = 0; i < value.length; i++)
367     hash[safeKey(value[i])] = 1;
369   for (var i = 0; i < fileList.length; i++) {
370     if (hash.hasOwnProperty(safeKey(fileList.item(i).fullPath)))
371       indexes.push(i);
372   }
373   this.fileListSelection_.selectedIndexes = indexes;
377  * @return {string} Lead item file path.
378  * @private
379  */
380 DirectoryModel.prototype.getLeadPath_ = function() {
381   var index = this.fileListSelection_.leadIndex;
382   return index >= 0 && this.getFileList().item(index).fullPath;
386  * @param {string} value The name of new lead index.
387  * @private
388  */
389 DirectoryModel.prototype.setLeadPath_ = function(value) {
390   var fileList = this.getFileList();
391   for (var i = 0; i < fileList.length; i++) {
392     if (fileList.item(i).fullPath === value) {
393       this.fileListSelection_.leadIndex = i;
394       return;
395     }
396   }
400  * Schedule rescan with short delay.
401  */
402 DirectoryModel.prototype.rescanSoon = function() {
403   this.scheduleRescan(SHORT_RESCAN_INTERVAL);
407  * Schedule rescan with delay. Designed to handle directory change
408  * notification.
409  */
410 DirectoryModel.prototype.rescanLater = function() {
411   this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL);
415  * Schedule rescan with delay. If another rescan has been scheduled does
416  * nothing. File operation may cause a few notifications what should cause
417  * a single refresh.
418  * @param {number} delay Delay in ms after which the rescan will be performed.
419  */
420 DirectoryModel.prototype.scheduleRescan = function(delay) {
421   if (this.rescanTime_) {
422     if (this.rescanTime_ <= Date.now() + delay)
423       return;
424     clearTimeout(this.rescanTimeoutId_);
425   }
427   this.rescanTime_ = Date.now() + delay;
428   this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay);
432  * Cancel a rescan on timeout if it is scheduled.
433  * @private
434  */
435 DirectoryModel.prototype.clearRescanTimeout_ = function() {
436   this.rescanTime_ = null;
437   if (this.rescanTimeoutId_) {
438     clearTimeout(this.rescanTimeoutId_);
439     this.rescanTimeoutId_ = null;
440   }
444  * Rescan current directory. May be called indirectly through rescanLater or
445  * directly in order to reflect user action. Will first cache all the directory
446  * contents in an array, then seamlessly substitute the fileList contents,
447  * preserving the select element etc.
449  * This should be to scan the contents of current directory (or search).
450  */
451 DirectoryModel.prototype.rescan = function() {
452   this.clearRescanTimeout_();
453   if (this.runningScan_) {
454     this.pendingRescan_ = true;
455     return;
456   }
458   var dirContents = this.currentDirContents_.clone();
459   dirContents.setFileList([]);
461   var successCallback = (function() {
462     this.replaceDirectoryContents_(dirContents);
463     cr.dispatchSimpleEvent(this, 'rescan-completed');
464   }).bind(this);
466   this.scan_(dirContents,
467              successCallback, function() {}, function() {}, function() {});
471  * Run scan on the current DirectoryContents. The active fileList is cleared and
472  * the entries are added directly.
474  * This should be used when changing directory or initiating a new search.
476  * @param {DirectoryContentes} newDirContents New DirectoryContents instance to
477  *     replace currentDirContents_.
478  * @param {function()=} opt_callback Called on success.
479  * @private
480  */
481 DirectoryModel.prototype.clearAndScan_ = function(newDirContents,
482                                                   opt_callback) {
483   if (this.currentDirContents_.isScanning())
484     this.currentDirContents_.cancelScan();
485   this.currentDirContents_ = newDirContents;
486   this.clearRescanTimeout_();
488   if (this.pendingScan_)
489     this.pendingScan_ = false;
491   if (this.runningScan_) {
492     if (this.runningScan_.isScanning())
493       this.runningScan_.cancelScan();
494     this.runningScan_ = null;
495   }
497   var onDone = function() {
498     cr.dispatchSimpleEvent(this, 'scan-completed');
499     if (opt_callback)
500       opt_callback();
501   }.bind(this);
503   var onFailed = function() {
504     cr.dispatchSimpleEvent(this, 'scan-failed');
505   }.bind(this);
507   var onUpdated = function() {
508     cr.dispatchSimpleEvent(this, 'scan-updated');
509   }.bind(this);
511   var onCancelled = function() {
512     cr.dispatchSimpleEvent(this, 'scan-cancelled');
513   }.bind(this);
515   // Clear the table, and start scanning.
516   cr.dispatchSimpleEvent(this, 'scan-started');
517   var fileList = this.getFileList();
518   fileList.splice(0, fileList.length);
519   this.scan_(this.currentDirContents_,
520              onDone, onFailed, onUpdated, onCancelled);
524  * Perform a directory contents scan. Should be called only from rescan() and
525  * clearAndScan_().
527  * @param {DirectoryContents} dirContents DirectoryContents instance on which
528  *     the scan will be run.
529  * @param {function()} successCallback Callback on success.
530  * @param {function()} failureCallback Callback on failure.
531  * @param {function()} updatedCallback Callback on update. Only on the last
532  *     update, {@code successCallback} is called instead of this.
533  * @param {function()} cancelledCallback Callback on cancel.
534  * @private
535  */
536 DirectoryModel.prototype.scan_ = function(
537     dirContents,
538     successCallback, failureCallback, updatedCallback, cancelledCallback) {
539   var self = this;
541   /**
542    * Runs pending scan if there is one.
543    *
544    * @return {boolean} Did pending scan exist.
545    */
546   var maybeRunPendingRescan = function() {
547     if (this.pendingRescan_) {
548       this.rescanSoon();
549       this.pendingRescan_ = false;
550       return true;
551     }
552     return false;
553   }.bind(this);
555   var onSuccess = function() {
556     // Record metric for Downloads directory.
557     if (!dirContents.isSearch()) {
558       var locationInfo =
559           this.volumeManager_.getLocationInfo(dirContents.getDirectoryEntry());
560       if (locationInfo.volumeInfo.volumeType === util.VolumeType.DOWNLOADS &&
561           locationInfo.isRootEntry) {
562         metrics.recordMediumCount('DownloadsCount',
563                                   dirContents.fileList_.length);
564       }
565     }
567     this.runningScan_ = null;
568     successCallback();
569     this.scanFailures_ = 0;
570     maybeRunPendingRescan();
571   }.bind(this);
573   var onFailure = function() {
574     this.runningScan_ = null;
575     this.scanFailures_++;
576     failureCallback();
578     if (maybeRunPendingRescan())
579       return;
581     if (this.scanFailures_ <= 1)
582       this.rescanLater();
583   }.bind(this);
585   this.runningScan_ = dirContents;
587   dirContents.addEventListener('scan-completed', onSuccess);
588   dirContents.addEventListener('scan-updated', updatedCallback);
589   dirContents.addEventListener('scan-failed', onFailure);
590   dirContents.addEventListener('scan-cancelled', cancelledCallback);
591   dirContents.scan();
595  * @param {DirectoryContents} dirContents DirectoryContents instance.
596  * @private
597  */
598 DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) {
599   cr.dispatchSimpleEvent(this, 'begin-update-files');
600   this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() {
601     var selectedPaths = this.getSelectedPaths_();
602     var selectedIndices = this.fileListSelection_.selectedIndexes;
604     // Restore leadIndex in case leadName no longer exists.
605     var leadIndex = this.fileListSelection_.leadIndex;
606     var leadPath = this.getLeadPath_();
608     this.currentDirContents_ = dirContents;
609     dirContents.replaceContextFileList();
611     this.setSelectedPaths_(selectedPaths);
612     this.fileListSelection_.leadIndex = leadIndex;
613     this.setLeadPath_(leadPath);
615     // If nothing is selected after update, then select file next to the
616     // latest selection
617     var forceChangeEvent = false;
618     if (this.fileListSelection_.selectedIndexes.length == 0 &&
619         selectedIndices.length != 0) {
620       var maxIdx = Math.max.apply(null, selectedIndices);
621       this.selectIndex(Math.min(maxIdx - selectedIndices.length + 2,
622                                 this.getFileList().length) - 1);
623       forceChangeEvent = true;
624     }
625     return forceChangeEvent;
626   }.bind(this));
628   cr.dispatchSimpleEvent(this, 'end-update-files');
632  * Callback when an entry is changed.
633  * @param {util.EntryChangedKind} kind How the entry is changed.
634  * @param {Entry} entry The changed entry.
635  */
636 DirectoryModel.prototype.onEntryChanged = function(kind, entry) {
637   // TODO(hidehiko): We should update directory model even the search result
638   // is shown.
639   var rootType = this.getCurrentRootType();
640   if ((rootType === RootType.DRIVE ||
641        rootType === RootType.DRIVE_SHARED_WITH_ME ||
642        rootType === RootType.DRIVE_RECENT ||
643        rootType === RootType.DRIVE_OFFLINE) &&
644       this.isSearching())
645     return;
647   if (kind == util.EntryChangedKind.CREATED) {
648     entry.getParent(function(parentEntry) {
649       if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) {
650         // Do nothing if current directory changed during async operations.
651         return;
652       }
653       this.currentDirContents_.prefetchMetadata([entry], function() {
654         if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) {
655           // Do nothing if current directory changed during async operations.
656           return;
657         }
659         var index = this.findIndexByEntry_(entry);
660         if (index >= 0)
661           this.getFileList().splice(index, 1, entry);
662         else
663           this.getFileList().push(entry);
664       }.bind(this));
665     }.bind(this));
666   } else {
667     // This is the delete event.
668     var index = this.findIndexByEntry_(entry);
669     if (index >= 0)
670       this.getFileList().splice(index, 1);
671   }
675  * @param {Entry} entry The entry to be searched.
676  * @return {number} The index in the fileList, or -1 if not found.
677  * @private
678  */
679 DirectoryModel.prototype.findIndexByEntry_ = function(entry) {
680   var fileList = this.getFileList();
681   for (var i = 0; i < fileList.length; i++) {
682     if (util.isSameEntry(fileList.item(i), entry))
683       return i;
684   }
685   return -1;
689  * Called when rename is done successfully.
690  * Note: conceptually, DirectoryModel should work without this, because entries
691  * can be renamed by other systems anytime and Files.app should reflect it
692  * correctly.
693  * TODO(hidehiko): investigate more background, and remove this if possible.
695  * @param {Entry} oldEntry The old entry.
696  * @param {Entry} newEntry The new entry.
697  * @param {function()} opt_callback Called on completion.
698  */
699 DirectoryModel.prototype.onRenameEntry = function(
700     oldEntry, newEntry, opt_callback) {
701   this.currentDirContents_.prefetchMetadata([newEntry], function() {
702     // If the current directory is the old entry, then quietly change to the
703     // new one.
704     if (util.isSameEntry(oldEntry, this.getCurrentDirEntry()))
705       this.changeDirectory(newEntry.fullPath);
707     // Look for the old entry.
708     // If the entry doesn't exist in the list, it has been updated from
709     // outside (probably by directory rescan).
710     var index = this.findIndexByEntry_(oldEntry);
711     if (index >= 0) {
712       // Update the content list and selection status.
713       var wasSelected = this.fileListSelection_.getIndexSelected(index);
714       this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() {
715         this.fileListSelection_.setIndexSelected(index, false);
716         this.getFileList().splice(index, 1, newEntry);
717         if (wasSelected) {
718           // We re-search the index, because splice may trigger sorting so that
719           // index may be stale.
720           this.fileListSelection_.setIndexSelected(
721               this.findIndexByEntry_(newEntry), true);
722         }
723         return true;
724       }.bind(this));
725     }
727     // Run callback, finally.
728     if (opt_callback)
729       opt_callback();
730   }.bind(this));
734  * Creates directory and updates the file list.
736  * @param {string} name Directory name.
737  * @param {function(DirectoryEntry)} successCallback Callback on success.
738  * @param {function(FileError)} errorCallback Callback on failure.
739  */
740 DirectoryModel.prototype.createDirectory = function(name, successCallback,
741                                                     errorCallback) {
742   var entry = this.getCurrentDirEntry();
743   if (!entry) {
744     errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR));
745     return;
746   }
748   var tracker = this.createDirectoryChangeTracker();
749   tracker.start();
751   var onSuccess = function(newEntry) {
752     // Do not change anything or call the callback if current
753     // directory changed.
754     tracker.stop();
755     if (tracker.hasChanged)
756       return;
758     var existing = this.getFileList().slice().filter(
759         function(e) {return e.name == name;});
761     if (existing.length) {
762       this.selectEntry(newEntry);
763       successCallback(existing[0]);
764     } else {
765       this.fileListSelection_.beginChange();
766       this.getFileList().splice(0, 0, newEntry);
767       this.selectEntry(newEntry);
768       this.fileListSelection_.endChange();
769       successCallback(newEntry);
770     }
771   };
773   this.currentDirContents_.createDirectory(name, onSuccess.bind(this),
774                                            errorCallback);
778  * Changes directory. Causes 'directory-change' event.
780  * @param {string} path New current directory path.
781  * @param {function(FileError)=} opt_errorCallback Executed if the change
782  *     directory failed.
783  */
784 DirectoryModel.prototype.changeDirectory = function(path, opt_errorCallback) {
785   if (PathUtil.isSpecialSearchRoot(path)) {
786     this.specialSearch(path, '');
787     return;
788   }
790   this.resolveDirectory(path, function(directoryEntry) {
791     this.changeDirectoryEntry(directoryEntry);
792   }.bind(this), function(error) {
793     console.error('Error changing directory to ' + path + ': ', error);
794     if (opt_errorCallback)
795       opt_errorCallback(error);
796   });
800  * Resolves absolute directory path. Handles Drive stub. If the drive is
801  * mounting, callbacks will be called after the mount is completed.
803  * @param {string} path Path to the directory.
804  * @param {function(DirectoryEntry)} successCallback Success callback.
805  * @param {function(FileError)} errorCallback Error callback.
806  */
807 DirectoryModel.prototype.resolveDirectory = function(
808     path, successCallback, errorCallback) {
809   if (PathUtil.getRootType(path) == RootType.DRIVE) {
810     if (!this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) {
811       errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
812       return;
813     }
814   }
816   var onError = function(error) {
817     // Handle the special case, when in offline mode, and there are no cached
818     // contents on the C++ side. In such case, let's display the stub.
819     // The INVALID_STATE_ERR error code is returned from the drive filesystem
820     // in such situation.
821     //
822     // TODO(mtomasz, hashimoto): Consider rewriting this logic.
823     //     crbug.com/253464.
824     if (PathUtil.getRootType(path) == RootType.DRIVE &&
825         error.code == FileError.INVALID_STATE_ERR) {
826       successCallback(DirectoryModel.fakeDriveEntry_);
827       return;
828     }
829     errorCallback(error);
830   }.bind(this);
832   // TODO(mtomasz): Use Entry instead of a path.
833   this.volumeManager_.resolveAbsolutePath(
834       path,
835       function(entry) {
836         if (entry.isFile) {
837           onError(util.createFileError(FileError.TYPE_MISMATCH_ERR));
838           return;
839         }
840         successCallback(entry);
841       },
842       onError);
846  * @param {DirectoryEntry} dirEntry The absolute path to the new directory.
847  * @param {function()=} opt_callback Executed if the directory loads
848  *     successfully.
849  * @private
850  */
851 DirectoryModel.prototype.changeDirectoryEntrySilent_ = function(dirEntry,
852                                                                 opt_callback) {
853   var onScanComplete = function() {
854     if (opt_callback)
855       opt_callback();
856     // For tests that open the dialog to empty directories, everything
857     // is loaded at this point.
858     chrome.test.sendMessage('directory-change-complete');
859   };
860   this.clearAndScan_(
861       DirectoryContents.createForDirectory(this.currentFileListContext_,
862                                            dirEntry),
863       onScanComplete.bind(this));
867  * Change the current directory to the directory represented by a
868  * DirectoryEntry.
870  * Dispatches the 'directory-changed' event when the directory is successfully
871  * changed.
873  * @param {DirectoryEntry} dirEntry The absolute path to the new directory.
874  * @param {function()=} opt_callback Executed if the directory loads
875  *     successfully.
876  */
877 DirectoryModel.prototype.changeDirectoryEntry = function(
878     dirEntry, opt_callback) {
879   this.fileWatcher_.changeWatchedDirectory(dirEntry, function() {
880     var previous = this.currentDirContents_.getDirectoryEntry();
881     this.clearSearch_();
882     this.changeDirectoryEntrySilent_(dirEntry, opt_callback);
884     var e = new Event('directory-changed');
885     e.previousDirEntry = previous;
886     e.newDirEntry = dirEntry;
887     this.dispatchEvent(e);
888   }.bind(this));
892  * Creates an object which could say whether directory has changed while it has
893  * been active or not. Designed for long operations that should be cancelled
894  * if the used change current directory.
895  * @return {Object} Created object.
896  */
897 DirectoryModel.prototype.createDirectoryChangeTracker = function() {
898   var tracker = {
899     dm_: this,
900     active_: false,
901     hasChanged: false,
903     start: function() {
904       if (!this.active_) {
905         this.dm_.addEventListener('directory-changed',
906                                   this.onDirectoryChange_);
907         this.active_ = true;
908         this.hasChanged = false;
909       }
910     },
912     stop: function() {
913       if (this.active_) {
914         this.dm_.removeEventListener('directory-changed',
915                                      this.onDirectoryChange_);
916         this.active_ = false;
917       }
918     },
920     onDirectoryChange_: function(event) {
921       tracker.stop();
922       tracker.hasChanged = true;
923     }
924   };
925   return tracker;
929  * @param {Entry} entry Entry to be selected.
930  */
931 DirectoryModel.prototype.selectEntry = function(entry) {
932   var fileList = this.getFileList();
933   for (var i = 0; i < fileList.length; i++) {
934     if (fileList.item(i).toURL() === entry.toURL()) {
935       this.selectIndex(i);
936       return;
937     }
938   }
942  * @param {Array.<string>} entries Array of entries.
943  */
944 DirectoryModel.prototype.selectEntries = function(entries) {
945   // URLs are needed here, since we are comparing Entries by URLs.
946   var urls = util.entriesToURLs(entries);
947   var fileList = this.getFileList();
948   this.fileListSelection_.beginChange();
949   this.fileListSelection_.unselectAll();
950   for (var i = 0; i < fileList.length; i++) {
951     if (urls.indexOf(fileList.item(i).toURL()) >= 0)
952       this.fileListSelection_.setIndexSelected(i, true);
953   }
954   this.fileListSelection_.endChange();
958  * @param {number} index Index of file.
959  */
960 DirectoryModel.prototype.selectIndex = function(index) {
961   // this.focusCurrentList_();
962   if (index >= this.getFileList().length)
963     return;
965   // If a list bound with the model it will do scrollIndexIntoView(index).
966   this.fileListSelection_.selectedIndex = index;
970  * Called when VolumeInfoList is updated.
972  * @param {Event} event Event of VolumeInfoList's 'splice'.
973  * @private
974  */
975 DirectoryModel.prototype.onVolumeInfoListUpdated_ = function(event) {
976   var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE);
977   if (driveVolume && !driveVolume.error) {
978     var currentDirEntry = this.getCurrentDirEntry();
979     if (currentDirEntry) {
980       if (currentDirEntry === DirectoryModel.fakeDriveEntry_) {
981         // Replace the fake entry by real DirectoryEntry silently.
982         this.volumeManager_.resolveAbsolutePath(
983             DirectoryModel.fakeDriveEntry_.fullPath,
984             function(entry) {
985               // If the current entry is still fake drive entry, replace it.
986               if (this.getCurrentDirEntry() === DirectoryModel.fakeDriveEntry_)
987                 this.changeDirectoryEntrySilent_(entry);
988             },
989             function(error) {});
990       } else if (PathUtil.isSpecialSearchRoot(currentDirEntry.fullPath)) {
991         for (var i = 0; i < event.added.length; i++) {
992           if (event.added[i].volumeType == util.VolumeType.DRIVE) {
993             // If the Drive volume is newly mounted, rescan it.
994             this.rescan();
995             break;
996           }
997         }
998       }
999     }
1000   }
1002   // When the volume where we are is unmounted, fallback to
1003   // DEFAULT_MOUNT_POINT. If current directory path is empty, stop the fallback
1004   // since the current directory is initializing now.
1005   // TODO(mtomasz): DEFAULT_MOUNT_POINT is deprecated. Use VolumeManager::
1006   // getDefaultVolume() after it is implemented.
1007   if (this.getCurrentDirPath() &&
1008       !this.volumeManager_.getVolumeInfo(this.getCurrentDirPath()))
1009     this.changeDirectory(PathUtil.DEFAULT_MOUNT_POINT);
1013  * Check if the root of the given path is mountable or not.
1015  * @param {string} path Path.
1016  * @return {boolean} Return true, if the given path is under mountable root.
1017  *     Otherwise, return false.
1018  */
1019 DirectoryModel.isMountableRoot = function(path) {
1020   var rootType = PathUtil.getRootType(path);
1021   switch (rootType) {
1022     case RootType.DOWNLOADS:
1023       return false;
1024     case RootType.ARCHIVE:
1025     case RootType.REMOVABLE:
1026     case RootType.DRIVE:
1027       return true;
1028     default:
1029       throw new Error('Unknown root type!');
1030   }
1034  * Performs search and displays results. The search type is dependent on the
1035  * current directory. If we are currently on drive, server side content search
1036  * over drive mount point. If the current directory is not on the drive, file
1037  * name search over current directory will be performed.
1039  * @param {string} query Query that will be searched for.
1040  * @param {function(Event)} onSearchRescan Function that will be called when the
1041  *     search directory is rescanned (i.e. search results are displayed).
1042  * @param {function()} onClearSearch Function to be called when search state
1043  *     gets cleared.
1044  * TODO(olege): Change callbacks to events.
1045  */
1046 DirectoryModel.prototype.search = function(query,
1047                                            onSearchRescan,
1048                                            onClearSearch) {
1049   query = query.trimLeft();
1051   this.clearSearch_();
1053   var currentDirEntry = this.getCurrentDirEntry();
1054   if (!currentDirEntry) {
1055     // Not yet initialized. Do nothing.
1056     return;
1057   }
1059   if (!query) {
1060     if (this.isSearching()) {
1061       var newDirContents = DirectoryContents.createForDirectory(
1062           this.currentFileListContext_,
1063           this.currentDirContents_.getLastNonSearchDirectoryEntry());
1064       this.clearAndScan_(newDirContents);
1065     }
1066     return;
1067   }
1069   this.onSearchCompleted_ = onSearchRescan;
1070   this.onClearSearch_ = onClearSearch;
1072   this.addEventListener('scan-completed', this.onSearchCompleted_);
1074   // If we are offline, let's fallback to file name search inside dir.
1075   // A search initiated from directories in Drive or special search results
1076   // should trigger Drive search.
1077   var newDirContents;
1078   if (!this.isDriveOffline() &&
1079       PathUtil.isDriveBasedPath(currentDirEntry.fullPath)) {
1080     // Drive search is performed over the whole drive, so pass  drive root as
1081     // |directoryEntry|.
1082     newDirContents = DirectoryContents.createForDriveSearch(
1083         this.currentFileListContext_,
1084         currentDirEntry,
1085         this.currentDirContents_.getLastNonSearchDirectoryEntry(),
1086         query);
1087   } else {
1088     newDirContents = DirectoryContents.createForLocalSearch(
1089         this.currentFileListContext_, currentDirEntry, query);
1090   }
1091   this.clearAndScan_(newDirContents);
1095  * Performs special search and displays results. e.g. Drive files available
1096  * offline, shared-with-me files, recently modified files.
1097  * @param {string} path Path string representing special search. See fake
1098  *     entries in PathUtil.RootDirectory.
1099  * @param {string=} opt_query Query string used for the search.
1100  */
1101 DirectoryModel.prototype.specialSearch = function(path, opt_query) {
1102   var query = opt_query || '';
1104   this.clearSearch_();
1106   this.onSearchCompleted_ = null;
1107   this.onClearSearch_ = null;
1109   var onDriveDirectoryResolved = function(driveRoot) {
1110     if (!driveRoot || driveRoot == DirectoryModel.fakeDriveEntry_) {
1111       // Drive root not available or not ready. onVolumeInfoListUpdated_()
1112       // handles the rescan if necessary.
1113       driveRoot = null;
1114     }
1116     var specialSearchType = PathUtil.getRootType(path);
1117     var searchOption;
1118     var dirEntry;
1119     if (specialSearchType == RootType.DRIVE_OFFLINE) {
1120       dirEntry = DirectoryModel.fakeDriveOfflineEntry_;
1121       searchOption =
1122           DriveMetadataSearchContentScanner.SearchType.SEARCH_OFFLINE;
1123     } else if (specialSearchType == RootType.DRIVE_SHARED_WITH_ME) {
1124       dirEntry = DirectoryModel.fakeDriveSharedWithMeEntry_;
1125       searchOption =
1126           DriveMetadataSearchContentScanner.SearchType.SEARCH_SHARED_WITH_ME;
1127     } else if (specialSearchType == RootType.DRIVE_RECENT) {
1128       dirEntry = DirectoryModel.fakeDriveRecentEntry_;
1129       searchOption =
1130           DriveMetadataSearchContentScanner.SearchType.SEARCH_RECENT_FILES;
1131     } else {
1132       // Unknown path.
1133       throw new Error('Unknown path for special search.');
1134     }
1136     var newDirContents = DirectoryContents.createForDriveMetadataSearch(
1137         this.currentFileListContext_,
1138         dirEntry, driveRoot, query, searchOption);
1139     var previous = this.currentDirContents_.getDirectoryEntry();
1140     this.clearAndScan_(newDirContents);
1142     var e = new Event('directory-changed');
1143     e.previousDirEntry = previous;
1144     e.newDirEntry = dirEntry;
1145     this.dispatchEvent(e);
1146   }.bind(this);
1148   this.resolveDirectory(DirectoryModel.fakeDriveEntry_.fullPath,
1149                         onDriveDirectoryResolved /* success */,
1150                         function() {} /* failed */);
1154  * In case the search was active, remove listeners and send notifications on
1155  * its canceling.
1156  * @private
1157  */
1158 DirectoryModel.prototype.clearSearch_ = function() {
1159   if (!this.isSearching())
1160     return;
1162   if (this.onSearchCompleted_) {
1163     this.removeEventListener('scan-completed', this.onSearchCompleted_);
1164     this.onSearchCompleted_ = null;
1165   }
1167   if (this.onClearSearch_) {
1168     this.onClearSearch_();
1169     this.onClearSearch_ = null;
1170   }