diff options
author | serya@google.com <serya@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-22 16:01:16 +0000 |
---|---|---|
committer | serya@google.com <serya@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-22 16:01:16 +0000 |
commit | 662e96ff6fc8ca0795a4142c3b67c85d31061438 (patch) | |
tree | a031e4ed16bc38adfa87afec05f471ba8f0474c7 | |
parent | a21a9a7e013dbefeb0522a7317b711dd9c7941b7 (diff) | |
download | chromium_src-662e96ff6fc8ca0795a4142c3b67c85d31061438.zip chromium_src-662e96ff6fc8ca0795a4142c3b67c85d31061438.tar.gz chromium_src-662e96ff6fc8ca0795a4142c3b67c85d31061438.tar.bz2 |
Refactoring file manager: moving volume mounting related code to a separate class.
Reason:
- Remove some complexity from file_manager.js
- Let FileTransferManager to mount gdata not having dependancy on FileManager.
BUG=127216
TEST=
Review URL: https://chromiumcodereview.appspot.com/10310163
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@138260 0039d316-1c4b-4281-b951-d872f2087c98
7 files changed, 1271 insertions, 786 deletions
diff --git a/chrome/browser/resources/file_manager/css/file_manager.css b/chrome/browser/resources/file_manager/css/file_manager.css index 84b1975..3f58fd3 100644 --- a/chrome/browser/resources/file_manager/css/file_manager.css +++ b/chrome/browser/resources/file_manager/css/file_manager.css @@ -8,17 +8,23 @@ html.col-resize * { /* Outer frame of the dialog. */ body { - -webkit-box-flex: 1; - -webkit-box-orient: vertical; - -webkit-transition: opacity 70ms linear; - -webkit-user-select: none; - display: -webkit-box; - height: 100%; - margin: 0; - opacity: 0; - padding: 0; - position: absolute; - width: 100%; + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + -webkit-transition: opacity 70ms linear; + -webkit-user-select: none; + display: -webkit-box; + height: 100%; + margin: 0; + opacity: 0; + padding: 0; + position: absolute; + width: 100%; +} + +body.loaded { + /* Do not use display:none because list will calculate metrics incorrectly. */ + /*display: -webkit-box;*/ + opacity: 1; } button, @@ -328,6 +334,7 @@ div.root-eject:hover { border-top: 1px solid #d4d4d4; display: -webkit-box; overflow: hidden; + position: relative; } /* Container for the ok/cancel buttons. */ @@ -1122,34 +1129,48 @@ div.shade[fadein] { } /* Message panel for unmounted GData */ -.dialog-container:not([unmounted]) .dialog-body > div.unmounted-panel, -.dialog-container[unmounted] .dialog-body > div:not(.unmounted-panel) { - display: none; -} - -div.unmounted-panel { +#unmounted-panel { + bottom: 0; color: #333; + display: none; + left: 0; padding-left: 50px; padding-top: 20px; + position: absolute; + right: 0; + top: 0; } -div.unmounted-panel > * { +.dialog-container[gdata='mounting'] #unmounted-panel, +.dialog-container[gdata='error'] #unmounted-panel { + display: block; +} + +.dialog-container[gdata='unmounted'] .filelist-panel, +.dialog-container[gdata='mounting'] .filelist-panel, +.dialog-container[gdata='error'] .filelist-panel { + /* Hide file list when GData is not mounted. + Use opacity to avoid manual resizing.*/ + opacity: 0; +} + +#unmounted-panel > * { height: 22px; margin-bottom: 10px; } -div.unmounted-panel > div { +#unmounted-panel > * { -webkit-box-align: center; -webkit-box-orient: horizontal; -webkit-box-pack: start; - display: -webkit-box; + display: none; } -.unmounted-panel > .gdata.loading { +#unmounted-panel > .loading { position: relative; } -.unmounted-panel > .gdata.loading .spinner-box { +#unmounted-panel > .loading > .spinner-box { bottom: 0; position: absolute; right: 100%; @@ -1157,17 +1178,17 @@ div.unmounted-panel > div { width: 40px; } -.unmounted-panel > .gdata.progress { - color: #999; - margin-top: -10px; +[gdata='mounting'] #unmounted-panel > .loading, +[gdata='mounting'] #unmounted-panel > .progress, +[gdata='error'] #unmounted-panel > .error, +#unmounted-panel.retry-enabled > .retry, +#unmounted-panel.retry-enabled > .learn-more { + display: -webkit-box; } -.unmounted-panel:not([loading]) > .gdata.loading, -.unmounted-panel:not([loading]) > .gdata.progress, -.unmounted-panel:not([error]) > .gdata.error, -.unmounted-panel:not([retry]) > .gdata.retry, -.unmounted-panel:not([retry]) > .gdata.learn-more { - display: none; +#unmounted-panel > .progress { + color: #999; + margin-top: -10px; } .plain-link { diff --git a/chrome/browser/resources/file_manager/js/directory_model.js b/chrome/browser/resources/file_manager/js/directory_model.js index 650f296..71e0f1b 100644 --- a/chrome/browser/resources/file_manager/js/directory_model.js +++ b/chrome/browser/resources/file_manager/js/directory_model.js @@ -16,8 +16,11 @@ var SHORT_RESCAN_INTERVAL = 100; * @param {boolean} singleSelection True if only one file could be selected * at the time. * @param {MetadataCache} metadataCache The metadata cache service. + * @param {VolumeManager} volumeManager The volume manager. + * @param {boolean} isGDataEnabled True if GDATA enabled (initial value). */ -function DirectoryModel(root, singleSelection, metadataCache) { +function DirectoryModel(root, singleSelection, + metadataCache, volumeManager, isGDataEnabled) { this.root_ = root; this.metadataCache_ = metadataCache; this.fileList_ = new cr.ui.ArrayDataModel([]); @@ -28,6 +31,7 @@ function DirectoryModel(root, singleSelection, metadataCache) { this.pendingScan_ = null; this.rescanTimeout_ = undefined; this.scanFailures_ = 0; + this.gDataEnabled_ = isGDataEnabled; // DirectoryEntry representing the current directory of the dialog. this.currentDirEntry_ = root; @@ -50,11 +54,7 @@ function DirectoryModel(root, singleSelection, metadataCache) { this.filters_ = {}; this.setFilterHidden(true); - /** - * @private - * @type {Object.<string, boolean>} - */ - this.volumeReadOnlyStatus_ = {}; + this.volumeManager_ = volumeManager; /** * Directory in which search results are displayed. Not null iff search @@ -106,19 +106,13 @@ DirectoryModel.DOWNLOADS_DIRECTORY = 'Downloads'; DirectoryModel.GDATA_DIRECTORY = 'drive'; /** - * GData access mode: disabled (no GData root displayed in the list). - */ -DirectoryModel.GDATA_ACCESS_DISABLED = 0; - -/** - * GData access mode: lazy (GData root displayed, no content is fetched yet). - */ -DirectoryModel.GDATA_ACCESS_LAZY = 1; - -/** - * GData access mode: full (GData root displayed, content is available). + * Fake entry to be used in currentDirEntry_ when current directory is + * unmounted GDATA. + * @private */ -DirectoryModel.GDATA_ACCESS_FULL = 2; +DirectoryModel.fakeGDataEntry_ = { + fullPath: '/' + DirectoryModel.GDATA_DIRECTORY +}; /** * Root path used for displaying gdata content search results. @@ -141,6 +135,15 @@ DirectoryModel.GDATA_SEARCH_ROOT_COMPONENTS = ['', 'drive', '.search']; DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype; /** + * Fills the root list and starts tracking changes. + */ +DirectoryModel.prototype.start = function() { + var volumesChangeHandler = this.onMountChanged_.bind(this); + this.volumeManager_.addEventListener('change', volumesChangeHandler); + this.updateRoots_(); +}; + +/** * @return {cr.ui.ArrayDataModel} Files in the current directory. */ DirectoryModel.prototype.getFileList = function() { @@ -155,6 +158,20 @@ DirectoryModel.prototype.getMetadataCache = function() { }; /** + * Sets whether GDATA appears in the roots list and + * if it could be used as current directory. + * @param {boolead} enabled True if GDATA enabled. + */ +DirectoryModel.prototype.setGDataEnabled = function(enabled) { + if (this.gDataEnabled_ == enabled) + return; + this.gDataEnabled_ = enabled; + this.updateRoots_(); + if (!enabled && this.getCurrentRootType() == DirectoryModel.RootType.GDATA) + this.changeDirectory(this.getDefaultDirectory()); +}; + +/** * Sort the file list. * @param {string} sortField Sort field. * @param {string} sortDirection "asc" or "desc". @@ -227,7 +244,7 @@ DirectoryModel.prototype.isOnGDataSearchDir = 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: @@ -369,6 +386,13 @@ DirectoryModel.prototype.getCurrentRootPath = function() { }; /** + * @return {DirectoryModel.RootType} A root type. + */ +DirectoryModel.prototype.getCurrentRootType = function() { + return DirectoryModel.getRootType(this.currentDirEntry_.fullPath); +}; + +/** * @return {cr.ui.ListSingleSelectionModel} Root list selection model. */ DirectoryModel.prototype.getRootsListSelectionModel = function() { @@ -544,10 +568,6 @@ 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_) { - onDone(); - return; - } this.runningScan_ = this.createScanner_(this.fileList_, onDone); this.runningScan_.run(); }; @@ -799,16 +819,14 @@ DirectoryModel.prototype.changeDirectory = function(path) { */ DirectoryModel.prototype.resolveDirectory = function(path, successCallback, errorCallback) { - if (this.unmountedGDataEntry_ && - 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 (DirectoryModel.getRootType(path) == DirectoryModel.RootType.GDATA) { + if (!this.isGDataMounted_()) { + if (path == DirectoryModel.fakeGDataEntry_.fullPath) + successCallback(DirectoryModel.fakeGDataEntry_); + else // Subdirectory. + errorCallback({ code: FileError.NOT_FOUND_ERR }); + return; + } } if (path == '/') { @@ -866,6 +884,9 @@ DirectoryModel.prototype.changeRoot = function(path) { */ DirectoryModel.prototype.changeDirectoryEntry_ = function(initial, dirEntry, opt_callback) { + if (dirEntry == DirectoryModel.fakeGDataEntry_) + this.volumeManager_.mountGData(function() {}, function() {}); + this.clearSearch_(); var previous = this.currentDirEntry_; this.currentDirEntry_ = dirEntry; @@ -888,6 +909,47 @@ DirectoryModel.prototype.changeDirectoryEntry_ = function(initial, dirEntry, }; /** + * Creates an object wich could say wether directory has changed while it has + * been active or not. Designed for long operations that should be canncelled + * if the used change current directory. + * @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) { + // this == tracker.dm_ here. + if (tracker.exceptInitialChange && event.initial) + return; + tracker.stop(); + tracker.hasChanged = true; + } + }; + return tracker; +}; + +/** * Change the state of the model to reflect the specified path (either a * file or directory). * @@ -905,103 +967,82 @@ 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 resolveCallback = function(exists) { - this.removeEventListener('directory-changed', onExternalDirChange); - if (opt_pathResolveCallback) - opt_pathResolveCallback(baseName, leafName, exists && !overridden); - }.bind(this); + var tracker = this.createDirectoryChangeTracker(); + tracker.start(); - var changeDirectoryEntry = function(entry, initial, exists, opt_callback) { - resolveCallback(exists); - if (!overridden) - this.changeDirectoryEntry_(initial, entry, opt_callback); - }.bind(this); + var self = this; + function resolveCallback(directoryPath, fileName, exists) { + tracker.stop(); + if (!opt_pathResolveCallback) + return; + opt_pathResolveCallback(directoryPath, fileName, + exists && !tracker.hasChanged); + } + + function changeDirectoryEntry(directoryEntry, initial, opt_callback) { + tracker.stop(); + if (!tracker.hasChanged) + self.changeDirectoryEntry_(initial, directoryEntry, opt_callback); + } var INITIAL = true; var EXISTS = true; - // Split the dirname from the basename. - var ary = path.match(/^(?:(.*)\/)?([^\/]*)$/); - - if (!ary) { - console.warn('Unable to split default path: ' + path); - changeDirectoryEntry(this.root_, INITIAL, !EXISTS); - return; + function changeToDefault() { + var def = self.getDefaultDirectory(); + self.resolveDirectory(def, function(directoryEntry) { + resolveCallback(def, '', !EXISTS); + changeDirectoryEntry(directoryEntry, INITIAL); + }, function(error) { + console.error('Failed to resolve default directory: ' + def, error); + resolveCallback('/', '', !EXISTS); + }); } - var baseName = ary[1]; - var leafName = ary[2]; - - function onLeafFound(baseDirEntry, leafEntry) { - if (leafEntry.isDirectory) { - baseName = path; - leafName = ''; - changeDirectoryEntry(leafEntry, INITIAL, EXISTS); - return; - } - - // Leaf is an existing file, cd to its parent directory and select it. - changeDirectoryEntry(baseDirEntry, - !INITIAL /*HACK*/, - EXISTS, - function() { - this.selectEntry(leafEntry.name); - if (opt_loadedCallback) - opt_loadedCallback(); - }.bind(this)); - // TODO(kaznacheev): Fix history.replaceState for the File Browser and - // change !INITIAL to INITIAL. Passing |false| makes things - // less ugly for now. + function noParentDirectory(error) { + console.log('Can\'t resolve parent directory: ' + path, error); + changeToDefault(); } - function onLeafError(baseDirEntry, err) { - // Usually, leaf does not exist, because it's just a suggested file name. - if (err.code != FileError.NOT_FOUND_ERR) - console.log('Unexpected error resolving default leaf: ' + err); - // |baseDirEntry| would point to a system directory if we are trying - // to change to a non-existing removable drive or an archive. - // Try to change to the default directory then. - if (DirectoryModel.isSystemDirectory(baseDirEntry.fullPath)) - onBaseError(err); - else - changeDirectoryEntry(baseDirEntry, INITIAL, !EXISTS); + if (DirectoryModel.isSystemDirectory(path)) { + changeToDefault(); + return; } - var onBaseError = function(err) { - console.log('Unexpected error resolving default base "' + - baseName + '": ' + err); - if (path != this.getDefaultDirectory()) { - // Can't find the provided path, let's go to default one instead. - resolveCallback(!EXISTS); - if (!overridden) - this.setupDefaultPath(opt_loadedCallback); + this.resolveDirectory(path, function(directoryEntry) { + resolveCallback(directoryEntry.fullPath, '', !EXISTS); + changeDirectoryEntry(directoryEntry, INITIAL); + }, function(error) { + // Usually, leaf does not exist, because it's just a suggested file name. + var fileExists = error.code == FileError.TYPE_MISMATCH_ERR; + if (fileExists || error.code == FileError.NOT_FOUND_ERR) { + var nameDelimiter = path.lastIndexOf('/'); + var parentDirectoryPath = path.substr(0, nameDelimiter); + if (DirectoryModel.isSystemDirectory(parentDirectoryPath)) { + changeToDefault(); + return; + } + self.resolveDirectory(parentDirectoryPath, + function(parentDirectoryEntry) { + var fileName = path.substr(nameDelimiter + 1); + resolveCallback(parentDirectoryEntry.fullPath, fileName, fileExists); + changeDirectoryEntry(parentDirectoryEntry, + !INITIAL /*HACK*/, + function() { + self.selectEntry(fileName); + if (opt_loadedCallback) + opt_loadedCallback(); + }); + // TODO(kaznacheev): Fix history.replaceState for the File Browser and + // change !INITIAL to INITIAL. Passing |false| makes things + // less ugly for now. + }, noParentDirectory); } else { - // Well, we can't find the downloads dir. Let's just show something, - // or we will get an infinite recursion. - changeDirectoryEntry(this.root_, opt_loadedCallback, INITIAL, !EXISTS); - } - }.bind(this); - - var onBaseFound = function(baseDirEntry) { - if (!leafName) { - // Default path is just a directory, cd to it and we're done. - changeDirectoryEntry(baseDirEntry, INITIAL, !EXISTS); - return; + // Unexpected errors. + console.error('Directory resolving error: ', error); + changeToDefault(); } - - util.resolvePath(this.root_, path, - onLeafFound.bind(this, baseDirEntry), - onLeafError.bind(this, baseDirEntry)); - }.bind(this); - - var root = this.root_; - if (!baseName) - baseName = this.getDefaultDirectory(); - root.getDirectory(baseName, {create: false}, onBaseFound, onBaseError); + }); }; /** @@ -1073,9 +1114,8 @@ DirectoryModel.prototype.prepareSortEntries_ = function(entries, field, * Get root entries asynchronously. * @private * @param {function(Array.<Entry>)} callback Called when roots are resolved. - * @param {number} gdataAccess One of GDATA_ACCESS_* constants. */ -DirectoryModel.prototype.resolveRoots_ = function(callback, gdataAccess) { +DirectoryModel.prototype.resolveRoots_ = function(callback) { var groups = { downloads: null, archives: null, @@ -1090,7 +1130,6 @@ DirectoryModel.prototype.resolveRoots_ = function(callback, gdataAccess) { if (!groups[i]) return; - self.updateVolumeReadOnlyStatus_(groups.removables); callback(groups.downloads. concat(groups.gdata). concat(groups.archives). @@ -1103,58 +1142,45 @@ DirectoryModel.prototype.resolveRoots_ = function(callback, gdataAccess) { done(); } - function onDownloads(entry) { - groups.downloads = [entry]; - done(); - } - - function onDownloadsError(error) { - groups.downloads = []; + function appendSingle(index, entry) { + groups[index] = [entry]; done(); } - function onGDataMounted(entry) { - console.log('GData mounted:', entry); - self.unmountedGDataEntry_ = null; - groups.gdata = [entry]; + function onSingleError(index, error, defaultValue) { + groups[index] = defailtValue || []; done(); } - function onGDataNotMounted(error) { - console.log('GData not mounted: ' + (error || 'lazy')); - self.unmountedGDataEntry_ = { - unmounted: true, // Clients use this field to distinguish a fake root. - error: error, - toURL: function() { return '' }, - fullPath: '/' + DirectoryModel.GDATA_DIRECTORY - }; - groups.gdata = [self.unmountedGDataEntry_]; - done(); + var root = this.root_; + function readSingle(dir, index, opt_defaultValue) { + root.getDirectory(dir, { create: false }, + appendSingle.bind(this, index), + onSingleError.bind(this, index, opt_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 (gdataAccess == DirectoryModel.GDATA_ACCESS_FULL) { - root.getDirectory(DirectoryModel.GDATA_DIRECTORY, { create: false }, - onGDataMounted, onGDataNotMounted); - } else if (gdataAccess == DirectoryModel.GDATA_ACCESS_LAZY) { - onGDataNotMounted(); + if (this.gDataEnabled_) { + var fake = [DirectoryModel.fakeGDataEntry_]; + if (this.isGDataMounted_()) + readSingle(DirectoryModel.GDATA_DIRECTORY, 'gdata', fake); + else + groups.gdata = fake; } else { groups.gdata = []; } }; /** - * @param {function} callback Called when all roots are resolved. - * @param {number} gdataAccess One of GDATA_ACCESS_* constants. + * Updates the roots list. + * @private */ -DirectoryModel.prototype.updateRoots = function(callback, gdataAccess) { +DirectoryModel.prototype.updateRoots_ = function() { var self = this; this.resolveRoots_(function(rootEntries) { var dm = self.rootsList_; @@ -1162,9 +1188,7 @@ DirectoryModel.prototype.updateRoots = function(callback, gdataAccess) { dm.splice.apply(dm, args); self.updateRootsListSelection_(); - - callback(); - }, gdataAccess); + }); }; /** @@ -1192,44 +1216,46 @@ DirectoryModel.prototype.updateRootsListSelection_ = function() { }; /** - * @param {Array.<DirectoryEntry>} roots Removable volumes entries. + * @return {true} True if GDATA mounted. * @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. + * Handler for the VolumeManager's event. + * @private */ -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); +DirectoryModel.prototype.onMountChanged_ = function() { + this.updateRoots_(); - // 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; - - // 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. + function onGotDirectory(entry) { + if (this.currentDirEntry_ == DirectoryModel.fakeGDataEntry_) { + this.currentDirEntry_ = entry; + this.rescan(); + } + } + this.root_.getDirectory('/' + DirectoryModel.GDATA_DIRECTORY, {}, + onGotDirectory.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_); + } } }; @@ -1238,6 +1264,7 @@ DirectoryModel.prototype.prepareUnmount = function(rootPath) { * @return {boolean} If current directory is system. */ DirectoryModel.isSystemDirectory = function(path) { + path = path.replace(/\/+$/, ''); return path == '/' + DirectoryModel.REMOVABLE_DIRECTORY || path == '/' + DirectoryModel.ARCHIVE_DIRECTORY; }; @@ -1359,7 +1386,7 @@ DirectoryModel.getRootName = function(path) { /** * @param {string} path A path. - * @return {string} A root type. + * @return {DirectoryModel.RootType} A root type. */ DirectoryModel.getRootType = function(path) { function isTop(dir) { @@ -1450,6 +1477,12 @@ DirectoryModel.Scanner.prototype.cancel = function() { * Start scanner. */ DirectoryModel.Scanner.prototype.run = function() { + if (this.dir_ == DirectoryModel.fakeGDataEntry_) { + if (!this.cancelled_) + this.successCallback_(); + return; + } + metrics.startInterval('DirectoryScan'); this.reader_ = this.dir_.createReader(); @@ -1506,3 +1539,128 @@ DirectoryModel.Scanner.prototype.recordMetrics_ = function() { metrics.recordMediumCount('DownloadsCount', this.list_.length); } }; + +/** + * @constructor + * @param {DirectoryEntry} root Root entry. + * @param {DirectoryModel} directoryModel Model to watch. + * @param {VolumeManager} volumeManager Manager to watch. + */ +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); +} + +/** + * Starts watching. + */ +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_(); +}; + +/** + * Stops watching (must be called before page unload). + */ +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); +}; + +/** + * @param {Object} event chrome.fileBrowserPrivate.onFileChanged event. + * @private + */ +FileWatcher.prototype.onFileChanged_ = function(event) { + if (encodeURI(event.fileUrl) == this.watchedDirectoryEntry_.toURL()) + this.onFileInWatchedDirectoryChanged(); +}; + +/** + * Called when file in the watched directory changed. + */ +FileWatcher.prototype.onFileInWatchedDirectoryChanged = function() { + this.dm_.rescanLater(); +}; + +/** + * Called when directory changed or volumes mounted/unmounted. + * @private + */ +FileWatcher.prototype.updateWatchedDirectory_ = function() { + var current = this.watchedDirectoryEntry_; + switch (this.dm_.getCurrentRootType()) { + case DirectoryModel.RootType.GDATA: + if (!this.vm_.isMounted('/' + DirectoryModel.GDATA_DIRECTORY)) + break; + case DirectoryModel.RootType.DOWNLOADS: + case DirectoryModel.RootType.REMOVABLE: + if (!current || current.fullPath != this.dm_.getCurrentDirPath()) { + // TODO(serya): Changed in readonly removable directoried don't + // need to be tracked. + this.root_.getDirectory(this.dm_.getCurrentDirPath(), {}, + this.changeWatchedEntry.bind(this), + this.changeWatchedEntry.bind(this, null)); + } + return; + } + if (current) + this.changeWatchedEntry(null); +}; + +/** + * @param {Entry?} entry Null if no directory need to be watched or + * directory to watch. + */ +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'); + if (this.watchedDirectoryEntry_ == entry) + this.watchedDirectoryEntry_ = null; + } + }.bind(this)); + } +}; + +/** + * @return {DirectoryEntry} Current watched directory entry. + */ +FileWatcher.prototype.getWatchedDirectoryEntry = function() { + return this.watchedDirectoryEntry_; +}; diff --git a/chrome/browser/resources/file_manager/js/file_manager.js b/chrome/browser/resources/file_manager/js/file_manager.js index c51ceaf..7788b21 100644 --- a/chrome/browser/resources/file_manager/js/file_manager.js +++ b/chrome/browser/resources/file_manager/js/file_manager.js @@ -21,6 +21,7 @@ function FileManager(dialogDom) { {}; this.listType_ = null; + this.showDelayTimeout_ = null; this.selection = null; @@ -28,7 +29,6 @@ function FileManager(dialogDom) { this.currentButter_ = null; this.butterLastShowTime_ = 0; - this.watchedDirectoryUrl_ = null; this.filesystemObserverId_ = null; this.gdataObserverId_ = null; @@ -50,9 +50,9 @@ function FileManager(dialogDom) { this.locale_ = new v8Locale(navigator.language); this.initFileSystem_(); + this.volumeManager_ = VolumeManager.getInstance(); this.initDom_(); this.initDialogType_(); - this.dialogDom_.style.opacity = '1'; } /** @@ -216,18 +216,6 @@ FileManager.prototype = { return child_path.indexOf(parent_path) == 0; } - /** - * Normalizes path not to start with / - * - * @param {string} path The file path. - */ - function normalizeAbsolutePath(x) { - if (x[0] == '/') - return x.slice(1); - else - return x; - } - function removeChildren(element) { element.textContent = ''; } @@ -270,6 +258,97 @@ FileManager.prototype = { }; /** + * FileWatcher that also watches for metadata changes. + * @extends {FileWatcher} + */ + FileManager.MetadataFileWatcher = function(fileManager) { + FileWatcher.call(this, + fileManager.filesystem_.root, + fileManager.directoryModel_, + fileManager.volumeManager_); + this.metadataCache_ = fileManager.metadataCache_; + + this.filesystemChanngeHandler_ = + fileManager.updateMetadataInUI_.bind(fileManager, 'filesystem'); + this.gdataChanngeHandler_ = + fileManager.updateMetadataInUI_.bind(fileManager, 'gdata'); + + this.filesystemObserverId_ = null; + this.gdataObserverId_ = null; + + // Holds the directories known to contain files with stale metadata + // as URL to bool map. + this.directoriesWithStaleMetadata_ = {}; + }; + + FileManager.MetadataFileWatcher.prototype.__proto__ = FileWatcher.prototype; + + /** + * Changed metadata observers for the new directory. + * @override + * @param {DirectoryEntryi?} entry New watched directory entry. + * @override + */ + FileManager.MetadataFileWatcher.prototype.changeWatchedEntry = + function(entry) { + FileWatcher.prototype.changeWatchedEntry.call(this, entry); + + if (this.filesystemObserverId_) + this.metadataCache_.removeObserver(this.filesystemObserverId_); + if (this.gdataObserverId_) + this.metadataCache_.removeObserver(this.gdataObserverId_); + this.filesystemObserverId_ = null; + this.gdataObserverId_ = null; + if (!entry) + return; + + this.filesystemObserverId_ = this.metadataCache_.addObserver( + entry, + MetadataCache.CHILDREN, + 'filesystem', + this.filesystemChanngeHandler_); + + if (DirectoryModel.getRootType(entry.fullPath) == + DirectoryModel.RootType.GDATA) { + this.gdataObserverId_ = this.metadataCache_.addObserver( + entry, + MetadataCache.CHILDREN, + 'gdata', + this.gdataChanngeHandler_); + } + }; + + /** + * @override + */ + FileManager.MetadataFileWatcher.prototype.onFileInWatchedDirectoryChanged = + function() { + FileWatcher.prototype.onFileInWatchedDirectoryChanged.apply(this); + delete this.directoriesWithStaleMetadata_[ + this.getWatchedDirectoryEntry().toURL()]; + }; + + /** + * Ask the GData service to re-fetch the metadata for the current directory. + */ + FileManager.MetadataFileWatcher.prototype.requestMetadataRefresh = + function(imageFileEntry) { + if (DirectoryModel.getRootType(imageFileEntry.fullPath) != + DirectoryModel.RootType.GDATA) { + return; + } + // TODO(kaznacheev) This does not really work with GData search. + var imageURL = imageFileEntry.toURL(); + var url = imageURL.substr(0, imageURL.lastIndexOf('/')); + // Skip if the current directory is now being refreshed. + if (this.directoriesWithStaleMetadata_[url]) + return; + + this.directoriesWithStaleMetadata_[url] = true; + chrome.fileBrowserPrivate.requestDirectoryRefresh(url); + }; + + /** * Load translated strings. */ FileManager.initStrings = function(callback) { @@ -280,6 +359,38 @@ FileManager.prototype = { }); }; + /** + * FileManager initially created hidden to prevent flickering. + * When DOM is almost constructed it need to be shown. Cancels + * delayed show. + */ + FileManager.prototype.show_ = function() { + if (this.showDelayTimeout_) { + clearTimeout(this.showDelayTimeout_); + showDelayTimeout_ = null; + } + this.dialogDom_.classList.add('loaded'); + }; + + /** + * If initialization code think that right after initialization + * something going to be shown instead of just a file list (like Gallery) + * it may delay show to prevent flickering. However initialization may take + * significant time and we don't want to keep it hidden for too long. + * So it will be shown not more than in 0.5 sec. If initialization completed + * the page must show immediatelly. + * + * @param {number} delay In milliseconds. + */ + FileManager.prototype.delayShow_ = function(delay) { + if (!this.showDelayTimeout_) { + this.showDelayTimeout_ = setTimeout(function() { + this.showDelayTimeout_ = null; + this.show_(); + }.bind(this), delay); + } + }; + // Instance methods. /** @@ -298,30 +409,23 @@ FileManager.prototype = { metrics.startInterval('Load.FileSystem'); var self = this; - - // The list of active mount points to distinct them from other directories. - chrome.fileBrowserPrivate.getMountPoints(function(mountPoints) { - self.setMountPoints_(mountPoints); - onDone(); - }); - - function onDone() { - if (self.mountPoints_ && self.filesystem_) + var downcount = 2; + function done() { + if (--downcount == 0) self.init_(); } chrome.fileBrowserPrivate.requestLocalFileSystem(function(filesystem) { metrics.recordInterval('Load.FileSystem'); self.filesystem_ = filesystem; - onDone(); + done(); }); - }; - FileManager.prototype.setMountPoints_ = function(mountPoints) { - this.mountPoints_ = mountPoints; - // Add gdata mount info if present. - if (this.gdataMounted_) - this.mountPoints_.push(this.gdataMountInfo_); + // GDATA preferences should be initialized before creating DirectoryModel + // to tot rebuild the roots list. + this.updateNetworkStateAndGDataPreferences_(function() { + done(); + }); }; /** @@ -360,78 +464,35 @@ FileManager.prototype = { window.addEventListener('popstate', this.onPopState_.bind(this)); window.addEventListener('unload', this.onUnload_.bind(this)); - this.directoryModel_.addEventListener('directory-changed', - this.onDirectoryChanged_.bind(this)); + var dm = this.directoryModel_; + dm.addEventListener('directory-changed', + this.onDirectoryChanged_.bind(this)); var self = this; - this.directoryModel_.addEventListener('begin-update-files', function() { + dm.addEventListener('begin-update-files', function() { self.currentList_.startBatchUpdates(); }); - this.directoryModel_.addEventListener('end-update-files', function() { + dm.addEventListener('end-update-files', function() { self.restoreItemBeingRenamed_(); self.currentList_.endBatchUpdates(); }); - this.directoryModel_.addEventListener('scan-started', - this.showSpinnerLater_.bind(this)); - this.directoryModel_.addEventListener('scan-completed', - this.showSpinner_.bind(this, false)); - this.directoryModel_.addEventListener('scan-completed', - this.refreshCurrentDirectoryMetadata_.bind(this)); - this.directoryModel_.addEventListener('rescan-completed', - this.refreshCurrentDirectoryMetadata_.bind(this)); + dm.addEventListener('scan-started', this.showSpinnerLater_.bind(this)); + dm.addEventListener('scan-completed', this.showSpinner_.bind(this, false)); + dm.addEventListener('scan-completed', + this.refreshCurrentDirectoryMetadata_.bind(this)); + dm.addEventListener('rescan-completed', + this.refreshCurrentDirectoryMetadata_.bind(this)); this.addEventListener('selection-summarized', this.onSelectionSummarized_.bind(this)); - // The list of archives requested to mount. We will show contents once - // archive is mounted, but only for mounts from within this filebrowser tab. - this.mountRequests_ = []; - this.unmountRequests_ = []; - chrome.fileBrowserPrivate.onMountCompleted.addListener( - this.onMountCompleted_.bind(this)); - - chrome.fileBrowserPrivate.onFileChanged.addListener( - this.onFileChanged_.bind(this)); - - var queryGDataPreferences = function() { - chrome.fileBrowserPrivate.getGDataPreferences( - this.onGDataPreferencesChanged_.bind(this)); - }.bind(this); - queryGDataPreferences(); - chrome.fileBrowserPrivate.onGDataPreferencesChanged. - addListener(queryGDataPreferences); - - var queryNetworkConnectionState = function() { - chrome.fileBrowserPrivate.getNetworkConnectionState( - this.onNetworkConnectionChanged_.bind(this)); - }.bind(this); - queryNetworkConnectionState(); - chrome.fileBrowserPrivate.onNetworkConnectionChanged. - addListener(queryNetworkConnectionState); - - var invokeHandler = !this.params_.selectOnly; - if (this.isStartingOnGData_()) { - // We are opening on a GData path. Mount GData and show - // "Loading Google Docs" message until the directory content loads. - this.dialogContainer_.setAttribute('unmounted', true); - this.initGData_(true /* dirChanged */); - // This is a one-time handler (will be nulled out on the first call). - this.setupCurrentDirectoryPostponed_ = function(event) { - this.directoryModel_.removeEventListener('directory-changed', - this.setupCurrentDirectoryPostponed_); - this.setupCurrentDirectoryPostponed_ = null; - if (event) // If called as an event handler just exit silently. - return; - this.setupCurrentDirectory_( - invokeHandler, false /* blankWhileOpeningAFile */); - }.bind(this); - this.directoryModel_.addEventListener('directory-changed', - this.setupCurrentDirectoryPostponed_); - } else { - this.setupCurrentDirectory_( - invokeHandler, true /* blankWhileOpeningAFile */); - } + this.setupCurrentDirectory_(true /* page loading */); - if (this.isGDataEnabled()) - this.setupGDataWelcome_(); + var stateChangeHandler = + this.onNetworkStateOrGDataPreferencesChanged_.bind(this); + chrome.fileBrowserPrivate.onGDataPreferencesChanged.addListener( + stateChangeHandler); + chrome.fileBrowserPrivate.onNetworkConnectionChanged.addListener( + stateChangeHandler); + stateChangeHandler(); this.summarizeSelection_(); @@ -447,10 +508,6 @@ FileManager.prototype = { this.metadataProvider_ = new MetadataProvider(this.filesystem_.root.toURL()); - // Holds the directories known to contain files with stale metadata - // as URL to bool map. - this.directoriesWithStaleMetadata_ = {}; - // PyAuto tests monitor this state by polling this variable this.__defineGetter__('workerInitialized_', function() { return self.getMetadataProvider().isInitialized(); @@ -462,6 +519,9 @@ FileManager.prototype = { this.table_.endBatchUpdates(); this.grid_.endBatchUpdates(); + // Show the page now unless it's already delayed. + this.delayShow_(0); + metrics.recordInterval('Load.DOM'); metrics.recordInterval('Load.Total'); }; @@ -550,7 +610,7 @@ FileManager.prototype = { this.spinner_ = this.dialogDom_.querySelector('.spinner'); this.showSpinner_(false); this.butter_ = this.dialogDom_.querySelector('.butter-bar'); - this.unmountedPanel_ = this.dialogDom_.querySelector('.unmounted-panel'); + this.unmountedPanel_ = this.dialogDom_.querySelector('#unmounted-panel'); cr.ui.decorate('#gdata-settings', cr.ui.MenuButton); cr.ui.Table.decorate(this.table_); @@ -600,11 +660,6 @@ FileManager.prototype = { this.dialogDom_.querySelector('#thumbnail-view').addEventListener( 'click', this.onThumbnailViewButtonClick_.bind(this)); - // When we show the page for the first time we want to avoid - // the GDrive settings button animation, so we set the attribute ASAP. - if (this.isStartingOnGData_()) - this.dialogContainer_.setAttribute('gdata', true); - this.syncButton = this.dialogDom_.querySelector('#gdata-sync-settings'); this.syncButton.addEventListener('click', this.onGDataPrefClick_.bind( this, 'cellularDisabled', false /* not inverted */)); @@ -650,7 +705,14 @@ FileManager.prototype = { this.directoryModel_ = new DirectoryModel( this.filesystem_.root, singleSelection, - this.metadataCache_); + this.metadataCache_, + this.volumeManager_, + this.isGDataEnabled()); + + this.directoryModel_.start(); + + this.fileWatcher_ = new FileManager.MetadataFileWatcher(this); + this.fileWatcher_.start(); var dataModel = this.directoryModel_.getFileList(); var collator = this.collator_; @@ -686,6 +748,12 @@ FileManager.prototype = { this.textSearchState_ = {text: '', date: new Date()}; + this.volumeManager_.addEventListener('gdata-status-changed', + this.updateGDataUnmountedPanel_.bind(this)); + if (this.params_.mountTriggered) { + this.volumeManager_.addEventListener('externally-unmounted', + this.onExternallyUnmounted_.bind(this)); + } // Update metadata to change 'Today' and 'Yesterday' dates. var today = new Date(); today.setHours(0); @@ -699,7 +767,6 @@ FileManager.prototype = { FileManager.prototype.initRootsList_ = function() { this.rootsList_ = this.dialogDom_.querySelector('#roots-list'); cr.ui.List.decorate(this.rootsList_); - this.rootsList_.startBatchUpdates(); var self = this; this.rootsList_.itemConstructor = function(entry) { @@ -711,118 +778,68 @@ FileManager.prototype = { // TODO(dgozman): add "Add a drive" item. this.rootsList_.dataModel = this.directoryModel_.getRootsList(); - this.directoryModel_.updateRoots(function() { - self.rootsList_.endBatchUpdates(); - }, this.getGDataAccessMode_()); }; /** - * @param {boolean} dirChanged True if we just changed to GData directory, - * False if "Retry" button clicked. + * Shows the panel when current directory is GDATA and it's unmounted. + * Hides it otherwise. The pannel shows spinner if GDATA is mounting or + * an error message if it failed. */ - FileManager.prototype.initGData_ = function(dirChanged) { - this.initGDataUnmountedPanel_(); - - this.unmountedPanel_.removeAttribute('error'); - if (dirChanged) { - // When changing to GData directory we want to see a clear panel. - this.unmountedPanel_.removeAttribute('retry'); - if (this.gdataLoadingTimer_) { // Show immediately if already loading. - this.unmountedPanel_.setAttribute('loading', true); - } else { - this.unmountedPanel_.removeAttribute('loading'); - setTimeout(function() { - if (this.gdataLoadingTimer_) { // Still loading. - this.unmountedPanel_.setAttribute('loading', true); - } - }.bind(this), 500); + FileManager.prototype.updateGDataUnmountedPanel_ = function() { + var node = this.dialogContainer_; + if (this.isOnGData()) { + var status = this.volumeManager_.getGDataStatus(); + if (status == VolumeManager.GDataStatus.MOUNTING || + status == VolumeManager.GDataStatus.ERROR) { + this.ensureGDataUnmountedPanelInitialized_(); } + if (status == VolumeManager.GDataStatus.ERROR) + this.unmountedPanel_.classList.add('retry-enabled'); + node.setAttribute('gdata', status); } else { - // When retrying we do not hide "Retry" and "Learn more". - this.unmountedPanel_.setAttribute('loading', true); + node.removeAttribute('gdata'); } - - // If the user changed to another directory and then back to GData we - // re-enter this method while the timer is still active. In this case - // we only update the UI but do not request the mount again. - if (this.gdataLoadingTimer_) - return; - - metrics.startInterval('Load.GData'); - chrome.fileBrowserPrivate.addMount('', 'gdata', {}, - function(sourcePath) {}); - - // This timer could fire before the mount succeeds. We will silently - // replace the error message with the correct directory contents. - this.gdataLoadingTimer_ = setTimeout(function() { - this.gdataLoadingTimer_ = null; - this.onGDataUnreachable_('GData load timeout'); - }.bind(this), - 15 * 60 * 1000); }; - FileManager.prototype.clearGDataLoadingTimer_ = function(message) { - if (this.gdataLoadingTimer_) { - clearTimeout(this.gdataLoadingTimer_); - this.gdataLoadingTimer_ = null; - } - }; - - FileManager.prototype.onGDataUnreachable_ = function(message) { - console.warn(message); - this.gdataMounted_ = false; - this.gdataMountInfo_ = null; - this.clearGDataLoadingTimer_(); - if (this.isOnGData()) { - this.unmountedPanel_.removeAttribute('loading'); - this.unmountedPanel_.setAttribute('error', true); - this.unmountedPanel_.setAttribute('retry', true); - } - }; - - FileManager.prototype.initGDataUnmountedPanel_ = function() { - if (this.unmountedPanel_.firstElementChild) + /** + * Creates contents for the GDATA unmounted panel. + */ + FileManager.prototype.ensureGDataUnmountedPanelInitialized_ = function() { + var panel = this.unmountedPanel_; + if (panel.firstElementChild) return; - var loading = this.document_.createElement('div'); - loading.className = 'gdata loading'; - loading.textContent = str('GDATA_LOADING'); - this.unmountedPanel_.appendChild(loading); - - var spinnerBox = this.document_.createElement('div'); - spinnerBox.className = 'spinner-box'; - loading.appendChild(spinnerBox); - - var spinner = this.document_.createElement('div'); - spinner.className = 'spinner'; - spinnerBox.appendChild(spinner); - - var progress = this.document_.createElement('div'); - progress.className = 'gdata progress'; - this.unmountedPanel_.appendChild(progress); + function create(parent, tag, className, opt_textContent) { + var div = panel.ownerDocument.createElement(tag); + div.className = className; + div.textContent = opt_textContent || ''; + parent.appendChild(div); + return div; + } + var loading = create(panel, 'div', 'loading', str('GDATA_LOADING')); + var spinnerBox = create(loading, 'div', 'spinner-box'); + create(spinnerBox, 'div', 'spinner'); + var progress = create(panel, 'div', 'progress'); chrome.fileBrowserPrivate.onDocumentFeedFetched.addListener( function(fileCount) { progress.textContent = strf('GDATA_LOADING_PROGRESS', fileCount); }); - var error = this.document_.createElement('div'); - error.className = 'gdata error'; - error.textContent = strf('GDATA_CANNOT_REACH', str('GDATA_PRODUCT_NAME')); - this.unmountedPanel_.appendChild(error); + create(panel, 'div', 'error', + strf('GDATA_CANNOT_REACH', str('GDATA_PRODUCT_NAME'))); - var retry = this.document_.createElement('button'); - retry.className = 'gdata retry'; - retry.textContent = str('GDATA_RETRY'); - retry.onclick = this.initGData_.bind(this, false /* retry */); - this.unmountedPanel_.appendChild(retry); + var retryButton = create(panel, 'button', 'retry', str('GDATA_RETRY')); + retryButton.hidden = true; + var vm = this.volumeManager_; + retryButton.onclick = function() { + vm.mountGData(function() {}, function() {}); + }; - var learnMore = this.document_.createElement('div'); - learnMore.className = 'gdata learn-more plain-link'; - learnMore.textContent = str('GDATA_LEARN_MORE'); - learnMore.addEventListener('click', - this.onExternalLinkClick_.bind(this, GOOGLE_DRIVE_ERROR_HELP_URL)); - this.unmountedPanel_.appendChild(learnMore); + var learnMore = create(panel, 'div', 'learn-more plain-link', + str('GDATA_LEARN_MORE')); + learnMore.onclick = this.onExternalLinkClick_.bind(this, + GOOGLE_DRIVE_ERROR_HELP_URL); }; FileManager.prototype.onDataModelSplice_ = function(event) { @@ -1316,8 +1333,7 @@ FileManager.prototype = { * update event). */ FileManager.prototype.onCopyManagerOperationComplete_ = function(event) { - var currentPath = - this.directoryModel_.getCurrentDirPath(); + var currentPath = this.directoryModel_.getCurrentDirPath(); if (this.isOnGData() && this.directoryModel_.isSearching()) return; @@ -1373,7 +1389,8 @@ FileManager.prototype = { return; case 'unmount': - this.unmountVolume_(this.directoryModel_.getCurrentRootDirEntry()); + this.unmountVolume_( + this.directoryModel_.getCurrentRootDirEntry()); return; case 'format': @@ -1391,12 +1408,7 @@ FileManager.prototype = { */ FileManager.prototype.onPopState_ = function(event) { this.closeFilePopup_(); - // Nothing left to do if the current directory is not changing. This happens - // if we are exiting the Gallery. - if (this.getPathFromUrlOrParams_() == - this.directoryModel_.getCurrentDirEntry().fullPath) - return; - this.setupCurrentDirectory_(true /* invokeHandler */); + this.setupCurrentDirectory_(false /* page loading */); }; FileManager.prototype.requestResize_ = function(timeout) { @@ -1436,12 +1448,6 @@ FileManager.prototype = { errorCallback); }; - FileManager.prototype.getPathFromUrlOrParams_ = function() { - return location.hash ? // Location hash has the highest priority. - decodeURI(location.hash.substr(1)) : - this.params_.defaultPath; - }; - /** * Restores current directory and may be a selected item after page load (or * reload) or popping a state (after click on back/forward). If location.hash @@ -1450,14 +1456,16 @@ FileManager.prototype = { * Default path may also contain a file name. Freshly opened file manager * window has neither. * - * @param {boolean} invokeHandler Whether to invoke the default handler on - * the selected file. - * @param {boolean} opt_blankWhileOpeningAFile Whether to show fade over - * the file manager. + * @param {boolean} pageLoading True if the page is loading, + false if popping state. */ - FileManager.prototype.setupCurrentDirectory_ = - function(invokeHandler, opt_blankWhileOpeningAFile) { - var path = this.getPathFromUrlOrParams_(); + FileManager.prototype.setupCurrentDirectory_ = function(pageLoading) { + var path = location.hash ? // Location hash has the highest priority. + decodeURI(location.hash.substr(1)) : + this.params_.defaultPath; + + if (!pageLoading && path == this.directoryModel_.getCurrentDirPath()) + return; if (!path) { this.directoryModel_.setupDefaultPath(); @@ -1467,22 +1475,63 @@ FileManager.prototype = { // In the FULL_PAGE mode if the hash path points to a file we might have // to invoke a task after selecting it. // If the file path is in params_ we only want to select the file. - if (invokeHandler && location.hash && - this.dialogType_ == FileManager.DialogType.FULL_PAGE) { - // To prevent the file list flickering for a moment before the action - // is executed we hide it under a white div. - var shade; - if (opt_blankWhileOpeningAFile) { - shade = this.document_.createElement('div'); - shade.className = 'overlay-pane'; - shade.style.backgroundColor = 'white'; - this.document_.body.appendChild(shade); + var invokeHandlers = pageLoading && !this.params_.selectOnly && + this.dialogType_ == FileManager.DialogType.FULL_PAGE && + !!location.hash; + + if (DirectoryModel.getRootType(path) == DirectoryModel.RootType.GDATA) { + var tracker = this.directoryModel_.createDirectoryChangeTracker(); + // Expected finish of setupPath to GData. + tracker.exceptInitialChange = true; + tracker.start(); + if (!this.isGDataEnabled()) { + if (pageLoading) + this.show_(); + this.directoryModel_.setupDefaultPath(); + return; + } + var gdataPath = '/' + DirectoryModel.GDATA_DIRECTORY; + if (this.volumeManager_.isMounted(gdataPath)) { + this.finishSetupCurrentDirectory_(path, invokeHandlers); + return; } - function removeShade() { - if (shade) - shade.parentNode.removeChild(shade); + if (pageLoading) + this.delayShow_(500); + // Reflect immediatelly in the UI we are on GData and display + // mounting UI. + this.directoryModel_.setupPath(gdataPath); + + if (!this.isOnGData()) { + // Since GDATA is not mounted it should be resolved synchronously + // (no need in asynchronous calls to filesystem API). It is important + // to prevent race condition. + console.error('Expected path set up synchronously'); } + var self = this; + this.volumeManager_.mountGData(function() { + tracker.stop(); + if (!tracker.hasChanged) { + self.finishSetupCurrentDirectory_(path, invokeHandlers); + } + }, function(error) { + tracker.stop(); + }); + } else { + if (invokeHandlers && pageLoading) + this.delayShow_(500); + this.finishSetupCurrentDirectory_(path, invokeHandlers); + } + }; + + /** + * @param {string} path Path to setup. + * @param {boolean} invokeHandlers If thrue and |path| points to a file + * then default handler is triggered. + */ + FileManager.prototype.finishSetupCurrentDirectory_ = function( + path, invokeHandlers) { + if (invokeHandlers) { // Keep track of whether the path is identified as an existing leaf // node. Note that onResolve is guaranteed to be called (exactly once) // before onLoadedActivateLeaf. @@ -1490,8 +1539,8 @@ FileManager.prototype = { function onResolve(baseName, leafName, exists) { if (!exists || leafName == '') { // Non-existent file or a directory. Remove the shade immediately. - removeShade(); foundLeaf = false; + self.show_(); } } @@ -1509,13 +1558,12 @@ FileManager.prototype = { if (FileType.isImageOrVideo(path)) { self.dispatchInternalTask_('gallery', self.selection.urls); } else if (FileType.getMediaType(path) == 'archive') { + self.show_(); self.dispatchInternalTask_('mount-archive', self.selection.urls); } else { - // Manually entered path, do nothing, remove the shade ASAP. - removeShade(); + self.show_(); return; } - setTimeout(removeShade, 1000); } } this.directoryModel_.setupPath(path, onLoadedActivateLeaf, onResolve); @@ -1531,6 +1579,7 @@ FileManager.prototype = { return; } + this.show_(); this.directoryModel_.setupPath(path); }; @@ -1692,22 +1741,6 @@ FileManager.prototype = { }; /** - * Ask the GData service to re-fetch the metadata for the current directory. - */ - FileManager.prototype.requestMetadataRefresh = function() { - if (!this.isOnGData()) - return; - // TODO(kaznacheev) This does not really work with GData search. - var url = this.getCurrentDirectoryURL(); - // Skip if the current directory is now being refreshed. - if (this.directoriesWithStaleMetadata_[url]) - return; - - this.directoriesWithStaleMetadata_[url] = true; - chrome.fileBrowserPrivate.requestDirectoryRefresh(url); - }; - - /** * Create a box containing a centered thumbnail image. * * @param {Entry} entry Entry which thumbnail is generating for. @@ -1745,12 +1778,11 @@ FileManager.prototype = { var cached = self.thumbnailUrlCache_[entry.fullPath]; if (!cached.failed) { cached.failed = true; - self.requestMetadataRefresh(); // Failing to fetch a thumbnail likely means that the thumbnail URL // is now stale. Request a refresh of the current directory, to get // the new thumbnail URLs. Once the directory is refreshed, we'll get // notified via onFileChanged event. - self.requestMetadataRefresh(); + self.fileWatcher_.requestMetadataRefresh(entry); } }; img.src = url; @@ -1843,43 +1875,34 @@ FileManager.prototype = { var div = this.document_.createElement('div'); div.className = 'root-label'; - var icon = rootType; - var deviceNumber = this.getDeviceNumber(entry); - - if (deviceNumber != undefined) { - var mountCondition = this.mountPoints_[deviceNumber].mountCondition; - if (mountCondition == 'unknown_filesystem' || - mountCondition == 'unsupported_filesystem') - icon = 'unreadable'; - } - - div.setAttribute('icon', icon); - div.textContent = this.getRootLabel_(entry.fullPath); li.appendChild(div); if (rootType == DirectoryModel.RootType.ARCHIVE || rootType == DirectoryModel.RootType.REMOVABLE) { - if (entry.unmounting) { - li.setAttribute('disabled', 'disabled'); - } else { - var eject = this.document_.createElement('div'); - eject.className = 'root-eject'; - eject.addEventListener('click', function(event) { - event.stopPropagation(); - this.unmountVolume_(entry); - }.bind(this)); - // Block other mouse handlers. - eject.addEventListener('mouseup', function(e) { e.stopPropagation() }); - eject.addEventListener('mousedown', function(e) { e.stopPropagation() }); - li.appendChild(eject); - - cr.ui.contextMenuHandler.setContextMenu(li, this.rootsContextMenu_); - } + var eject = this.document_.createElement('div'); + eject.className = 'root-eject'; + eject.addEventListener('click', function(event) { + event.stopPropagation(); + this.unmountVolume_(entry); + }.bind(this)); + // Block other mouse handlers. + eject.addEventListener('mouseup', function(e) { e.stopPropagation() }); + eject.addEventListener('mousedown', function(e) { e.stopPropagation() }); + li.appendChild(eject); + + cr.ui.contextMenuHandler.setContextMenu(li, this.rootsContextMenu_); } cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR); cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR); + + var icon = rootType; + if (this.volumeManager_.isUnreadable(entry.fullPath)) { + icon = 'unreadable'; + } + div.setAttribute('icon', icon); + return li; }; @@ -1888,9 +1911,16 @@ FileManager.prototype = { * @param {Entry} entry The entry to unmount. */ FileManager.prototype.unmountVolume_ = function(entry) { - this.directoryModel_.prepareUnmount(entry.fullPath); - this.unmountRequests_.push(entry.fullPath); - chrome.fileBrowserPrivate.removeMount(entry.toURL()); + listItem = this.rootsList_.getListItem(entry); + if (listItem) + listItem.setAttribute('disabled', ''); + var self = this; + this.volumeManager_.unmount(entry.fullPath, function() {}, + function(error) { + if (listItem) + listItem.removeAttribute('disabled'); + self.alert.show(strf('UNMOUNT_FAILED', error.message)); + }); }; FileManager.prototype.updateGDataStyle_ = function( @@ -2319,9 +2349,12 @@ FileManager.prototype = { }.bind(this)); if (this.isOnGData()) { + function predicate(p) { + return !(p && p.availableOffline); + } this.metadataCache_.get(selection.urls, 'gdata', function(props) { selection.allGDataFilesPresent = - props.filter(function(p) {return !p.availableOffline}).length == 0; + props.filter(predicate).length == 0; this.updateOkButton_(); }.bind(this)); } @@ -2460,44 +2493,9 @@ FileManager.prototype = { } }; - FileManager.prototype.isGDataEnabled = function() { - return this.getGDataPreferences_().driveEnabled; - }; - - FileManager.prototype.updateGDataAccess_ = function() { - if (this.isGDataEnabled()) - this.setupGDataWelcome_(); - else - this.cleanupGDataWelcome_(); - - var changeDirectory = !this.isGDataEnabled() && this.isOnGData(); - - this.directoryModel_.updateRoots(function() { - if (changeDirectory) - this.directoryModel_.changeDirectory( - this.directoryModel_.getDefaultDirectory()); - }.bind(this), this.getGDataAccessMode_()); - }; - - FileManager.prototype.getGDataAccessMode_ = function() { - if (!this.isGDataEnabled()) - return DirectoryModel.GDATA_ACCESS_DISABLED; - if (!this.gdataMounted_) - return DirectoryModel.GDATA_ACCESS_LAZY; - return DirectoryModel.GDATA_ACCESS_FULL; - }; - FileManager.prototype.isOnGData = function() { - return this.directoryModel_ && - this.directoryModel_.getCurrentRootPath() == - '/' + DirectoryModel.GDATA_DIRECTORY; - }; - - FileManager.prototype.isStartingOnGData_ = function() { - var path = this.getPathFromUrlOrParams_(); - return path && - this.isGDataEnabled() && - DirectoryModel.getRootType(path) == DirectoryModel.RootType.GDATA; + return this.directoryModel_.getCurrentRootType() == + DirectoryModel.RootType.GDATA; }; FileManager.prototype.getMetadataProvider = function() { @@ -2737,160 +2735,88 @@ FileManager.prototype = { } }; - FileManager.prototype.getGDataPreferences_ = function() { - return this.gdataPreferences_ || - { driveEnabled: loadTimeData.getBoolean('ENABLE_GDATA') }; - }; + FileManager.prototype.updateNetworkStateAndGDataPreferences_ = function( + callback) { + var self = this; + var downcount = 2; + function done() { + if (--downcount == 0) + callback(); + } - FileManager.prototype.getNetworkConnectionState_ = function() { - return this.networkConnectionState_ || {}; - }; + chrome.fileBrowserPrivate.getGDataPreferences(function(prefs) { + self.gdataPreferences_ = prefs; + done(); + }); - FileManager.prototype.onNetworkConnectionChanged_ = function(state) { - console.log(state.online ? 'online' : 'offline', state.type); - this.networkConnectionState_ = state; - this.directoryModel_.setOffline(!state.online); - this.updateConnectionState_(); + chrome.fileBrowserPrivate.getNetworkConnectionState(function(netwokState) { + self.networkState_ = netwokState; + done(); + }); }; - FileManager.prototype.onGDataPreferencesChanged_ = function(preferences) { - var gdataWasEnabled = this.isGDataEnabled(); - this.gdataPreferences_ = preferences; - if (gdataWasEnabled != this.isGDataEnabled()) - this.updateGDataAccess_(); + FileManager.prototype.onNetworkStateOrGDataPreferencesChanged_ = function() { + var self = this; + this.updateNetworkStateAndGDataPreferences_(function() { + var gdata = self.gdataPreferences_; + var network = self.networkState_; - if (preferences.cellularDisabled) - this.syncButton.setAttribute('checked', 'checked'); - else - this.syncButton.removeAttribute('checked'); + self.directoryModel_.setGDataEnabled(self.isGDataEnabled()); - if (!preferences.hostedFilesDisabled) - this.hostedButton.setAttribute('checked', 'checked'); - else - this.hostedButton.removeAttribute('checked'); + if (self.isGDataEnabled()) + self.setupGDataWelcome_(); + else + self.cleanupGDataWelcome_(); - this.updateConnectionState_(); - }; + if (gdata.cellularDisabled) + self.syncButton.setAttribute('checked', ''); + else + self.syncButton.removeAttribute('checked'); - FileManager.prototype.updateConnectionState_ = function() { - if (this.isOffline()) - this.dialogContainer_.setAttribute('connection', 'offline'); - else if (this.isOnMeteredConnection()) - this.dialogContainer_.setAttribute('connection', 'metered'); - else - this.dialogContainer_.removeAttribute('connection'); + if (!gdata.hostedFilesDisabled) + self.hostedButton.setAttribute('checked', ''); + else + self.hostedButton.removeAttribute('checked'); + + if (network.online) { + if (gdata.cellularDisabled && network.type == 'cellular') + self.dialogContainer_.setAttribute('connection', 'metered'); + else + self.dialogContainer_.removeAttribute('connection'); + } else { + self.dialogContainer_.setAttribute('connection', 'offline'); + } + }); }; FileManager.prototype.isOnMeteredConnection = function() { - return this.getGDataPreferences_().cellularDisabled && - this.getNetworkConnectionState_().online && - this.getNetworkConnectionState_().type == 'cellular'; + return this.gdataPreferences_.cellularDisabled && + this.networkState_.online && + this.networkState_.type == 'cellular'; }; FileManager.prototype.isOffline = function() { - return !this.getNetworkConnectionState_().online; + return !this.networkState_.online; + }; + + FileManager.prototype.isGDataEnabled = function() { + return !('driveEnabled' in this.gdataPreferences_) || + this.gdataPreferences_.driveEnabled; }; FileManager.prototype.isOnReadonlyDirectory = function() { return this.directoryModel_.isReadOnly(); }; - /** - * Event handler called when some volume was mounted or unmouted. - */ - FileManager.prototype.onMountCompleted_ = function(event) { - var changeDirectoryTo = null; - - if (event && event.mountType == 'gdata') { - var mounted = (event.eventType == 'mount'); - metrics.recordInterval('Load.GData'); - console.log('GData ' + (mounted ? 'mounted' : 'unmounted')); - if (mounted && event.status == 'success') { - this.gdataMounted_ = true; - this.gdataMountInfo_ = { - 'mountPath': event.mountPath, - 'sourcePath': event.sourcePath, - 'mountType': event.mountType, - 'mountCondition': event.status - }; - // Not calling clearGDataLoadingTimer_ here because we want to keep - // "Loading Google Docs" message until the directory loads. It is OK if - // the timer fires after the mount because onDirectoryChanged_ will hide - // the unmounted panel. - if (this.setupCurrentDirectoryPostponed_) { - this.setupCurrentDirectoryPostponed_(false /* execute */); - } else if (this.isOnGData() && - this.directoryModel_.getCurrentDirEntry().unmounted) { - // We are currently on an unmounted GData directory, force a rescan. - changeDirectoryTo = this.directoryModel_.getCurrentRootPath(); - } - } else { - this.onGDataUnreachable_('GData ' + - (mounted ? ('mount failed: ' + event.status) : 'unmounted')); - if (this.setupCurrentDirectoryPostponed_) { - this.setupCurrentDirectoryPostponed_(true /* cancel */); - // Change to unmounted GData root. - changeDirectoryTo = '/' + DirectoryModel.GDATA_DIRECTORY; - } + FileManager.prototype.onExternallyUnmounted_ = function(event) { + if (event.mountPath == this.directoryModel_.getCurrentRootPath()) { + if (this.params_.mountTriggered) { + // TODO(serya): What if 2 USB sticks plugged? + chrome.tabs.getCurrent(function(tab) { + chrome.tabs.remove(tab.id); + }); } } - - chrome.fileBrowserPrivate.getMountPoints(function(mountPoints) { - this.setMountPoints_(mountPoints); - - if (event.eventType == 'mount' && event.mountType != 'gdata') { - // Mount request finished - remove it. - // Currently we only request mounts for archive files. - var index = this.mountRequests_.indexOf(event.sourcePath); - if (index != -1) { - this.mountRequests_.splice(index, 1); - if (event.status == 'success') { - // Successful mount requested from this tab, go to the drive root. - changeDirectoryTo = event.mountPath; - } else { - // Request initiated from this tab failed, report the error. - var fileName = event.sourcePath.split('/').pop(); - this.alert.show( - strf('ARCHIVE_MOUNT_FAILED', fileName, event.status)); - } - } - } - - if (event.eventType == 'unmount' && event.mountType != 'gdata') { - // Unmount request finished - remove it. - var index = this.unmountRequests_.indexOf(event.mountPath); - if (index != -1) { - this.unmountRequests_.splice(index, 1); - if (event.status != 'success') - this.alert.show(strf('UNMOUNT_FAILED', event.status)); - } - - if (event.status == 'success' && - event.mountPath == this.directoryModel_.getCurrentRootPath()) { - if (this.params_.mountTriggered && index == -1) { - // This device mount was the reason this File Manager instance was - // created. Now the device is unmounted from another instance - // or the user removed the device manually. Close this instance. - // window.close() sometimes doesn't work. - chrome.tabs.getCurrent(function(tab) { - chrome.tabs.remove(tab.id); - }); - return; - } - // Current directory just unmounted. Move to the 'Downloads'. - changeDirectoryTo = this.directoryModel_.getDefaultDirectory(); - } - } - - // Even if something failed root list should be rescanned. - // Failed mounts can "give" us new devices which might be formatted, - // so we have to refresh root list then. - this.directoryModel_.updateRoots(function() { - if (changeDirectoryTo) { - this.directoryModel_.changeDirectory(changeDirectoryTo); - } - }.bind(this), this.getGDataAccessMode_()); - }.bind(this)); }; /** @@ -2909,14 +2835,25 @@ FileManager.prototype = { chrome.mediaPlayerPrivate.play(urls, position); } else if (id == 'mount-archive') { var self = this; + var tracker = this.directoryModel_.createDirectoryChangeTracker(); + tracker.start(); this.resolveSelectResults_(urls, function(urls) { for (var index = 0; index < urls.length; ++index) { - // Url in MountCompleted event won't be escaped, so let's make sure - // we don't use escaped one in mountRequests_. - var unescapedUrl = unescape(urls[index]); - chrome.fileBrowserPrivate.addMount(unescapedUrl, 'file', {}, - function(sourcePath) { - self.mountRequests_.push(sourcePath); + var path = /^filesystem:[\w-]*:\/\/[\w]*\/external(\/.*)$/. + exec(urls[index])[1]; + if (!path) + continue; + path = decodeURIComponent(path); + self.volumeManager_.mountArchive(path, function(mountPath) { + console.log('Mounted at: ', mountPath); + tracker.stop(); + if (!tracker.hasChanged) + self.directoryModel_.changeDirectory(mountPath); + }, function(error) { + tracker.stop(); + var namePos = path.lastIndexOf('/'); + self.alert.show(strf('ARCHIVE_MOUNT_FAILED', + path.substr(namePos + 1), error)); }); } }); @@ -2935,17 +2872,6 @@ FileManager.prototype = { } }; - FileManager.prototype.getDeviceNumber = function(entry) { - if (!entry.isDirectory) return undefined; - for (var i = 0; i < this.mountPoints_.length; i++) { - if (normalizeAbsolutePath(entry.fullPath) == - normalizeAbsolutePath(this.mountPoints_[i].mountPath)) { - return i; - } - } - return undefined; - }; - /** * Show a modal-like file viewer/editor on top of the File Manager UI. * @@ -3056,6 +2982,7 @@ FileManager.prototype = { this.updateLocation_(false /*push*/, dirPath); galleryFrame.onload = function() { + self.show_(); galleryFrame.contentWindow.ImageUtil.metrics = metrics; galleryFrame.contentWindow.FileType = FileType; galleryFrame.contentWindow.util = util; @@ -3105,7 +3032,7 @@ FileManager.prototype = { removeChildren(bc); var rootPath = this.directoryModel_.getCurrentRootPath(); - var relativePath = this.directoryModel_.getCurrentDirEntry().fullPath. + var relativePath = this.directoryModel_.getCurrentDirPath(). substring(rootPath.length).replace(/\/$/, ''); var pathNames = relativePath.replace(/\/$/, '').split('/'); @@ -3277,7 +3204,7 @@ FileManager.prototype = { */ FileManager.prototype.getCurrentDirectory = function() { return this.directoryModel_ && - this.directoryModel_.getCurrentDirEntry().fullPath; + this.directoryModel_.getCurrentDirPath(); }; /** @@ -3589,6 +3516,13 @@ FileManager.prototype = { * changed. */ FileManager.prototype.onDirectoryAction = function(entry) { + var mountError = this.volumeManager_.getMountError( + DirectoryModel.getRootPath(entry.fullPath)); + if (mountError == VolumeManager.Error.UNKNOWN_FILESYSTEM) { + return this.showButter(str('UNKNOWN_FILESYSTEM_WARNING')); + } else if (mountError == VolumeManager.Error.UNSUPPORTED_FILESYSTEM) { + return this.showButter(str('UNSUPPORTED_FILESYSTEM_WARNING')); + } if (!DirectoryModel.isGDataSearchPath(entry.fullPath)) return this.directoryModel_.changeDirectory(entry.fullPath); @@ -3672,81 +3606,13 @@ FileManager.prototype = { this.updateColumnModel_(); this.updateSearchBoxOnDirChange_(); - // Sometimes we rescan the same directory (when mounting GData lazily first, - // then for real). Do not update the location then. - if (event.newDirEntry.fullPath != event.previousDirEntry.fullPath) { - this.updateLocation_(event.initial, event.newDirEntry.fullPath); - } - + this.updateLocation_(event.initial, this.getCurrentDirectory()); this.checkFreeSpace_(this.getCurrentDirectory()); this.updateTitle_(); - - if (this.filesystemObserverId_) - this.metadataCache_.removeObserver(this.filesystemObserverId_); - if (this.gdataObserverId_) - this.metadataCache_.removeObserver(this.gdataObserverId_); - - this.filesystemObserverId_ = this.metadataCache_.addObserver( - this.directoryModel_.getCurrentDirEntry(), - MetadataCache.CHILDREN, - 'filesystem', - this.updateMetadataInUI_.bind(this, 'filesystem')); - - if (this.isOnGData()) { - this.gdataObserverId_ = this.metadataCache_.addObserver( - this.directoryModel_.getCurrentDirEntry(), - MetadataCache.CHILDREN, - 'gdata', - this.updateMetadataInUI_.bind(this, 'gdata')); - } - - var self = this; - - if (this.watchedDirectoryUrl_) { - if (this.watchedDirectoryUrl_ != event.previousDirEntry.toURL()) { - console.warn('event.previousDirEntry does not match File Manager state', - event, this.watchedDirectoryUrl_); - } - chrome.fileBrowserPrivate.removeFileWatch(this.watchedDirectoryUrl_, - function(result) { - if (!result) { - console.log('Failed to remove file watch'); - } - }); - this.watchedDirectoryUrl_ = null; - } - - if (event.newDirEntry.fullPath != '/' && !event.newDirEntry.unmounted) { - this.watchedDirectoryUrl_ = event.newDirEntry.toURL(); - chrome.fileBrowserPrivate.addFileWatch(this.watchedDirectoryUrl_, - function(result) { - if (!result) { - console.log('Failed to add file watch'); - this.watchedDirectoryUrl_ = null; - } - }.bind(this)); - } - - if (event.newDirEntry.unmounted) - this.dialogContainer_.setAttribute('unmounted', true); - else { - this.dialogContainer_.removeAttribute('unmounted'); - // Need to resize explicitly because the list container had display:none. - this.onResize_(); - } - - if (this.isOnGData()) { - this.dialogContainer_.setAttribute('gdata', true); - if (event.newDirEntry.unmounted) { - if (event.newDirEntry.error) - this.onGDataUnreachable_('File error ' + event.newDirEntry.error); - else - this.initGData_(true /* directory changed */); - } - } else { - this.dialogContainer_.removeAttribute('gdata'); - } + this.updateGDataUnmountedPanel_(); + if (this.isOnGData()) + this.unmountedPanel_.classList.remove('retry-enabled'); }; FileManager.prototype.findListItemForEvent_ = function(event) { @@ -3765,28 +3631,7 @@ FileManager.prototype = { * return. */ FileManager.prototype.onUnload_ = function() { - if (this.watchedDirectoryUrl_) { - chrome.fileBrowserPrivate.removeFileWatch( - this.watchedDirectoryUrl_, - function(result) { - if (!result) { - console.log('Failed to remove file watch'); - } - }); - this.watchedDirectoryUrl_ = null; - } - }; - - FileManager.prototype.onFileChanged_ = function(event) { - // We receive a lot of events even in folders we are not interested in. - if (encodeURI(event.fileUrl) == this.getSearchOrCurrentDirectoryURL()) { - // This event is not necessarily caused by the metadata refresh - // completion. We clear the map knowing that if the metadata is still - // stale then a new re-fetch will be requested. - delete this.directoriesWithStaleMetadata_[ - this.getSearchOrCurrentDirectoryURL()]; - this.directoryModel_.rescanLater(); - } + this.fileWatcher_.stop(); }; FileManager.prototype.initiateRename_ = function() { @@ -4637,6 +4482,8 @@ FileManager.prototype = { }; FileManager.prototype.setupGDataWelcome_ = function() { + if (this.gdataWelcomeHandler_) + return; this.gdataWelcomeHandler_ = this.createGDataWelcomeHandler_(); if (this.gdataWelcomeHandler_) { this.directoryModel_.addEventListener('scan-completed', @@ -4766,3 +4613,4 @@ FileManager.prototype = { return maybeShowBanner; }; })(); + diff --git a/chrome/browser/resources/file_manager/js/main_scripts.js b/chrome/browser/resources/file_manager/js/main_scripts.js index fe85e94..1c54612 100644 --- a/chrome/browser/resources/file_manager/js/main_scripts.js +++ b/chrome/browser/resources/file_manager/js/main_scripts.js @@ -50,6 +50,7 @@ //<include src="combobutton.js"/> // //<include src="util.js"/> +//<include src="volume_manager.js"/> //<include src="directory_model.js"/> //<include src="file_copy_manager.js"/> //<include src="file_manager.js"/> diff --git a/chrome/browser/resources/file_manager/js/util.js b/chrome/browser/resources/file_manager/js/util.js index d8a6cc8..5f7997b 100644 --- a/chrome/browser/resources/file_manager/js/util.js +++ b/chrome/browser/resources/file_manager/js/util.js @@ -647,3 +647,12 @@ util.getFileAndDisplayNameForGDataSearchResult = function(path) { return null; } }; + +/** + * Makes filesystem: URL from the path. + * @param {string} path File or directory path. + * @return {string} URL. + */ +util.makeFilesystemUrl = function(path) { + return 'filesystem:' + chrome.extension.getURL('external' + path); +}; diff --git a/chrome/browser/resources/file_manager/js/volume_manager.js b/chrome/browser/resources/file_manager/js/volume_manager.js new file mode 100644 index 0000000..f6633fa --- /dev/null +++ b/chrome/browser/resources/file_manager/js/volume_manager.js @@ -0,0 +1,447 @@ +// 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. + +/** + * VolumeManager is responsible for tracking list of mounted volumes. + * + * @constructor + * @extends {cr.EventTarget} + */ +function VolumeManager() { + /** + * The list of archives requested to mount. We will show contents once + * archive is mounted, but only for mounts from within this filebrowser tab. + * @type {Object.<string, Object>} + * @private + */ + this.requests_ = {}; + + /** + * @type {Object.<string, Object>} + * @private + */ + this.mountedVolumes_ = {}; + + this.initMountPoints_(); + chrome.fileBrowserPrivate.onMountCompleted.addListener( + this.onMountCompleted_.bind(this)); + this.gDataStatus_ = VolumeManager.GDataStatus.UNMOUNTED; +} + +/** + * VolumeManager extends cr.EventTarget. + */ +VolumeManager.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * @enum + */ +VolumeManager.Error = { + /* Internal errors */ + NOT_MOUNTED: 'not_mounted', + TIMEOUT: 'timeout', + + /* System events */ + UNKNOWN: 'error_unknown', + INTERNAL: 'error_internal', + UNKNOWN_FILESYSTEM: 'error_unknown_filesystem', + UNSUPPORTED_FILESYSTEM: 'error_unsuported_filesystem', + INVALID_ARCHIVE: 'error_invalid_archive', + LIBCROS_MISSING: 'error_libcros_missing', + AUTHENTICATION: 'error_authentication', + PATH_UNMOUNTED: 'error_path_unmounted' +}; + +/** + * @enum + */ +VolumeManager.GDataStatus = { + UNMOUNTED: 'unmounted', + MOUNTING: 'mounting', + ERROR: 'error', + MOUNTED: 'mounted' +}; + +/** + * Time in milliseconds that we wait a respone for. If no response on + * mount/unmount received the request supposed failed. + */ +VolumeManager.TIMEOUT = 15 * 60 * 1000; + +/** + * Delay in milliseconds GDATA changes its state from |UNMOUNTED| to + * |MOUNTING|. Used to display progress in the UI. + */ +VolumeManager.MOUNTING_DELAY = 500; + +/** + * @return {VolumeManager} Singleton instance. + */ +VolumeManager.getInstance = function() { + return VolumeManager.instance_ = VolumeManager.instance_ || + new VolumeManager(); +}; + +/** + * @param {VolumeManager.GDataStatus} newStatus New GDATA status. + * @private + */ +VolumeManager.prototype.setGDataStatus_ = function(newStatus) { + if (this.gDataStatus_ != newStatus) { + this.gDataStatus_ = newStatus; + cr.dispatchSimpleEvent(this, 'gdata-status-changed'); + } +}; + +/** + * @return {VolumeManager.GDataStatus} Current GDATA status. + */ +VolumeManager.prototype.getGDataStatus = function() { + return this.gDataStatus_; +}; + +/** + * @param {string} mountPath Volume root path. + * @return {boolean} True if mounted. + */ +VolumeManager.prototype.isMounted = function(mountPath) { + this.validateMountPath_(mountPath); + return mountPath in this.mountedVolumes_; +}; + +/** + * Initialized mount points. + * @private + */ +VolumeManager.prototype.initMountPoints_ = function() { + var mountedVolumes = []; + var self = this; + var index = 0; + function step(mountPoints) { + if (index < mountPoints.length) { + var info = mountPoints[index]; + if (info.mountType == 'gdata') + console.error('GData is not expected initially mounted'); + var error = info.mountCondition ? 'error_' + info.mountCondition : ''; + function onVolumeInfo(volume) { + mountedVolumes.push(volume); + index++; + step(mountPoints); + } + self.makeVolumeInfo_('/' + info.mountPath, error, onVolumeInfo); + } else { + for (var i = 0; i < mountedVolumes.length; i++) { + var volume = mountedVolumes[i]; + self.mountedVolumes_[volume.mountPath] = volume; + } + if (mountedVolumes.length > 0) + cr.dispatchSimpleEvent(self, 'change'); + } + } + + chrome.fileBrowserPrivate.getMountPoints(step); +}; + +/** + * Event handler called when some volume was mounted or unmouted. + * @param {MountCompletedEvent} event Received event. + * @private + */ +VolumeManager.prototype.onMountCompleted_ = function(event) { + if (event.eventType == 'mount') { + if (event.mountPath) { + var requestKey = this.makeRequestKey_( + 'mount', event.mountType, event.sourcePath); + var error = event.status == 'success' ? '' : event.status; + this.makeVolumeInfo_(event.mountPath, error, function(volume) { + this.mountedVolumes_[volume.mountPath] = volume; + this.finishRequest_(requestKey, event.status, event.mountPath); + cr.dispatchSimpleEvent(this, 'change'); + }.bind(this)); + } else { + console.log('No mount path'); + this.finishRequest_(requestKey, event.status); + } + } else if (event.eventType == 'unmount') { + var mountPath = event.mountPath; + this.validateMountPath_(mountPath); + var status = event.status; + if (status == VolumeManager.Error.PATH_UNMOUNTED) { + console.log('Volume already unmounted: ', mountPath); + status = 'success'; + } + var requestKey = this.makeRequestKey_('unmount', '', event.mountPath); + var requested = requestKey in this.requests_; + if (event.status == 'success' && !requested && + mountPath in this.mountedVolumes_) { + console.log('Mounted volume without a request: ', mountPath); + var e = new cr.Event('externally-unmounted'); + e.mountPath = mountPath; + this.dispatchEvent(e); + } + this.finishRequest_(requestKey, status); + + if (event.status == 'success') { + delete this.mountedVolumes_[mountPath]; + cr.dispatchSimpleEvent(this, 'change'); + } + } + + if (event.mountType == 'gdata') { + if (event.status == 'success') { + if (event.eventType == 'mount') + this.setGDataStatus_(VolumeManager.GDataStatus.MOUNTED); + else if (event.eventType == 'unmount') + this.setGDataStatus_(VolumeManager.GDataStatus.UMOUNTED); + } + } +}; + +/** + * @param {string} mountPath Path to the volume. + * @param {VolumeManager?} error Mounting error if any. + * @param {function(Object)} callback Result acceptor. + * @private + */ +VolumeManager.prototype.makeVolumeInfo_ = function( + mountPath, error, callback) { + if (error) + this.validateError_(error); + this.validateMountPath_(mountPath); + function onVolumeMetadata(metadata) { + callback({ + mountPath: mountPath, + error: error, + readonly: !!metadata && metadata.isReadOnly + }); + } + chrome.fileBrowserPrivate.getVolumeMetadata( + util.makeFilesystemUrl(mountPath), onVolumeMetadata); +}; + +/** + * Creates string to match mount events with requests. + * @param {string} requestType 'mount' | 'unmount'. + * @param {string} mountType 'device' | 'file' | 'network' | 'gdata'. + * @param {string} mountOrSourcePath Source path provided by API after + * resolving mount request or mountPath for unmount request. + * @return {string} Key for |this.requests_|. + * @private + */ +VolumeManager.prototype.makeRequestKey_ = function(requestType, + mountType, + mountOrSourcePath) { + return requestType + ':' + mountType + ':' + mountOrSourcePath; +}; + + +/** + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ +VolumeManager.prototype.mountGData = function(successCallback, errorCallback) { + if (this.getGDataStatus() == VolumeManager.GDataStatus.ERROR) { + this.setGDataStatus_(VolumeManager.GDataStatus.UNMOUNTED); + } + var self = this; + var timeout = setTimeout(function() { + if (self.getGDataStatus() == VolumeManager.GDataStatus.UNMOUNTED) + self.setGDataStatus_(VolumeManager.GDataStatus.MOUNTING); + timeout = null; + }, VolumeManager.MOUNTING_DELAY); + this.mount_('', 'gdata', function(mountPath) { + if (timeout !== null) + clearTimeout(timeout); + successCallback(mountPath); + }, function(error) { + if (self.getGDataStatus() != VolumeManager.GDataStatus.MOUNTED) + self.setGDataStatus_(VolumeManager.GDataStatus.ERROR); + if (timeout != null) + clearTimeout(timeout); + errorCallback(error); + }); +}; + +/** + * @param {string} fullPath Path to the archive file. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ +VolumeManager.prototype.mountArchive = function(fullPath, successCallback, + errorCallback) { + this.mount_(util.makeFilesystemUrl(fullPath), + 'file', successCallback, errorCallback); +}; + +/** + * Unmounts volume. + * @param {string} mountPath Volume mounted path. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ +VolumeManager.prototype.unmount = function(mountPath, + successCallback, + errorCallback) { + this.validateMountPath_(mountPath); + var volumeInfo = this.mountedVolumes_[mountPath]; + if (!volumeInfo) { + errorCallback(VolumeManager.Error.NOT_MOUNTED); + return; + } + + chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath)); + var requestKey = this.makeRequestKey_('unmount', '', volumeInfo.mountPath); + this.startRequest_(requestKey, successCallback, errorCallback); +}; + +/** + * @param {string} mountPath Volume mounted path. + * @return {VolumeManager.Error?} Returns mount error code + * or undefined if no error. + */ +VolumeManager.prototype.getMountError = function(mountPath) { + return this.getVolumeInfo_(mountPath).error; +}; + +/** + * @param {string} mountPath Volume mounted path. + * @return {boolean} True if volume at |mountedPath| is mounted but not usable. + */ +VolumeManager.prototype.isUnreadable = function(mountPath) { + var error = this.getMountError(mountPath); + return error == VolumeManager.Error.UNKNOWN_FILESYSTEM || + error == VolumeManager.Error.UNSUPPORTED_FILESYSTEM; +}; + +/** + * @param {string} mountPath Volume mounted path. + * @return {boolean} True if volume at |mountedPath| is read only. + */ +VolumeManager.prototype.isReadOnly = function(mountPath) { + return !!this.getVolumeInfo_(mountPath).readonly; +}; + +/** + * Helper method. + * @param {string} mountPath Volume mounted path. + * @return {Object} Structure created in |startRequest_|. + * @private + */ +VolumeManager.prototype.getVolumeInfo_ = function(mountPath) { + this.validateMountPath_(mountPath); + return this.mountedVolumes_[mountPath] || {}; +}; + +/** + * @param {string} url URL for for |fileBrowserPrivate.addMount|. + * @param {'gdata'|'file'} mountType Mount type for + * |fileBrowserPrivate.addMount|. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + * @private + */ +VolumeManager.prototype.mount_ = function(url, mountType, + successCallback, errorCallback) { + chrome.fileBrowserPrivate.addMount(url, mountType, {}, + function(sourcePath) { + console.log('Mount request: url=' + url + '; mountType=' + mountType + + '; sourceUrl=' + sourcePath); + var requestKey = this.makeRequestKey_('mount', mountType, sourcePath); + this.startRequest_(requestKey, successCallback, errorCallback); + }.bind(this)); +}; + +/** + * @param {sting} key Key produced by |makeRequestKey_|. + * @param {Function} successCallback To be called when request finishes + * successfully. + * @param {Function} errorCallback To be called when request fails. + * @private + */ +VolumeManager.prototype.startRequest_ = function(key, + successCallback, errorCallback) { + if (key in this.requests_) { + var request = this.requests_[key]; + request.successCallbacks.push(successCallback); + request.errorCallbacks.push(errorCallback); + } else { + this.requests_[key] = { + successCallbacks: [successCallback], + errorCallbacks: [errorCallback], + + timeout: setTimeout(this.onTimeout_.bind(this, key), + VolumeManager.TIMEOUT) + }; + } +}; + +/** + * Called if no response received in |TIMEOUT|. + * @param {sting} key Key produced by |makeRequestKey_|. + * @private + */ +VolumeManager.prototype.onTimeout_ = function(key) { + this.invokeRequestCallbacks_(this.requests_[key], + VolumeManager.Error.TIMEOUT); + delete this.requests_[key]; +}; + +/** + * @param {sting} key Key produced by |makeRequestKey_|. + * @param {VolumeManager.Error|'success'} status Status received from the API. + * @param {string} opt_mountPath Mount path. + * @private + */ +VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) { + var request = this.requests_[key]; + if (!request) + return; + + clearTimeout(request.timeout); + this.invokeRequestCallbacks_(request, status, opt_mountPath); + delete this.requests_[key]; +}; + +/** + * @param {object} request Structure created in |startRequest_|. + * @param {VolumeManager.Error|string} status If status == 'success' + * success callbacks are called. + * @param {string} opt_mountPath Mount path. Required if success. + * @private + */ +VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status, + opt_mountPath) { + function callEach(callbacks, self, args) { + for (var i = 0; i < callbacks.length; i++) { + callbacks[i].apply(self, args); + } + } + if (status == 'success') { + callEach(request.successCallbacks, this, [opt_mountPath]); + } else { + this.validateError_(status); + callEach(request.errorCallbacks, this, [status]); + } +}; + +/** + * @param {VolumeManager.Error} error Status string iusually received from API. + * @private + */ +VolumeManager.prototype.validateError_ = function(error) { + for (var i in VolumeManager.Error) { + if (error == VolumeManager.Error[i]) + return; + } + throw new Error('Invalid mount error: ', error); +}; + +/** + * @param {string} mountPath Mount path. + * @private + */ +VolumeManager.prototype.validateMountPath_ = function(mountPath) { + if (!/^\/(((archive|removable)\/[^\/]+)|drive|Downloads)$/.test(mountPath)) + throw new Error('Invalid mount path: ', mountPath); +}; diff --git a/chrome/browser/resources/file_manager/main.html b/chrome/browser/resources/file_manager/main.html index 6f89436..9929a2b 100644 --- a/chrome/browser/resources/file_manager/main.html +++ b/chrome/browser/resources/file_manager/main.html @@ -68,6 +68,7 @@ <script src="js/combobutton.js"></script> <script src="js/util.js"></script> + <script src="js/volume_manager.js"></script> <script src="js/directory_model.js"></script> <script src="js/file_copy_manager.js"></script> <script src="js/file_manager.js"></script> @@ -188,7 +189,7 @@ </button> </div> </div> - <div class='unmounted-panel'></div> + <div id='unmounted-panel'></div> </div> </div> </div> |