OLD | NEW |
| (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); | |
OLD | NEW |