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 * Type of a Files.app's instance launch. | |
9 * @enum {number} | |
10 */ | |
11 var LaunchType = Object.freeze({ | |
12 ALWAYS_CREATE: 0, | |
13 FOCUS_ANY_OR_CREATE: 1, | |
14 FOCUS_SAME_OR_CREATE: 2 | |
15 }); | |
16 | |
17 /** | |
18 * Root class of the background page. | |
19 * @constructor | |
20 */ | |
21 function Background() { | |
22 /** | |
23 * Map of all currently open app windows. The key is an app id. | |
24 * @type {Object.<string, AppWindow>} | |
25 */ | |
26 this.appWindows = {}; | |
27 | |
28 /** | |
29 * Synchronous queue for asynchronous calls. | |
30 * @type {AsyncUtil.Queue} | |
31 */ | |
32 this.queue = new AsyncUtil.Queue(); | |
33 | |
34 /** | |
35 * Progress center of the background page. | |
36 * @type {ProgressCenter} | |
37 */ | |
38 this.progressCenter = new ProgressCenter(); | |
39 | |
40 /** | |
41 * File operation manager. | |
42 * @type {FileOperationManager} | |
43 */ | |
44 this.fileOperationManager = new FileOperationManager(); | |
45 | |
46 /** | |
47 * Event handler for progress center. | |
48 * @type {FileOperationHandler} | |
49 * @private | |
50 */ | |
51 this.fileOperationHandler_ = new FileOperationHandler(this); | |
52 | |
53 /** | |
54 * Event handler for C++ sides notifications. | |
55 * @type {DeviceHandler} | |
56 * @private | |
57 */ | |
58 this.deviceHandler_ = new DeviceHandler(); | |
59 | |
60 /** | |
61 * Drive sync handler. | |
62 * @type {DriveSyncHandler} | |
63 * @private | |
64 */ | |
65 this.driveSyncHandler_ = new DriveSyncHandler(this.progressCenter); | |
66 this.driveSyncHandler_.addEventListener( | |
67 DriveSyncHandler.COMPLETED_EVENT, | |
68 function() { this.tryClose(); }.bind(this)); | |
69 | |
70 /** | |
71 * String assets. | |
72 * @type {Object.<string, string>} | |
73 */ | |
74 this.stringData = null; | |
75 | |
76 /** | |
77 * Callback list to be invoked after initialization. | |
78 * It turns to null after initialization. | |
79 * | |
80 * @type {Array.<function()>} | |
81 * @private | |
82 */ | |
83 this.initializeCallbacks_ = []; | |
84 | |
85 /** | |
86 * Last time when the background page can close. | |
87 * | |
88 * @type {number} | |
89 * @private | |
90 */ | |
91 this.lastTimeCanClose_ = null; | |
92 | |
93 // Seal self. | |
94 Object.seal(this); | |
95 | |
96 // Initialize handlers. | |
97 chrome.fileBrowserHandler.onExecute.addListener(this.onExecute_.bind(this)); | |
98 chrome.app.runtime.onLaunched.addListener(this.onLaunched_.bind(this)); | |
99 chrome.app.runtime.onRestarted.addListener(this.onRestarted_.bind(this)); | |
100 chrome.contextMenus.onClicked.addListener( | |
101 this.onContextMenuClicked_.bind(this)); | |
102 | |
103 // Fetch strings and initialize the context menu. | |
104 this.queue.run(function(callNextStep) { | |
105 chrome.fileBrowserPrivate.getStrings(function(strings) { | |
106 // Initialize string assets. | |
107 this.stringData = strings; | |
108 loadTimeData.data = strings; | |
109 this.initContextMenu_(); | |
110 | |
111 // Invoke initialize callbacks. | |
112 for (var i = 0; i < this.initializeCallbacks_.length; i++) { | |
113 this.initializeCallbacks_[i](); | |
114 } | |
115 this.initializeCallbacks_ = null; | |
116 | |
117 callNextStep(); | |
118 }.bind(this)); | |
119 }.bind(this)); | |
120 } | |
121 | |
122 /** | |
123 * A number of delay milliseconds from the first call of tryClose to the actual | |
124 * close action. | |
125 * @type {number} | |
126 * @const | |
127 * @private | |
128 */ | |
129 Background.CLOSE_DELAY_MS_ = 5000; | |
130 | |
131 /** | |
132 * Make a key of window geometry preferences for the given initial URL. | |
133 * @param {string} url Initialize URL that the window has. | |
134 * @return {string} Key of window geometry preferences. | |
135 */ | |
136 Background.makeGeometryKey = function(url) { | |
137 return 'windowGeometry' + ':' + url; | |
138 }; | |
139 | |
140 /** | |
141 * Key for getting and storing the last window state (maximized or not). | |
142 * @const | |
143 * @private | |
144 */ | |
145 Background.MAXIMIZED_KEY_ = 'isMaximized'; | |
146 | |
147 /** | |
148 * Register callback to be invoked after initialization. | |
149 * If the initialization is already done, the callback is invoked immediately. | |
150 * | |
151 * @param {function()} callback Initialize callback to be registered. | |
152 */ | |
153 Background.prototype.ready = function(callback) { | |
154 if (this.initializeCallbacks_ !== null) | |
155 this.initializeCallbacks_.push(callback); | |
156 else | |
157 callback(); | |
158 }; | |
159 | |
160 /** | |
161 * Checks the current condition of background page and closes it if possible. | |
162 */ | |
163 Background.prototype.tryClose = function() { | |
164 // If the file operation is going, the background page cannot close. | |
165 if (this.fileOperationManager.hasQueuedTasks() || | |
166 this.driveSyncHandler_.syncing) { | |
167 this.lastTimeCanClose_ = null; | |
168 return; | |
169 } | |
170 | |
171 var views = chrome.extension.getViews(); | |
172 var closing = false; | |
173 for (var i = 0; i < views.length; i++) { | |
174 // If the window that is not the background page itself and it is not | |
175 // closing, the background page cannot close. | |
176 if (views[i] !== window && !views[i].closing) { | |
177 this.lastTimeCanClose_ = null; | |
178 return; | |
179 } | |
180 closing = closing || views[i].closing; | |
181 } | |
182 | |
183 // If some windows are closing, or the background page can close but could not | |
184 // 5 seconds ago, We need more time for sure. | |
185 if (closing || | |
186 this.lastTimeCanClose_ === null || | |
187 Date.now() - this.lastTimeCanClose_ < Background.CLOSE_DELAY_MS_) { | |
188 if (this.lastTimeCanClose_ === null) | |
189 this.lastTimeCanClose_ = Date.now(); | |
190 setTimeout(this.tryClose.bind(this), Background.CLOSE_DELAY_MS_); | |
191 return; | |
192 } | |
193 | |
194 // Otherwise we can close the background page. | |
195 close(); | |
196 }; | |
197 | |
198 /** | |
199 * Gets similar windows, it means with the same initial url. | |
200 * @param {string} url URL that the obtained windows have. | |
201 * @return {Array.<AppWindow>} List of similar windows. | |
202 */ | |
203 Background.prototype.getSimilarWindows = function(url) { | |
204 var result = []; | |
205 for (var appID in this.appWindows) { | |
206 if (this.appWindows[appID].contentWindow.appInitialURL === url) | |
207 result.push(this.appWindows[appID]); | |
208 } | |
209 return result; | |
210 }; | |
211 | |
212 /** | |
213 * Wrapper for an app window. | |
214 * | |
215 * Expects the following from the app scripts: | |
216 * 1. The page load handler should initialize the app using |window.appState| | |
217 * and call |util.platform.saveAppState|. | |
218 * 2. Every time the app state changes the app should update |window.appState| | |
219 * and call |util.platform.saveAppState| . | |
220 * 3. The app may have |unload| function to persist the app state that does not | |
221 * fit into |window.appState|. | |
222 * | |
223 * @param {string} url App window content url. | |
224 * @param {string} id App window id. | |
225 * @param {Object} options Options object to create it. | |
226 * @constructor | |
227 */ | |
228 function AppWindowWrapper(url, id, options) { | |
229 this.url_ = url; | |
230 this.id_ = id; | |
231 // Do deep copy for the template of options to assign customized params later. | |
232 this.options_ = JSON.parse(JSON.stringify(options)); | |
233 this.window_ = null; | |
234 this.appState_ = null; | |
235 this.openingOrOpened_ = false; | |
236 this.queue = new AsyncUtil.Queue(); | |
237 Object.seal(this); | |
238 } | |
239 | |
240 AppWindowWrapper.prototype = { | |
241 /** | |
242 * @return {AppWindow} Wrapped application window. | |
243 */ | |
244 get rawAppWindow() { | |
245 return this.window_; | |
246 } | |
247 }; | |
248 | |
249 /** | |
250 * Focuses the window on the specified desktop. | |
251 * @param {AppWindow} appWindow Application window. | |
252 * @param {string=} opt_profileId The profiled ID of the target window. If it is | |
253 * dropped, the window is focused on the current window. | |
254 */ | |
255 AppWindowWrapper.focusOnDesktop = function(appWindow, opt_profileId) { | |
256 new Promise(function(onFulfilled, onRejected) { | |
257 if (opt_profileId) { | |
258 onFulfilled(opt_profileId); | |
259 } else { | |
260 chrome.fileBrowserPrivate.getProfiles(function(profiles, | |
261 currentId, | |
262 displayedId) { | |
263 onFulfilled(currentId); | |
264 }); | |
265 } | |
266 }).then(function(profileId) { | |
267 appWindow.contentWindow.chrome.fileBrowserPrivate.visitDesktop( | |
268 profileId, function() { | |
269 appWindow.focus(); | |
270 }); | |
271 }); | |
272 }; | |
273 | |
274 /** | |
275 * Shift distance to avoid overlapping windows. | |
276 * @type {number} | |
277 * @const | |
278 */ | |
279 AppWindowWrapper.SHIFT_DISTANCE = 40; | |
280 | |
281 /** | |
282 * Sets the icon of the window. | |
283 * @param {string} iconPath Path of the icon. | |
284 */ | |
285 AppWindowWrapper.prototype.setIcon = function(iconPath) { | |
286 this.window_.setIcon(iconPath); | |
287 }; | |
288 | |
289 /** | |
290 * Opens the window. | |
291 * | |
292 * @param {Object} appState App state. | |
293 * @param {boolean} reopen True if the launching is triggered automatically. | |
294 * False otherwise. | |
295 * @param {function()=} opt_callback Completion callback. | |
296 */ | |
297 AppWindowWrapper.prototype.launch = function(appState, reopen, opt_callback) { | |
298 // Check if the window is opened or not. | |
299 if (this.openingOrOpened_) { | |
300 console.error('The window is already opened.'); | |
301 if (opt_callback) | |
302 opt_callback(); | |
303 return; | |
304 } | |
305 this.openingOrOpened_ = true; | |
306 | |
307 // Save application state. | |
308 this.appState_ = appState; | |
309 | |
310 // Get similar windows, it means with the same initial url, eg. different | |
311 // main windows of Files.app. | |
312 var similarWindows = background.getSimilarWindows(this.url_); | |
313 | |
314 // Restore maximized windows, to avoid hiding them to tray, which can be | |
315 // confusing for users. | |
316 this.queue.run(function(callback) { | |
317 for (var index = 0; index < similarWindows.length; index++) { | |
318 if (similarWindows[index].isMaximized()) { | |
319 var createWindowAndRemoveListener = function() { | |
320 similarWindows[index].onRestored.removeListener( | |
321 createWindowAndRemoveListener); | |
322 callback(); | |
323 }; | |
324 similarWindows[index].onRestored.addListener( | |
325 createWindowAndRemoveListener); | |
326 similarWindows[index].restore(); | |
327 return; | |
328 } | |
329 } | |
330 // If no maximized windows, then create the window immediately. | |
331 callback(); | |
332 }); | |
333 | |
334 // Obtains the last geometry and window state (maximized or not). | |
335 var lastBounds; | |
336 var isMaximized = false; | |
337 this.queue.run(function(callback) { | |
338 var boundsKey = Background.makeGeometryKey(this.url_); | |
339 var maximizedKey = Background.MAXIMIZED_KEY_; | |
340 chrome.storage.local.get([boundsKey, maximizedKey], function(preferences) { | |
341 if (!chrome.runtime.lastError) { | |
342 lastBounds = preferences[boundsKey]; | |
343 isMaximized = preferences[maximizedKey]; | |
344 } | |
345 callback(); | |
346 }); | |
347 }.bind(this)); | |
348 | |
349 // Closure creating the window, once all preprocessing tasks are finished. | |
350 this.queue.run(function(callback) { | |
351 // Apply the last bounds. | |
352 if (lastBounds) | |
353 this.options_.bounds = lastBounds; | |
354 if (isMaximized) | |
355 this.options_.state = 'maximized'; | |
356 | |
357 // Create a window. | |
358 chrome.app.window.create(this.url_, this.options_, function(appWindow) { | |
359 this.window_ = appWindow; | |
360 callback(); | |
361 }.bind(this)); | |
362 }.bind(this)); | |
363 | |
364 // After creating. | |
365 this.queue.run(function(callback) { | |
366 // If there is another window in the same position, shift the window. | |
367 var makeBoundsKey = function(bounds) { | |
368 return bounds.left + '/' + bounds.top; | |
369 }; | |
370 var notAvailablePositions = {}; | |
371 for (var i = 0; i < similarWindows.length; i++) { | |
372 var key = makeBoundsKey(similarWindows[i].getBounds()); | |
373 notAvailablePositions[key] = true; | |
374 } | |
375 var candidateBounds = this.window_.getBounds(); | |
376 while (true) { | |
377 var key = makeBoundsKey(candidateBounds); | |
378 if (!notAvailablePositions[key]) | |
379 break; | |
380 // Make the position available to avoid an infinite loop. | |
381 notAvailablePositions[key] = false; | |
382 var nextLeft = candidateBounds.left + AppWindowWrapper.SHIFT_DISTANCE; | |
383 var nextRight = nextLeft + candidateBounds.width; | |
384 candidateBounds.left = nextRight >= screen.availWidth ? | |
385 nextRight % screen.availWidth : nextLeft; | |
386 var nextTop = candidateBounds.top + AppWindowWrapper.SHIFT_DISTANCE; | |
387 var nextBottom = nextTop + candidateBounds.height; | |
388 candidateBounds.top = nextBottom >= screen.availHeight ? | |
389 nextBottom % screen.availHeight : nextTop; | |
390 } | |
391 this.window_.moveTo(candidateBounds.left, candidateBounds.top); | |
392 | |
393 // Save the properties. | |
394 var appWindow = this.window_; | |
395 background.appWindows[this.id_] = appWindow; | |
396 var contentWindow = appWindow.contentWindow; | |
397 contentWindow.appID = this.id_; | |
398 contentWindow.appState = this.appState_; | |
399 contentWindow.appReopen = reopen; | |
400 contentWindow.appInitialURL = this.url_; | |
401 if (window.IN_TEST) | |
402 contentWindow.IN_TEST = true; | |
403 | |
404 // Register event listeners. | |
405 appWindow.onBoundsChanged.addListener(this.onBoundsChanged_.bind(this)); | |
406 appWindow.onClosed.addListener(this.onClosed_.bind(this)); | |
407 | |
408 // Callback. | |
409 if (opt_callback) | |
410 opt_callback(); | |
411 callback(); | |
412 }.bind(this)); | |
413 }; | |
414 | |
415 /** | |
416 * Handles the onClosed extension API event. | |
417 * @private | |
418 */ | |
419 AppWindowWrapper.prototype.onClosed_ = function() { | |
420 // Remember the last window state (maximized or normal). | |
421 var preferences = {}; | |
422 preferences[Background.MAXIMIZED_KEY_] = this.window_.isMaximized(); | |
423 chrome.storage.local.set(preferences); | |
424 | |
425 // Unload the window. | |
426 var appWindow = this.window_; | |
427 var contentWindow = this.window_.contentWindow; | |
428 if (contentWindow.unload) | |
429 contentWindow.unload(); | |
430 this.window_ = null; | |
431 this.openingOrOpened_ = false; | |
432 | |
433 // Updates preferences. | |
434 if (contentWindow.saveOnExit) { | |
435 contentWindow.saveOnExit.forEach(function(entry) { | |
436 util.AppCache.update(entry.key, entry.value); | |
437 }); | |
438 } | |
439 chrome.storage.local.remove(this.id_); // Forget the persisted state. | |
440 | |
441 // Remove the window from the set. | |
442 delete background.appWindows[this.id_]; | |
443 | |
444 // If there is no application window, reset window ID. | |
445 if (!Object.keys(background.appWindows).length) | |
446 nextFileManagerWindowID = 0; | |
447 background.tryClose(); | |
448 }; | |
449 | |
450 /** | |
451 * Handles onBoundsChanged extension API event. | |
452 * @private | |
453 */ | |
454 AppWindowWrapper.prototype.onBoundsChanged_ = function() { | |
455 if (!this.window_.isMaximized()) { | |
456 var preferences = {}; | |
457 preferences[Background.makeGeometryKey(this.url_)] = | |
458 this.window_.getBounds(); | |
459 chrome.storage.local.set(preferences); | |
460 } | |
461 }; | |
462 | |
463 /** | |
464 * Wrapper for a singleton app window. | |
465 * | |
466 * In addition to the AppWindowWrapper requirements the app scripts should | |
467 * have |reload| method that re-initializes the app based on a changed | |
468 * |window.appState|. | |
469 * | |
470 * @param {string} url App window content url. | |
471 * @param {Object|function()} options Options object or a function to return it. | |
472 * @constructor | |
473 */ | |
474 function SingletonAppWindowWrapper(url, options) { | |
475 AppWindowWrapper.call(this, url, url, options); | |
476 } | |
477 | |
478 /** | |
479 * Inherits from AppWindowWrapper. | |
480 */ | |
481 SingletonAppWindowWrapper.prototype = {__proto__: AppWindowWrapper.prototype}; | |
482 | |
483 /** | |
484 * Open the window. | |
485 * | |
486 * Activates an existing window or creates a new one. | |
487 * | |
488 * @param {Object} appState App state. | |
489 * @param {boolean} reopen True if the launching is triggered automatically. | |
490 * False otherwise. | |
491 * @param {function()=} opt_callback Completion callback. | |
492 */ | |
493 SingletonAppWindowWrapper.prototype.launch = | |
494 function(appState, reopen, opt_callback) { | |
495 // If the window is not opened yet, just call the parent method. | |
496 if (!this.openingOrOpened_) { | |
497 AppWindowWrapper.prototype.launch.call( | |
498 this, appState, reopen, opt_callback); | |
499 return; | |
500 } | |
501 | |
502 // If the window is already opened, reload the window. | |
503 // The queue is used to wait until the window is opened. | |
504 this.queue.run(function(nextStep) { | |
505 this.window_.contentWindow.appState = appState; | |
506 this.window_.contentWindow.appReopen = reopen; | |
507 this.window_.contentWindow.reload(); | |
508 if (opt_callback) | |
509 opt_callback(); | |
510 nextStep(); | |
511 }.bind(this)); | |
512 }; | |
513 | |
514 /** | |
515 * Reopen a window if its state is saved in the local storage. | |
516 * @param {function()=} opt_callback Completion callback. | |
517 */ | |
518 SingletonAppWindowWrapper.prototype.reopen = function(opt_callback) { | |
519 chrome.storage.local.get(this.id_, function(items) { | |
520 var value = items[this.id_]; | |
521 if (!value) { | |
522 opt_callback && opt_callback(); | |
523 return; // No app state persisted. | |
524 } | |
525 | |
526 try { | |
527 var appState = JSON.parse(value); | |
528 } catch (e) { | |
529 console.error('Corrupt launch data for ' + this.id_, value); | |
530 opt_callback && opt_callback(); | |
531 return; | |
532 } | |
533 this.launch(appState, true, opt_callback); | |
534 }.bind(this)); | |
535 }; | |
536 | |
537 /** | |
538 * Prefix for the file manager window ID. | |
539 */ | |
540 var FILES_ID_PREFIX = 'files#'; | |
541 | |
542 /** | |
543 * Regexp matching a file manager window ID. | |
544 */ | |
545 var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$'); | |
546 | |
547 /** | |
548 * Value of the next file manager window ID. | |
549 */ | |
550 var nextFileManagerWindowID = 0; | |
551 | |
552 /** | |
553 * File manager window create options. | |
554 * @type {Object} | |
555 * @const | |
556 */ | |
557 var FILE_MANAGER_WINDOW_CREATE_OPTIONS = Object.freeze({ | |
558 bounds: Object.freeze({ | |
559 left: Math.round(window.screen.availWidth * 0.1), | |
560 top: Math.round(window.screen.availHeight * 0.1), | |
561 width: Math.round(window.screen.availWidth * 0.8), | |
562 height: Math.round(window.screen.availHeight * 0.8) | |
563 }), | |
564 minWidth: 480, | |
565 minHeight: 240, | |
566 frame: 'none', | |
567 hidden: true, | |
568 transparentBackground: true | |
569 }); | |
570 | |
571 /** | |
572 * @param {Object=} opt_appState App state. | |
573 * @param {number=} opt_id Window id. | |
574 * @param {LaunchType=} opt_type Launch type. Default: ALWAYS_CREATE. | |
575 * @param {function(string)=} opt_callback Completion callback with the App ID. | |
576 */ | |
577 function launchFileManager(opt_appState, opt_id, opt_type, opt_callback) { | |
578 var type = opt_type || LaunchType.ALWAYS_CREATE; | |
579 | |
580 // Wait until all windows are created. | |
581 background.queue.run(function(onTaskCompleted) { | |
582 // Check if there is already a window with the same URL. If so, then | |
583 // reuse it instead of opening a new one. | |
584 if (type == LaunchType.FOCUS_SAME_OR_CREATE || | |
585 type == LaunchType.FOCUS_ANY_OR_CREATE) { | |
586 if (opt_appState) { | |
587 for (var key in background.appWindows) { | |
588 if (!key.match(FILES_ID_PATTERN)) | |
589 continue; | |
590 | |
591 var contentWindow = background.appWindows[key].contentWindow; | |
592 if (!contentWindow.appState) | |
593 continue; | |
594 | |
595 // Different current directories. | |
596 if (opt_appState.currentDirectoryURL !== | |
597 contentWindow.appState.currentDirectoryURL) { | |
598 continue; | |
599 } | |
600 | |
601 // Selection URL specified, and it is different. | |
602 if (opt_appState.selectionURL && | |
603 opt_appState.selectionURL !== | |
604 contentWindow.appState.selectionURL) { | |
605 continue; | |
606 } | |
607 | |
608 AppWindowWrapper.focusOnDesktop( | |
609 background.appWindows[key], opt_appState.displayedId); | |
610 if (opt_callback) | |
611 opt_callback(key); | |
612 onTaskCompleted(); | |
613 return; | |
614 } | |
615 } | |
616 } | |
617 | |
618 // Focus any window if none is focused. Try restored first. | |
619 if (type == LaunchType.FOCUS_ANY_OR_CREATE) { | |
620 // If there is already a focused window, then finish. | |
621 for (var key in background.appWindows) { | |
622 if (!key.match(FILES_ID_PATTERN)) | |
623 continue; | |
624 | |
625 // The isFocused() method should always be available, but in case | |
626 // Files.app's failed on some error, wrap it with try catch. | |
627 try { | |
628 if (background.appWindows[key].contentWindow.isFocused()) { | |
629 if (opt_callback) | |
630 opt_callback(key); | |
631 onTaskCompleted(); | |
632 return; | |
633 } | |
634 } catch (e) { | |
635 console.error(e.message); | |
636 } | |
637 } | |
638 // Try to focus the first non-minimized window. | |
639 for (var key in background.appWindows) { | |
640 if (!key.match(FILES_ID_PATTERN)) | |
641 continue; | |
642 | |
643 if (!background.appWindows[key].isMinimized()) { | |
644 AppWindowWrapper.focusOnDesktop( | |
645 background.appWindows[key], (opt_appState || {}).displayedId); | |
646 if (opt_callback) | |
647 opt_callback(key); | |
648 onTaskCompleted(); | |
649 return; | |
650 } | |
651 } | |
652 // Restore and focus any window. | |
653 for (var key in background.appWindows) { | |
654 if (!key.match(FILES_ID_PATTERN)) | |
655 continue; | |
656 | |
657 AppWindowWrapper.focusOnDesktop( | |
658 background.appWindows[key], (opt_appState || {}).displayedId); | |
659 if (opt_callback) | |
660 opt_callback(key); | |
661 onTaskCompleted(); | |
662 return; | |
663 } | |
664 } | |
665 | |
666 // Create a new instance in case of ALWAYS_CREATE type, or as a fallback | |
667 // for other types. | |
668 | |
669 var id = opt_id || nextFileManagerWindowID; | |
670 nextFileManagerWindowID = Math.max(nextFileManagerWindowID, id + 1); | |
671 var appId = FILES_ID_PREFIX + id; | |
672 | |
673 var appWindow = new AppWindowWrapper( | |
674 'main.html', | |
675 appId, | |
676 FILE_MANAGER_WINDOW_CREATE_OPTIONS); | |
677 appWindow.launch(opt_appState || {}, false, function() { | |
678 AppWindowWrapper.focusOnDesktop( | |
679 appWindow.window_, (opt_appState || {}).displayedId); | |
680 if (opt_callback) | |
681 opt_callback(appId); | |
682 onTaskCompleted(); | |
683 }); | |
684 }); | |
685 } | |
686 | |
687 /** | |
688 * Executes a file browser task. | |
689 * | |
690 * @param {string} action Task id. | |
691 * @param {Object} details Details object. | |
692 * @private | |
693 */ | |
694 Background.prototype.onExecute_ = function(action, details) { | |
695 var urls = details.entries.map(function(e) { return e.toURL(); }); | |
696 | |
697 switch (action) { | |
698 case 'play': | |
699 launchAudioPlayer({items: urls, position: 0}); | |
700 break; | |
701 | |
702 default: | |
703 var launchEnable = null; | |
704 var queue = new AsyncUtil.Queue(); | |
705 queue.run(function(nextStep) { | |
706 // If it is not auto-open (triggered by mounting external devices), we | |
707 // always launch Files.app. | |
708 if (action != 'auto-open') { | |
709 launchEnable = true; | |
710 nextStep(); | |
711 return; | |
712 } | |
713 // If the disable-default-apps flag is on, Files.app is not opened | |
714 // automatically on device mount not to obstruct the manual test. | |
715 chrome.commandLinePrivate.hasSwitch('disable-default-apps', | |
716 function(flag) { | |
717 launchEnable = !flag; | |
718 nextStep(); | |
719 }); | |
720 }); | |
721 queue.run(function(nextStep) { | |
722 if (!launchEnable) { | |
723 nextStep(); | |
724 return; | |
725 } | |
726 | |
727 // Every other action opens a Files app window. | |
728 var appState = { | |
729 params: { | |
730 action: action | |
731 }, | |
732 // It is not allowed to call getParent() here, since there may be | |
733 // no permissions to access it at this stage. Therefore we are passing | |
734 // the selectionURL only, and the currentDirectory will be resolved | |
735 // later. | |
736 selectionURL: details.entries[0].toURL() | |
737 }; | |
738 // For mounted devices just focus any Files.app window. The mounted | |
739 // volume will appear on the navigation list. | |
740 var type = action == 'auto-open' ? LaunchType.FOCUS_ANY_OR_CREATE : | |
741 LaunchType.FOCUS_SAME_OR_CREATE; | |
742 launchFileManager(appState, /* App ID */ undefined, type, nextStep); | |
743 }); | |
744 break; | |
745 } | |
746 }; | |
747 | |
748 /** | |
749 * Icon of the audio player. | |
750 * TODO(yoshiki): Consider providing an exact size icon, instead of relying | |
751 * on downsampling by ash. | |
752 * | |
753 * @type {string} | |
754 * @const | |
755 */ | |
756 var AUDIO_PLAYER_ICON = 'audio_player/icons/audio-player-64.png'; | |
757 | |
758 // The instance of audio player. Until it's ready, this is null. | |
759 var audioPlayer = null; | |
760 | |
761 // Queue to serializes the initialization, launching and reloading of the audio | |
762 // player, so races won't happen. | |
763 var audioPlayerInitializationQueue = new AsyncUtil.Queue(); | |
764 | |
765 audioPlayerInitializationQueue.run(function(callback) { | |
766 // TODO(yoshiki): Remove '--file-manager-enable-new-audio-player' flag after | |
767 // the feature is launched. | |
768 var newAudioPlayerEnabled = true; | |
769 | |
770 var audioPlayerHTML = | |
771 newAudioPlayerEnabled ? 'audio_player.html' : 'mediaplayer.html'; | |
772 | |
773 /** | |
774 * Audio player window create options. | |
775 * @type {Object} | |
776 */ | |
777 var audioPlayerCreateOptions = Object.freeze({ | |
778 type: 'panel', | |
779 hidden: true, | |
780 minHeight: | |
781 newAudioPlayerEnabled ? | |
782 (44 + 73) : // 44px: track, 73px: controller | |
783 (35 + 58), // 35px: track, 58px: controller | |
784 minWidth: newAudioPlayerEnabled ? 292 : 280, | |
785 height: newAudioPlayerEnabled ? (44 + 73) : (35 + 58), // collapsed | |
786 width: newAudioPlayerEnabled ? 292 : 280, | |
787 }); | |
788 | |
789 audioPlayer = new SingletonAppWindowWrapper(audioPlayerHTML, | |
790 audioPlayerCreateOptions); | |
791 callback(); | |
792 }); | |
793 | |
794 /** | |
795 * Launches the audio player. | |
796 * @param {Object} playlist Playlist. | |
797 * @param {string=} opt_displayedId ProfileID of the desktop where the audio | |
798 * player should show. | |
799 */ | |
800 function launchAudioPlayer(playlist, opt_displayedId) { | |
801 audioPlayerInitializationQueue.run(function(callback) { | |
802 audioPlayer.launch(playlist, false, function(appWindow) { | |
803 audioPlayer.setIcon(AUDIO_PLAYER_ICON); | |
804 AppWindowWrapper.focusOnDesktop(audioPlayer.rawAppWindow, | |
805 opt_displayedId); | |
806 }); | |
807 callback(); | |
808 }); | |
809 } | |
810 | |
811 /** | |
812 * Launches the app. | |
813 * @private | |
814 */ | |
815 Background.prototype.onLaunched_ = function() { | |
816 if (nextFileManagerWindowID == 0) { | |
817 // The app just launched. Remove window state records that are not needed | |
818 // any more. | |
819 chrome.storage.local.get(function(items) { | |
820 for (var key in items) { | |
821 if (items.hasOwnProperty(key)) { | |
822 if (key.match(FILES_ID_PATTERN)) | |
823 chrome.storage.local.remove(key); | |
824 } | |
825 } | |
826 }); | |
827 } | |
828 launchFileManager(null, null, LaunchType.FOCUS_ANY_OR_CREATE); | |
829 }; | |
830 | |
831 /** | |
832 * Restarted the app, restore windows. | |
833 * @private | |
834 */ | |
835 Background.prototype.onRestarted_ = function() { | |
836 // Reopen file manager windows. | |
837 chrome.storage.local.get(function(items) { | |
838 for (var key in items) { | |
839 if (items.hasOwnProperty(key)) { | |
840 var match = key.match(FILES_ID_PATTERN); | |
841 if (match) { | |
842 var id = Number(match[1]); | |
843 try { | |
844 var appState = JSON.parse(items[key]); | |
845 launchFileManager(appState, id); | |
846 } catch (e) { | |
847 console.error('Corrupt launch data for ' + id); | |
848 } | |
849 } | |
850 } | |
851 } | |
852 }); | |
853 | |
854 // Reopen audio player. | |
855 audioPlayerInitializationQueue.run(function(callback) { | |
856 audioPlayer.reopen(function() { | |
857 // If the audioPlayer is reopened, change its window's icon. Otherwise | |
858 // there is no reopened window so just skip the call of setIcon. | |
859 if (audioPlayer.rawAppWindow) | |
860 audioPlayer.setIcon(AUDIO_PLAYER_ICON); | |
861 }); | |
862 callback(); | |
863 }); | |
864 }; | |
865 | |
866 /** | |
867 * Handles clicks on a custom item on the launcher context menu. | |
868 * @param {OnClickData} info Event details. | |
869 * @private | |
870 */ | |
871 Background.prototype.onContextMenuClicked_ = function(info) { | |
872 if (info.menuItemId == 'new-window') { | |
873 // Find the focused window (if any) and use it's current url for the | |
874 // new window. If not found, then launch with the default url. | |
875 for (var key in background.appWindows) { | |
876 try { | |
877 if (background.appWindows[key].contentWindow.isFocused()) { | |
878 var appState = { | |
879 // Do not clone the selection url, only the current directory. | |
880 currentDirectoryURL: background.appWindows[key].contentWindow. | |
881 appState.currentDirectoryURL | |
882 }; | |
883 launchFileManager(appState); | |
884 return; | |
885 } | |
886 } catch (ignore) { | |
887 // The isFocused method may not be defined during initialization. | |
888 // Therefore, wrapped with a try-catch block. | |
889 } | |
890 } | |
891 | |
892 // Launch with the default URL. | |
893 launchFileManager(); | |
894 } | |
895 }; | |
896 | |
897 /** | |
898 * Initializes the context menu. Recreates if already exists. | |
899 * @private | |
900 */ | |
901 Background.prototype.initContextMenu_ = function() { | |
902 try { | |
903 // According to the spec [1], the callback is optional. But no callback | |
904 // causes an error for some reason, so we call it with null-callback to | |
905 // prevent the error. http://crbug.com/353877 | |
906 // - [1] https://developer.chrome.com/extensions/contextMenus#method-remove | |
907 chrome.contextMenus.remove('new-window', function() {}); | |
908 } catch (ignore) { | |
909 // There is no way to detect if the context menu is already added, therefore | |
910 // try to recreate it every time. | |
911 } | |
912 chrome.contextMenus.create({ | |
913 id: 'new-window', | |
914 contexts: ['launcher'], | |
915 title: str('NEW_WINDOW_BUTTON_LABEL') | |
916 }); | |
917 }; | |
918 | |
919 /** | |
920 * Singleton instance of Background. | |
921 * @type {Background} | |
922 */ | |
923 window.background = new Background(); | |
OLD | NEW |