| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // If directory files changes too often, don't rescan directory more than once |
| // per specified interval |
| var SIMULTANEOUS_RESCAN_INTERVAL = 500; |
| // Used for operations that require almost instant rescan. |
| var SHORT_RESCAN_INTERVAL = 100; |
| |
| /** |
| * Data model of the file manager. |
| * |
| * @constructor |
| * @extends {cr.EventTarget} |
| * |
| * @param {boolean} singleSelection True if only one file could be selected |
| * at the time. |
| * @param {FileFilter} fileFilter Instance of FileFilter. |
| * @param {!MetadataModel} metadataModel Metadata model. |
| * service. |
| * @param {!VolumeManager} volumeManager The volume manager. |
| * @param {!FileOperationManager} fileOperationManager File operation manager. |
| */ |
| function DirectoryModel( |
| singleSelection, fileFilter, metadataModel, volumeManager, |
| fileOperationManager) { |
| this.fileListSelection_ = singleSelection ? |
| new FileListSingleSelectionModel() : new FileListSelectionModel(); |
| |
| this.runningScan_ = null; |
| this.pendingScan_ = null; |
| this.rescanTime_ = null; |
| this.scanFailures_ = 0; |
| this.changeDirectorySequence_ = 0; |
| |
| /** |
| * @private {boolean} |
| */ |
| this.ignoreCurrentDirectoryDeletion_ = false; |
| |
| this.directoryChangeQueue_ = new AsyncUtil.Queue(); |
| this.rescanAggregator_ = new AsyncUtil.Aggregator( |
| this.rescanSoon.bind(this, true), 500); |
| |
| this.fileFilter_ = fileFilter; |
| this.fileFilter_.addEventListener('changed', |
| this.onFilterChanged_.bind(this)); |
| |
| this.currentFileListContext_ = |
| new FileListContext(fileFilter, metadataModel); |
| this.currentDirContents_ = |
| DirectoryContents.createForDirectory(this.currentFileListContext_, null); |
| /** |
| * Empty file list which is used as a dummy for inactive view of file list. |
| * @private {!FileListModel} |
| */ |
| this.emptyFileList_ = new FileListModel(metadataModel); |
| |
| this.metadataModel_ = metadataModel; |
| |
| this.volumeManager_ = volumeManager; |
| this.volumeManager_.volumeInfoList.addEventListener( |
| 'splice', this.onVolumeInfoListUpdated_.bind(this)); |
| |
| /** |
| * File watcher. |
| * @private {!FileWatcher} |
| * @const |
| */ |
| this.fileWatcher_ = new FileWatcher(); |
| this.fileWatcher_.addEventListener( |
| 'watcher-directory-changed', |
| this.onWatcherDirectoryChanged_.bind(this)); |
| util.addEventListenerToBackgroundComponent( |
| fileOperationManager, |
| 'entries-changed', |
| this.onEntriesChanged_.bind(this)); |
| |
| /** @private {string} */ |
| this.lastSearchQuery_ = ''; |
| |
| /** @private {FilesAppDirEntry} */ |
| this.myFilesEntry_ = null; |
| } |
| |
| /** |
| * DirectoryModel extends cr.EventTarget. |
| */ |
| DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype; |
| |
| /** |
| * Disposes the directory model by removing file watchers. |
| */ |
| DirectoryModel.prototype.dispose = function() { |
| this.fileWatcher_.dispose(); |
| }; |
| |
| /** |
| * @return {FileListModel} Files in the current directory. |
| */ |
| DirectoryModel.prototype.getFileList = function() { |
| return this.currentFileListContext_.fileList; |
| }; |
| |
| /** |
| * @return {!FileListModel} File list which is always empty. |
| */ |
| DirectoryModel.prototype.getEmptyFileList = function() { |
| return this.emptyFileList_; |
| }; |
| |
| /** |
| * @return {!FileListSelectionModel|!FileListSingleSelectionModel} Selection |
| * in the fileList. |
| */ |
| DirectoryModel.prototype.getFileListSelection = function() { |
| return this.fileListSelection_; |
| }; |
| |
| /** |
| * Obtains current volume information. |
| * @return {VolumeInfo} |
| */ |
| DirectoryModel.prototype.getCurrentVolumeInfo = function() { |
| var entry = this.getCurrentDirEntry(); |
| if (!entry) |
| return null; |
| return this.volumeManager_.getVolumeInfo(entry); |
| }; |
| |
| /** |
| * @return {?VolumeManagerCommon.RootType} Root type of current root, or null if |
| * not found. |
| */ |
| DirectoryModel.prototype.getCurrentRootType = function() { |
| var entry = this.currentDirContents_.getDirectoryEntry(); |
| if (!entry) |
| return null; |
| |
| var locationInfo = this.volumeManager_.getLocationInfo(entry); |
| if (!locationInfo) |
| return null; |
| |
| return locationInfo.rootType; |
| }; |
| |
| /** |
| * Metadata property names that are expected to be Prefetched. |
| * @return {!Array<string>} |
| */ |
| DirectoryModel.prototype.getPrefetchPropertyNames = function() { |
| return this.currentFileListContext_.prefetchPropertyNames; |
| }; |
| |
| /** |
| * @return {boolean} True if the current directory is read only. If there is |
| * no entry set, then returns true. |
| */ |
| DirectoryModel.prototype.isReadOnly = function() { |
| var currentDirEntry = this.getCurrentDirEntry(); |
| if (currentDirEntry) { |
| var locationInfo = this.volumeManager_.getLocationInfo(currentDirEntry); |
| if (locationInfo) |
| return locationInfo.isReadOnly; |
| } |
| return true; |
| }; |
| |
| /** |
| * @return {boolean} True if the a scan is active. |
| */ |
| DirectoryModel.prototype.isScanning = function() { |
| return this.currentDirContents_.isScanning(); |
| }; |
| |
| /** |
| * @return {boolean} True if search is in progress. |
| */ |
| DirectoryModel.prototype.isSearching = function() { |
| return this.currentDirContents_.isSearch(); |
| }; |
| |
| /** |
| * @return {boolean} True if it's on Drive. |
| */ |
| DirectoryModel.prototype.isOnDrive = function() { |
| return this.isCurrentRootVolumeType_(VolumeManagerCommon.VolumeType.DRIVE); |
| }; |
| |
| /** |
| * @return {boolean} True if it's on MTP volume. |
| */ |
| DirectoryModel.prototype.isOnMTP = function() { |
| return this.isCurrentRootVolumeType_(VolumeManagerCommon.VolumeType.MTP); |
| }; |
| |
| /** |
| * @param {VolumeManagerCommon.VolumeType} volumeType Volume Type |
| * @return {boolean} True if current root volume type is equal to specified |
| * volume type. |
| * @private |
| */ |
| DirectoryModel.prototype.isCurrentRootVolumeType_ = function(volumeType) { |
| var rootType = this.getCurrentRootType(); |
| return rootType != null && rootType != VolumeManagerCommon.RootType.RECENT && |
| VolumeManagerCommon.getVolumeTypeFromRootType(rootType) === volumeType; |
| }; |
| |
| /** |
| * Updates the selection by using the updateFunc and publish the change event. |
| * If updateFunc returns true, it force to dispatch the change event even if the |
| * selection index is not changed. |
| * |
| * @param {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} selection |
| * Selection to be updated. |
| * @param {function(): boolean} updateFunc Function updating the selection. |
| * @private |
| */ |
| DirectoryModel.prototype.updateSelectionAndPublishEvent_ = |
| function(selection, updateFunc) { |
| // Begin change. |
| selection.beginChange(); |
| |
| // If dispatchNeeded is true, we should ensure the change event is |
| // dispatched. |
| var dispatchNeeded = updateFunc(); |
| |
| // Check if the change event is dispatched in the endChange function |
| // or not. |
| var eventDispatched = function() { dispatchNeeded = false; }; |
| selection.addEventListener('change', eventDispatched); |
| selection.endChange(); |
| selection.removeEventListener('change', eventDispatched); |
| |
| // If the change event have been already dispatched, dispatchNeeded is false. |
| if (dispatchNeeded) { |
| var event = new Event('change'); |
| // The selection status (selected or not) is not changed because |
| // this event is caused by the change of selected item. |
| event.changes = []; |
| selection.dispatchEvent(event); |
| } |
| }; |
| |
| /** |
| * Sets to ignore current directory deletion. This method is used to prevent |
| * going up to the volume root with the deletion of current directory by rename |
| * operation in directory tree. |
| * @param {boolean} value True to ignore current directory deletion. |
| */ |
| DirectoryModel.prototype.setIgnoringCurrentDirectoryDeletion = function(value) { |
| this.ignoreCurrentDirectoryDeletion_ = value; |
| }; |
| |
| /** |
| * Invoked when a change in the directory is detected by the watcher. |
| * @param {Event} event Event object. |
| * @private |
| */ |
| DirectoryModel.prototype.onWatcherDirectoryChanged_ = function(event) { |
| var directoryEntry = this.getCurrentDirEntry(); |
| |
| if (!this.ignoreCurrentDirectoryDeletion_) { |
| // If the change is deletion of currentDir, move up to its parent directory. |
| directoryEntry.getDirectory( |
| directoryEntry.fullPath, {create: false}, function() {}, |
| function() { |
| var volumeInfo = |
| this.volumeManager_.getVolumeInfo(assert(directoryEntry)); |
| if (volumeInfo) { |
| volumeInfo.resolveDisplayRoot().then(function(displayRoot) { |
| this.changeDirectoryEntry(displayRoot); |
| }.bind(this)); |
| } |
| }.bind(this)); |
| } |
| |
| if (event.changedFiles) { |
| var addedOrUpdatedFileUrls = []; |
| var deletedFileUrls = []; |
| event.changedFiles.forEach(function(change) { |
| if (change.changes.length === 1 && change.changes[0] === 'delete') |
| deletedFileUrls.push(change.url); |
| else |
| addedOrUpdatedFileUrls.push(change.url); |
| }); |
| |
| util.URLsToEntries(addedOrUpdatedFileUrls).then(function(result) { |
| deletedFileUrls = deletedFileUrls.concat(result.failureUrls); |
| |
| // Passing the resolved entries and failed URLs as the removed files. |
| // The URLs are removed files and they chan't be resolved. |
| this.partialUpdate_(result.entries, deletedFileUrls); |
| }.bind(this)).catch(function(error) { |
| console.error('Error in proceeding the changed event.', error, |
| 'Fallback to force-refresh'); |
| this.rescanAggregator_.run(); |
| }.bind(this)); |
| } else { |
| // Invokes force refresh if the detailed information isn't provided. |
| // This can occur very frequently (e.g. when copying files into Downlaods) |
| // and rescan is heavy operation, so we keep some interval for each rescan. |
| this.rescanAggregator_.run(); |
| } |
| }; |
| |
| /** |
| * Invoked when filters are changed. |
| * @private |
| */ |
| DirectoryModel.prototype.onFilterChanged_ = function() { |
| const currentDirectory = this.getCurrentDirEntry(); |
| if (currentDirectory && util.isNativeEntry(currentDirectory) && |
| !this.fileFilter_.filter( |
| /** @type {!DirectoryEntry} */ (currentDirectory))) { |
| // If the current directory should be hidden in the new filter setting, |
| // change the current directory to the current volume's root. |
| const volumeInfo = this.volumeManager_.getVolumeInfo(currentDirectory); |
| if (volumeInfo) { |
| volumeInfo.resolveDisplayRoot().then(displayRoot => { |
| this.changeDirectoryEntry(displayRoot); |
| }); |
| } |
| } else { |
| this.rescanSoon(false); |
| } |
| }; |
| |
| /** |
| * Returns the filter. |
| * @return {FileFilter} The file filter. |
| */ |
| DirectoryModel.prototype.getFileFilter = function() { |
| return this.fileFilter_; |
| }; |
| |
| /** |
| * @return {DirectoryEntry|FakeEntry|FilesAppDirEntry} Current directory. |
| */ |
| DirectoryModel.prototype.getCurrentDirEntry = function() { |
| return this.currentDirContents_.getDirectoryEntry(); |
| }; |
| |
| /** |
| * @return {Array<Entry>} Array of selected entries. |
| * @private |
| */ |
| DirectoryModel.prototype.getSelectedEntries_ = function() { |
| var indexes = this.fileListSelection_.selectedIndexes; |
| var fileList = this.getFileList(); |
| if (fileList) { |
| return indexes.map(function(i) { |
| return fileList.item(i); |
| }); |
| } |
| return []; |
| }; |
| |
| /** |
| * @param {Array<Entry>} value List of selected entries. |
| * @private |
| */ |
| DirectoryModel.prototype.setSelectedEntries_ = function(value) { |
| var indexes = []; |
| var fileList = this.getFileList(); |
| var urls = util.entriesToURLs(value); |
| |
| for (var i = 0; i < fileList.length; i++) { |
| if (urls.indexOf(fileList.item(i).toURL()) !== -1) |
| indexes.push(i); |
| } |
| this.fileListSelection_.selectedIndexes = indexes; |
| }; |
| |
| /** |
| * @return {Entry} Lead entry. |
| * @private |
| */ |
| DirectoryModel.prototype.getLeadEntry_ = function() { |
| var index = this.fileListSelection_.leadIndex; |
| return index >= 0 ? |
| /** @type {Entry} */ (this.getFileList().item(index)) : null; |
| }; |
| |
| /** |
| * @param {Entry} value The new lead entry. |
| * @private |
| */ |
| DirectoryModel.prototype.setLeadEntry_ = function(value) { |
| var fileList = this.getFileList(); |
| for (var i = 0; i < fileList.length; i++) { |
| if (util.isSameEntry(/** @type {Entry} */ (fileList.item(i)), value)) { |
| this.fileListSelection_.leadIndex = i; |
| return; |
| } |
| } |
| }; |
| |
| /** |
| * Schedule rescan with short delay. |
| * @param {boolean} refresh True to refresh metadata, or false to use cached |
| * one. |
| */ |
| DirectoryModel.prototype.rescanSoon = function(refresh) { |
| this.scheduleRescan(SHORT_RESCAN_INTERVAL, refresh); |
| }; |
| |
| /** |
| * Schedule rescan with delay. Designed to handle directory change |
| * notification. |
| * @param {boolean} refresh True to refresh metadata, or false to use cached |
| * one. |
| */ |
| DirectoryModel.prototype.rescanLater = function(refresh) { |
| this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL, refresh); |
| }; |
| |
| /** |
| * Schedule rescan with delay. If another rescan has been scheduled does |
| * nothing. File operation may cause a few notifications what should cause |
| * a single refresh. |
| * @param {number} delay Delay in ms after which the rescan will be performed. |
| * @param {boolean} refresh True to refresh metadata, or false to use cached |
| * one. |
| */ |
| DirectoryModel.prototype.scheduleRescan = function(delay, refresh) { |
| if (this.rescanTime_) { |
| if (this.rescanTime_ <= Date.now() + delay) |
| return; |
| clearTimeout(this.rescanTimeoutId_); |
| } |
| |
| var sequence = this.changeDirectorySequence_; |
| |
| this.rescanTime_ = Date.now() + delay; |
| this.rescanTimeoutId_ = setTimeout(function() { |
| this.rescanTimeoutId_ = null; |
| if (sequence === this.changeDirectorySequence_) |
| this.rescan(refresh); |
| }.bind(this), delay); |
| }; |
| |
| /** |
| * Cancel a rescan on timeout if it is scheduled. |
| * @private |
| */ |
| DirectoryModel.prototype.clearRescanTimeout_ = function() { |
| this.rescanTime_ = null; |
| if (this.rescanTimeoutId_) { |
| clearTimeout(this.rescanTimeoutId_); |
| this.rescanTimeoutId_ = null; |
| } |
| }; |
| |
| /** |
| * Rescan current directory. May be called indirectly through rescanLater or |
| * directly in order to reflect user action. Will first cache all the directory |
| * contents in an array, then seamlessly substitute the fileList contents, |
| * preserving the select element etc. |
| * |
| * This should be to scan the contents of current directory (or search). |
| * |
| * @param {boolean} refresh True to refresh metadata, or false to use cached |
| * one. |
| */ |
| DirectoryModel.prototype.rescan = function(refresh) { |
| this.clearRescanTimeout_(); |
| if (this.runningScan_) { |
| this.pendingRescan_ = true; |
| return; |
| } |
| |
| var dirContents = this.currentDirContents_.clone(); |
| dirContents.setFileList([]); |
| dirContents.setMetadataSnapshot( |
| this.currentDirContents_.createMetadataSnapshot()); |
| |
| var sequence = this.changeDirectorySequence_; |
| |
| var successCallback = (function() { |
| if (sequence === this.changeDirectorySequence_) { |
| this.replaceDirectoryContents_(dirContents); |
| cr.dispatchSimpleEvent(this, 'rescan-completed'); |
| } |
| }).bind(this); |
| |
| this.scan_(dirContents, |
| refresh, |
| successCallback, function() {}, function() {}, function() {}); |
| }; |
| |
| /** |
| * Run scan on the current DirectoryContents. The active fileList is cleared and |
| * the entries are added directly. |
| * |
| * This should be used when changing directory or initiating a new search. |
| * |
| * @param {DirectoryContents} newDirContents New DirectoryContents instance to |
| * replace currentDirContents_. |
| * @param {function(boolean)} callback Callback with result. True if the scan |
| * is completed successfully, false if the scan is failed. |
| * @private |
| */ |
| DirectoryModel.prototype.clearAndScan_ = function(newDirContents, |
| callback) { |
| if (this.currentDirContents_.isScanning()) |
| this.currentDirContents_.cancelScan(); |
| this.currentDirContents_ = newDirContents; |
| this.clearRescanTimeout_(); |
| |
| if (this.pendingScan_) |
| this.pendingScan_ = false; |
| |
| if (this.runningScan_) { |
| if (this.runningScan_.isScanning()) |
| this.runningScan_.cancelScan(); |
| this.runningScan_ = null; |
| } |
| |
| var sequence = this.changeDirectorySequence_; |
| var cancelled = false; |
| |
| var onDone = function() { |
| if (cancelled) |
| return; |
| |
| cr.dispatchSimpleEvent(this, 'scan-completed'); |
| callback(true); |
| }.bind(this); |
| |
| /** @param {DOMError} error error. */ |
| var onFailed = function(error) { |
| if (cancelled) |
| return; |
| |
| var event = new Event('scan-failed'); |
| event.error = error; |
| this.dispatchEvent(event); |
| callback(false); |
| }.bind(this); |
| |
| var onUpdated = function() { |
| if (cancelled) |
| return; |
| |
| if (this.changeDirectorySequence_ !== sequence) { |
| cancelled = true; |
| cr.dispatchSimpleEvent(this, 'scan-cancelled'); |
| callback(false); |
| return; |
| } |
| |
| cr.dispatchSimpleEvent(this, 'scan-updated'); |
| }.bind(this); |
| |
| var onCancelled = function() { |
| if (cancelled) |
| return; |
| |
| cancelled = true; |
| cr.dispatchSimpleEvent(this, 'scan-cancelled'); |
| callback(false); |
| }.bind(this); |
| |
| // Clear metadata information for the old (no longer visible) items in the |
| // file list. |
| var fileList = this.getFileList(); |
| let removedUrls = []; |
| for (var i = 0; i < fileList.length; i++) { |
| removedUrls.push(fileList.item(i).toURL()); |
| } |
| this.metadataModel_.notifyEntriesRemoved(removedUrls); |
| |
| // Retrieve metadata information for the newly selected directory. |
| const currentEntry = this.currentDirContents_.getDirectoryEntry(); |
| if (currentEntry && !util.isFakeEntry(assert(currentEntry))) { |
| this.metadataModel_.get( |
| [currentEntry], |
| constants.LIST_CONTAINER_METADATA_PREFETCH_PROPERTY_NAMES); |
| } |
| |
| // Clear the table, and start scanning. |
| cr.dispatchSimpleEvent(this, 'scan-started'); |
| fileList.splice(0, fileList.length); |
| this.scan_(this.currentDirContents_, false, |
| onDone, onFailed, onUpdated, onCancelled); |
| }; |
| |
| /** |
| * Adds/removes/updates items of file list. |
| * @param {Array<Entry>} changedEntries Entries of updated/added files. |
| * @param {Array<string>} removedUrls URLs of removed files. |
| * @private |
| */ |
| DirectoryModel.prototype.partialUpdate_ = |
| function(changedEntries, removedUrls) { |
| // This update should be included in the current running update. |
| if (this.pendingScan_) |
| return; |
| |
| if (this.runningScan_) { |
| // Do update after the current scan is finished. |
| var previousScan = this.runningScan_; |
| var onPreviousScanCompleted = function() { |
| previousScan.removeEventListener('scan-completed', |
| onPreviousScanCompleted); |
| // Run the update asynchronously. |
| Promise.resolve().then(function() { |
| this.partialUpdate_(changedEntries, removedUrls); |
| }.bind(this)); |
| }.bind(this); |
| previousScan.addEventListener('scan-completed', onPreviousScanCompleted); |
| return; |
| } |
| |
| var onFinish = function() { |
| this.runningScan_ = null; |
| |
| this.currentDirContents_.removeEventListener( |
| 'scan-completed', onCompleted); |
| this.currentDirContents_.removeEventListener('scan-failed', onFailure); |
| this.currentDirContents_.removeEventListener( |
| 'scan-cancelled', onCancelled); |
| }.bind(this); |
| |
| var onCompleted = function() { |
| onFinish(); |
| cr.dispatchSimpleEvent(this, 'rescan-completed'); |
| }.bind(this); |
| |
| var onFailure = function() { |
| onFinish(); |
| }; |
| |
| var onCancelled = function() { |
| onFinish(); |
| }; |
| |
| this.runningScan_ = this.currentDirContents_; |
| this.currentDirContents_.addEventListener('scan-completed', onCompleted); |
| this.currentDirContents_.addEventListener('scan-failed', onFailure); |
| this.currentDirContents_.addEventListener('scan-cancelled', onCancelled); |
| this.currentDirContents_.update(changedEntries, removedUrls); |
| }; |
| |
| /** |
| * Perform a directory contents scan. Should be called only from rescan() and |
| * clearAndScan_(). |
| * |
| * @param {DirectoryContents} dirContents DirectoryContents instance on which |
| * the scan will be run. |
| * @param {boolean} refresh True to refresh metadata, or false to use cached |
| * one. |
| * @param {function()} successCallback Callback on success. |
| * @param {function(DOMError)} failureCallback Callback on failure. |
| * @param {function()} updatedCallback Callback on update. Only on the last |
| * update, {@code successCallback} is called instead of this. |
| * @param {function()} cancelledCallback Callback on cancel. |
| * @private |
| */ |
| DirectoryModel.prototype.scan_ = function( |
| dirContents, |
| refresh, |
| successCallback, failureCallback, updatedCallback, cancelledCallback) { |
| var self = this; |
| |
| /** |
| * Runs pending scan if there is one. |
| * |
| * @return {boolean} Did pending scan exist. |
| */ |
| var maybeRunPendingRescan = function() { |
| if (this.pendingRescan_) { |
| this.rescanSoon(refresh); |
| this.pendingRescan_ = false; |
| return true; |
| } |
| return false; |
| }.bind(this); |
| |
| var onFinished = function() { |
| dirContents.removeEventListener('scan-completed', onSuccess); |
| dirContents.removeEventListener('scan-updated', updatedCallback); |
| dirContents.removeEventListener('scan-failed', onFailure); |
| dirContents.removeEventListener('scan-cancelled', cancelledCallback); |
| }; |
| |
| var onSuccess = function() { |
| onFinished(); |
| |
| // Record metric for Downloads directory. |
| if (!dirContents.isSearch()) { |
| var locationInfo = |
| this.volumeManager_.getLocationInfo( |
| assert(dirContents.getDirectoryEntry())); |
| var volumeInfo = locationInfo && locationInfo.volumeInfo; |
| if (volumeInfo && |
| volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DOWNLOADS && |
| locationInfo.isRootEntry) { |
| metrics.recordMediumCount('DownloadsCount', |
| dirContents.fileList_.length); |
| } |
| } |
| |
| this.runningScan_ = null; |
| successCallback(); |
| this.scanFailures_ = 0; |
| maybeRunPendingRescan(); |
| }.bind(this); |
| |
| var onFailure = function(event) { |
| onFinished(); |
| |
| this.runningScan_ = null; |
| this.scanFailures_++; |
| failureCallback(event.error); |
| |
| if (maybeRunPendingRescan()) |
| return; |
| |
| // Do not rescan for crostini errors. |
| if (event.error.name === DirectoryModel.CROSTINI_CONNECT_ERR) |
| return; |
| |
| if (this.scanFailures_ <= 1) |
| this.rescanLater(refresh); |
| }.bind(this); |
| |
| var onCancelled = function() { |
| onFinished(); |
| cancelledCallback(); |
| }; |
| |
| this.runningScan_ = dirContents; |
| |
| dirContents.addEventListener('scan-completed', onSuccess); |
| dirContents.addEventListener('scan-updated', updatedCallback); |
| dirContents.addEventListener('scan-failed', onFailure); |
| dirContents.addEventListener('scan-cancelled', onCancelled); |
| dirContents.scan(refresh); |
| }; |
| |
| /** |
| * @param {DirectoryContents} dirContents DirectoryContents instance. This must |
| * be a different instance from this.currentDirContents_. |
| * @private |
| */ |
| DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) { |
| console.assert(this.currentDirContents_ !== dirContents, |
| 'Give directory contents instance must be different from current one.'); |
| cr.dispatchSimpleEvent(this, 'begin-update-files'); |
| this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() { |
| var selectedEntries = this.getSelectedEntries_(); |
| var selectedIndices = this.fileListSelection_.selectedIndexes; |
| |
| // Restore leadIndex in case leadName no longer exists. |
| var leadIndex = this.fileListSelection_.leadIndex; |
| var leadEntry = this.getLeadEntry_(); |
| const isCheckSelectMode = this.fileListSelection_.getCheckSelectMode(); |
| |
| var previousDirContents = this.currentDirContents_; |
| this.currentDirContents_ = dirContents; |
| this.currentDirContents_.replaceContextFileList(); |
| |
| this.setSelectedEntries_(selectedEntries); |
| this.fileListSelection_.leadIndex = leadIndex; |
| this.setLeadEntry_(leadEntry); |
| |
| // If nothing is selected after update, then select file next to the |
| // latest selection |
| var forceChangeEvent = false; |
| if (this.fileListSelection_.selectedIndexes.length == 0 && |
| selectedIndices.length != 0) { |
| var maxIdx = Math.max.apply(null, selectedIndices); |
| this.selectIndex(Math.min(maxIdx - selectedIndices.length + 2, |
| this.getFileList().length) - 1); |
| forceChangeEvent = true; |
| } else if (isCheckSelectMode) { |
| // Otherwise, ensure check select mode is retained if it was previously |
| // active. |
| this.fileListSelection_.setCheckSelectMode(true); |
| } |
| return forceChangeEvent; |
| }.bind(this)); |
| |
| cr.dispatchSimpleEvent(this, 'end-update-files'); |
| }; |
| |
| /** |
| * Callback when an entry is changed. |
| * @param {EntriesChangedEvent} event Entry change event. |
| * @private |
| */ |
| DirectoryModel.prototype.onEntriesChanged_ = function(event) { |
| var kind = event.kind; |
| var entries = event.entries; |
| // TODO(hidehiko): We should update directory model even the search result |
| // is shown. |
| var rootType = this.getCurrentRootType(); |
| if ((rootType === VolumeManagerCommon.RootType.DRIVE || |
| rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME || |
| rootType === VolumeManagerCommon.RootType.DRIVE_RECENT || |
| rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE) && |
| this.isSearching()) |
| return; |
| |
| switch (kind) { |
| case util.EntryChangedKind.CREATED: |
| var parentPromises = []; |
| for (var i = 0; i < entries.length; i++) { |
| parentPromises.push(new Promise(function(resolve, reject) { |
| entries[i].getParent(resolve, reject); |
| })); |
| } |
| Promise.all(parentPromises).then(function(parents) { |
| var entriesToAdd = []; |
| for (var i = 0; i < parents.length; i++) { |
| if (!util.isSameEntry(parents[i], this.getCurrentDirEntry())) |
| continue; |
| var index = this.findIndexByEntry_(entries[i]); |
| if (index >= 0) { |
| this.getFileList().replaceItem( |
| this.getFileList().item(index), entries[i]); |
| } else { |
| entriesToAdd.push(entries[i]); |
| } |
| } |
| this.partialUpdate_(entriesToAdd, []); |
| }.bind(this)).catch(function(error) { |
| console.error(error.stack || error); |
| }); |
| break; |
| |
| case util.EntryChangedKind.DELETED: |
| // This is the delete event. |
| this.partialUpdate_([], util.entriesToURLs(entries)); |
| break; |
| |
| default: |
| console.error('Invalid EntryChangedKind: ' + kind); |
| break; |
| } |
| }; |
| |
| /** |
| * @param {Entry} entry The entry to be searched. |
| * @return {number} The index in the fileList, or -1 if not found. |
| * @private |
| */ |
| DirectoryModel.prototype.findIndexByEntry_ = function(entry) { |
| var fileList = this.getFileList(); |
| for (var i = 0; i < fileList.length; i++) { |
| if (util.isSameEntry(/** @type {Entry} */ (fileList.item(i)), entry)) |
| return i; |
| } |
| return -1; |
| }; |
| |
| /** |
| * Called when rename is done successfully. |
| * Note: conceptually, DirectoryModel should work without this, because entries |
| * can be renamed by other systems anytime and the Files app should reflect it |
| * correctly. |
| * TODO(hidehiko): investigate more background, and remove this if possible. |
| * |
| * @param {!Entry} oldEntry The old entry. |
| * @param {!Entry} newEntry The new entry. |
| * @param {function()=} opt_callback Called on completion. |
| */ |
| DirectoryModel.prototype.onRenameEntry = function( |
| oldEntry, newEntry, opt_callback) { |
| this.currentDirContents_.prefetchMetadata([newEntry], true, function() { |
| // If the current directory is the old entry, then quietly change to the |
| // new one. |
| if (util.isSameEntry(oldEntry, this.getCurrentDirEntry())) { |
| this.changeDirectoryEntry( |
| /** @type {!DirectoryEntry|!FilesAppDirEntry} */ (newEntry)); |
| } |
| |
| // Replace the old item with the new item. oldEntry instance itself may |
| // have been removed/replaced from the list during the async process, we |
| // find an entry which should be replaced by checking toURL(). |
| var list = this.getFileList(); |
| var oldEntryExist = false; |
| var newEntryExist = false; |
| var oldEntryUrl = oldEntry.toURL(); |
| var newEntryUrl = newEntry.toURL(); |
| |
| for (var i = 0; i < list.length; i++) { |
| var item = list.item(i); |
| var url = item.toURL(); |
| if (url === oldEntryUrl) { |
| list.replaceItem(item, newEntry); |
| oldEntryExist = true; |
| break; |
| } |
| |
| if (url === newEntryUrl) { |
| newEntryExist = true; |
| } |
| } |
| |
| // When both old and new entries don't exist, it may be in the middle of |
| // update process. In DirectoryContent.update deletion is executed at first |
| // and insertion is executed as a async call. There is a chance that this |
| // method is called in the middle of update process. |
| if (!oldEntryExist && !newEntryExist) |
| list.push(newEntry); |
| |
| // Run callback, finally. |
| if (opt_callback) |
| opt_callback(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Updates data model and selects new directory. |
| * @param {!DirectoryEntry} newDirectory Directory entry to be selected. |
| * @return {Promise} A promise which is resolved when new directory is selected. |
| * If current directory has changed during the operation, this will be |
| * rejected. |
| */ |
| DirectoryModel.prototype.updateAndSelectNewDirectory = function(newDirectory) { |
| // Refresh the cache. |
| this.metadataModel_.notifyEntriesCreated([newDirectory]); |
| var dirContents = this.currentDirContents_; |
| |
| return new Promise(function(onFulfilled, onRejected) { |
| dirContents.prefetchMetadata( |
| [newDirectory], false, onFulfilled); |
| }).then(function(sequence) { |
| // If current directory has changed during the prefetch, do not try to |
| // select new directory. |
| if (sequence !== this.changeDirectorySequence_) |
| return Promise.reject(); |
| |
| // If target directory is already in the list, just select it. |
| var existing = this.getFileList().slice().filter( |
| function(e) { return e.name === newDirectory.name; }); |
| if (existing.length) { |
| this.selectEntry(newDirectory); |
| } else { |
| this.fileListSelection_.beginChange(); |
| this.getFileList().splice(0, 0, newDirectory); |
| this.selectEntry(newDirectory); |
| this.fileListSelection_.endChange(); |
| } |
| }.bind(this, this.changeDirectorySequence_)); |
| }; |
| |
| /** |
| * Sets the current MyFilesEntry. |
| * @param {FilesAppDirEntry} myFilesEntry |
| */ |
| DirectoryModel.prototype.setMyFiles = function(myFilesEntry) { |
| this.myFilesEntry_ = myFilesEntry; |
| }; |
| |
| /** |
| * Changes the current directory to the directory represented by |
| * a DirectoryEntry or a fake entry. |
| * |
| * Dispatches the 'directory-changed' event when the directory is successfully |
| * changed. |
| * |
| * Note : if this is called from UI, please consider to use DirectoryModel. |
| * activateDirectoryEntry instead of this, which is higher-level function and |
| * cares about the selection. |
| * |
| * @param {!DirectoryEntry|!FilesAppDirEntry} dirEntry The entry of the new |
| * directory to be opened. |
| * @param {function()=} opt_callback Executed if the directory loads |
| * successfully. |
| */ |
| DirectoryModel.prototype.changeDirectoryEntry = function( |
| dirEntry, opt_callback) { |
| // Increment the sequence value. |
| this.changeDirectorySequence_++; |
| this.clearSearch_(); |
| |
| // When switching to MyFiles volume, we should use a FilesAppEntry if |
| // available because it returns UI-only entries too, like Linux files and Play |
| // files. |
| const locationInfo = this.volumeManager_.getLocationInfo(dirEntry); |
| if (util.isMyFilesVolumeEnabled() && locationInfo && this.myFilesEntry_ && |
| locationInfo.rootType === VolumeManagerCommon.RootType.DOWNLOADS && |
| locationInfo.isRootEntry) { |
| dirEntry = this.myFilesEntry_; |
| } |
| |
| // If there is on-going scan, cancel it. |
| if (this.currentDirContents_.isScanning()) |
| this.currentDirContents_.cancelScan(); |
| |
| this.directoryChangeQueue_.run(function(sequence, queueTaskCallback) { |
| this.fileWatcher_.changeWatchedDirectory(dirEntry) |
| .then(function() { |
| if (this.changeDirectorySequence_ !== sequence) { |
| queueTaskCallback(); |
| return; |
| } |
| |
| var newDirectoryContents = this.createDirectoryContents_( |
| this.currentFileListContext_, dirEntry, ''); |
| if (!newDirectoryContents) { |
| queueTaskCallback(); |
| return; |
| } |
| |
| var previousDirEntry = |
| this.currentDirContents_.getDirectoryEntry(); |
| this.clearAndScan_( |
| newDirectoryContents, |
| function(result) { |
| // Calls the callback of the method when successful. |
| if (result && opt_callback) |
| opt_callback(); |
| |
| // Notify that the current task of this.directoryChangeQueue_ |
| // is completed. |
| setTimeout(queueTaskCallback, 0); |
| }); |
| |
| // For tests that open the dialog to empty directories, everything |
| // is loaded at this point. |
| util.testSendMessage('directory-change-complete'); |
| var previousVolumeInfo = |
| previousDirEntry ? |
| this.volumeManager_.getVolumeInfo(previousDirEntry) : null; |
| // VolumeInfo for dirEntry. |
| var currentVolumeInfo = this.getCurrentVolumeInfo(); |
| var event = new Event('directory-changed'); |
| event.previousDirEntry = previousDirEntry; |
| event.newDirEntry = dirEntry; |
| event.volumeChanged = previousVolumeInfo !== currentVolumeInfo; |
| this.dispatchEvent(event); |
| }.bind(this)); |
| }.bind(this, this.changeDirectorySequence_)); |
| }; |
| |
| /** |
| * Activates the given directory. |
| * This method: |
| * - Changes the current directory, if the given directory is not the current |
| * directory. |
| * - Clears the selection, if the given directory is the current directory. |
| * |
| * @param {!DirectoryEntry|!FilesAppDirEntry} dirEntry The entry of the new |
| * directory to be opened. |
| * @param {function()=} opt_callback Executed if the directory loads |
| * successfully. |
| */ |
| DirectoryModel.prototype.activateDirectoryEntry = function( |
| dirEntry, opt_callback) { |
| var currentDirectoryEntry = this.getCurrentDirEntry(); |
| if (currentDirectoryEntry && |
| util.isSameEntry(dirEntry, currentDirectoryEntry)) { |
| // On activating the current directory, clear the selection on the filelist. |
| this.clearSelection(); |
| } else { |
| // Otherwise, changes the current directory. |
| this.changeDirectoryEntry(dirEntry, opt_callback); |
| } |
| }; |
| |
| /** |
| * Clears the selection in the file list. |
| */ |
| DirectoryModel.prototype.clearSelection = function() { |
| this.setSelectedEntries_([]); |
| }; |
| |
| /** |
| * Creates an object which could say whether directory has changed while it has |
| * been active or not. Designed for long operations that should be cancelled |
| * if the used change current directory. |
| * @return {Object} Created object. |
| */ |
| DirectoryModel.prototype.createDirectoryChangeTracker = function() { |
| var tracker = { |
| dm_: this, |
| active_: false, |
| hasChanged: false, |
| |
| start: function() { |
| if (!this.active_) { |
| this.dm_.addEventListener('directory-changed', |
| this.onDirectoryChange_); |
| this.active_ = true; |
| this.hasChanged = false; |
| } |
| }, |
| |
| stop: function() { |
| if (this.active_) { |
| this.dm_.removeEventListener('directory-changed', |
| this.onDirectoryChange_); |
| this.active_ = false; |
| } |
| }, |
| |
| onDirectoryChange_: function(event) { |
| tracker.stop(); |
| tracker.hasChanged = true; |
| } |
| }; |
| return tracker; |
| }; |
| |
| /** |
| * @param {Entry} entry Entry to be selected. |
| */ |
| DirectoryModel.prototype.selectEntry = function(entry) { |
| var fileList = this.getFileList(); |
| for (var i = 0; i < fileList.length; i++) { |
| if (fileList.item(i).toURL() === entry.toURL()) { |
| this.selectIndex(i); |
| return; |
| } |
| } |
| }; |
| |
| /** |
| * @param {Array<Entry>} entries Array of entries. |
| */ |
| DirectoryModel.prototype.selectEntries = function(entries) { |
| // URLs are needed here, since we are comparing Entries by URLs. |
| var urls = util.entriesToURLs(entries); |
| var fileList = this.getFileList(); |
| this.fileListSelection_.beginChange(); |
| this.fileListSelection_.unselectAll(); |
| for (var i = 0; i < fileList.length; i++) { |
| if (urls.indexOf(fileList.item(i).toURL()) >= 0) |
| this.fileListSelection_.setIndexSelected(i, true); |
| } |
| this.fileListSelection_.endChange(); |
| }; |
| |
| /** |
| * @param {number} index Index of file. |
| */ |
| DirectoryModel.prototype.selectIndex = function(index) { |
| // this.focusCurrentList_(); |
| if (index >= this.getFileList().length) |
| return; |
| |
| // If a list bound with the model it will do scrollIndexIntoView(index). |
| this.fileListSelection_.selectedIndex = index; |
| }; |
| |
| /** |
| * Handles update of VolumeInfoList. |
| * @param {Event} event Event of VolumeInfoList's 'splice'. |
| * @private |
| */ |
| DirectoryModel.prototype.onVolumeInfoListUpdated_ = function(event) { |
| // Fallback to the default volume's root if the current volume is unmounted. |
| if (this.hasCurrentDirEntryBeenUnmounted_(event.removed)) { |
| this.volumeManager_.getDefaultDisplayRoot((displayRoot) => { |
| if (displayRoot) |
| this.changeDirectoryEntry(displayRoot); |
| }); |
| } |
| |
| // If a volume within My files is mounted, rescan the contents. |
| // TODO(crbug.com/901690): Remove this special case. |
| if (this.getCurrentRootType() === VolumeManagerCommon.RootType.MY_FILES) { |
| for (let newVolume of event.added) { |
| if (newVolume.volumeType === VolumeManagerCommon.VolumeType.DOWNLOADS || |
| newVolume.volumeType === |
| VolumeManagerCommon.VolumeType.ANDROID_FILES || |
| newVolume.volumeType === VolumeManagerCommon.VolumeType.CROSTINI) { |
| this.rescan(false); |
| break; |
| } |
| } |
| } |
| |
| // If the current directory is the Drive placeholder and the real Drive is |
| // mounted, switch to it. |
| if (this.getCurrentRootType() === |
| VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT) { |
| for (let newVolume of event.added) { |
| if (newVolume.volumeType === VolumeManagerCommon.VolumeType.DRIVE) { |
| newVolume.resolveDisplayRoot().then((displayRoot) => { |
| this.changeDirectoryEntry(displayRoot); |
| }); |
| } |
| } |
| } |
| // If a new file backed provided volume is mounted, |
| // then redirect to it in the focused window. |
| // If crostini is mounted, redirect even if window is not focused. |
| // Note, that this is a temporary solution for https://crbug.com/427776. |
| if (event.added.length !== 1) |
| return; |
| if ((window.isFocused() && |
| event.added[0].volumeType === VolumeManagerCommon.VolumeType.PROVIDED && |
| event.added[0].source === VolumeManagerCommon.Source.FILE) || |
| event.added[0].volumeType === VolumeManagerCommon.VolumeType.CROSTINI) { |
| event.added[0].resolveDisplayRoot().then((displayRoot) => { |
| // Resolving a display root on FSP volumes is instant, despite the |
| // asynchronous call. |
| this.changeDirectoryEntry(event.added[0].displayRoot); |
| }); |
| } |
| }; |
| |
| /** |
| * Returns whether the current directory entry has been unmounted. |
| * |
| * @param {!Array<!VolumeInfo>} removedVolumes The removed volumes. |
| * @private |
| */ |
| DirectoryModel.prototype.hasCurrentDirEntryBeenUnmounted_ = function( |
| removedVolumes) { |
| const entry = this.getCurrentDirEntry(); |
| if (!entry) { |
| return false; |
| } |
| |
| if (util.isNativeEntry(entry)) { |
| return !this.volumeManager_.getVolumeInfo(entry); |
| } |
| |
| const rootType = this.getCurrentRootType(); |
| for (let volume of removedVolumes) { |
| if (volume.fakeEntries[rootType]) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| /** |
| * Creates directory contents for the entry and query. |
| * |
| * @param {FileListContext} context File list context. |
| * @param {!DirectoryEntry|!FilesAppEntry} entry Current directory. |
| * @param {string=} opt_query Search query string. |
| * @return {DirectoryContents} Directory contents. |
| * @private |
| */ |
| DirectoryModel.prototype.createDirectoryContents_ = |
| function(context, entry, opt_query) { |
| var query = (opt_query || '').trimLeft(); |
| var locationInfo = this.volumeManager_.getLocationInfo(entry); |
| var canUseDriveSearch = this.volumeManager_.getDriveConnectionState().type !== |
| VolumeManagerCommon.DriveConnectionType.OFFLINE && |
| (locationInfo && locationInfo.isDriveBased); |
| |
| if (entry.rootType == VolumeManagerCommon.RootType.RECENT) { |
| return DirectoryContents.createForRecent( |
| context, /** @type {!FakeEntry} */ (entry), query); |
| } |
| if (entry.rootType == VolumeManagerCommon.RootType.CROSTINI) { |
| return DirectoryContents.createForCrostiniMounter( |
| context, /** @type {!FakeEntry} */ (entry)); |
| } |
| if (entry.rootType == VolumeManagerCommon.RootType.MY_FILES) { |
| return DirectoryContents.createForDirectory( |
| context, /** @type {!FilesAppDirEntry} */ (entry)); |
| } |
| if (entry.rootType == VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT) { |
| return DirectoryContents.createForFakeDrive( |
| context, /** @type {!FakeEntry} */ (entry)); |
| } |
| if (query && canUseDriveSearch) { |
| // Drive search. |
| return DirectoryContents.createForDriveSearch( |
| context, /** @type {!DirectoryEntry} */ (entry), query); |
| } |
| if (query) { |
| // Local search. |
| return DirectoryContents.createForLocalSearch( |
| context, /** @type {!DirectoryEntry} */ (entry), query); |
| } |
| |
| if (!locationInfo) |
| return null; |
| |
| if (locationInfo.rootType == VolumeManagerCommon.RootType.MEDIA_VIEW) { |
| return DirectoryContents.createForMediaView( |
| context, /** @type {!DirectoryEntry} */ (entry)); |
| } |
| |
| if (locationInfo.isSpecialSearchRoot) { |
| // Drive special search. |
| var searchType; |
| switch (locationInfo.rootType) { |
| case VolumeManagerCommon.RootType.DRIVE_OFFLINE: |
| searchType = |
| DriveMetadataSearchContentScanner.SearchType.SEARCH_OFFLINE; |
| break; |
| case VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME: |
| searchType = |
| DriveMetadataSearchContentScanner.SearchType.SEARCH_SHARED_WITH_ME; |
| break; |
| case VolumeManagerCommon.RootType.DRIVE_RECENT: |
| searchType = |
| DriveMetadataSearchContentScanner.SearchType.SEARCH_RECENT_FILES; |
| break; |
| default: |
| // Unknown special search entry. |
| throw new Error('Unknown special search type.'); |
| } |
| return DirectoryContents.createForDriveMetadataSearch( |
| context, |
| /** @type {!FakeEntry} */ (entry), |
| searchType); |
| } |
| // Local fetch or search. |
| return DirectoryContents.createForDirectory( |
| context, /** @type {!DirectoryEntry} */ (entry)); |
| }; |
| |
| /** |
| * Gets the last search query. |
| * @return {string} the last search query. |
| */ |
| DirectoryModel.prototype.getLastSearchQuery = function() { |
| return this.lastSearchQuery_; |
| }; |
| |
| /** |
| * Clears the last search query with the empty string. |
| */ |
| DirectoryModel.prototype.clearLastSearchQuery = function() { |
| this.lastSearchQuery_ = ''; |
| }; |
| |
| /** |
| * Performs search and displays results. The search type is dependent on the |
| * current directory. If we are currently on drive, server side content search |
| * over drive mount point. If the current directory is not on the drive, file |
| * name search over current directory will be performed. |
| * |
| * @param {string} query Query that will be searched for. |
| * @param {function(Event)} onSearchRescan Function that will be called when the |
| * search directory is rescanned (i.e. search results are displayed). |
| * @param {function()} onClearSearch Function to be called when search state |
| * gets cleared. |
| * TODO(olege): Change callbacks to events. |
| */ |
| DirectoryModel.prototype.search = function(query, |
| onSearchRescan, |
| onClearSearch) { |
| this.lastSearchQuery_ = query; |
| this.clearSearch_(); |
| var currentDirEntry = this.getCurrentDirEntry(); |
| if (!currentDirEntry) { |
| // Not yet initialized. Do nothing. |
| return; |
| } |
| |
| this.changeDirectorySequence_++; |
| this.directoryChangeQueue_.run(function(sequence, callback) { |
| if (this.changeDirectorySequence_ !== sequence) { |
| callback(); |
| return; |
| } |
| |
| if (!(query || '').trimLeft()) { |
| if (this.isSearching()) { |
| var newDirContents = this.createDirectoryContents_( |
| this.currentFileListContext_, |
| assert(currentDirEntry)); |
| this.clearAndScan_(newDirContents, |
| callback); |
| } else { |
| callback(); |
| } |
| return; |
| } |
| |
| var newDirContents = this.createDirectoryContents_( |
| this.currentFileListContext_, assert(currentDirEntry), query); |
| if (!newDirContents) { |
| callback(); |
| return; |
| } |
| |
| this.onSearchCompleted_ = onSearchRescan; |
| this.onClearSearch_ = onClearSearch; |
| this.addEventListener('scan-completed', this.onSearchCompleted_); |
| this.clearAndScan_(newDirContents, |
| callback); |
| }.bind(this, this.changeDirectorySequence_)); |
| }; |
| |
| /** |
| * In case the search was active, remove listeners and send notifications on |
| * its canceling. |
| * @private |
| */ |
| DirectoryModel.prototype.clearSearch_ = function() { |
| if (!this.isSearching()) |
| return; |
| |
| if (this.onSearchCompleted_) { |
| this.removeEventListener('scan-completed', this.onSearchCompleted_); |
| this.onSearchCompleted_ = null; |
| } |
| |
| if (this.onClearSearch_) { |
| this.onClearSearch_(); |
| this.onClearSearch_ = null; |
| } |
| }; |
| |
| /** |
| * DOMError type for crostini connection failure. |
| * @const {string} |
| */ |
| DirectoryModel.CROSTINI_CONNECT_ERR = 'CrostiniConnectErr'; |