| // 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. |
| |
| 'use strict'; |
| |
| /** |
| * Number of runtime errors catched in the background page. |
| * @type {number} |
| */ |
| var JSErrorCount = 0; |
| |
| /** |
| * Map of all currently open app window. The key is an app id. |
| * @type {Object.<string, AppWindow>} |
| */ |
| var appWindows = {}; |
| |
| /** |
| * Synchronous queue for asynchronous calls. |
| * @type {AsyncUtil.Queue} |
| */ |
| var queue = new AsyncUtil.Queue(); |
| |
| /** |
| * @return {Array.<DOMWindow>} Array of content windows for all currently open |
| * app windows. |
| */ |
| function getContentWindows() { |
| var views = []; |
| for (var key in appWindows) { |
| if (appWindows.hasOwnProperty(key)) |
| views.push(appWindows[key].contentWindow); |
| } |
| return views; |
| } |
| |
| /** |
| * 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 |
| }; |
| |
| /** |
| * Wrapper for an app window. |
| * |
| * Expects the following from the app scripts: |
| * 1. The page load handler should initialize the app using |window.appState| |
| * and call |util.platform.saveAppState|. |
| * 2. Every time the app state changes the app should update |window.appState| |
| * and call |util.platform.saveAppState| . |
| * 3. The app may have |unload| function to persist the app state that does not |
| * fit into |window.appState|. |
| * |
| * @param {string} url App window content url. |
| * @param {string} id App window id. |
| * @param {Object|function()} options Options object or a function to create it. |
| * @constructor |
| */ |
| function AppWindowWrapper(url, id, options) { |
| this.url_ = url; |
| this.id_ = id; |
| this.options_ = options; |
| this.window_ = null; |
| this.appState_ = null; |
| this.openingOrOpened_ = false; |
| this.queue = new AsyncUtil.Queue(); |
| Object.seal(this); |
| } |
| |
| /** |
| * Shift distance to avoid overlapping windows. |
| * @type {number} |
| * @const |
| */ |
| AppWindowWrapper.SHIFT_DISTANCE = 40; |
| |
| /** |
| * Gets similar windows, it means with the same initial url. |
| * @return {Array.<AppWindow>} List of similar windows. |
| * @private |
| */ |
| AppWindowWrapper.prototype.getSimilarWindows_ = function() { |
| var result = []; |
| for (var appID in appWindows) { |
| if (appWindows[appID].contentWindow.appInitialURL == this.url_) |
| result.push(appWindows[appID]); |
| } |
| return result; |
| }; |
| |
| /** |
| * Opens the window. |
| * |
| * @param {Object} appState App state. |
| * @param {function()=} opt_callback Completion callback. |
| */ |
| AppWindowWrapper.prototype.launch = function(appState, opt_callback) { |
| // Check if the window is opened or not. |
| if (this.openingOrOpened_) { |
| console.error('The window is already opened.'); |
| if (opt_callback) |
| opt_callback(); |
| return; |
| } |
| this.openingOrOpened_ = true; |
| |
| // Save application state. |
| this.appState_ = appState; |
| |
| // Window options. |
| var options = this.options_; |
| if (typeof options == 'function') |
| options = options(); |
| options.id = this.url_; // This is to make Chrome reuse window geometries. |
| options.singleton = false; |
| |
| // Get similar windows, it means with the same initial url, eg. different |
| // main windows of Files.app. |
| var similarWindows = this.getSimilarWindows_(); |
| |
| // Restore maximized windows, to avoid hiding them to tray, which can be |
| // confusing for users. |
| this.queue.run(function(nextStep) { |
| for (var index = 0; index < similarWindows.length; index++) { |
| if (similarWindows[index].isMaximized()) { |
| var createWindowAndRemoveListener = function() { |
| similarWindows[index].onRestored.removeListener( |
| createWindowAndRemoveListener); |
| nextStep(); |
| }; |
| similarWindows[index].onRestored.addListener( |
| createWindowAndRemoveListener); |
| similarWindows[index].restore(); |
| return; |
| } |
| } |
| // If no maximized windows, then create the window immediately. |
| nextStep(); |
| }); |
| |
| // Closure creating the window, once all preprocessing tasks are finished. |
| this.queue.run(function(nextStep) { |
| chrome.app.window.create(this.url_, options, function(appWindow) { |
| this.window_ = appWindow; |
| nextStep(); |
| }.bind(this)); |
| }.bind(this)); |
| |
| // After creating. |
| this.queue.run(function(nextStep) { |
| var appWindow = this.window_; |
| if (similarWindows.length) { |
| // If we have already another window of the same kind, then shift this |
| // window to avoid overlapping with the previous one. |
| |
| var bounds = appWindow.getBounds(); |
| appWindow.moveTo(bounds.left + AppWindowWrapper.SHIFT_DISTANCE, |
| bounds.top + AppWindowWrapper.SHIFT_DISTANCE); |
| } |
| |
| appWindows[this.id_] = appWindow; |
| var contentWindow = appWindow.contentWindow; |
| contentWindow.appID = this.id_; |
| contentWindow.appState = this.appState_; |
| contentWindow.appInitialURL = this.url_; |
| if (window.IN_TEST) |
| contentWindow.IN_TEST = true; |
| |
| appWindow.onClosed.addListener(function() { |
| if (contentWindow.unload) |
| contentWindow.unload(); |
| if (contentWindow.saveOnExit) { |
| contentWindow.saveOnExit.forEach(function(entry) { |
| util.AppCache.update(entry.key, entry.value); |
| }); |
| } |
| delete appWindows[this.id_]; |
| chrome.storage.local.remove(this.id_); // Forget the persisted state. |
| this.window_ = null; |
| this.openingOrOpened_ = false; |
| maybeCloseBackgroundPage(); |
| }.bind(this)); |
| |
| if (opt_callback) |
| opt_callback(); |
| |
| nextStep(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Wrapper for a singleton app window. |
| * |
| * In addition to the AppWindowWrapper requirements the app scripts should |
| * have |reload| method that re-initializes the app based on a changed |
| * |window.appState|. |
| * |
| * @param {string} url App window content url. |
| * @param {Object|function()} options Options object or a function to return it. |
| * @constructor |
| */ |
| function SingletonAppWindowWrapper(url, options) { |
| AppWindowWrapper.call(this, url, url, options); |
| } |
| |
| /** |
| * Inherits from AppWindowWrapper. |
| */ |
| SingletonAppWindowWrapper.prototype = {__proto__: AppWindowWrapper.prototype}; |
| |
| /** |
| * Open the window. |
| * |
| * Activates an existing window or creates a new one. |
| * |
| * @param {Object} appState App state. |
| * @param {function()=} opt_callback Completion callback. |
| */ |
| SingletonAppWindowWrapper.prototype.launch = function(appState, opt_callback) { |
| // If the window is not opened yet, just call the parent method. |
| if (!this.openingOrOpened_) { |
| AppWindowWrapper.prototype.launch.call(this, appState, opt_callback); |
| return; |
| } |
| |
| // If the window is already opened, reload the window. |
| // The queue is used to wait until the window is opened. |
| this.queue.run(function(nextStep) { |
| this.window_.contentWindow.appState = appState; |
| this.window_.contentWindow.reload(); |
| this.window_.focus(); |
| if (opt_callback) |
| opt_callback(); |
| nextStep(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Reopen a window if its state is saved in the local storage. |
| */ |
| SingletonAppWindowWrapper.prototype.reopen = function() { |
| chrome.storage.local.get(this.id_, function(items) { |
| var value = items[this.id_]; |
| if (!value) |
| return; // No app state persisted. |
| |
| try { |
| var appState = JSON.parse(value); |
| } catch (e) { |
| console.error('Corrupt launch data for ' + this.id_, value); |
| return; |
| } |
| this.launch(appState); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Prefix for the file manager window ID. |
| */ |
| var FILES_ID_PREFIX = 'files#'; |
| |
| /** |
| * Regexp matching a file manager window ID. |
| */ |
| var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$'); |
| |
| /** |
| * Value of the next file manager window ID. |
| */ |
| var nextFileManagerWindowID = 0; |
| |
| /** |
| * @return {Object} File manager window create options. |
| */ |
| function createFileManagerOptions() { |
| return { |
| defaultLeft: Math.round(window.screen.availWidth * 0.1), |
| defaultTop: Math.round(window.screen.availHeight * 0.1), |
| defaultWidth: Math.round(window.screen.availWidth * 0.8), |
| defaultHeight: Math.round(window.screen.availHeight * 0.8), |
| minWidth: 320, |
| minHeight: 240, |
| frame: 'none', |
| hidden: true, |
| transparentBackground: 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. |
| queue.run(function(onTaskCompleted) { |
| // Check if there is already a window with the same path. 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 && opt_appState.defaultPath) { |
| for (var key in appWindows) { |
| var contentWindow = appWindows[key].contentWindow; |
| if (contentWindow.appState && |
| opt_appState.defaultPath == contentWindow.appState.defaultPath) { |
| appWindows[key].focus(); |
| 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 appWindows) { |
| // The isFocused() method should always be available, but in case |
| // Files.app's failed on some error, wrap it with try catch. |
| try { |
| if (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 appWindows) { |
| if (!appWindows[key].isMinimized()) { |
| appWindows[key].focus(); |
| if (opt_callback) |
| opt_callback(key); |
| onTaskCompleted(); |
| return; |
| } |
| } |
| // Restore and focus any window. |
| for (var key in appWindows) { |
| appWindows[key].focus(); |
| 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, |
| createFileManagerOptions); |
| appWindow.launch(opt_appState || {}, function() { |
| if (opt_callback) |
| opt_callback(appId); |
| onTaskCompleted(); |
| }); |
| }); |
| } |
| |
| /** |
| * Relaunch file manager windows based on the persisted state. |
| */ |
| function reopenFileManagers() { |
| 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 = JSON.parse(items[key]); |
| launchFileManager(appState, id); |
| } catch (e) { |
| console.error('Corrupt launch data for ' + id); |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Executes a file browser task. |
| * |
| * @param {string} action Task id. |
| * @param {Object} details Details object. |
| */ |
| function onExecute(action, details) { |
| var urls = details.entries.map(function(e) { return e.toURL(); }); |
| |
| switch (action) { |
| case 'play': |
| launchAudioPlayer({items: urls, position: 0}); |
| break; |
| |
| case 'watch': |
| launchVideoPlayer(urls[0]); |
| break; |
| |
| default: |
| var launchEnable = null; |
| var queue = new AsyncUtil.Queue(); |
| queue.run(function(nextStep) { |
| // If it is not auto-open (triggered by mounting external devices), we |
| // always launch Files.app. |
| if (action != 'auto-open') { |
| launchEnable = true; |
| nextStep(); |
| return; |
| } |
| // If the disable-default-apps flag is on, Files.app is not opened |
| // automatically on device mount because it obstculs the manual test. |
| chrome.commandLinePrivate.hasSwitch('disable-default-apps', |
| function(flag) { |
| launchEnable = !flag; |
| nextStep(); |
| }); |
| }); |
| queue.run(function(nextStep) { |
| if (!launchEnable) { |
| nextStep(); |
| return; |
| } |
| |
| // Every other action opens a Files app window. |
| var appState = { |
| params: { |
| action: action |
| }, |
| defaultPath: details.entries[0].fullPath |
| }; |
| // For mounted devices just focus any Files.app window. The mounted |
| // volume will appear on the navigation list. |
| var type = action == 'auto-open' ? LaunchType.FOCUS_ANY_OR_CREATE : |
| LaunchType.FOCUS_SAME_OR_CREATE; |
| launchFileManager(appState, /* App ID */ undefined, type, nextStep); |
| }); |
| break; |
| } |
| } |
| |
| /** |
| * @return {Object} Audio player window create options. |
| */ |
| function createAudioPlayerOptions() { |
| var WIDTH = 280; |
| var HEIGHT = 35 + 58; |
| return { |
| type: 'panel', |
| hidden: true, |
| minHeight: HEIGHT, |
| minWidth: WIDTH, |
| height: HEIGHT, |
| width: WIDTH |
| }; |
| } |
| |
| var audioPlayer = new SingletonAppWindowWrapper('mediaplayer.html', |
| createAudioPlayerOptions); |
| |
| /** |
| * Launch the audio player. |
| * @param {Object} playlist Playlist. |
| */ |
| function launchAudioPlayer(playlist) { |
| audioPlayer.launch(playlist); |
| } |
| |
| var videoPlayer = new SingletonAppWindowWrapper('video_player.html', |
| {hidden: true}); |
| |
| /** |
| * Launch the video player. |
| * @param {string} url Video url. |
| */ |
| function launchVideoPlayer(url) { |
| videoPlayer.launch({url: url}); |
| } |
| |
| /** |
| * Launches the app. |
| */ |
| function onLaunched() { |
| 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(); |
| } |
| |
| /** |
| * Restarted the app, restore windows. |
| */ |
| function onRestarted() { |
| reopenFileManagers(); |
| audioPlayer.reopen(); |
| videoPlayer.reopen(); |
| } |
| |
| /** |
| * Handles clicks on a custom item on the launcher context menu. |
| * @param {OnClickData} info Event details. |
| */ |
| function onContextMenuClicked(info) { |
| if (info.menuItemId == 'new-window') { |
| // Find the focused window (if any) and use it's current path for the |
| // new window. If not found, then launch with the default path. |
| for (var key in appWindows) { |
| try { |
| if (appWindows[key].contentWindow.isFocused()) { |
| var appState = { |
| defaultPath: appWindows[key].contentWindow.appState.defaultPath |
| }; |
| 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 path. |
| launchFileManager(); |
| } |
| } |
| |
| /** |
| * Closes the background page, if it is not needed. |
| */ |
| function maybeCloseBackgroundPage() { |
| if (Object.keys(appWindows).length === 0 && |
| !FileOperationManager.getInstance().hasQueuedTasks()) |
| close(); |
| } |
| |
| /** |
| * Initializes the context menu. Recreates if already exists. |
| * @param {Object} strings Hash array of strings. |
| */ |
| function initContextMenu(strings) { |
| try { |
| chrome.contextMenus.remove('new-window'); |
| } 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: strings['NEW_WINDOW_BUTTON_LABEL'] |
| }); |
| } |
| |
| /** |
| * Initializes the background page of Files.app. |
| */ |
| function initApp() { |
| // Initialize handlers. |
| chrome.fileBrowserHandler.onExecute.addListener(onExecute); |
| chrome.app.runtime.onLaunched.addListener(onLaunched); |
| chrome.app.runtime.onRestarted.addListener(onRestarted); |
| chrome.contextMenus.onClicked.addListener(onContextMenuClicked); |
| |
| // Fetch strings and initialize the context menu. |
| queue.run(function(callback) { |
| chrome.fileBrowserPrivate.getStrings(function(strings) { |
| loadTimeData.data = strings; |
| initContextMenu(strings); |
| chrome.storage.local.set({strings: strings}, callback); |
| }); |
| }); |
| |
| // Count runtime JavaScript errors. |
| window.onerror = function() { |
| JSErrorCount++; |
| }; |
| } |
| |
| // Initialize Files.app. |
| initApp(); |
| |
| /** |
| * Progress center of the background page. |
| * @type {ProgressCenter} |
| */ |
| window.progressCenter = new ProgressCenter(); |
| |
| /** |
| * Event handler for progress center. |
| * @type {ProgressCenter} |
| */ |
| var progressCenterHandler = new ProgressCenterHandler( |
| FileOperationManager.getInstance(), |
| window.progressCenter); |