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