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.
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;
14 * Data model of the file manager.
16 * @param {boolean} singleSelection True if only one file could be selected
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.
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));
54 * Fake entry to be used in currentDirEntry_ when current directory is
55 * unmounted DRIVE. TODO(haruki): Support "drive/root" and "drive/other".
60 DirectoryModel.fakeDriveEntry_ = {
61 fullPath: RootDirectory.DRIVE + '/' + DriveSubRootDirectory.ROOT,
63 rootType: RootType.DRIVE
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
74 DirectoryModel.fakeDriveOfflineEntry_ = {
75 fullPath: RootDirectory.DRIVE_OFFLINE,
77 rootType: RootType.DRIVE_OFFLINE
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.
88 DirectoryModel.fakeDriveSharedWithMeEntry_ = {
89 fullPath: RootDirectory.DRIVE_SHARED_WITH_ME,
91 rootType: RootType.DRIVE_SHARED_WITH_ME
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()).
104 DirectoryModel.fakeDriveRecentEntry_ = {
105 fullPath: RootDirectory.DRIVE_RECENT,
107 rootType: RootType.DRIVE_RECENT
111 * List of fake entries for special searches.
113 * @type {Array.<Object>}
116 DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES = [
117 DirectoryModel.fakeDriveSharedWithMeEntry_,
118 DirectoryModel.fakeDriveRecentEntry_,
119 DirectoryModel.fakeDriveOfflineEntry_
123 * DirectoryModel extends cr.EventTarget.
125 DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype;
128 * Disposes the directory model by removing file watchers.
130 DirectoryModel.prototype.dispose = function() {
131 this.fileWatcher_.dispose();
135 * @return {cr.ui.ArrayDataModel} Files in the current directory.
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".
146 DirectoryModel.prototype.sortFileList = function(sortField, sortDirection) {
147 this.getFileList().sort(sortField, sortDirection);
151 * @return {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} Selection
154 DirectoryModel.prototype.getFileListSelection = function() {
155 return this.fileListSelection_;
159 * @return {RootType} Root type of current root.
161 DirectoryModel.prototype.getCurrentRootType = function() {
162 var entry = this.currentDirContents_.getDirectoryEntry();
163 return PathUtil.getRootType(entry ? entry.fullPath : '');
167 * @return {string} Root path.
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
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.
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
199 * @return {boolean} True if the root for the current directory is read only.
201 DirectoryModel.prototype.isReadOnly = function() {
202 return this.isPathReadOnly(this.getCurrentRootPath());
206 * @return {boolean} True if the a scan is active.
208 DirectoryModel.prototype.isScanning = function() {
209 return this.currentDirContents_.isScanning();
213 * @return {boolean} True if search is in progress.
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.
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:
235 case RootType.DOWNLOADS:
238 // TODO(haruki): Maybe add DRIVE_OFFLINE as well to allow renaming in the
240 return this.isDriveOffline();
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.
256 DirectoryModel.prototype.updateSelectionAndPublishEvent_ =
257 function(selection, updateFunc) {
259 selection.beginChange();
261 // If dispatchNeeded is true, we should ensure the change event is
263 var dispatchNeeded = updateFunc();
265 // Check if the change event is dispatched in the endChange function
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.
278 selection.dispatchEvent(event);
283 * Invoked when a change in the directory is detected by the watcher.
286 DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() {
291 * Invoked when filters are changed.
294 DirectoryModel.prototype.onFilterChanged_ = function() {
299 * Returns the filter.
300 * @return {FileFilter} The file filter.
302 DirectoryModel.prototype.getFileFilter = function() {
303 return this.fileFilter_;
307 * @return {DirectoryEntry} Current directory.
309 DirectoryModel.prototype.getCurrentDirEntry = function() {
310 return this.currentDirContents_.getDirectoryEntry();
314 * @return {string} URL of the current directory. or null if unavailable.
316 DirectoryModel.prototype.getCurrentDirectoryURL = function() {
317 var entry = this.currentDirContents_.getDirectoryEntry();
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.
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.
338 DirectoryModel.prototype.getSelectedPaths_ = function() {
339 var indexes = this.fileListSelection_.selectedIndexes;
340 var fileList = this.getFileList();
342 return indexes.map(function(i) {
343 return fileList.item(i).fullPath;
350 * @param {Array.<string>} value List of file paths of selected files.
353 DirectoryModel.prototype.setSelectedPaths_ = function(value) {
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.
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)))
373 this.fileListSelection_.selectedIndexes = indexes;
377 * @return {string} Lead item file path.
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.
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;
400 * Schedule rescan with short delay.
402 DirectoryModel.prototype.rescanSoon = function() {
403 this.scheduleRescan(SHORT_RESCAN_INTERVAL);
407 * Schedule rescan with delay. Designed to handle directory change
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
418 * @param {number} delay Delay in ms after which the rescan will be performed.
420 DirectoryModel.prototype.scheduleRescan = function(delay) {
421 if (this.rescanTime_) {
422 if (this.rescanTime_ <= Date.now() + delay)
424 clearTimeout(this.rescanTimeoutId_);
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.
435 DirectoryModel.prototype.clearRescanTimeout_ = function() {
436 this.rescanTime_ = null;
437 if (this.rescanTimeoutId_) {
438 clearTimeout(this.rescanTimeoutId_);
439 this.rescanTimeoutId_ = null;
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).
451 DirectoryModel.prototype.rescan = function() {
452 this.clearRescanTimeout_();
453 if (this.runningScan_) {
454 this.pendingRescan_ = true;
458 var dirContents = this.currentDirContents_.clone();
459 dirContents.setFileList([]);
461 var successCallback = (function() {
462 this.replaceDirectoryContents_(dirContents);
463 cr.dispatchSimpleEvent(this, 'rescan-completed');
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.
481 DirectoryModel.prototype.clearAndScan_ = function(newDirContents,
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;
497 var onDone = function() {
498 cr.dispatchSimpleEvent(this, 'scan-completed');
503 var onFailed = function() {
504 cr.dispatchSimpleEvent(this, 'scan-failed');
507 var onUpdated = function() {
508 cr.dispatchSimpleEvent(this, 'scan-updated');
511 var onCancelled = function() {
512 cr.dispatchSimpleEvent(this, 'scan-cancelled');
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
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.
536 DirectoryModel.prototype.scan_ = function(
538 successCallback, failureCallback, updatedCallback, cancelledCallback) {
542 * Runs pending scan if there is one.
544 * @return {boolean} Did pending scan exist.
546 var maybeRunPendingRescan = function() {
547 if (this.pendingRescan_) {
549 this.pendingRescan_ = false;
555 var onSuccess = function() {
556 // Record metric for Downloads directory.
557 if (!dirContents.isSearch()) {
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);
567 this.runningScan_ = null;
569 this.scanFailures_ = 0;
570 maybeRunPendingRescan();
573 var onFailure = function() {
574 this.runningScan_ = null;
575 this.scanFailures_++;
578 if (maybeRunPendingRescan())
581 if (this.scanFailures_ <= 1)
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);
595 * @param {DirectoryContents} dirContents DirectoryContents instance.
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
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;
625 return forceChangeEvent;
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.
636 DirectoryModel.prototype.onEntryChanged = function(kind, entry) {
637 // TODO(hidehiko): We should update directory model even the search result
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) &&
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.
653 this.currentDirContents_.prefetchMetadata([entry], function() {
654 if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) {
655 // Do nothing if current directory changed during async operations.
659 var index = this.findIndexByEntry_(entry);
661 this.getFileList().splice(index, 1, entry);
663 this.getFileList().push(entry);
667 // This is the delete event.
668 var index = this.findIndexByEntry_(entry);
670 this.getFileList().splice(index, 1);
675 * @param {Entry} entry The entry to be searched.
676 * @return {number} The index in the fileList, or -1 if not found.
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))
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
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.
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
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);
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);
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);
727 // Run callback, finally.
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.
740 DirectoryModel.prototype.createDirectory = function(name, successCallback,
742 var entry = this.getCurrentDirEntry();
744 errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR));
748 var tracker = this.createDirectoryChangeTracker();
751 var onSuccess = function(newEntry) {
752 // Do not change anything or call the callback if current
753 // directory changed.
755 if (tracker.hasChanged)
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]);
765 this.fileListSelection_.beginChange();
766 this.getFileList().splice(0, 0, newEntry);
767 this.selectEntry(newEntry);
768 this.fileListSelection_.endChange();
769 successCallback(newEntry);
773 this.currentDirContents_.createDirectory(name, onSuccess.bind(this),
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
784 DirectoryModel.prototype.changeDirectory = function(path, opt_errorCallback) {
785 if (PathUtil.isSpecialSearchRoot(path)) {
786 this.specialSearch(path, '');
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);
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.
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));
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.
822 // TODO(mtomasz, hashimoto): Consider rewriting this logic.
824 if (PathUtil.getRootType(path) == RootType.DRIVE &&
825 error.code == FileError.INVALID_STATE_ERR) {
826 successCallback(DirectoryModel.fakeDriveEntry_);
829 errorCallback(error);
832 // TODO(mtomasz): Use Entry instead of a path.
833 this.volumeManager_.resolveAbsolutePath(
837 onError(util.createFileError(FileError.TYPE_MISMATCH_ERR));
840 successCallback(entry);
846 * @param {DirectoryEntry} dirEntry The absolute path to the new directory.
847 * @param {function()=} opt_callback Executed if the directory loads
851 DirectoryModel.prototype.changeDirectoryEntrySilent_ = function(dirEntry,
853 var onScanComplete = function() {
856 // For tests that open the dialog to empty directories, everything
857 // is loaded at this point.
858 chrome.test.sendMessage('directory-change-complete');
861 DirectoryContents.createForDirectory(this.currentFileListContext_,
863 onScanComplete.bind(this));
867 * Change the current directory to the directory represented by a
870 * Dispatches the 'directory-changed' event when the directory is successfully
873 * @param {DirectoryEntry} dirEntry The absolute path to the new directory.
874 * @param {function()=} opt_callback Executed if the directory loads
877 DirectoryModel.prototype.changeDirectoryEntry = function(
878 dirEntry, opt_callback) {
879 this.fileWatcher_.changeWatchedDirectory(dirEntry, function() {
880 var previous = this.currentDirContents_.getDirectoryEntry();
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);
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.
897 DirectoryModel.prototype.createDirectoryChangeTracker = function() {
905 this.dm_.addEventListener('directory-changed',
906 this.onDirectoryChange_);
908 this.hasChanged = false;
914 this.dm_.removeEventListener('directory-changed',
915 this.onDirectoryChange_);
916 this.active_ = false;
920 onDirectoryChange_: function(event) {
922 tracker.hasChanged = true;
929 * @param {Entry} entry Entry to be selected.
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()) {
942 * @param {Array.<string>} entries Array of entries.
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);
954 this.fileListSelection_.endChange();
958 * @param {number} index Index of file.
960 DirectoryModel.prototype.selectIndex = function(index) {
961 // this.focusCurrentList_();
962 if (index >= this.getFileList().length)
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'.
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,
985 // If the current entry is still fake drive entry, replace it.
986 if (this.getCurrentDirEntry() === DirectoryModel.fakeDriveEntry_)
987 this.changeDirectoryEntrySilent_(entry);
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.
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.
1019 DirectoryModel.isMountableRoot = function(path) {
1020 var rootType = PathUtil.getRootType(path);
1022 case RootType.DOWNLOADS:
1024 case RootType.ARCHIVE:
1025 case RootType.REMOVABLE:
1026 case RootType.DRIVE:
1029 throw new Error('Unknown root type!');
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
1044 * TODO(olege): Change callbacks to events.
1046 DirectoryModel.prototype.search = function(query,
1049 query = query.trimLeft();
1051 this.clearSearch_();
1053 var currentDirEntry = this.getCurrentDirEntry();
1054 if (!currentDirEntry) {
1055 // Not yet initialized. Do nothing.
1060 if (this.isSearching()) {
1061 var newDirContents = DirectoryContents.createForDirectory(
1062 this.currentFileListContext_,
1063 this.currentDirContents_.getLastNonSearchDirectoryEntry());
1064 this.clearAndScan_(newDirContents);
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.
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_,
1085 this.currentDirContents_.getLastNonSearchDirectoryEntry(),
1088 newDirContents = DirectoryContents.createForLocalSearch(
1089 this.currentFileListContext_, currentDirEntry, query);
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.
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.
1116 var specialSearchType = PathUtil.getRootType(path);
1119 if (specialSearchType == RootType.DRIVE_OFFLINE) {
1120 dirEntry = DirectoryModel.fakeDriveOfflineEntry_;
1122 DriveMetadataSearchContentScanner.SearchType.SEARCH_OFFLINE;
1123 } else if (specialSearchType == RootType.DRIVE_SHARED_WITH_ME) {
1124 dirEntry = DirectoryModel.fakeDriveSharedWithMeEntry_;
1126 DriveMetadataSearchContentScanner.SearchType.SEARCH_SHARED_WITH_ME;
1127 } else if (specialSearchType == RootType.DRIVE_RECENT) {
1128 dirEntry = DirectoryModel.fakeDriveRecentEntry_;
1130 DriveMetadataSearchContentScanner.SearchType.SEARCH_RECENT_FILES;
1133 throw new Error('Unknown path for special search.');
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);
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
1158 DirectoryModel.prototype.clearSearch_ = function() {
1159 if (!this.isSearching())
1162 if (this.onSearchCompleted_) {
1163 this.removeEventListener('scan-completed', this.onSearchCompleted_);
1164 this.onSearchCompleted_ = null;
1167 if (this.onClearSearch_) {
1168 this.onClearSearch_();
1169 this.onClearSearch_ = null;