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