1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 * Number of runtime errors catched in the background page.
14 * Counts runtime JavaScript errors.
16 window.onerror = function() { JSErrorCount++; };
19 * Type of a Files.app's instance launch.
22 var LaunchType = Object.freeze({
24 FOCUS_ANY_OR_CREATE: 1,
25 FOCUS_SAME_OR_CREATE: 2
29 * Root class of the background page.
32 function Background() {
34 * Map of all currently open app windows. The key is an app id.
35 * @type {Object.<string, AppWindow>}
40 * Synchronous queue for asynchronous calls.
41 * @type {AsyncUtil.Queue}
43 this.queue = new AsyncUtil.Queue();
46 * Progress center of the background page.
47 * @type {ProgressCenter}
49 this.progressCenter = new ProgressCenter();
52 * File operation manager.
53 * @type {FileOperationManager}
55 this.fileOperationManager = FileOperationManager.getInstance();
58 * Event handler for progress center.
59 * @type {FileOperationHandler}
62 this.fileOperationHandler_ = new FileOperationHandler(this);
66 * @type {Object.<string, string>}
68 this.stringData = null;
71 * Callback list to be invoked after initialization.
72 * It turns to null after initialization.
74 * @type {Array.<function()>}
77 this.initializeCallbacks_ = [];
80 * Last time when the background page can close.
85 this.lastTimeCanClose_ = null;
90 // Initialize handlers.
91 chrome.fileBrowserHandler.onExecute.addListener(this.onExecute_.bind(this));
92 chrome.app.runtime.onLaunched.addListener(this.onLaunched_.bind(this));
93 chrome.app.runtime.onRestarted.addListener(this.onRestarted_.bind(this));
94 chrome.contextMenus.onClicked.addListener(
95 this.onContextMenuClicked_.bind(this));
97 // Fetch strings and initialize the context menu.
98 this.queue.run(function(callNextStep) {
99 chrome.fileBrowserPrivate.getStrings(function(strings) {
100 // Initialize string assets.
101 this.stringData = strings;
102 loadTimeData.data = strings;
103 this.initContextMenu_();
105 // Invoke initialize callbacks.
106 for (var i = 0; i < this.initializeCallbacks_.length; i++) {
107 this.initializeCallbacks_[i]();
109 this.initializeCallbacks_ = null;
117 * A number of delay milliseconds from the first call of tryClose to the actual
123 Background.CLOSE_DELAY_MS_ = 5000;
126 * Make a key of window geometry preferences for the given initial URL.
127 * @param {string} url Initialize URL that the window has.
128 * @return {string} Key of window geometry preferences.
130 Background.makeGeometryKey = function(url) {
131 return 'windowGeometry' + ':' + url;
135 * Register callback to be invoked after initialization.
136 * If the initialization is already done, the callback is invoked immediately.
138 * @param {function()} callback Initialize callback to be registered.
140 Background.prototype.ready = function(callback) {
141 if (this.initializeCallbacks_ !== null)
142 this.initializeCallbacks_.push(callback);
148 * Checks the current condition of background page and closes it if possible.
150 Background.prototype.tryClose = function() {
151 // If the file operation is going, the background page cannot close.
152 if (this.fileOperationManager.hasQueuedTasks()) {
153 this.lastTimeCanClose_ = null;
157 var views = chrome.extension.getViews();
159 for (var i = 0; i < views.length; i++) {
160 // If the window that is not the background page itself and it is not
161 // closing, the background page cannot close.
162 if (views[i] !== window && !views[i].closing) {
163 this.lastTimeCanClose_ = null;
166 closing = closing || views[i].closing;
169 // If some windows are closing, or the background page can close but could not
170 // 5 seconds ago, We need more time for sure.
172 this.lastTimeCanClose_ === null ||
173 Date.now() - this.lastTimeCanClose_ < Background.CLOSE_DELAY_MS_) {
174 if (this.lastTimeCanClose_ === null)
175 this.lastTimeCanClose_ = Date.now();
176 setTimeout(this.tryClose.bind(this), Background.CLOSE_DELAY_MS_);
180 // Otherwise we can close the background page.
185 * Gets similar windows, it means with the same initial url.
186 * @param {string} url URL that the obtained windows have.
187 * @return {Array.<AppWindow>} List of similar windows.
189 Background.prototype.getSimilarWindows = function(url) {
191 for (var appID in this.appWindows) {
192 if (this.appWindows[appID].contentWindow.appInitialURL === url)
193 result.push(this.appWindows[appID]);
199 * Wrapper for an app window.
201 * Expects the following from the app scripts:
202 * 1. The page load handler should initialize the app using |window.appState|
203 * and call |util.platform.saveAppState|.
204 * 2. Every time the app state changes the app should update |window.appState|
205 * and call |util.platform.saveAppState| .
206 * 3. The app may have |unload| function to persist the app state that does not
207 * fit into |window.appState|.
209 * @param {string} url App window content url.
210 * @param {string} id App window id.
211 * @param {Object} options Options object to create it.
214 function AppWindowWrapper(url, id, options) {
217 // Do deep copy for the template of options to assign customized params later.
218 this.options_ = JSON.parse(JSON.stringify(options));
220 this.appState_ = null;
221 this.openingOrOpened_ = false;
222 this.queue = new AsyncUtil.Queue();
227 * Shift distance to avoid overlapping windows.
231 AppWindowWrapper.SHIFT_DISTANCE = 40;
237 * @param {Object} appState App state.
238 * @param {function()=} opt_callback Completion callback.
240 AppWindowWrapper.prototype.launch = function(appState, opt_callback) {
241 // Check if the window is opened or not.
242 if (this.openingOrOpened_) {
243 console.error('The window is already opened.');
248 this.openingOrOpened_ = true;
250 // Save application state.
251 this.appState_ = appState;
253 // Get similar windows, it means with the same initial url, eg. different
254 // main windows of Files.app.
255 var similarWindows = background.getSimilarWindows(this.url_);
257 // Restore maximized windows, to avoid hiding them to tray, which can be
258 // confusing for users.
259 this.queue.run(function(callback) {
260 for (var index = 0; index < similarWindows.length; index++) {
261 if (similarWindows[index].isMaximized()) {
262 var createWindowAndRemoveListener = function() {
263 similarWindows[index].onRestored.removeListener(
264 createWindowAndRemoveListener);
267 similarWindows[index].onRestored.addListener(
268 createWindowAndRemoveListener);
269 similarWindows[index].restore();
273 // If no maximized windows, then create the window immediately.
277 // Obtains the last geometry.
279 this.queue.run(function(callback) {
280 var key = Background.makeGeometryKey(this.url_);
281 chrome.storage.local.get(key, function(preferences) {
282 if (!chrome.runtime.lastError)
283 lastBounds = preferences[key];
288 // Closure creating the window, once all preprocessing tasks are finished.
289 this.queue.run(function(callback) {
290 // Apply the last bounds.
292 this.options_.bounds = lastBounds;
295 chrome.app.window.create(this.url_, this.options_, function(appWindow) {
296 this.window_ = appWindow;
302 this.queue.run(function(callback) {
303 // If there is another window in the same position, shift the window.
304 var makeBoundsKey = function(bounds) {
305 return bounds.left + '/' + bounds.top;
307 var notAvailablePositions = {};
308 for (var i = 0; i < similarWindows.length; i++) {
309 var key = makeBoundsKey(similarWindows[i].getBounds());
310 notAvailablePositions[key] = true;
312 var candidateBounds = this.window_.getBounds();
314 var key = makeBoundsKey(candidateBounds);
315 if (!notAvailablePositions[key])
317 // Make the position available to avoid an infinite loop.
318 notAvailablePositions[key] = false;
319 var nextLeft = candidateBounds.left + AppWindowWrapper.SHIFT_DISTANCE;
320 var nextRight = nextLeft + candidateBounds.width;
321 candidateBounds.left = nextRight >= screen.availWidth ?
322 nextRight % screen.availWidth : nextLeft;
323 var nextTop = candidateBounds.top + AppWindowWrapper.SHIFT_DISTANCE;
324 var nextBottom = nextTop + candidateBounds.height;
325 candidateBounds.top = nextBottom >= screen.availHeight ?
326 nextBottom % screen.availHeight : nextTop;
328 this.window_.moveTo(candidateBounds.left, candidateBounds.top);
330 // Save the properties.
331 var appWindow = this.window_;
332 background.appWindows[this.id_] = appWindow;
333 var contentWindow = appWindow.contentWindow;
334 contentWindow.appID = this.id_;
335 contentWindow.appState = this.appState_;
336 contentWindow.appInitialURL = this.url_;
338 contentWindow.IN_TEST = true;
340 // Register event listners.
341 appWindow.onBoundsChanged.addListener(this.onBoundsChanged_.bind(this));
342 appWindow.onClosed.addListener(this.onClosed_.bind(this));
352 * Handles the onClosed extension API event.
355 AppWindowWrapper.prototype.onClosed_ = function() {
356 // Unload the window.
357 var appWindow = this.window_;
358 var contentWindow = this.window_.contentWindow;
359 if (contentWindow.unload)
360 contentWindow.unload();
362 this.openingOrOpened_ = false;
364 // Updates preferences.
365 if (contentWindow.saveOnExit) {
366 contentWindow.saveOnExit.forEach(function(entry) {
367 util.AppCache.update(entry.key, entry.value);
370 chrome.storage.local.remove(this.id_); // Forget the persisted state.
372 // Remove the window from the set.
373 delete background.appWindows[this.id_];
375 // If there is no application window, reset window ID.
376 if (!Object.keys(background.appWindows).length)
377 nextFileManagerWindowID = 0;
378 background.tryClose();
382 * Handles onBoundsChanged extension API event.
385 AppWindowWrapper.prototype.onBoundsChanged_ = function() {
386 var preferences = {};
387 preferences[Background.makeGeometryKey(this.url_)] =
388 this.window_.getBounds();
389 chrome.storage.local.set(preferences);
393 * Wrapper for a singleton app window.
395 * In addition to the AppWindowWrapper requirements the app scripts should
396 * have |reload| method that re-initializes the app based on a changed
399 * @param {string} url App window content url.
400 * @param {Object|function()} options Options object or a function to return it.
403 function SingletonAppWindowWrapper(url, options) {
404 AppWindowWrapper.call(this, url, url, options);
408 * Inherits from AppWindowWrapper.
410 SingletonAppWindowWrapper.prototype = {__proto__: AppWindowWrapper.prototype};
415 * Activates an existing window or creates a new one.
417 * @param {Object} appState App state.
418 * @param {function()=} opt_callback Completion callback.
420 SingletonAppWindowWrapper.prototype.launch = function(appState, opt_callback) {
421 // If the window is not opened yet, just call the parent method.
422 if (!this.openingOrOpened_) {
423 AppWindowWrapper.prototype.launch.call(this, appState, opt_callback);
427 // If the window is already opened, reload the window.
428 // The queue is used to wait until the window is opened.
429 this.queue.run(function(nextStep) {
430 this.window_.contentWindow.appState = appState;
431 this.window_.contentWindow.reload();
432 this.window_.focus();
440 * Reopen a window if its state is saved in the local storage.
442 SingletonAppWindowWrapper.prototype.reopen = function() {
443 chrome.storage.local.get(this.id_, function(items) {
444 var value = items[this.id_];
446 return; // No app state persisted.
449 var appState = JSON.parse(value);
451 console.error('Corrupt launch data for ' + this.id_, value);
454 this.launch(appState);
459 * Prefix for the file manager window ID.
461 var FILES_ID_PREFIX = 'files#';
464 * Regexp matching a file manager window ID.
466 var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$');
469 * Value of the next file manager window ID.
471 var nextFileManagerWindowID = 0;
474 * File manager window create options.
478 var FILE_MANAGER_WINDOW_CREATE_OPTIONS = Object.freeze({
479 bounds: Object.freeze({
480 left: Math.round(window.screen.availWidth * 0.1),
481 top: Math.round(window.screen.availHeight * 0.1),
482 width: Math.round(window.screen.availWidth * 0.8),
483 height: Math.round(window.screen.availHeight * 0.8)
489 transparentBackground: true
493 * @param {Object=} opt_appState App state.
494 * @param {number=} opt_id Window id.
495 * @param {LaunchType=} opt_type Launch type. Default: ALWAYS_CREATE.
496 * @param {function(string)=} opt_callback Completion callback with the App ID.
498 function launchFileManager(opt_appState, opt_id, opt_type, opt_callback) {
499 var type = opt_type || LaunchType.ALWAYS_CREATE;
501 // Wait until all windows are created.
502 background.queue.run(function(onTaskCompleted) {
503 // Check if there is already a window with the same path. If so, then
504 // reuse it instead of opening a new one.
505 if (type == LaunchType.FOCUS_SAME_OR_CREATE ||
506 type == LaunchType.FOCUS_ANY_OR_CREATE) {
507 if (opt_appState && opt_appState.defaultPath) {
508 for (var key in background.appWindows) {
509 if (!key.match(FILES_ID_PATTERN))
512 var contentWindow = background.appWindows[key].contentWindow;
513 if (contentWindow.appState &&
514 opt_appState.defaultPath == contentWindow.appState.defaultPath) {
515 background.appWindows[key].focus();
525 // Focus any window if none is focused. Try restored first.
526 if (type == LaunchType.FOCUS_ANY_OR_CREATE) {
527 // If there is already a focused window, then finish.
528 for (var key in background.appWindows) {
529 if (!key.match(FILES_ID_PATTERN))
532 // The isFocused() method should always be available, but in case
533 // Files.app's failed on some error, wrap it with try catch.
535 if (background.appWindows[key].contentWindow.isFocused()) {
542 console.error(e.message);
545 // Try to focus the first non-minimized window.
546 for (var key in background.appWindows) {
547 if (!key.match(FILES_ID_PATTERN))
550 if (!background.appWindows[key].isMinimized()) {
551 background.appWindows[key].focus();
558 // Restore and focus any window.
559 for (var key in background.appWindows) {
560 if (!key.match(FILES_ID_PATTERN))
563 background.appWindows[key].focus();
571 // Create a new instance in case of ALWAYS_CREATE type, or as a fallback
574 var id = opt_id || nextFileManagerWindowID;
575 nextFileManagerWindowID = Math.max(nextFileManagerWindowID, id + 1);
576 var appId = FILES_ID_PREFIX + id;
578 var appWindow = new AppWindowWrapper(
581 FILE_MANAGER_WINDOW_CREATE_OPTIONS);
582 appWindow.launch(opt_appState || {}, function() {
591 * Executes a file browser task.
593 * @param {string} action Task id.
594 * @param {Object} details Details object.
597 Background.prototype.onExecute_ = function(action, details) {
598 var urls = details.entries.map(function(e) { return e.toURL(); });
602 launchAudioPlayer({items: urls, position: 0});
606 launchVideoPlayer(urls[0]);
610 var launchEnable = null;
611 var queue = new AsyncUtil.Queue();
612 queue.run(function(nextStep) {
613 // If it is not auto-open (triggered by mounting external devices), we
614 // always launch Files.app.
615 if (action != 'auto-open') {
620 // If the disable-default-apps flag is on, Files.app is not opened
621 // automatically on device mount because it obstculs the manual test.
622 chrome.commandLinePrivate.hasSwitch('disable-default-apps',
624 launchEnable = !flag;
628 queue.run(function(nextStep) {
634 // Every other action opens a Files app window.
639 defaultPath: details.entries[0].fullPath
641 // For mounted devices just focus any Files.app window. The mounted
642 // volume will appear on the navigation list.
643 var type = action == 'auto-open' ? LaunchType.FOCUS_ANY_OR_CREATE :
644 LaunchType.FOCUS_SAME_OR_CREATE;
645 launchFileManager(appState, /* App ID */ undefined, type, nextStep);
652 * Audio player window create options.
656 var AUDIO_PLAYER_CREATE_OPTIONS = Object.freeze({
665 var audioPlayer = new SingletonAppWindowWrapper('mediaplayer.html',
666 AUDIO_PLAYER_CREATE_OPTIONS);
669 * Launch the audio player.
670 * @param {Object} playlist Playlist.
672 function launchAudioPlayer(playlist) {
673 audioPlayer.launch(playlist);
676 var videoPlayer = new SingletonAppWindowWrapper('video_player.html',
680 * Launch the video player.
681 * @param {string} url Video url.
683 function launchVideoPlayer(url) {
684 videoPlayer.launch({url: url});
691 Background.prototype.onLaunched_ = function() {
692 if (nextFileManagerWindowID == 0) {
693 // The app just launched. Remove window state records that are not needed
695 chrome.storage.local.get(function(items) {
696 for (var key in items) {
697 if (items.hasOwnProperty(key)) {
698 if (key.match(FILES_ID_PATTERN))
699 chrome.storage.local.remove(key);
704 launchFileManager(null, null, LaunchType.FOCUS_ANY_OR_CREATE);
708 * Restarted the app, restore windows.
711 Background.prototype.onRestarted_ = function() {
712 // Reopen file manager windows.
713 chrome.storage.local.get(function(items) {
714 for (var key in items) {
715 if (items.hasOwnProperty(key)) {
716 var match = key.match(FILES_ID_PATTERN);
718 var id = Number(match[1]);
720 var appState = JSON.parse(items[key]);
721 launchFileManager(appState, id);
723 console.error('Corrupt launch data for ' + id);
730 // Reopen sub-applications.
731 audioPlayer.reopen();
732 videoPlayer.reopen();
736 * Handles clicks on a custom item on the launcher context menu.
737 * @param {OnClickData} info Event details.
740 Background.prototype.onContextMenuClicked_ = function(info) {
741 if (info.menuItemId == 'new-window') {
742 // Find the focused window (if any) and use it's current path for the
743 // new window. If not found, then launch with the default path.
744 for (var key in background.appWindows) {
746 if (background.appWindows[key].contentWindow.isFocused()) {
748 defaultPath: background.appWindows[key].contentWindow.
751 launchFileManager(appState);
755 // The isFocused method may not be defined during initialization.
756 // Therefore, wrapped with a try-catch block.
760 // Launch with the default path.
766 * Initializes the context menu. Recreates if already exists.
769 Background.prototype.initContextMenu_ = function() {
771 chrome.contextMenus.remove('new-window');
773 // There is no way to detect if the context menu is already added, therefore
774 // try to recreate it every time.
776 chrome.contextMenus.create({
778 contexts: ['launcher'],
779 title: str('NEW_WINDOW_BUTTON_LABEL')
784 * Singleton instance of Background.
787 window.background = new Background();