blob: 086eb568e51495bcd2698270beb46693e0a69fe7 [file] [log] [blame]
// 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.
/**
* Root class of the background page.
* @constructor
* @implements {FileBrowserBackgroundFull}
* @extends {BackgroundBase}
* @struct
*/
function FileBrowserBackgroundImpl() {
BackgroundBase.call(this);
/** @type {!analytics.Tracker} */
this.tracker = metrics.getTracker();
/**
* Progress center of the background page.
* @type {!ProgressCenter}
*/
this.progressCenter = new ProgressCenter();
/**
* File operation manager.
* @type {FileOperationManager}
*/
this.fileOperationManager = null;
/**
* Class providing loading of import history, used in
* cloud import.
*
* @type {!importer.HistoryLoader}
*/
this.historyLoader = new importer.RuntimeHistoryLoader(this.tracker);
/**
* Event handler for progress center.
* @private {FileOperationHandler}
*/
this.fileOperationHandler_ = null;
/**
* Event handler for C++ sides notifications.
* @private {!DeviceHandler}
*/
this.deviceHandler_ = new DeviceHandler();
// Handle device navigation requests.
this.deviceHandler_.addEventListener(
DeviceHandler.VOLUME_NAVIGATION_REQUESTED,
this.handleViewEvent_.bind(this));
/**
* Drive sync handler.
* @type {!DriveSyncHandler}
*/
this.driveSyncHandler = new DriveSyncHandler(this.progressCenter);
/**
* @type {!importer.DispositionChecker.CheckerFunction}
*/
this.dispositionChecker_ = importer.DispositionChecker.createChecker(
this.historyLoader, this.tracker);
/**
* Provides support for scaning media devices as part of Cloud Import.
* @type {!importer.MediaScanner}
*/
this.mediaScanner = new importer.DefaultMediaScanner(
importer.createMetadataHashcode,
this.dispositionChecker_,
importer.DefaultDirectoryWatcher.create);
/**
* Handles importing of user media (e.g. photos, videos) from removable
* devices.
* @type {!importer.MediaImportHandler}
*/
this.mediaImportHandler = new importer.MediaImportHandler(
this.progressCenter, this.historyLoader, this.dispositionChecker_,
this.driveSyncHandler);
/** @type {!Crostini} */
this.crostini = new Crostini();
/**
* String assets.
* @type {Object<string>}
*/
this.stringData = null;
/**
* Provides drive search to app launcher.
* @private {!LauncherSearch}
*/
this.launcherSearch_ = new LauncherSearch();
// Initialize handlers.
chrome.fileBrowserHandler.onExecute.addListener(this.onExecute_.bind(this));
chrome.runtime.onMessageExternal.addListener(
this.onExternalMessageReceived_.bind(this));
chrome.contextMenus.onClicked.addListener(
this.onContextMenuClicked_.bind(this));
// Initialize string and volume manager related stuffs.
this.initializationPromise_.then(function(strings) {
this.stringData = strings;
this.initContextMenu_();
volumeManagerFactory.getInstance().then(function(volumeManager) {
volumeManager.addEventListener(
VolumeManagerCommon.VOLUME_ALREADY_MOUNTED,
this.handleViewEvent_.bind(this));
this.crostini.init(volumeManager);
this.crostini.listen();
}.bind(this));
this.fileOperationManager = new FileOperationManager();
this.fileOperationHandler_ = new FileOperationHandler(
this.fileOperationManager, this.progressCenter);
}.bind(this));
// Handle newly mounted FSP file systems. Workaround for crbug.com/456648.
// TODO(mtomasz): Replace this hack with a proper solution.
chrome.fileManagerPrivate.onMountCompleted.addListener(
this.onMountCompleted_.bind(this));
launcher.queue.run(function(callback) {
this.initializationPromise_.then(callback);
}.bind(this));
}
FileBrowserBackgroundImpl.prototype.__proto__ = BackgroundBase.prototype;
/**
* Register callback to be invoked after initialization.
* If the initialization is already done, the callback is invoked immediately.
*
* @param {function()} callback Initialize callback to be registered.
*/
FileBrowserBackgroundImpl.prototype.ready = function(callback) {
this.initializationPromise_.then(callback);
};
/**
* Opens the volume root (or opt directoryPath) in main UI.
*
* @param {!Event} event An event with the volumeId or
* devicePath.
* @private
*/
FileBrowserBackgroundImpl.prototype.handleViewEvent_ = function(event) {
util.doIfPrimaryContext(() => {
this.handleViewEventInternal_(event);
});
};
/**
* @param {!Event} event An event with the volumeId or
* devicePath.
* @private
*/
FileBrowserBackgroundImpl.prototype.handleViewEventInternal_ = function(event) {
volumeManagerFactory.getInstance()
.then(
(/**
* Retrieves the root file entry of the volume on the requested
* device.
* @param {!VolumeManager} volumeManager
*/
function(volumeManager) {
if (event.devicePath) {
let volume = volumeManager.findByDevicePath(event.devicePath);
if (volume) {
this.navigateToVolumeRoot_(volume, event.filePath);
} else {
console.error('Got view event with invalid volume id.');
}
} else if (event.volumeId) {
if (event.type === VolumeManagerCommon.VOLUME_ALREADY_MOUNTED)
this.navigateToVolumeInFocusedWindowWhenReady_(
event.volumeId, event.filePath);
else
this.navigateToVolumeWhenReady_(event.volumeId, event.filePath);
} else {
console.error('Got view event with no actionable destination.');
}
}).bind(this));
};
/**
* Retrieves the root file entry of the volume on the requested device.
*
* @param {!string} volumeId ID of the volume to navigate to.
* @return {!Promise<VolumeInfo>}
* @private
*/
FileBrowserBackgroundImpl.prototype.retrieveVolumeInfo_ = function(volumeId) {
return volumeManagerFactory.getInstance().then(
(/**
* @param {!VolumeManager} volumeManager
*/
(volumeManager) => {
return volumeManager.whenVolumeInfoReady(volumeId).catch((e) => {
console.error(
'Unable to find volume for id: ' + volumeId +
'. Error: ' + e.message);
});
}));
};
/**
* Opens the volume root (or opt directoryPath) in main UI.
*
* @param {!string} volumeId ID of the volume to navigate to.
* @param {!string=} opt_directoryPath Optional path to be opened.
* @private
*/
FileBrowserBackgroundImpl.prototype.navigateToVolumeWhenReady_ = function(
volumeId, opt_directoryPath) {
this.retrieveVolumeInfo_(volumeId).then(function(volume) {
this.navigateToVolumeRoot_(volume, opt_directoryPath);
}.bind(this));
};
/**
* Opens the volume root (or opt directoryPath) in the main UI of the focused
* window.
*
* @param {!string} volumeId ID of the volume to navigate to.
* @param {!string=} opt_directoryPath Optional path to be opened.
* @private
*/
FileBrowserBackgroundImpl.prototype.navigateToVolumeInFocusedWindowWhenReady_ =
function(volumeId, opt_directoryPath) {
this.retrieveVolumeInfo_(volumeId).then(function(volume) {
this.navigateToVolumeInFocusedWindow_(volume, opt_directoryPath);
}.bind(this));
};
/**
* If a path was specified, retrieve that directory entry,
* otherwise return the root entry of the volume.
*
* @param {!VolumeInfo} volume
* @param {string=} opt_directoryPath Optional directory path to be opened.
* @return {!Promise<!DirectoryEntry>}
* @private
*/
FileBrowserBackgroundImpl.prototype.retrieveEntryInVolume_ = function(
volume, opt_directoryPath) {
return volume.resolveDisplayRoot().then(function(root) {
if (opt_directoryPath) {
return new Promise(
root.getDirectory.bind(root, opt_directoryPath, {create: false}));
} else {
return Promise.resolve(root);
}
});
};
/**
* Opens the volume root (or opt directoryPath) in main UI.
*
* @param {!VolumeInfo} volume
* @param {string=} opt_directoryPath Optional directory path to be opened.
* @private
*/
FileBrowserBackgroundImpl.prototype.navigateToVolumeRoot_ = function(
volume, opt_directoryPath) {
this.retrieveEntryInVolume_(volume, opt_directoryPath)
.then(
/**
* Launches app opened on {@code directory}.
* @param {DirectoryEntry} directory
*/
function(directory) {
launcher.launchFileManager(
{currentDirectoryURL: directory.toURL()},
/* App ID */ undefined, LaunchType.FOCUS_SAME_OR_CREATE);
});
};
/**
* Opens the volume root (or opt directoryPath) in main UI of the focused
* window.
*
* @param {!VolumeInfo} volume
* @param {string=} opt_directoryPath Optional directory path to be opened.
* @private
*/
FileBrowserBackgroundImpl.prototype.navigateToVolumeInFocusedWindow_ = function(
volume, opt_directoryPath) {
this.retrieveEntryInVolume_(volume, opt_directoryPath)
.then(function(directoryEntry) {
if (directoryEntry)
volumeManagerFactory.getInstance().then(function(volumeManager) {
volumeManager.dispatchEvent(
VolumeManagerCommon.createArchiveOpenedEvent(directoryEntry));
}.bind(this));
});
};
/**
* Prefix for the dialog ID.
* @type {!string}
* @const
*/
var DIALOG_ID_PREFIX = 'dialog#';
/**
* Value of the next file manager dialog ID.
* @type {number}
*/
var nextFileManagerDialogID = 0;
/**
* Registers dialog window to the background page.
*
* @param {!Window} dialogWindow Window of the dialog.
*/
function registerDialog(dialogWindow) {
var id = DIALOG_ID_PREFIX + (nextFileManagerDialogID++);
window.background.dialogs[id] = dialogWindow;
if (window.IN_TEST)
dialogWindow.IN_TEST = true;
dialogWindow.addEventListener('pagehide', function() {
delete window.background.dialogs[id];
});
}
/**
* Executes a file browser task.
*
* @param {string} action Task id.
* @param {Object} details Details object.
* @private
*/
FileBrowserBackgroundImpl.prototype.onExecute_ = function(action, details) {
var appState = {
params: {action: action},
// It is not allowed to call getParent() here, since there may be
// no permissions to access it at this stage. Therefore we are passing
// the selectionURL only, and the currentDirectory will be resolved
// later.
selectionURL: details.entries[0].toURL()
};
// Every other action opens a Files app window.
// For mounted devices just focus any Files app window. The mounted
// volume will appear on the navigation list.
launcher.launchFileManager(
appState,
/* App ID */ undefined,
LaunchType.FOCUS_SAME_OR_CREATE);
};
/**
* Launches the app.
* @private
* @override
*/
FileBrowserBackgroundImpl.prototype.onLaunched_ = function() {
metrics.startInterval('Load.BackgroundLaunch');
this.initializationPromise_.then(function() {
if (nextFileManagerWindowID == 0) {
// The app just launched. Remove window state records that are not needed
// any more.
chrome.storage.local.get(function(items) {
for (var key in items) {
if (items.hasOwnProperty(key)) {
if (key.match(FILES_ID_PATTERN))
chrome.storage.local.remove(key);
}
}
});
}
launcher.launchFileManager(
null, undefined, LaunchType.FOCUS_ANY_OR_CREATE,
function() { metrics.recordInterval('Load.BackgroundLaunch'); });
});
};
/** @const {!string} */
var GPLUS_PHOTOS_APP_ID = 'efjnaogkjbogokcnohkmnjdojkikgobo';
/**
* Handles a message received via chrome.runtime.sendMessageExternal.
*
* @param {*} message
* @param {MessageSender} sender
*/
FileBrowserBackgroundImpl.prototype.onExternalMessageReceived_ =
function(message, sender) {
if ('id' in sender && sender.id === GPLUS_PHOTOS_APP_ID) {
importer.handlePhotosAppMessage(message);
}
};
/**
* Restarted the app, restore windows.
* @private
* @override
*/
FileBrowserBackgroundImpl.prototype.onRestarted_ = function() {
// Reopen file manager windows.
chrome.storage.local.get(function(items) {
for (var key in items) {
if (items.hasOwnProperty(key)) {
var match = key.match(FILES_ID_PATTERN);
if (match) {
metrics.startInterval('Load.BackgroundRestart');
var id = Number(match[1]);
try {
var appState = /** @type {Object} */ (JSON.parse(items[key]));
launcher.launchFileManager(appState, id, undefined, function() {
metrics.recordInterval('Load.BackgroundRestart');
});
} catch (e) {
console.error('Corrupt launch data for ' + id);
}
}
}
}
});
};
/**
* Handles clicks on a custom item on the launcher context menu.
* @param {!Object} info Event details.
* @private
*/
FileBrowserBackgroundImpl.prototype.onContextMenuClicked_ = function(info) {
if (info.menuItemId == 'new-window') {
// Find the focused window (if any) and use it's current url for the
// new window. If not found, then launch with the default url.
this.findFocusedWindow_().then(function(key) {
if (!key) {
launcher.launchFileManager(appState);
return;
}
var appState = {
// Do not clone the selection url, only the current directory.
currentDirectoryURL: window.appWindows[key].
contentWindow.appState.currentDirectoryURL
};
launcher.launchFileManager(appState);
}).catch(function(error) {
console.error(error.stack || error);
});
}
};
/**
* Looks for a focused window.
*
* @return {!Promise<?string>} Promise fulfilled with a key of the focused
* window, or null if not found.
* @private
*/
FileBrowserBackgroundImpl.prototype.findFocusedWindow_ = function() {
return new Promise(function(fulfill, reject) {
for (var key in window.appWindows) {
try {
if (window.appWindows[key].contentWindow.isFocused()) {
fulfill(key);
return;
}
} catch (ignore) {
// The isFocused method may not be defined during initialization.
// Therefore, wrapped with a try-catch block.
}
}
fulfill(null);
});
};
/**
* Handles mounted FSP volumes and fires the Files app. This is a quick fix for
* crbug.com/456648.
* @param {!Object} event Event details.
* @private
*/
FileBrowserBackgroundImpl.prototype.onMountCompleted_ = function(event) {
util.doIfPrimaryContext(() => {
this.onMountCompletedInternal_(event);
});
};
/**
* @param {!Object} event Event details.
* @private
*/
FileBrowserBackgroundImpl.prototype.onMountCompletedInternal_ = function(
event) {
// If there is no focused window, then create a new one opened on the
// mounted volume.
this.findFocusedWindow_()
.then(function(key) {
let statusOK = event.status === 'success' ||
event.status === 'error_path_already_mounted';
let volumeTypeOK = event.volumeMetadata.volumeType ===
VolumeManagerCommon.VolumeType.PROVIDED &&
event.volumeMetadata.source === VolumeManagerCommon.Source.FILE;
if (key === null && event.eventType === 'mount' && statusOK &&
event.volumeMetadata.mountContext === 'user' && volumeTypeOK) {
this.navigateToVolumeWhenReady_(event.volumeMetadata.volumeId);
}
}.bind(this))
.catch(function(error) {
console.error(error.stack || error);
});
};
/**
* Initializes the context menu. Recreates if already exists.
* @private
*/
FileBrowserBackgroundImpl.prototype.initContextMenu_ = function() {
try {
// According to the spec [1], the callback is optional. But no callback
// causes an error for some reason, so we call it with null-callback to
// prevent the error. http://crbug.com/353877
// Also, we read the runtime.lastError here not to output the message on the
// console as an unchecked error.
// - [1] https://developer.chrome.com/extensions/contextMenus#method-remove
chrome.contextMenus.remove('new-window', function() {
var ignore = chrome.runtime.lastError;
});
} catch (ignore) {
// There is no way to detect if the context menu is already added, therefore
// try to recreate it every time.
}
chrome.contextMenus.create({
id: 'new-window',
contexts: ['launcher'],
title: str('NEW_WINDOW_BUTTON_LABEL')
});
};
/**
* Singleton instance of Background object.
* @type {!FileBrowserBackgroundImpl}
*/
window.background = new FileBrowserBackgroundImpl();
/**
* Lastly, end recording of the background page Load.BackgroundScript metric.
* NOTE: This call must come after the call to metrics.clearUserId.
*/
metrics.recordInterval('Load.BackgroundScript');