| OLD | NEW |
| (Empty) |
| 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(); | |
| OLD | NEW |