summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorserya@google.com <serya@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2012-05-22 16:01:16 +0000
committerserya@google.com <serya@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2012-05-22 16:01:16 +0000
commit662e96ff6fc8ca0795a4142c3b67c85d31061438 (patch)
treea031e4ed16bc38adfa87afec05f471ba8f0474c7
parenta21a9a7e013dbefeb0522a7317b711dd9c7941b7 (diff)
downloadchromium_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
-rw-r--r--chrome/browser/resources/file_manager/css/file_manager.css83
-rw-r--r--chrome/browser/resources/file_manager/js/directory_model.js530
-rw-r--r--chrome/browser/resources/file_manager/js/file_manager.js984
-rw-r--r--chrome/browser/resources/file_manager/js/main_scripts.js1
-rw-r--r--chrome/browser/resources/file_manager/js/util.js9
-rw-r--r--chrome/browser/resources/file_manager/js/volume_manager.js447
-rw-r--r--chrome/browser/resources/file_manager/main.html3
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>