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.
6 * Type of a Files.app's instance launch.
11 FOCUS_ANY_OR_CREATE: 1,
12 FOCUS_SAME_OR_CREATE: 2
14 Object.freeze(LaunchType);
17 * Root class of the background page.
19 * @extends {BackgroundBase}
21 function FileBrowserBackground() {
22 BackgroundBase.call(this);
25 * Map of all currently open file dialogs. The key is an app ID.
26 * @type {Object.<string, Window>}
31 * Synchronous queue for asynchronous calls.
32 * @type {AsyncUtil.Queue}
34 this.queue = new AsyncUtil.Queue();
37 * Progress center of the background page.
38 * @type {ProgressCenter}
40 this.progressCenter = new ProgressCenter();
43 * File operation manager.
44 * @type {FileOperationManager}
46 this.fileOperationManager = new FileOperationManager();
49 * Manages loading of import history necessary for decorating files
50 * in some views and integral to local dedupling files during the
51 * cloud import process.
53 * @type {HistoryLoader}
55 this.historyLoader = null;
57 chrome.commandLinePrivate.hasSwitch(
58 'enable-cloud-backup',
60 * @param {boolean} enabled
61 * @this {!FileBrowserBackground}
65 this.historyLoader = new SynchronizedHistoryLoader(
66 new ChromeSyncFileEntryProvider());
71 * Event handler for progress center.
72 * @type {FileOperationHandler}
75 this.fileOperationHandler_ = new FileOperationHandler(this);
78 * Event handler for C++ sides notifications.
79 * @type {DeviceHandler}
82 this.deviceHandler_ = new DeviceHandler();
83 this.deviceHandler_.addEventListener(
84 DeviceHandler.VOLUME_NAVIGATION_REQUESTED,
86 this.navigateToVolume_(event.devicePath);
91 * @type {DriveSyncHandler}
93 this.driveSyncHandler = new DriveSyncHandler(this.progressCenter);
94 this.driveSyncHandler.addEventListener(
95 DriveSyncHandler.COMPLETED_EVENT,
96 function() { this.tryClose(); }.bind(this));
99 * Promise of string data.
102 this.stringDataPromise = new Promise(function(fulfill) {
103 chrome.fileManagerPrivate.getStrings(fulfill);
108 * @type {Object.<string, string>}
110 this.stringData = null;
113 * Callback list to be invoked after initialization.
114 * It turns to null after initialization.
116 * @type {Array.<function()>}
119 this.initializeCallbacks_ = [];
122 * Last time when the background page can close.
127 this.lastTimeCanClose_ = null;
132 // Initialize handlers.
133 chrome.fileBrowserHandler.onExecute.addListener(this.onExecute_.bind(this));
134 chrome.app.runtime.onLaunched.addListener(this.onLaunched_.bind(this));
135 chrome.app.runtime.onRestarted.addListener(this.onRestarted_.bind(this));
136 chrome.contextMenus.onClicked.addListener(
137 this.onContextMenuClicked_.bind(this));
139 this.queue.run(function(callback) {
140 this.stringDataPromise.then(function(strings) {
142 this.stringData = strings;
143 loadTimeData.data = strings;
145 // Init context menu.
146 this.initContextMenu_();
149 }.bind(this)).catch(function(error) {
150 console.error(error.stack || error);
157 * A number of delay milliseconds from the first call of tryClose to the actual
163 FileBrowserBackground.CLOSE_DELAY_MS_ = 5000;
165 FileBrowserBackground.prototype = {
166 __proto__: BackgroundBase.prototype
170 * Register callback to be invoked after initialization.
171 * If the initialization is already done, the callback is invoked immediately.
173 * @param {function()} callback Initialize callback to be registered.
175 FileBrowserBackground.prototype.ready = function(callback) {
176 this.stringDataPromise.then(callback);
180 * Checks the current condition of background page.
181 * @return {boolean} True if the background page is closable, false if not.
183 FileBrowserBackground.prototype.canClose = function() {
184 // If the file operation is going, the background page cannot close.
185 if (this.fileOperationManager.hasQueuedTasks() ||
186 this.driveSyncHandler.syncing) {
187 this.lastTimeCanClose_ = null;
191 var views = chrome.extension.getViews();
193 for (var i = 0; i < views.length; i++) {
194 // If the window that is not the background page itself and it is not
195 // closing, the background page cannot close.
196 if (views[i] !== window && !views[i].closing) {
197 this.lastTimeCanClose_ = null;
200 closing = closing || views[i].closing;
203 // If some windows are closing, or the background page can close but could not
204 // 5 seconds ago, We need more time for sure.
206 this.lastTimeCanClose_ === null ||
207 (Date.now() - this.lastTimeCanClose_ <
208 FileBrowserBackground.CLOSE_DELAY_MS_)) {
209 if (this.lastTimeCanClose_ === null)
210 this.lastTimeCanClose_ = Date.now();
211 setTimeout(this.tryClose.bind(this), FileBrowserBackground.CLOSE_DELAY_MS_);
215 // Otherwise we can close the background page.
220 * Opens the root directory of the volume in Files.app.
221 * @param {string} devicePath Device path to a volume to be opened.
224 FileBrowserBackground.prototype.navigateToVolume_ = function(devicePath) {
225 VolumeManager.getInstance().then(function(volumeManager) {
226 var volumeInfoList = volumeManager.volumeInfoList;
227 for (var i = 0; i < volumeInfoList.length; i++) {
228 if (volumeInfoList.item(i).devicePath == devicePath)
229 return volumeInfoList.item(i).resolveDisplayRoot();
231 return Promise.reject(
232 'Volume having the device path: ' + devicePath + ' is not found.');
233 }).then(function(entry) {
235 {currentDirectoryURL: entry.toURL()},
236 /* App ID */ undefined,
237 LaunchType.FOCUS_SAME_OR_CREATE);
238 }).catch(function(error) {
239 console.error(error.stack || error);
244 * Prefix for the file manager window ID.
248 var FILES_ID_PREFIX = 'files#';
251 * Regexp matching a file manager window ID.
255 var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$');
258 * Prefix for the dialog ID.
262 var DIALOG_ID_PREFIX = 'dialog#';
265 * Value of the next file manager window ID.
268 var nextFileManagerWindowID = 0;
271 * Value of the next file manager dialog ID.
274 var nextFileManagerDialogID = 0;
277 * File manager window create options.
281 var FILE_MANAGER_WINDOW_CREATE_OPTIONS = Object.freeze({
282 bounds: Object.freeze({
283 left: Math.round(window.screen.availWidth * 0.1),
284 top: Math.round(window.screen.availHeight * 0.1),
285 width: Math.round(window.screen.availWidth * 0.8),
286 height: Math.round(window.screen.availHeight * 0.8)
294 * @param {Object=} opt_appState App state.
295 * @param {number=} opt_id Window id.
296 * @param {LaunchType=} opt_type Launch type. Default: ALWAYS_CREATE.
297 * @param {function(string)=} opt_callback Completion callback with the App ID.
299 function launchFileManager(opt_appState, opt_id, opt_type, opt_callback) {
300 var type = opt_type || LaunchType.ALWAYS_CREATE;
304 * {currentDirectoryURL: (string|undefined),
305 * selectionURL: (string|undefined),
306 * displayedId: (string|undefined)})}
310 // Wait until all windows are created.
311 window.background.queue.run(function(onTaskCompleted) {
312 // Check if there is already a window with the same URL. If so, then
313 // reuse it instead of opening a new one.
314 if (type == LaunchType.FOCUS_SAME_OR_CREATE ||
315 type == LaunchType.FOCUS_ANY_OR_CREATE) {
317 for (var key in window.background.appWindows) {
318 if (!key.match(FILES_ID_PATTERN))
321 var contentWindow = window.background.appWindows[key].contentWindow;
322 if (!contentWindow.appState)
325 // Different current directories.
326 if (opt_appState.currentDirectoryURL !==
327 contentWindow.appState.currentDirectoryURL) {
331 // Selection URL specified, and it is different.
332 if (opt_appState.selectionURL &&
333 opt_appState.selectionURL !==
334 contentWindow.appState.selectionURL) {
338 AppWindowWrapper.focusOnDesktop(
339 window.background.appWindows[key], opt_appState.displayedId);
348 // Focus any window if none is focused. Try restored first.
349 if (type == LaunchType.FOCUS_ANY_OR_CREATE) {
350 // If there is already a focused window, then finish.
351 for (var key in window.background.appWindows) {
352 if (!key.match(FILES_ID_PATTERN))
355 // The isFocused() method should always be available, but in case
356 // Files.app's failed on some error, wrap it with try catch.
358 if (window.background.appWindows[key].contentWindow.isFocused()) {
365 console.error(e.message);
368 // Try to focus the first non-minimized window.
369 for (var key in window.background.appWindows) {
370 if (!key.match(FILES_ID_PATTERN))
373 if (!window.background.appWindows[key].isMinimized()) {
374 AppWindowWrapper.focusOnDesktop(
375 window.background.appWindows[key],
376 (opt_appState || {}).displayedId);
383 // Restore and focus any window.
384 for (var key in window.background.appWindows) {
385 if (!key.match(FILES_ID_PATTERN))
388 AppWindowWrapper.focusOnDesktop(
389 window.background.appWindows[key],
390 (opt_appState || {}).displayedId);
398 // Create a new instance in case of ALWAYS_CREATE type, or as a fallback
401 var id = opt_id || nextFileManagerWindowID;
402 nextFileManagerWindowID = Math.max(nextFileManagerWindowID, id + 1);
403 var appId = FILES_ID_PREFIX + id;
405 var appWindow = new AppWindowWrapper(
408 FILE_MANAGER_WINDOW_CREATE_OPTIONS);
409 appWindow.launch(opt_appState || {}, false, function() {
410 AppWindowWrapper.focusOnDesktop(
411 appWindow.rawAppWindow, (opt_appState || {}).displayedId);
420 * Registers dialog window to the background page.
422 * @param {Window} dialogWindow Window of the dialog.
424 function registerDialog(dialogWindow) {
425 var id = DIALOG_ID_PREFIX + (nextFileManagerDialogID++);
426 window.background.dialogs[id] = dialogWindow;
427 dialogWindow.addEventListener('pagehide', function() {
428 delete window.background.dialogs[id];
433 * Executes a file browser task.
435 * @param {string} action Task id.
436 * @param {Object} details Details object.
439 FileBrowserBackground.prototype.onExecute_ = function(action, details) {
441 params: {action: action},
442 // It is not allowed to call getParent() here, since there may be
443 // no permissions to access it at this stage. Therefore we are passing
444 // the selectionURL only, and the currentDirectory will be resolved
446 selectionURL: details.entries[0].toURL()
449 // Every other action opens a Files app window.
450 // For mounted devices just focus any Files.app window. The mounted
451 // volume will appear on the navigation list.
454 /* App ID */ undefined,
455 LaunchType.FOCUS_SAME_OR_CREATE);
462 FileBrowserBackground.prototype.onLaunched_ = function() {
463 if (nextFileManagerWindowID == 0) {
464 // The app just launched. Remove window state records that are not needed
466 chrome.storage.local.get(function(items) {
467 for (var key in items) {
468 if (items.hasOwnProperty(key)) {
469 if (key.match(FILES_ID_PATTERN))
470 chrome.storage.local.remove(key);
475 launchFileManager(null, undefined, LaunchType.FOCUS_ANY_OR_CREATE);
479 * Restarted the app, restore windows.
482 FileBrowserBackground.prototype.onRestarted_ = function() {
483 // Reopen file manager windows.
484 chrome.storage.local.get(function(items) {
485 for (var key in items) {
486 if (items.hasOwnProperty(key)) {
487 var match = key.match(FILES_ID_PATTERN);
489 var id = Number(match[1]);
491 var appState = /** @type {Object} */ (JSON.parse(items[key]));
492 launchFileManager(appState, id);
494 console.error('Corrupt launch data for ' + id);
503 * Handles clicks on a custom item on the launcher context menu.
504 * @param {!Object} info Event details.
507 FileBrowserBackground.prototype.onContextMenuClicked_ = function(info) {
508 if (info.menuItemId == 'new-window') {
509 // Find the focused window (if any) and use it's current url for the
510 // new window. If not found, then launch with the default url.
511 for (var key in window.background.appWindows) {
513 if (window.background.appWindows[key].contentWindow.isFocused()) {
515 // Do not clone the selection url, only the current directory.
516 currentDirectoryURL: window.background.appWindows[key].
517 contentWindow.appState.currentDirectoryURL
519 launchFileManager(appState);
523 // The isFocused method may not be defined during initialization.
524 // Therefore, wrapped with a try-catch block.
528 // Launch with the default URL.
534 * Initializes the context menu. Recreates if already exists.
537 FileBrowserBackground.prototype.initContextMenu_ = function() {
539 // According to the spec [1], the callback is optional. But no callback
540 // causes an error for some reason, so we call it with null-callback to
541 // prevent the error. http://crbug.com/353877
542 // - [1] https://developer.chrome.com/extensions/contextMenus#method-remove
543 chrome.contextMenus.remove('new-window', function() {});
545 // There is no way to detect if the context menu is already added, therefore
546 // try to recreate it every time.
548 chrome.contextMenus.create({
550 contexts: ['launcher'],
551 title: str('NEW_WINDOW_BUTTON_LABEL')
556 * Singleton instance of Background.
557 * @type {FileBrowserBackground}
559 window.background = new FileBrowserBackground();