blob: f65d0b3e8efd84e50fd50129d89a187ea3ab2e53 [file] [log] [blame]
// Copyright 2013 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.
/**
* Implementation of VolumeInfoList for FilteredVolumeManager.
* In foreground/ we want to enforce this list to be filtered, so we forbid
* adding/removing/splicing of the list.
* The inner list ownership is shared between FilteredVolumeInfoList and
* FilteredVolumeManager to enforce these constraints.
*
* @final
* @implements {VolumeInfoList}
*/
class FilteredVolumeInfoList {
/**
* @param {!cr.ui.ArrayDataModel} list
*/
constructor(list) {
/** @private @const */
this.list_ = list;
}
/** @override */
get length() {
return this.list_.length;
}
/** @override */
addEventListener(type, handler) {
this.list_.addEventListener(type, handler);
}
/** @override */
removeEventListener(type, handler) {
this.list_.removeEventListener(type, handler);
}
/** @override */
add(volumeInfo) {
throw new Error('FilteredVolumeInfoList.add not allowed in foreground');
}
/** @override */
remove(volumeInfo) {
throw new Error('FilteredVolumeInfoList.remove not allowed in foreground');
}
/** @override */
item(index) {
return /** @type {!VolumeInfo} */ (this.list_.item(index));
}
}
/**
* Thin wrapper for VolumeManager. This should be an interface proxy to talk
* to VolumeManager. This class also filters some "disallowed" volumes;
* for example, Drive volumes are dropped if Drive is disabled, and read-only
* volumes are dropped in save-as dialogs.
*
* @implements {VolumeManager}
*/
class FilteredVolumeManager extends cr.EventTarget {
/**
*
* @param {!AllowedPaths} allowedPaths Which paths are supported in the Files
* app dialog.
* @param {boolean} writableOnly If true, only writable volumes are returned.
* @param {Window=} opt_backgroundPage Window object of the background
* page. If this is specified, the class skips to get background page.
* TODO(hirono): Let all clients of the class pass the background page and
* make the argument not optional.
*/
constructor(allowedPaths, writableOnly, opt_backgroundPage) {
super();
this.allowedPaths_ = allowedPaths;
this.writableOnly_ = writableOnly;
// Internal list holds filtered VolumeInfo instances.
/** @private */
this.list_ = new cr.ui.ArrayDataModel([]);
// Public VolumeManager.volumeInfoList property accessed by callers.
this.volumeInfoList = new FilteredVolumeInfoList(this.list_);
/** @private {?VolumeManager} */
this.volumeManager_ = null;
this.onEventBound_ = this.onEvent_.bind(this);
this.onVolumeInfoListUpdatedBound_ =
this.onVolumeInfoListUpdated_.bind(this);
this.disposed_ = false;
/** private {Window} */
this.backgroundPage_ = opt_backgroundPage;
/**
* Tracks async initialization of volume manager.
* @private @const {!Promise<void> }
*/
this.initialized_ = this.initialize_();
}
/**
* Checks if a volume type is allowed.
*
* Note that even if a volume type is allowed, a volume of that type might be
* disallowed for other restrictions. To check if a specific volume is allowed
* or not, use isAllowedVolume_() instead.
*
* @param {VolumeManagerCommon.VolumeType} volumeType
* @return {boolean}
*/
isAllowedVolumeType_(volumeType) {
switch (this.allowedPaths_) {
case AllowedPaths.ANY_PATH:
case AllowedPaths.ANY_PATH_OR_URL:
return true;
case AllowedPaths.NATIVE_OR_DRIVE_PATH:
return (
VolumeManagerCommon.VolumeType.isNative(volumeType) ||
volumeType == VolumeManagerCommon.VolumeType.DRIVE);
case AllowedPaths.NATIVE_PATH:
return VolumeManagerCommon.VolumeType.isNative(volumeType);
}
return false;
}
/**
* Checks if a volume is allowed.
*
* @param {!VolumeInfo} volumeInfo
* @return {boolean}
*/
isAllowedVolume_(volumeInfo) {
if (!this.isAllowedVolumeType_(volumeInfo.volumeType)) {
return false;
}
if (this.writableOnly_ && volumeInfo.isReadOnly) {
return false;
}
return true;
}
/**
* Async part of the initialization.
* @private
*/
async initialize_() {
if (!this.backgroundPage_) {
this.backgroundPage_ = await new Promise(
resolve => chrome.runtime.getBackgroundPage(resolve));
}
this.volumeManager_ =
await this.backgroundPage_.volumeManagerFactory.getInstance();
if (this.disposed_) {
return;
}
// Subscribe to VolumeManager.
this.volumeManager_.addEventListener(
'drive-connection-changed', this.onEventBound_);
this.volumeManager_.addEventListener(
'externally-unmounted', this.onEventBound_);
this.volumeManager_.addEventListener(
VolumeManagerCommon.ARCHIVE_OPENED_EVENT_TYPE, this.onEventBound_);
// Dispatch 'drive-connection-changed' to listeners, since the return value
// of FilteredVolumeManager.getDriveConnectionState() can be changed by
// setting this.volumeManager_.
cr.dispatchSimpleEvent(this, 'drive-connection-changed');
// Cache volumeInfoList.
const volumeInfoList = [];
for (var i = 0; i < this.volumeManager_.volumeInfoList.length; i++) {
const volumeInfo = this.volumeManager_.volumeInfoList.item(i);
// TODO(hidehiko): Filter mounted volumes located on Drive File System.
if (!this.isAllowedVolume_(volumeInfo)) {
continue;
}
volumeInfoList.push(volumeInfo);
}
this.list_.splice.apply(
this.list_, [0, this.volumeInfoList.length].concat(volumeInfoList));
// Subscribe to VolumeInfoList.
// In VolumeInfoList, we only use 'splice' event.
this.volumeManager_.volumeInfoList.addEventListener(
'splice', this.onVolumeInfoListUpdatedBound_);
}
/**
* Disposes the instance. After the invocation of this method, any other
* method should not be called.
*/
dispose() {
this.disposed_ = true;
if (!this.volumeManager_) {
return;
}
// TODO(crbug.com/972849): Consider using EventTracker instead.
this.volumeManager_.removeEventListener(
'drive-connection-changed', this.onEventBound_);
this.volumeManager_.removeEventListener(
'externally-unmounted', this.onEventBound_);
this.volumeManager_.volumeInfoList.removeEventListener(
'splice', this.onVolumeInfoListUpdatedBound_);
}
/**
* Called on events sent from VolumeManager. This has responsibility to
* re-dispatch the event to the listeners.
* @param {!Event} event Event object sent from VolumeManager.
* @private
*/
onEvent_(event) {
// Note: Can not re-dispatch the same |event| object, because it throws a
// runtime "The event is already being dispatched." error.
switch (event.type) {
case 'drive-connection-changed':
if (this.isAllowedVolumeType_(VolumeManagerCommon.VolumeType.DRIVE)) {
cr.dispatchSimpleEvent(this, 'drive-connection-changed');
}
break;
case 'externally-unmounted':
event = /** @type {!ExternallyUnmountedEvent} */ (event);
if (this.isAllowedVolume_(event.detail)) {
this.dispatchEvent(
new CustomEvent('externally-unmount', {detail: event.detail}));
}
break;
case VolumeManagerCommon.ARCHIVE_OPENED_EVENT_TYPE:
this.dispatchEvent(new CustomEvent(
VolumeManagerCommon.ARCHIVE_OPENED_EVENT_TYPE,
{detail: event.detail}));
break;
}
}
/**
* Called on events of modifying VolumeInfoList.
* @param {Event} event Event object sent from VolumeInfoList.
* @private
*/
onVolumeInfoListUpdated_(event) {
// Filters some volumes.
let index = event.index;
for (var i = 0; i < event.index; i++) {
var volumeInfo = this.volumeManager_.volumeInfoList.item(i);
if (!this.isAllowedVolume_(volumeInfo)) {
index--;
}
}
let numRemovedVolumes = 0;
for (var i = 0; i < event.removed.length; i++) {
var volumeInfo = event.removed[i];
if (this.isAllowedVolume_(volumeInfo)) {
numRemovedVolumes++;
}
}
const addedVolumes = [];
for (var i = 0; i < event.added.length; i++) {
var volumeInfo = event.added[i];
if (this.isAllowedVolume_(volumeInfo)) {
addedVolumes.push(volumeInfo);
}
}
this.list_.splice.apply(
this.list_, [index, numRemovedVolumes].concat(addedVolumes));
}
/**
* Ensures the VolumeManager is initialized, and then invokes callback.
* If the VolumeManager is already initialized, callback will be called
* immediately.
* @param {function()} callback Called on initialization completion.
*/
ensureInitialized(callback) {
this.initialized_.then(callback);
}
/**
* @return {VolumeManagerCommon.DriveConnectionState} Current drive connection
* state.
*/
getDriveConnectionState() {
if (!this.isAllowedVolumeType_(VolumeManagerCommon.VolumeType.DRIVE) ||
!this.volumeManager_) {
return {
type: VolumeManagerCommon.DriveConnectionType.OFFLINE,
reason: VolumeManagerCommon.DriveConnectionReason.NO_SERVICE
};
}
return this.volumeManager_.getDriveConnectionState();
}
/** @override */
getVolumeInfo(entry) {
return this.filterDisallowedVolume_(
this.volumeManager_ && this.volumeManager_.getVolumeInfo(entry));
}
/**
* Obtains a volume information of the current profile.
* @param {VolumeManagerCommon.VolumeType} volumeType Volume type.
* @return {?VolumeInfo} Found volume info.
*/
getCurrentProfileVolumeInfo(volumeType) {
return this.filterDisallowedVolume_(
this.volumeManager_ &&
this.volumeManager_.getCurrentProfileVolumeInfo(volumeType));
}
/** @override */
getDefaultDisplayRoot(callback) {
this.ensureInitialized(() => {
const defaultVolume = this.getCurrentProfileVolumeInfo(
VolumeManagerCommon.VolumeType.DOWNLOADS);
if (!defaultVolume) {
console.warn('Cannot get default display root');
callback(null);
return;
}
defaultVolume.resolveDisplayRoot(callback, () => {
// defaultVolume is DOWNLOADS and resolveDisplayRoot should succeed.
console.error('Cannot resolve default display root');
callback(null);
});
});
}
/**
* Obtains location information from an entry.
*
* @param {(!Entry|!FilesAppEntry)} entry File or directory entry.
* @return {?EntryLocation} Location information.
*/
getLocationInfo(entry) {
const locationInfo =
this.volumeManager_ && this.volumeManager_.getLocationInfo(entry);
if (!locationInfo) {
return null;
}
if (locationInfo.volumeInfo &&
!this.filterDisallowedVolume_(locationInfo.volumeInfo)) {
return null;
}
return locationInfo;
}
/** @override */
findByDevicePath(devicePath) {
for (let i = 0; i < this.volumeInfoList.length; i++) {
const volumeInfo = this.volumeInfoList.item(i);
if (volumeInfo.devicePath && volumeInfo.devicePath === devicePath) {
return this.filterDisallowedVolume_(volumeInfo);
}
}
return null;
}
/**
* Returns a promise that will be resolved when volume info, identified
* by {@code volumeId} is created.
*
* @param {string} volumeId
* @return {!Promise<!VolumeInfo>} The VolumeInfo. Will not resolve
* if the volume is never mounted.
*/
async whenVolumeInfoReady(volumeId) {
await this.initialized_;
const volumeInfo = this.filterDisallowedVolume_(
await this.volumeManager_.whenVolumeInfoReady(volumeId));
if (!volumeInfo) {
throw new Error(`Volume not allowed: ${volumeId}`);
}
return volumeInfo;
}
/**
* Requests to mount the archive file.
* @param {string} fileUrl The path to the archive file to be mounted.
* @param {function(VolumeInfo)} successCallback Called with the VolumeInfo
* instance.
* @param {function(VolumeManagerCommon.VolumeError)} errorCallback Called
* when an error occurs.
*/
mountArchive(fileUrl, successCallback, errorCallback) {
this.ensureInitialized(() => {
this.volumeManager_.mountArchive(fileUrl, successCallback, errorCallback);
});
}
/**
* Requests unmount the specified volume.
* @param {!VolumeInfo} volumeInfo Volume to be unmounted.
* @param {function()} successCallback Called on success.
* @param {function(VolumeManagerCommon.VolumeError)} errorCallback Called
* when an error occurs.
*/
unmount(volumeInfo, successCallback, errorCallback) {
this.ensureInitialized(() => {
this.volumeManager_.unmount(volumeInfo, successCallback, errorCallback);
});
}
/**
* Requests configuring of the specified volume.
* @param {!VolumeInfo} volumeInfo Volume to be configured.
* @return {!Promise} Fulfilled on success, otherwise rejected with an error
* message.
*/
async configure(volumeInfo) {
await this.initialized_;
return this.volumeManager_.configure(volumeInfo);
}
/**
* Filters volume info by isAllowedVolume_().
*
* @param {?VolumeInfo} volumeInfo Volume info.
* @return {?VolumeInfo} Null if the volume is disallowed. Otherwise just
* returns the volume.
* @private
*/
filterDisallowedVolume_(volumeInfo) {
if (volumeInfo && this.isAllowedVolume_(volumeInfo)) {
return volumeInfo;
} else {
return null;
}
}
}