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