Upstream version 7.36.149.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    * 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();