Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(317)

Side by Side Diff: chrome/browser/resources/file_manager/js/background.js

Issue 39123003: [Files.app] Split the JavaScript files into subdirectories: common, background, and foreground (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fixed test failure. Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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.
4
5 'use strict';
6
7 /**
8 * Number of runtime errors catched in the background page.
9 * @type {number}
10 */
11 var JSErrorCount = 0;
12
13 /**
14 * Map of all currently open app window. The key is an app id.
15 * @type {Object.<string, AppWindow>}
16 */
17 var appWindows = {};
18
19 /**
20 * Synchronous queue for asynchronous calls.
21 * @type {AsyncUtil.Queue}
22 */
23 var queue = new AsyncUtil.Queue();
24
25 /**
26 * Type of a Files.app's instance launch.
27 * @enum {number}
28 */
29 var LaunchType = Object.freeze({
30 ALWAYS_CREATE: 0,
31 FOCUS_ANY_OR_CREATE: 1,
32 FOCUS_SAME_OR_CREATE: 2
33 });
34
35 /**
36 * Wrapper for an app window.
37 *
38 * Expects the following from the app scripts:
39 * 1. The page load handler should initialize the app using |window.appState|
40 * and call |util.platform.saveAppState|.
41 * 2. Every time the app state changes the app should update |window.appState|
42 * and call |util.platform.saveAppState| .
43 * 3. The app may have |unload| function to persist the app state that does not
44 * fit into |window.appState|.
45 *
46 * @param {string} url App window content url.
47 * @param {string} id App window id.
48 * @param {Object} options Options object to create it.
49 * @constructor
50 */
51 function AppWindowWrapper(url, id, options) {
52 this.url_ = url;
53 this.id_ = id;
54 // Do deep copy for the template of options to assign own ID to the option
55 // params.
56 this.options_ = JSON.parse(JSON.stringify(options));
57 this.options_.id = url; // This is to make Chrome reuse window geometries.
58 this.window_ = null;
59 this.appState_ = null;
60 this.openingOrOpened_ = false;
61 this.queue = new AsyncUtil.Queue();
62 Object.seal(this);
63 }
64
65 /**
66 * Shift distance to avoid overlapping windows.
67 * @type {number}
68 * @const
69 */
70 AppWindowWrapper.SHIFT_DISTANCE = 40;
71
72 /**
73 * Gets similar windows, it means with the same initial url.
74 * @return {Array.<AppWindow>} List of similar windows.
75 * @private
76 */
77 AppWindowWrapper.prototype.getSimilarWindows_ = function() {
78 var result = [];
79 for (var appID in appWindows) {
80 if (appWindows[appID].contentWindow.appInitialURL == this.url_)
81 result.push(appWindows[appID]);
82 }
83 return result;
84 };
85
86 /**
87 * Opens the window.
88 *
89 * @param {Object} appState App state.
90 * @param {function()=} opt_callback Completion callback.
91 */
92 AppWindowWrapper.prototype.launch = function(appState, opt_callback) {
93 // Check if the window is opened or not.
94 if (this.openingOrOpened_) {
95 console.error('The window is already opened.');
96 if (opt_callback)
97 opt_callback();
98 return;
99 }
100 this.openingOrOpened_ = true;
101
102 // Save application state.
103 this.appState_ = appState;
104
105 // Get similar windows, it means with the same initial url, eg. different
106 // main windows of Files.app.
107 var similarWindows = this.getSimilarWindows_();
108
109 // Restore maximized windows, to avoid hiding them to tray, which can be
110 // confusing for users.
111 this.queue.run(function(nextStep) {
112 for (var index = 0; index < similarWindows.length; index++) {
113 if (similarWindows[index].isMaximized()) {
114 var createWindowAndRemoveListener = function() {
115 similarWindows[index].onRestored.removeListener(
116 createWindowAndRemoveListener);
117 nextStep();
118 };
119 similarWindows[index].onRestored.addListener(
120 createWindowAndRemoveListener);
121 similarWindows[index].restore();
122 return;
123 }
124 }
125 // If no maximized windows, then create the window immediately.
126 nextStep();
127 });
128
129 // Closure creating the window, once all preprocessing tasks are finished.
130 this.queue.run(function(nextStep) {
131 chrome.app.window.create(this.url_, this.options_, function(appWindow) {
132 this.window_ = appWindow;
133 nextStep();
134 }.bind(this));
135 }.bind(this));
136
137 // After creating.
138 this.queue.run(function(nextStep) {
139 var appWindow = this.window_;
140 if (similarWindows.length) {
141 // If we have already another window of the same kind, then shift this
142 // window to avoid overlapping with the previous one.
143
144 var bounds = appWindow.getBounds();
145 appWindow.moveTo(bounds.left + AppWindowWrapper.SHIFT_DISTANCE,
146 bounds.top + AppWindowWrapper.SHIFT_DISTANCE);
147 }
148
149 appWindows[this.id_] = appWindow;
150 var contentWindow = appWindow.contentWindow;
151 contentWindow.appID = this.id_;
152 contentWindow.appState = this.appState_;
153 contentWindow.appInitialURL = this.url_;
154 if (window.IN_TEST)
155 contentWindow.IN_TEST = true;
156
157 appWindow.onClosed.addListener(function() {
158 if (contentWindow.unload)
159 contentWindow.unload();
160 if (contentWindow.saveOnExit) {
161 contentWindow.saveOnExit.forEach(function(entry) {
162 util.AppCache.update(entry.key, entry.value);
163 });
164 }
165 delete appWindows[this.id_];
166 chrome.storage.local.remove(this.id_); // Forget the persisted state.
167 this.window_ = null;
168 this.openingOrOpened_ = false;
169 maybeCloseBackgroundPage();
170 }.bind(this));
171
172 if (opt_callback)
173 opt_callback();
174
175 nextStep();
176 }.bind(this));
177 };
178
179 /**
180 * Wrapper for a singleton app window.
181 *
182 * In addition to the AppWindowWrapper requirements the app scripts should
183 * have |reload| method that re-initializes the app based on a changed
184 * |window.appState|.
185 *
186 * @param {string} url App window content url.
187 * @param {Object|function()} options Options object or a function to return it.
188 * @constructor
189 */
190 function SingletonAppWindowWrapper(url, options) {
191 AppWindowWrapper.call(this, url, url, options);
192 }
193
194 /**
195 * Inherits from AppWindowWrapper.
196 */
197 SingletonAppWindowWrapper.prototype = {__proto__: AppWindowWrapper.prototype};
198
199 /**
200 * Open the window.
201 *
202 * Activates an existing window or creates a new one.
203 *
204 * @param {Object} appState App state.
205 * @param {function()=} opt_callback Completion callback.
206 */
207 SingletonAppWindowWrapper.prototype.launch = function(appState, opt_callback) {
208 // If the window is not opened yet, just call the parent method.
209 if (!this.openingOrOpened_) {
210 AppWindowWrapper.prototype.launch.call(this, appState, opt_callback);
211 return;
212 }
213
214 // If the window is already opened, reload the window.
215 // The queue is used to wait until the window is opened.
216 this.queue.run(function(nextStep) {
217 this.window_.contentWindow.appState = appState;
218 this.window_.contentWindow.reload();
219 this.window_.focus();
220 if (opt_callback)
221 opt_callback();
222 nextStep();
223 }.bind(this));
224 };
225
226 /**
227 * Reopen a window if its state is saved in the local storage.
228 */
229 SingletonAppWindowWrapper.prototype.reopen = function() {
230 chrome.storage.local.get(this.id_, function(items) {
231 var value = items[this.id_];
232 if (!value)
233 return; // No app state persisted.
234
235 try {
236 var appState = JSON.parse(value);
237 } catch (e) {
238 console.error('Corrupt launch data for ' + this.id_, value);
239 return;
240 }
241 this.launch(appState);
242 }.bind(this));
243 };
244
245 /**
246 * Prefix for the file manager window ID.
247 */
248 var FILES_ID_PREFIX = 'files#';
249
250 /**
251 * Regexp matching a file manager window ID.
252 */
253 var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$');
254
255 /**
256 * Value of the next file manager window ID.
257 */
258 var nextFileManagerWindowID = 0;
259
260 /**
261 * File manager window create options.
262 * @type {Object}
263 * @const
264 */
265 var FILE_MANAGER_WINDOW_CREATE_OPTIONS = Object.freeze({
266 defaultLeft: Math.round(window.screen.availWidth * 0.1),
267 defaultTop: Math.round(window.screen.availHeight * 0.1),
268 defaultWidth: Math.round(window.screen.availWidth * 0.8),
269 defaultHeight: Math.round(window.screen.availHeight * 0.8),
270 minWidth: 320,
271 minHeight: 240,
272 frame: 'none',
273 hidden: true,
274 transparentBackground: true,
275 singleton: false
276 });
277
278 /**
279 * @param {Object=} opt_appState App state.
280 * @param {number=} opt_id Window id.
281 * @param {LaunchType=} opt_type Launch type. Default: ALWAYS_CREATE.
282 * @param {function(string)=} opt_callback Completion callback with the App ID.
283 */
284 function launchFileManager(opt_appState, opt_id, opt_type, opt_callback) {
285 var type = opt_type || LaunchType.ALWAYS_CREATE;
286
287 // Wait until all windows are created.
288 queue.run(function(onTaskCompleted) {
289 // Check if there is already a window with the same path. If so, then
290 // reuse it instead of opening a new one.
291 if (type == LaunchType.FOCUS_SAME_OR_CREATE ||
292 type == LaunchType.FOCUS_ANY_OR_CREATE) {
293 if (opt_appState && opt_appState.defaultPath) {
294 for (var key in appWindows) {
295 var contentWindow = appWindows[key].contentWindow;
296 if (contentWindow.appState &&
297 opt_appState.defaultPath == contentWindow.appState.defaultPath) {
298 appWindows[key].focus();
299 if (opt_callback)
300 opt_callback(key);
301 onTaskCompleted();
302 return;
303 }
304 }
305 }
306 }
307
308 // Focus any window if none is focused. Try restored first.
309 if (type == LaunchType.FOCUS_ANY_OR_CREATE) {
310 // If there is already a focused window, then finish.
311 for (var key in appWindows) {
312 // The isFocused() method should always be available, but in case
313 // Files.app's failed on some error, wrap it with try catch.
314 try {
315 if (appWindows[key].contentWindow.isFocused()) {
316 if (opt_callback)
317 opt_callback(key);
318 onTaskCompleted();
319 return;
320 }
321 } catch (e) {
322 console.error(e.message);
323 }
324 }
325 // Try to focus the first non-minimized window.
326 for (var key in appWindows) {
327 if (!appWindows[key].isMinimized()) {
328 appWindows[key].focus();
329 if (opt_callback)
330 opt_callback(key);
331 onTaskCompleted();
332 return;
333 }
334 }
335 // Restore and focus any window.
336 for (var key in appWindows) {
337 appWindows[key].focus();
338 if (opt_callback)
339 opt_callback(key);
340 onTaskCompleted();
341 return;
342 }
343 }
344
345 // Create a new instance in case of ALWAYS_CREATE type, or as a fallback
346 // for other types.
347
348 var id = opt_id || nextFileManagerWindowID;
349 nextFileManagerWindowID = Math.max(nextFileManagerWindowID, id + 1);
350 var appId = FILES_ID_PREFIX + id;
351
352 var appWindow = new AppWindowWrapper(
353 'main.html',
354 appId,
355 FILE_MANAGER_WINDOW_CREATE_OPTIONS);
356 appWindow.launch(opt_appState || {}, function() {
357 if (opt_callback)
358 opt_callback(appId);
359 onTaskCompleted();
360 });
361 });
362 }
363
364 /**
365 * Relaunch file manager windows based on the persisted state.
366 */
367 function reopenFileManagers() {
368 chrome.storage.local.get(function(items) {
369 for (var key in items) {
370 if (items.hasOwnProperty(key)) {
371 var match = key.match(FILES_ID_PATTERN);
372 if (match) {
373 var id = Number(match[1]);
374 try {
375 var appState = JSON.parse(items[key]);
376 launchFileManager(appState, id);
377 } catch (e) {
378 console.error('Corrupt launch data for ' + id);
379 }
380 }
381 }
382 }
383 });
384 }
385
386 /**
387 * Executes a file browser task.
388 *
389 * @param {string} action Task id.
390 * @param {Object} details Details object.
391 */
392 function onExecute(action, details) {
393 var urls = details.entries.map(function(e) { return e.toURL(); });
394
395 switch (action) {
396 case 'play':
397 launchAudioPlayer({items: urls, position: 0});
398 break;
399
400 case 'watch':
401 launchVideoPlayer(urls[0]);
402 break;
403
404 default:
405 var launchEnable = null;
406 var queue = new AsyncUtil.Queue();
407 queue.run(function(nextStep) {
408 // If it is not auto-open (triggered by mounting external devices), we
409 // always launch Files.app.
410 if (action != 'auto-open') {
411 launchEnable = true;
412 nextStep();
413 return;
414 }
415 // If the disable-default-apps flag is on, Files.app is not opened
416 // automatically on device mount because it obstculs the manual test.
417 chrome.commandLinePrivate.hasSwitch('disable-default-apps',
418 function(flag) {
419 launchEnable = !flag;
420 nextStep();
421 });
422 });
423 queue.run(function(nextStep) {
424 if (!launchEnable) {
425 nextStep();
426 return;
427 }
428
429 // Every other action opens a Files app window.
430 var appState = {
431 params: {
432 action: action
433 },
434 defaultPath: details.entries[0].fullPath
435 };
436 // For mounted devices just focus any Files.app window. The mounted
437 // volume will appear on the navigation list.
438 var type = action == 'auto-open' ? LaunchType.FOCUS_ANY_OR_CREATE :
439 LaunchType.FOCUS_SAME_OR_CREATE;
440 launchFileManager(appState, /* App ID */ undefined, type, nextStep);
441 });
442 break;
443 }
444 }
445
446 /**
447 * Audio player window create options.
448 * @type {Object}
449 * @const
450 */
451 var AUDIO_PLAYER_CREATE_OPTIONS = Object.freeze({
452 type: 'panel',
453 hidden: true,
454 minHeight: 35 + 58,
455 minWidth: 280,
456 height: 35 + 58,
457 width: 280,
458 singleton: false
459 });
460
461 var audioPlayer = new SingletonAppWindowWrapper('mediaplayer.html',
462 AUDIO_PLAYER_CREATE_OPTIONS);
463
464 /**
465 * Launch the audio player.
466 * @param {Object} playlist Playlist.
467 */
468 function launchAudioPlayer(playlist) {
469 audioPlayer.launch(playlist);
470 }
471
472 var videoPlayer = new SingletonAppWindowWrapper('video_player.html',
473 {hidden: true});
474
475 /**
476 * Launch the video player.
477 * @param {string} url Video url.
478 */
479 function launchVideoPlayer(url) {
480 videoPlayer.launch({url: url});
481 }
482
483 /**
484 * Launches the app.
485 */
486 function onLaunched() {
487 if (nextFileManagerWindowID == 0) {
488 // The app just launched. Remove window state records that are not needed
489 // any more.
490 chrome.storage.local.get(function(items) {
491 for (var key in items) {
492 if (items.hasOwnProperty(key)) {
493 if (key.match(FILES_ID_PATTERN))
494 chrome.storage.local.remove(key);
495 }
496 }
497 });
498 }
499
500 launchFileManager();
501 }
502
503 /**
504 * Restarted the app, restore windows.
505 */
506 function onRestarted() {
507 reopenFileManagers();
508 audioPlayer.reopen();
509 videoPlayer.reopen();
510 }
511
512 /**
513 * Handles clicks on a custom item on the launcher context menu.
514 * @param {OnClickData} info Event details.
515 */
516 function onContextMenuClicked(info) {
517 if (info.menuItemId == 'new-window') {
518 // Find the focused window (if any) and use it's current path for the
519 // new window. If not found, then launch with the default path.
520 for (var key in appWindows) {
521 try {
522 if (appWindows[key].contentWindow.isFocused()) {
523 var appState = {
524 defaultPath: appWindows[key].contentWindow.appState.defaultPath
525 };
526 launchFileManager(appState);
527 return;
528 }
529 } catch (ignore) {
530 // The isFocused method may not be defined during initialization.
531 // Therefore, wrapped with a try-catch block.
532 }
533 }
534
535 // Launch with the default path.
536 launchFileManager();
537 }
538 }
539
540 /**
541 * Closes the background page, if it is not needed.
542 */
543 function maybeCloseBackgroundPage() {
544 if (Object.keys(appWindows).length === 0 &&
545 !FileOperationManager.getInstance().hasQueuedTasks())
546 close();
547 }
548
549 /**
550 * Initializes the context menu. Recreates if already exists.
551 * @param {Object} strings Hash array of strings.
552 */
553 function initContextMenu(strings) {
554 try {
555 chrome.contextMenus.remove('new-window');
556 } catch (ignore) {
557 // There is no way to detect if the context menu is already added, therefore
558 // try to recreate it every time.
559 }
560 chrome.contextMenus.create({
561 id: 'new-window',
562 contexts: ['launcher'],
563 title: strings['NEW_WINDOW_BUTTON_LABEL']
564 });
565 }
566
567 /**
568 * Initializes the background page of Files.app.
569 */
570 function initApp() {
571 // Initialize handlers.
572 chrome.fileBrowserHandler.onExecute.addListener(onExecute);
573 chrome.app.runtime.onLaunched.addListener(onLaunched);
574 chrome.app.runtime.onRestarted.addListener(onRestarted);
575 chrome.contextMenus.onClicked.addListener(onContextMenuClicked);
576
577 // Fetch strings and initialize the context menu.
578 queue.run(function(callback) {
579 chrome.fileBrowserPrivate.getStrings(function(strings) {
580 loadTimeData.data = strings;
581 initContextMenu(strings);
582 chrome.storage.local.set({strings: strings}, callback);
583 });
584 });
585
586 // Count runtime JavaScript errors.
587 window.onerror = function() {
588 JSErrorCount++;
589 };
590 }
591
592 // Initialize Files.app.
593 initApp();
594
595 /**
596 * Progress center of the background page.
597 * @type {ProgressCenter}
598 */
599 window.progressCenter = new ProgressCenter();
600
601 /**
602 * Event handler for progress center.
603 * @type {ProgressCenter}
604 */
605 var progressCenterHandler = new ProgressCenterHandler(
606 FileOperationManager.getInstance(),
607 window.progressCenter);
OLDNEW
« no previous file with comments | « chrome/browser/resources/file_manager/js/async_util.js ('k') | chrome/browser/resources/file_manager/js/butter_bar.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698