Chromium Code Reviews| Index: chrome/browser/resources/file_manager/js/directory_model.js |
| diff --git a/chrome/browser/resources/file_manager/js/directory_model.js b/chrome/browser/resources/file_manager/js/directory_model.js |
| index 89747a4c7b3822cf40c6b3c83dc1c1cb2342bc9f..1a590a07f072263c0a8e327d67052206081c1803 100644 |
| --- a/chrome/browser/resources/file_manager/js/directory_model.js |
| +++ b/chrome/browser/resources/file_manager/js/directory_model.js |
| @@ -19,7 +19,8 @@ var SHORT_RESCAN_INTERVAL = 100; |
| * (regardless of its mounts status). |
| * @param {MetadataCache} metadataCache The metadata cache service. |
| */ |
| -function DirectoryModel(root, singleSelection, showGData, metadataCache) { |
| +function DirectoryModel(root, singleSelection, showGData, |
| + metadataCache, volumeManager) { |
| this.root_ = root; |
| this.metadataCache_ = metadataCache; |
| this.fileList_ = new cr.ui.ArrayDataModel([]); |
| @@ -54,11 +55,18 @@ function DirectoryModel(root, singleSelection, showGData, metadataCache) { |
| this.filters_ = {}; |
| this.setFilterHidden(true); |
| - /** |
| - * @private |
| - * @type {Object.<string, boolean>} |
| - */ |
| - this.volumeReadOnlyStatus_ = {}; |
| + if (this.showGData_) { |
| + this.unmountedGDataEntry_ = { |
| + unmouted: true, |
| + fullPath: '/' + DirectoryModel.GDATA_DIRECTORY |
| + }; |
| + } |
| + |
| + this.volumeManager_ = volumeManager; |
| + var volumesChangeHandler = this.onMountChanged_.bind(this); |
| + this.updateRoots(function() { |
| + volumeManager.addEventListener('change', volumesChangeHandler); |
| + }); |
| } |
| /** |
| @@ -94,6 +102,10 @@ DirectoryModel.DOWNLOADS_DIRECTORY = 'Downloads'; |
| */ |
| DirectoryModel.GDATA_DIRECTORY = 'drive'; |
| +DirectoryModel.fakeGDataEntry_ = { |
| + fullPath: '/' + DirectoryModel.GDATA_DIRECTORY |
| +}; |
| + |
| /** |
| * DirectoryModel extends cr.EventTarget. |
| */ |
| @@ -172,7 +184,7 @@ DirectoryModel.prototype.isReadOnly = function() { |
| DirectoryModel.prototype.isPathReadOnly = function(path) { |
| switch (DirectoryModel.getRootType(path)) { |
| case DirectoryModel.RootType.REMOVABLE: |
| - return !!this.volumeReadOnlyStatus_[DirectoryModel.getRootPath(path)]; |
| + return !!this.volumeManager_.isReadOnly(DirectoryModel.getRootPath(path)); |
| case DirectoryModel.RootType.ARCHIVE: |
| return true; |
| case DirectoryModel.RootType.DOWNLOADS: |
| @@ -313,6 +325,10 @@ DirectoryModel.prototype.getCurrentRootPath = function() { |
| return DirectoryModel.getRootPath(this.currentDirEntry_.fullPath); |
| }; |
| +DirectoryModel.prototype.getCurrentRootType = function() { |
| + return DirectoryModel.getRootType(this.currentDirEntry_.fullPath); |
| +}; |
| + |
| /** |
| * @return {cr.ui.ListSingleSelectionModel} Root list selection model. |
| */ |
| @@ -488,7 +504,7 @@ DirectoryModel.prototype.scan_ = function(callback) { |
| // Clear the table first. |
| this.fileList_.splice(0, this.fileList_.length); |
| cr.dispatchSimpleEvent(this, 'scan-started'); |
| - if (this.currentDirEntry_ == this.unmountedGDataEntry_) { |
| + if (this.currentDirEntry_ == DirectoryModel.fakeGDataEntry_) { |
| onDone(); |
| return; |
| } |
| @@ -691,16 +707,20 @@ DirectoryModel.prototype.changeDirectory = function(path) { |
| */ |
| DirectoryModel.prototype.resolveDirectory = function(path, successCallback, |
| errorCallback) { |
| - if (this.unmountedGDataEntry_ && |
| - DirectoryModel.getRootType(path) == DirectoryModel.RootType.GDATA) { |
| + if (DirectoryModel.getRootType(path) == DirectoryModel.RootType.GDATA) { |
| // TODO(kaznacheeev): Currently if path points to some GData subdirectory |
| // and GData is not mounted we will change to the fake GData root and |
| // ignore the rest of the path. Consider remembering the path and |
| // changing to it once GDdata is mounted. This is only relevant for cases |
| // when we open the File Manager with an URL pointing to GData (e.g. via |
| // a bookmark). |
| - successCallback(this.unmountedGDataEntry_); |
| - return; |
| + if (!this.isGDataMounted_()) { |
| + if (path == DirectoryModel.fakeGDataEntry_.fullPath) |
| + successCallback(DirectoryModel.fakeGDataEntry_); |
| + else // Subdirectory. |
| + errorCallback({ code: FileError.NOT_FOUND_ERR }); |
| + return; |
| + } |
| } |
| if (path == '/') { |
| @@ -758,6 +778,9 @@ DirectoryModel.prototype.changeRoot = function(path) { |
| */ |
| DirectoryModel.prototype.changeDirectoryEntry_ = function(initial, dirEntry, |
| opt_callback) { |
| + if (dirEntry == DirectoryModel.fakeGDataEntry_) |
| + this.volumeManager_.mountGData(function() {}, function() {}); |
| + |
| var previous = this.currentDirEntry_; |
| this.currentDirEntry_ = dirEntry; |
| function onRescanComplete() { |
| @@ -779,6 +802,47 @@ DirectoryModel.prototype.changeDirectoryEntry_ = function(initial, dirEntry, |
| }; |
| /** |
| + * Creates an object wich could cay wether directory has changed while it has |
|
dgozman
2012/05/16 14:31:46
typo: cay -> say
|
| + * been active or not. Designed for long operations that should be canncelled |
| + * if the used change current directory. |
|
dgozman
2012/05/16 14:31:46
typo: used -> user
|
| + * @return {Object} Created object. |
| + */ |
| +DirectoryModel.prototype.createDirectoryChangeTracker = function() { |
| + var tracker = { |
| + dm_: this, |
| + active_: false, |
| + hasChanged: false, |
| + exceptInitialChange: 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_); |
| + active_ = false; |
| + } |
| + }, |
| + |
| + onDirectoryChange_: function(event) { |
| + if (tracker.exceptInitialChange && event.initial) |
| + return; |
| + // This is incorrect here. |
|
dgozman
2012/05/16 14:31:46
I agree that this is incorrect. Fix?
|
| + tracker.stop(); |
| + tracker.hasChange = true; |
| + } |
| + }; |
| + return tracker; |
| +}; |
| + |
| +/** |
| * Change the state of the model to reflect the specified path (either a |
| * file or directory). |
| * |
| @@ -796,19 +860,21 @@ DirectoryModel.prototype.changeDirectoryEntry_ = function(initial, dirEntry, |
| */ |
| DirectoryModel.prototype.setupPath = function(path, opt_loadedCallback, |
| opt_pathResolveCallback) { |
| - var overridden = false; |
| - function onExternalDirChange() { overridden = true } |
| - this.addEventListener('directory-changed', onExternalDirChange); |
| + var tracker = this.createDirectoryChangeTracker(); |
| + tracker.start(); |
| var resolveCallback = function(exists) { |
| - this.removeEventListener('directory-changed', onExternalDirChange); |
| - if (opt_pathResolveCallback) |
| - opt_pathResolveCallback(baseName, leafName, exists && !overridden); |
| + tracker.stop(); |
| + if (opt_pathResolveCallback) { |
| + opt_pathResolveCallback(baseName, leafName, |
| + exists && !tracker.hasChanged); |
| + } |
| }.bind(this); |
| var changeDirectoryEntry = function(entry, initial, exists, opt_callback) { |
| + tracker.stop(); |
| resolveCallback(exists); |
| - if (!overridden) |
| + if (!tracker.hasChanged) |
| this.changeDirectoryEntry_(initial, entry, opt_callback); |
| }.bind(this); |
| @@ -862,7 +928,7 @@ DirectoryModel.prototype.setupPath = function(path, opt_loadedCallback, |
| if (path != this.getDefaultDirectory()) { |
| // Can't find the provided path, let's go to default one instead. |
| resolveCallback(!EXISTS); |
| - if (!overridden) |
| + if (!tracker.hasChanged) |
| this.setupDefaultPath(opt_loadedCallback); |
| } else { |
| // Well, we can't find the downloads dir. Let's just show something, |
| @@ -958,9 +1024,8 @@ DirectoryModel.prototype.prepareSortEntries_ = function(entries, field, |
| * Get root entries asynchronously. |
| * @private |
| * @param {function(Array.<Entry>)} callback Called when roots are resolved. |
| - * @param {boolean} resolveGData See comment for updateRoots. |
| */ |
| -DirectoryModel.prototype.resolveRoots_ = function(callback, resolveGData) { |
| +DirectoryModel.prototype.resolveRoots_ = function(callback) { |
| var groups = { |
| downloads: null, |
| archives: null, |
| @@ -975,7 +1040,6 @@ DirectoryModel.prototype.resolveRoots_ = function(callback, resolveGData) { |
| if (!groups[i]) |
| return; |
| - self.updateVolumeReadOnlyStatus_(groups.removables); |
| callback(groups.downloads. |
| concat(groups.gdata). |
| concat(groups.archives). |
| @@ -988,60 +1052,43 @@ DirectoryModel.prototype.resolveRoots_ = function(callback, resolveGData) { |
| done(); |
| } |
| - function onDownloads(entry) { |
| - groups.downloads = [entry]; |
| + function appendSingle(index, entry) { |
| + groups[index] = [entry]; |
| done(); |
| } |
| - function onDownloadsError(error) { |
| - groups.downloads = []; |
| + function onSingleError(index, error, defaultValue) { |
| + groups[index] = defailtValue || []; |
| done(); |
| } |
| - function onGData(entry) { |
| - console.log('GData found:', entry); |
| - self.unmountedGDataEntry_ = null; |
| - groups.gdata = [entry]; |
| - done(); |
| - } |
| - |
| - function onGDataError(error) { |
| - console.log('GData error: ' + error); |
| - self.unmountedGDataEntry_ = { |
| - unmounted: true, // Clients use this field to distinguish a fake root. |
| - toURL: function() { return '' }, |
| - fullPath: '/' + DirectoryModel.GDATA_DIRECTORY |
| - }; |
| - groups.gdata = [self.unmountedGDataEntry_]; |
| - done(); |
| + var root = this.root_; |
| + function readSingle(dir, index, defaultValue) { |
| + root.getDirectory(dir, { create: false }, |
| + appendSingle.bind(this, index), |
| + onSingleError.bind(this, index, defaultValue)); |
| } |
| - var root = this.root_; |
| - root.getDirectory(DirectoryModel.DOWNLOADS_DIRECTORY, { create: false }, |
| - onDownloads, onDownloadsError); |
| + readSingle(DirectoryModel.DOWNLOADS_DIRECTORY, 'downloads'); |
| util.readDirectory(root, DirectoryModel.ARCHIVE_DIRECTORY, |
| append.bind(this, 'archives')); |
| util.readDirectory(root, DirectoryModel.REMOVABLE_DIRECTORY, |
| append.bind(this, 'removables')); |
| if (this.showGData_) { |
| - if (resolveGData) { |
| - root.getDirectory(DirectoryModel.GDATA_DIRECTORY, { create: false }, |
| - onGData, onGDataError); |
| - } else { |
| - onGDataError('lazy mount'); |
| - } |
| + var fake = [DirectoryModel.fakeGDataEntry_]; |
| + if (this.isGDataMounted_()) |
| + readSingle(DirectoryModel.GDATA_DIRECTORY, 'gdata', fake); |
| + else |
| + groups.gdata = fake; |
| } else { |
| groups.gdata = []; |
| } |
| }; |
| /** |
| -* @param {function} opt_callback Called when all roots are resolved. |
| -* @param {boolean} opt_resolveGData If true GData should be resolved for real, |
| -* If false a stub entry should be created. |
| +* @param {functioni?} opt_callback Called when all roots are resolved. |
|
dgozman
2012/05/16 14:31:46
typo: functioni?
|
| */ |
| -DirectoryModel.prototype.updateRoots = function(opt_callback, |
| - opt_resolveGData) { |
| +DirectoryModel.prototype.updateRoots = function(opt_callback) { |
| var self = this; |
| this.resolveRoots_(function(rootEntries) { |
| var dm = self.rootsList_; |
| @@ -1049,10 +1096,8 @@ DirectoryModel.prototype.updateRoots = function(opt_callback, |
| dm.splice.apply(dm, args); |
| self.updateRootsListSelection_(); |
| - |
| - if (opt_callback) |
| - opt_callback(); |
| - }, opt_resolveGData); |
| + opt_callback && opt_callback(); |
|
dgozman
2012/05/16 14:31:46
I like the |if| construction much more.
|
| + }); |
| }; |
| /** |
| @@ -1079,45 +1124,38 @@ DirectoryModel.prototype.updateRootsListSelection_ = function() { |
| this.rootsListSelection_.selectedIndex = this.findRootsListItem_(rootPath); |
| }; |
| -/** |
| - * @param {Array.<DirectoryEntry>} roots Removable volumes entries. |
| - * @private |
| - */ |
| -DirectoryModel.prototype.updateVolumeReadOnlyStatus_ = function(roots) { |
| - var status = this.volumeReadOnlyStatus_ = {}; |
| - for (var i = 0; i < roots.length; i++) { |
| - status[roots[i].fullPath] = false; |
| - chrome.fileBrowserPrivate.getVolumeMetadata(roots[i].toURL(), |
| - function(systemMetadata, path) { |
| - status[path] = !!(systemMetadata && systemMetadata.isReadOnly); |
| - }.bind(null, roots[i].fullPath)); |
| - } |
| +DirectoryModel.prototype.isGDataMounted_ = function() { |
| + return this.volumeManager_.isMounted('/' + DirectoryModel.GDATA_DIRECTORY); |
| }; |
| -/** |
| - * Prepare the root for the unmount. |
| - * |
| - * @param {string} rootPath The path to the root. |
| - */ |
| -DirectoryModel.prototype.prepareUnmount = function(rootPath) { |
| - var index = this.findRootsListItem_(rootPath); |
| - if (index == -1) { |
| - console.error('Unknown root entry', rootPath); |
| - return; |
| - } |
| - var entry = this.rootsList_.item(index); |
| - |
| - // We never need to remove this attribute because even if the unmount fails |
| - // the onMountCompleted handler calls updateRoots which creates a new entry |
| - // object for this volume. |
| - entry.unmounting = true; |
| +DirectoryModel.prototype.onMountChanged_ = function() { |
| + this.updateRoots(); |
| - // Re-place the entry into the roots data model to force re-rendering. |
| - this.rootsList_.splice(index, 1, entry); |
| + if (this.getCurrentRootType() != DirectoryModel.RootType.GDATA) |
| + return; |
| - if (rootPath == this.rootPath) { |
| - // TODO(kaznacheev): Consider changing to the most recently used root. |
| - this.changeDirectory(this.getDefaultDirectory()); |
| + var mounted = this.isGDataMounted_(); |
| + if (this.currentDirEntry_ == DirectoryModel.fakeGDataEntry_) { |
| + if (mounted) { |
| + // Change fake entry to real one and rescan. |
| + this.root_.getDirectory('/' + DirectoryModel.GDATA_DIRECTORY, {}, |
| + function(entry) { |
| + if (this.currentDirEntry_ == DirectoryModel.fakeGDataEntry_) { |
| + this.currentDirEntry_ = entry; |
| + this.rescan(); |
| + } |
| + }.bind(this)); |
| + } |
| + } else if (!mounted) { |
| + // Current entry unmounted. replace with fake one. |
| + if (this.currentDirEntry_.fullPath == |
| + DirectoryModel.fakeGDataEntry_.fullPath) { |
| + // Replace silently and rescan. |
| + this.currentDirEntry_ = DirectoryModel.fakeGDataEntry_; |
| + this.rescan(); |
| + } else { |
| + this.changeDirectoryEntry_(false, DirectoryModel.fakeGDataEntry_); |
| + } |
| } |
| }; |
| @@ -1282,3 +1320,86 @@ DirectoryModel.Scanner.prototype.recordMetrics_ = function() { |
| metrics.recordMediumCount('DownloadsCount', this.list_.length); |
| } |
| }; |
| + |
| +function FileWatcher(root, directoryModel, volumeManager) { |
| + this.root_ = root; |
| + this.dm_ = directoryModel; |
| + this.vm_ = volumeManager; |
| + this.watchedDirectoryEntry_ = null; |
| + this.updateWatchedDirectoryBound_ = |
| + this.updateWatchedDirectory_.bind(this); |
| + this.onFileChangedBound_ = |
| + this.onFileChanged_.bind(this); |
| +} |
| + |
| +FileWatcher.prototype.start = function() { |
| + chrome.fileBrowserPrivate.onFileChanged.addListener( |
| + this.onFileChangedBound_); |
| + |
| + this.dm_.addEventListener('directory-changed', |
| + this.updateWatchedDirectoryBound_); |
| + this.vm_.addEventListener('changed', |
| + this.updateWatchedDirectoryBound_); |
| + |
| + this.updateWatchedDirectory_(); |
| +}; |
| + |
| +FileWatcher.prototype.stop = function() { |
| + chrome.fileBrowserPrivate.onFileChanged.removeListener( |
| + this.onFileChangedBound_); |
| + |
| + this.dm_.removeEventListener('directory-changed', |
| + this.updateWatchedDirectoryBound_); |
| + this.vm_.removeEventListener('changed', |
| + this.updateWatchedDirectoryBound_); |
| + |
| + if (this.watchedDirectoryEntry_) |
| + this.changeWatchedEntry(null); |
| +}; |
| + |
| +FileWatcher.prototype.onFileChanged_ = function(event) { |
| + if (encodeURI(event.fileUrl) == this.watchedDirectoryEntry_.toURL()) |
| + this.dm_.rescanLater(); |
| +}; |
| + |
| +FileWatcher.prototype.updateWatchedDirectory_ = function() { |
| + var current = this.watchedDirectoryEntry_; |
| + switch (this.dm_.getCurrentRootType()) { |
| + case DirectoryModel.RootType.GDATA: |
| + if (!this.vm_.isMounted('/' + DirectoryModel.GDATA_DIRECTORY)) |
| + break; |
|
dgozman
2012/05/16 14:31:46
I think, you mean |return| instead of |break| here
SeRya
2012/05/18 11:31:48
Break is correct. The code below need to be execut
|
| + case DirectoryModel.RootType.DOWNLOADS: |
| + case DirectoryModel.RootType.REMOVABLE: |
|
dgozman
2012/05/16 14:31:46
What about RootType.ARCHIVE?
SeRya
2012/05/18 11:31:48
I assume they never change so we don't need to wat
|
| + if (!current || current.fullPath != this.dm_.getCurrentDirPath()) { |
| + this.root_.getDirectory(this.dm_.getCurrentDirPath(), {}, |
|
dgozman
2012/05/16 14:31:46
Why not use dm_.getCurrentDirectory ?
SeRya
2012/05/18 11:31:48
getCurrentDirectory not always returns a valid Ent
|
| + this.changeWatchedEntry.bind(this)); |
| + } |
| + break; |
| + } |
| + if (current) |
| + this.changeWatchedEntry(null); |
| +}; |
| + |
| +FileWatcher.prototype.changeWatchedEntry = function(entry) { |
| + if (this.watchedDirectoryEntry_) { |
| + chrome.fileBrowserPrivate.removeFileWatch( |
| + this.watchedDirectoryEntry_.toURL(), |
| + function(result) { |
| + if (!result) { |
| + console.log('Failed to remove file watch'); |
| + } |
| + }); |
| + } |
| + this.watchedDirectoryEntry_ = entry; |
| + |
| + if (this.watchedDirectoryEntry_) { |
| + chrome.fileBrowserPrivate.addFileWatch( |
| + this.watchedDirectoryEntry_.toURL(), |
| + function(result) { |
| + if (!result) { |
| + console.log('Failed to add file watch'); |
| + this.watchedDirectoryEntry_ = null; |
| + } |
| + }.bind(this)); |
| + } |
| +}; |