blob: 7ae18963600e528848d653a55f25652efa9efac2 [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.
/**
* Type of a Files.app's instance launch.
* @enum {number}
*/
var LaunchType = {
ALWAYS_CREATE: 0,
FOCUS_ANY_OR_CREATE: 1,
FOCUS_SAME_OR_CREATE: 2
};
Object.freeze(LaunchType);
/**
* Root class of the background page.
* @constructor
* @extends {BackgroundBase}
*/
function FileBrowserBackground() {
BackgroundBase.call(this);
/**
* Map of all currently open file dialogs. The key is an app ID.
* @type {Object.<string, Window>}
*/
this.dialogs = {};
/**
* Synchronous queue for asynchronous calls.
* @type {AsyncUtil.Queue}
*/
this.queue = new AsyncUtil.Queue();
/**
* Progress center of the background page.
* @type {ProgressCenter}
*/
this.progressCenter = new ProgressCenter();
/**
* File operation manager.
* @type {FileOperationManager}
*/
this.fileOperationManager = new FileOperationManager();
/**
* Event handler for progress center.
* @type {FileOperationHandler}
* @private
*/
this.fileOperationHandler_ = new FileOperationHandler(this);
/**
* Event handler for C++ sides notifications.
* @type {DeviceHandler}
* @private
*/
this.deviceHandler_ = new DeviceHandler();
this.deviceHandler_.addEventListener(
DeviceHandler.VOLUME_NAVIGATION_REQUESTED,
function(event) {
this.navigateToVolume_(event.devicePath);
}.bind(this));
/**
* Drive sync handler.
* @type {DriveSyncHandler}
* @private
*/
this.driveSyncHandler_ = new DriveSyncHandler(this.progressCenter);
this.driveSyncHandler_.addEventListener(
DriveSyncHandler.COMPLETED_EVENT,
function() { this.tryClose(); }.bind(this));
/**
* Promise of string data.
* @type {Promise}
*/
this.stringDataPromise = new Promise(function(fulfill) {
chrome.fileManagerPrivate.getStrings(fulfill);
});
/**
* String assets.
* @type {Object.<string, string>}
*/
this.stringData = null;
/**
* Callback list to be invoked after initialization.
* It turns to null after initialization.
*
* @type {Array.<function()>}
* @private
*/
this.initializeCallbacks_ = [];
/**
* Last time when the background page can close.
*
* @type {?number}
* @private
*/
this.lastTimeCanClose_ = null;
// Seal self.
Object.seal(this);
// Initialize handlers.
chrome.fileBrowserHandler.onExecute.addListener(this.onExecute_.bind(this));
chrome.app.runtime.onLaunched.addListener(this.onLaunched_.bind(this));
chrome.app.runtime.onRestarted.addListener(this.onRestarted_.bind(this));
chrome.contextMenus.onClicked.addListener(
this.onContextMenuClicked_.bind(this));
this.queue.run(function(callback) {
this.stringDataPromise.then(function(strings) {
// Init string data.
this.stringData = strings;
loadTimeData.data = strings;
// Init context menu.
this.initContextMenu_();
callback();
}.bind(this)).catch(function(error) {
console.error(error.stack || error);
callback();
});
}.bind(this));
}
/**
* A number of delay milliseconds from the first call of tryClose to the actual
* close action.
* @type {number}
* @const
* @private
*/
FileBrowserBackground.CLOSE_DELAY_MS_ = 5000;
FileBrowserBackground.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.
*/
FileBrowserBackground.prototype.ready = function(callback) {
this.stringDataPromise.then(callback);
};
/**
* Checks the current condition of background page.
* @return {boolean} True if the background page is closable, false if not.
*/
FileBrowserBackground.prototype.canClose = function() {
// If the file operation is going, the background page cannot close.
if (this.fileOperationManager.hasQueuedTasks() ||
this.driveSyncHandler_.syncing) {
this.lastTimeCanClose_ = null;
return false;
}
var views = chrome.extension.getViews();
var closing = false;
for (var i = 0; i < views.length; i++) {
// If the window that is not the background page itself and it is not
// closing, the background page cannot close.
if (views[i] !== window && !views[i].closing) {
this.lastTimeCanClose_ = null;
return false;
}
closing = closing || views[i].closing;
}
// If some windows are closing, or the background page can close but could not
// 5 seconds ago, We need more time for sure.
if (closing ||
this.lastTimeCanClose_ === null ||
(Date.now() - this.lastTimeCanClose_ <
FileBrowserBackground.CLOSE_DELAY_MS_)) {
if (this.lastTimeCanClose_ === null)
this.lastTimeCanClose_ = Date.now();
setTimeout(this.tryClose.bind(this), FileBrowserBackground.CLOSE_DELAY_MS_);
return false;
}
// Otherwise we can close the background page.
return true;
};
/**
* Opens the root directory of the volume in Files.app.
* @param {string} devicePath Device path to a volume to be opened.
* @private
*/
FileBrowserBackground.prototype.navigateToVolume_ = function(devicePath) {
VolumeManager.getInstance().then(function(volumeManager) {
var volumeInfoList = volumeManager.volumeInfoList;
for (var i = 0; i < volumeInfoList.length; i++) {
if (volumeInfoList.item(i).devicePath == devicePath)
return volumeInfoList.item(i).resolveDisplayRoot();
}
return Promise.reject(
'Volume having the device path: ' + devicePath + ' is not found.');
}).then(function(entry) {
launchFileManager(
{currentDirectoryURL: entry.toURL()},
/* App ID */ undefined,
LaunchType.FOCUS_SAME_OR_CREATE);
}).catch(function(error) {
console.error(error.stack || error);
});
};
/**
* Prefix for the file manager window ID.
* @type {string}
* @const
*/
var FILES_ID_PREFIX = 'files#';
/**
* Regexp matching a file manager window ID.
* @type {RegExp}
* @const
*/
var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$');
/**
* Prefix for the dialog ID.
* @type {string}
* @const
*/
var DIALOG_ID_PREFIX = 'dialog#';
/**
* Value of the next file manager window ID.
* @type {number}
*/
var nextFileManagerWindowID = 0;
/**
* Value of the next file manager dialog ID.
* @type {number}
*/
var nextFileManagerDialogID = 0;
/**
* File manager window create options.
* @type {Object}
* @const
*/
var FILE_MANAGER_WINDOW_CREATE_OPTIONS = Object.freeze({
bounds: Object.freeze({
left: Math.round(window.screen.availWidth * 0.1),
top: Math.round(window.screen.availHeight * 0.1),
width: Math.round(window.screen.availWidth * 0.8),
height: Math.round(window.screen.availHeight * 0.8)
}),
minWidth: 480,
minHeight: 300,
hidden: true
});
/**
* @param {Object=} opt_appState App state.
* @param {number=} opt_id Window id.
* @param {LaunchType=} opt_type Launch type. Default: ALWAYS_CREATE.
* @param {function(string)=} opt_callback Completion callback with the App ID.
*/
function launchFileManager(opt_appState, opt_id, opt_type, opt_callback) {
var type = opt_type || LaunchType.ALWAYS_CREATE;
// Wait until all windows are created.
background.queue.run(function(onTaskCompleted) {
// Check if there is already a window with the same URL. If so, then
// reuse it instead of opening a new one.
if (type == LaunchType.FOCUS_SAME_OR_CREATE ||
type == LaunchType.FOCUS_ANY_OR_CREATE) {
if (opt_appState) {
for (var key in background.appWindows) {
if (!key.match(FILES_ID_PATTERN))
continue;
var contentWindow = background.appWindows[key].contentWindow;
if (!contentWindow.appState)
continue;
// Different current directories.
if (opt_appState.currentDirectoryURL !==
contentWindow.appState.currentDirectoryURL) {
continue;
}
// Selection URL specified, and it is different.
if (opt_appState.selectionURL &&
opt_appState.selectionURL !==
contentWindow.appState.selectionURL) {
continue;
}
AppWindowWrapper.focusOnDesktop(
background.appWindows[key], opt_appState.displayedId);
if (opt_callback)
opt_callback(key);
onTaskCompleted();
return;
}
}
}
// Focus any window if none is focused. Try restored first.
if (type == LaunchType.FOCUS_ANY_OR_CREATE) {
// If there is already a focused window, then finish.
for (var key in background.appWindows) {
if (!key.match(FILES_ID_PATTERN))
continue;
// The isFocused() method should always be available, but in case
// Files.app's failed on some error, wrap it with try catch.
try {
if (background.appWindows[key].contentWindow.isFocused()) {
if (opt_callback)
opt_callback(key);
onTaskCompleted();
return;
}
} catch (e) {
console.error(e.message);
}
}
// Try to focus the first non-minimized window.
for (var key in background.appWindows) {
if (!key.match(FILES_ID_PATTERN))
continue;
if (!background.appWindows[key].isMinimized()) {
AppWindowWrapper.focusOnDesktop(
background.appWindows[key], (opt_appState || {}).displayedId);
if (opt_callback)
opt_callback(key);
onTaskCompleted();
return;
}
}
// Restore and focus any window.
for (var key in background.appWindows) {
if (!key.match(FILES_ID_PATTERN))
continue;
AppWindowWrapper.focusOnDesktop(
background.appWindows[key], (opt_appState || {}).displayedId);
if (opt_callback)
opt_callback(key);
onTaskCompleted();
return;
}
}
// Create a new instance in case of ALWAYS_CREATE type, or as a fallback
// for other types.
var id = opt_id || nextFileManagerWindowID;
nextFileManagerWindowID = Math.max(nextFileManagerWindowID, id + 1);
var appId = FILES_ID_PREFIX + id;
var appWindow = new AppWindowWrapper(
'main.html',
appId,
FILE_MANAGER_WINDOW_CREATE_OPTIONS);
appWindow.launch(opt_appState || {}, false, function() {
AppWindowWrapper.focusOnDesktop(
appWindow.rawAppWindow, (opt_appState || {}).displayedId);
if (opt_callback)
opt_callback(appId);
onTaskCompleted();
});
});
}
/**
* Registers dialog window to the background page.
*
* @param {Window} dialogWindow Window of the dialog.
*/
function registerDialog(dialogWindow) {
var id = DIALOG_ID_PREFIX + (nextFileManagerDialogID++);
background.dialogs[id] = dialogWindow;
dialogWindow.addEventListener('pagehide', function() {
delete background.dialogs[id];
});
}
/**
* Executes a file browser task.
*
* @param {string} action Task id.
* @param {Object} details Details object.
* @private
*/
FileBrowserBackground.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.
launchFileManager(
appState,
/* App ID */ undefined,
LaunchType.FOCUS_SAME_OR_CREATE);
};
/**
* Launches the app.
* @private
*/
FileBrowserBackground.prototype.onLaunched_ = 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);
}
}
});
}
launchFileManager(null, undefined, LaunchType.FOCUS_ANY_OR_CREATE);
};
/**
* Restarted the app, restore windows.
* @private
*/
FileBrowserBackground.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) {
var id = Number(match[1]);
try {
var appState = /** @type {Object} */ (JSON.parse(items[key]));
launchFileManager(appState, id);
} catch (e) {
console.error('Corrupt launch data for ' + id);
}
}
}
}
});
};
/**
* Handles clicks on a custom item on the launcher context menu.
* @param {OnClickData} info Event details.
* @private
*/
FileBrowserBackground.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.
for (var key in background.appWindows) {
try {
if (background.appWindows[key].contentWindow.isFocused()) {
var appState = {
// Do not clone the selection url, only the current directory.
currentDirectoryURL: background.appWindows[key].contentWindow.
appState.currentDirectoryURL
};
launchFileManager(appState);
return;
}
} catch (ignore) {
// The isFocused method may not be defined during initialization.
// Therefore, wrapped with a try-catch block.
}
}
// Launch with the default URL.
launchFileManager();
}
};
/**
* Initializes the context menu. Recreates if already exists.
* @private
*/
FileBrowserBackground.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
// - [1] https://developer.chrome.com/extensions/contextMenus#method-remove
chrome.contextMenus.remove('new-window', function() {});
} 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.
* @type {FileBrowserBackground}
*/
window.background = new FileBrowserBackground();