• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
11var 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 */
21function 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 */
135Background.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 */
142Background.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 */
151Background.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 */
159Background.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 */
169Background.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 */
209Background.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 */
234function 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
246AppWindowWrapper.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 */
261AppWindowWrapper.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 */
285AppWindowWrapper.SHIFT_DISTANCE = 40;
286
287/**
288 * Sets the icon of the window.
289 * @param {string} iconPath Path of the icon.
290 */
291AppWindowWrapper.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 */
303AppWindowWrapper.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 */
425AppWindowWrapper.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 */
460AppWindowWrapper.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 */
480function SingletonAppWindowWrapper(url, options) {
481  AppWindowWrapper.call(this, url, url, options);
482}
483
484/**
485 * Inherits from AppWindowWrapper.
486 */
487SingletonAppWindowWrapper.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 */
499SingletonAppWindowWrapper.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 */
524SingletonAppWindowWrapper.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 */
548var FILES_ID_PREFIX = 'files#';
549
550/**
551 * Regexp matching a file manager window ID.
552 * @type {RegExp}
553 * @const
554 */
555var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$');
556
557/**
558 * Prefix for the dialog ID.
559 * @type {string}
560 * @const
561 */
562var DIALOG_ID_PREFIX = 'dialog#';
563
564/**
565 * Value of the next file manager window ID.
566 * @type {number}
567 */
568var nextFileManagerWindowID = 0;
569
570/**
571 * Value of the next file manager dialog ID.
572 * @type {number}
573 */
574var nextFileManagerDialogID = 0;
575
576/**
577 * File manager window create options.
578 * @type {Object}
579 * @const
580 */
581var 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 */
601function 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 */
716function 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 */
731Background.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 */
793var AUDIO_PLAYER_ICON = 'audio_player/icons/audio-player-64.png';
794
795// The instance of audio player. Until it's ready, this is null.
796var audioPlayer = null;
797
798// Queue to serializes the initialization, launching and reloading of the audio
799// player, so races won't happen.
800var audioPlayerInitializationQueue = new AsyncUtil.Queue();
801
802audioPlayerInitializationQueue.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 */
837function 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 */
852Background.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 */
872Background.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 */
908Background.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 */
938Background.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 */
960window.background = new Background();
961