Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview | 6 * @fileoverview |
| 7 * This class implements the functionality that is specific to application | 7 * This class implements the functionality that is specific to application |
| 8 * remoting ("AppRemoting" or AR). | 8 * remoting ("AppRemoting" or AR). |
| 9 */ | 9 */ |
| 10 | 10 |
| 11 'use strict'; | 11 'use strict'; |
| 12 | 12 |
| 13 /** @suppress {duplicate} */ | 13 /** @suppress {duplicate} */ |
| 14 var remoting = remoting || {}; | 14 var remoting = remoting || {}; |
| 15 | 15 |
| 16 /** | 16 /** |
| 17 * Interval to test the connection speed. | |
| 18 * @const {number} | |
| 19 */ | |
| 20 var CONNECTION_SPEED_PING_INTERVAL_MS = 10 * 1000; | |
| 21 | |
| 22 /** | |
| 23 * Interval to refresh the google drive access token. | |
| 24 * @const {number} | |
| 25 */ | |
| 26 var DRIVE_ACCESS_TOKEN_REFRESH_INTERVAL_MS = 15 * 60 * 1000; | |
| 27 | |
| 28 /** | |
| 29 * @param {Array<string>} appCapabilities Array of application capabilities. | |
| 30 * @constructor | 17 * @constructor |
| 31 * @implements {remoting.ApplicationInterface} | 18 * @implements {remoting.ApplicationInterface} |
| 32 * @implements {remoting.ProtocolExtension} | |
| 33 * @implements {remoting.ClientSession.EventHandler} | |
| 34 * @extends {remoting.Application} | 19 * @extends {remoting.Application} |
| 35 */ | 20 */ |
| 36 remoting.AppRemoting = function(appCapabilities) { | 21 remoting.AppRemoting = function() { |
| 37 base.inherits(this, remoting.Application); | 22 base.inherits(this, remoting.Application); |
| 38 | 23 |
| 39 /** @private {remoting.ApplicationContextMenu} */ | 24 /** @private {remoting.Activity} */ |
| 40 this.contextMenu_ = null; | 25 this.activity_ = null; |
| 41 | |
| 42 /** @private {remoting.KeyboardLayoutsMenu} */ | |
| 43 this.keyboardLayoutsMenu_ = null; | |
| 44 | |
| 45 /** @private {remoting.WindowActivationMenu} */ | |
| 46 this.windowActivationMenu_ = null; | |
| 47 | |
| 48 /** @private {remoting.AppConnectedView} */ | |
| 49 this.connectedView_ = null; | |
| 50 | |
| 51 /** @private {base.Disposables} */ | |
| 52 this.extensionDisposables_ = null; | |
| 53 | |
| 54 /** @private */ | |
| 55 this.supportsGoogleDrive_ = false; | |
| 56 | |
| 57 /** @private */ | |
| 58 this.sessionConnector_ = remoting.SessionConnector.factory.createConnector( | |
| 59 document.getElementById('client-container'), | |
| 60 appCapabilities, this); | |
| 61 }; | 26 }; |
| 62 | 27 |
| 63 /** | 28 /** |
| 64 * Type definition for the RunApplicationResponse returned by the API. | |
| 65 * | |
| 66 * @constructor | |
| 67 * @private | |
| 68 */ | |
| 69 remoting.AppRemoting.AppHostResponse = function() { | |
| 70 /** @type {string} */ | |
| 71 this.status = ''; | |
| 72 /** @type {string} */ | |
| 73 this.hostJid = ''; | |
| 74 /** @type {string} */ | |
| 75 this.authorizationCode = ''; | |
| 76 /** @type {string} */ | |
| 77 this.sharedSecret = ''; | |
| 78 | |
| 79 this.host = { | |
| 80 /** @type {string} */ | |
| 81 applicationId: '', | |
| 82 | |
| 83 /** @type {string} */ | |
| 84 hostId: ''}; | |
| 85 }; | |
| 86 | |
| 87 /** | |
| 88 * @return {string} Application product name to be used in UI. | 29 * @return {string} Application product name to be used in UI. |
| 89 * @override {remoting.ApplicationInterface} | 30 * @override {remoting.ApplicationInterface} |
| 90 */ | 31 */ |
| 91 remoting.AppRemoting.prototype.getApplicationName = function() { | 32 remoting.AppRemoting.prototype.getApplicationName = function() { |
| 92 var manifest = chrome.runtime.getManifest(); | 33 var manifest = chrome.runtime.getManifest(); |
| 93 return manifest.name; | 34 return manifest.name; |
| 94 }; | 35 }; |
| 95 | 36 |
| 96 /** | 37 /** |
| 97 * @param {!remoting.Error} error The failure reason. | 38 * @param {!remoting.Error} error The failure reason. |
| 98 * @override {remoting.ApplicationInterface} | 39 * @override {remoting.ApplicationInterface} |
| 99 */ | 40 */ |
| 100 remoting.AppRemoting.prototype.signInFailed_ = function(error) { | 41 remoting.AppRemoting.prototype.signInFailed_ = function(error) { |
| 101 this.onError(error); | 42 remoting.MessageWindow.showErrorMessage( |
| 43 chrome.i18n.getMessage(/*i18n-content*/'CONNECTION_FAILED'), | |
| 44 chrome.i18n.getMessage(error.getTag())); | |
| 102 }; | 45 }; |
| 103 | 46 |
| 104 /** | 47 /** |
| 105 * @override {remoting.ApplicationInterface} | 48 * @override {remoting.ApplicationInterface} |
| 106 */ | 49 */ |
| 107 remoting.AppRemoting.prototype.initApplication_ = function() { | 50 remoting.AppRemoting.prototype.initApplication_ = function() { |
| 108 // TODO(jamiewalch): Remove ClientSession's dependency on remoting.fullscreen | |
|
kelvinp
2015/04/14 00:46:33
No longer needed, as we did such a good job cleanu
garykac
2015/04/15 00:52:37
Wahoo!
| |
| 109 // so that this is no longer required. | |
| 110 remoting.fullscreen = new remoting.FullscreenAppsV2(); | |
| 111 | |
| 112 remoting.windowShape.updateClientWindowShape(); | 51 remoting.windowShape.updateClientWindowShape(); |
| 113 | 52 |
| 114 // Initialize the context menus. | 53 this.activity_ = new remoting.AppRemotingActivity(); |
| 115 if (remoting.platformIsChromeOS()) { | |
| 116 var adapter = new remoting.ContextMenuChrome(); | |
| 117 } else { | |
| 118 var adapter = new remoting.ContextMenuDom( | |
| 119 document.getElementById('context-menu')); | |
| 120 } | |
| 121 this.contextMenu_ = new remoting.ApplicationContextMenu(adapter); | |
| 122 this.keyboardLayoutsMenu_ = new remoting.KeyboardLayoutsMenu(adapter); | |
| 123 this.windowActivationMenu_ = new remoting.WindowActivationMenu(adapter); | |
| 124 }; | 54 }; |
| 125 | 55 |
| 126 /** | 56 /** |
| 127 * @param {string} token An OAuth access token. | 57 * @param {string} token An OAuth access token. |
| 128 * @override {remoting.ApplicationInterface} | 58 * @override {remoting.ApplicationInterface} |
| 129 */ | 59 */ |
| 130 remoting.AppRemoting.prototype.startApplication_ = function(token) { | 60 remoting.AppRemoting.prototype.startApplication_ = function(token) { |
|
kelvinp
2015/04/14 00:46:33
Moved to AppRemotingActivity
| |
| 131 remoting.LoadingWindow.show(); | 61 this.activity_.start(); |
| 132 | |
| 133 /** @type {remoting.AppRemoting} */ | |
| 134 var that = this; | |
| 135 | |
| 136 /** @param {!remoting.Xhr.Response} xhrResponse */ | |
| 137 var parseAppHostResponse = function(xhrResponse) { | |
| 138 if (xhrResponse.status == 200) { | |
| 139 var response = /** @type {remoting.AppRemoting.AppHostResponse} */ | |
| 140 (base.jsonParseSafe(xhrResponse.getText())); | |
| 141 if (response && | |
| 142 response.status && | |
| 143 response.status == 'done' && | |
| 144 response.hostJid && | |
| 145 response.authorizationCode && | |
| 146 response.sharedSecret && | |
| 147 response.host && | |
| 148 response.host.hostId) { | |
| 149 var hostJid = response.hostJid; | |
| 150 that.contextMenu_.setHostId(response.host.hostId); | |
| 151 var host = new remoting.Host(response.host.hostId); | |
| 152 host.jabberId = hostJid; | |
| 153 host.authorizationCode = response.authorizationCode; | |
| 154 host.sharedSecret = response.sharedSecret; | |
| 155 | |
| 156 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); | |
| 157 | |
| 158 var idleDetector = new remoting.IdleDetector( | |
| 159 document.getElementById('idle-dialog'), | |
| 160 remoting.app.disconnect.bind(remoting.app)); | |
| 161 | |
| 162 /** | |
| 163 * @param {string} tokenUrl Token-issue URL received from the host. | |
| 164 * @param {string} hostPublicKey Host public key (DER and Base64 | |
| 165 * encoded). | |
| 166 * @param {string} scope OAuth scope to request the token for. | |
| 167 * @param {function(string, string):void} onThirdPartyTokenFetched | |
| 168 * Callback. | |
| 169 */ | |
| 170 var fetchThirdPartyToken = function( | |
| 171 tokenUrl, hostPublicKey, scope, onThirdPartyTokenFetched) { | |
| 172 // Use the authentication tokens returned by the app-remoting server. | |
| 173 onThirdPartyTokenFetched(host['authorizationCode'], | |
| 174 host['sharedSecret']); | |
| 175 }; | |
| 176 | |
| 177 that.sessionConnector_.connect( | |
| 178 remoting.Application.Mode.APP_REMOTING, host, | |
| 179 new remoting.CredentialsProvider( | |
| 180 {fetchThirdPartyToken: fetchThirdPartyToken})); | |
| 181 } else if (response && response.status == 'pending') { | |
| 182 that.onError(new remoting.Error( | |
| 183 remoting.Error.Tag.SERVICE_UNAVAILABLE)); | |
| 184 } | |
| 185 } else { | |
| 186 console.error('Invalid "runApplication" response from server.'); | |
| 187 that.onError(remoting.Error.fromHttpStatus(xhrResponse.status)); | |
| 188 } | |
| 189 }; | |
| 190 | |
| 191 new remoting.Xhr({ | |
| 192 method: 'POST', | |
| 193 url: that.runApplicationUrl_(), | |
| 194 oauthToken: token | |
| 195 }).start().then(parseAppHostResponse); | |
| 196 }; | 62 }; |
| 197 | 63 |
| 198 /** | 64 /** |
| 199 * @override {remoting.ApplicationInterface} | 65 * @override {remoting.ApplicationInterface} |
| 200 */ | 66 */ |
| 201 remoting.AppRemoting.prototype.exitApplication_ = function() { | 67 remoting.AppRemoting.prototype.exitApplication_ = function() { |
| 202 remoting.LoadingWindow.close(); | 68 this.activity_.dispose(); |
| 203 this.closeMainWindow_(); | 69 this.closeMainWindow_(); |
| 204 }; | 70 }; |
| 205 | |
| 206 /** | |
| 207 * @param {remoting.ConnectionInfo} connectionInfo | |
|
kelvinp
2015/04/14 00:46:33
Moved to AppRemotingActivity
| |
| 208 */ | |
| 209 remoting.AppRemoting.prototype.onConnected = function(connectionInfo) { | |
| 210 this.supportsGoogleDrive_ = connectionInfo.session().hasCapability( | |
| 211 remoting.ClientSession.Capability.GOOGLE_DRIVE); | |
| 212 | |
| 213 connectionInfo.plugin().extensions().register(this); | |
| 214 | |
| 215 this.connectedView_ = new remoting.AppConnectedView( | |
| 216 document.getElementById('client-container'), connectionInfo); | |
| 217 | |
| 218 // Map Cmd to Ctrl on Mac since hosts typically use Ctrl for keyboard | |
| 219 // shortcuts, but we want them to act as natively as possible. | |
| 220 if (remoting.platformIsMac()) { | |
| 221 connectionInfo.plugin().setRemapKeys('0x0700e3>0x0700e0,0x0700e7>0x0700e4'); | |
| 222 } | |
| 223 }; | |
| 224 | |
| 225 remoting.AppRemoting.prototype.onDisconnected = function() { | |
| 226 base.dispose(this.connectedView_); | |
| 227 this.connectedView_ = null; | |
| 228 this.stopExtension_(); | |
| 229 chrome.app.window.current().close(); | |
| 230 }; | |
| 231 | |
| 232 /** | |
| 233 * @param {!remoting.Error} error | |
| 234 */ | |
| 235 remoting.AppRemoting.prototype.onConnectionFailed = function(error) { | |
| 236 this.onError(error); | |
| 237 }; | |
| 238 | |
| 239 /** | |
| 240 * @param {!remoting.Error} error The error to be localized and displayed. | |
| 241 */ | |
| 242 remoting.AppRemoting.prototype.onError = function(error) { | |
| 243 console.error('Connection failed: ' + error.toString()); | |
| 244 remoting.LoadingWindow.close(); | |
| 245 remoting.MessageWindow.showErrorMessage( | |
| 246 chrome.i18n.getMessage(/*i18n-content*/'CONNECTION_FAILED'), | |
| 247 chrome.i18n.getMessage(error.getTag())); | |
| 248 this.stopExtension_(); | |
| 249 }; | |
| 250 | |
| 251 | |
| 252 /** | |
|
kelvinp
2015/04/14 00:46:33
Moved to AppConnectedView
| |
| 253 * @return {Array<string>} | |
| 254 * @override {remoting.ProtocolExtension} | |
| 255 */ | |
| 256 remoting.AppRemoting.prototype.getExtensionTypes = function() { | |
| 257 return ['openURL', 'onWindowRemoved', 'onWindowAdded', | |
| 258 'onAllWindowsMinimized', 'setKeyboardLayouts', 'pingResponse']; | |
| 259 }; | |
| 260 | |
| 261 /** | |
| 262 * @param {function(string,string)} sendMessageToHost Callback to send a message | |
| 263 * to the host. | |
| 264 * @override {remoting.ProtocolExtension} | |
| 265 */ | |
| 266 remoting.AppRemoting.prototype.startExtension = function(sendMessageToHost) { | |
| 267 this.windowActivationMenu_.setExtensionMessageSender(sendMessageToHost); | |
| 268 this.keyboardLayoutsMenu_.setExtensionMessageSender(sendMessageToHost); | |
| 269 | |
| 270 remoting.identity.getUserInfo().then(function(userInfo) { | |
| 271 sendMessageToHost('setUserDisplayInfo', | |
| 272 JSON.stringify({fullName: userInfo.name})); | |
| 273 }); | |
| 274 | |
| 275 base.dispose(this.extensionDisposables_); | |
| 276 | |
| 277 var onRestoreHook = new base.ChromeEventHook( | |
| 278 chrome.app.window.current().onRestored, function() { | |
| 279 sendMessageToHost('restoreAllWindows', ''); | |
| 280 }); | |
| 281 | |
| 282 var pingTimer = new base.RepeatingTimer(function() { | |
| 283 var message = {timestamp: new Date().getTime()}; | |
| 284 sendMessageToHost('pingRequest', JSON.stringify(message)); | |
| 285 }, CONNECTION_SPEED_PING_INTERVAL_MS); | |
| 286 | |
| 287 this.extensionDisposables_ = new base.Disposables(onRestoreHook, pingTimer); | |
| 288 | |
| 289 if (this.supportsGoogleDrive_) { | |
| 290 this.extensionDisposables_.add(new base.RepeatingTimer( | |
| 291 this.sendGoogleDriveAccessToken_.bind(this, sendMessageToHost), | |
| 292 DRIVE_ACCESS_TOKEN_REFRESH_INTERVAL_MS, true)); | |
| 293 } | |
| 294 }; | |
| 295 | |
| 296 /** @private */ | |
| 297 remoting.AppRemoting.prototype.stopExtension_ = function() { | |
| 298 this.windowActivationMenu_.setExtensionMessageSender(base.doNothing); | |
| 299 this.keyboardLayoutsMenu_.setExtensionMessageSender(base.doNothing); | |
| 300 | |
| 301 base.dispose(this.extensionDisposables_); | |
| 302 this.extensionDisposables_ = null; | |
| 303 }; | |
| 304 | |
| 305 /** | |
| 306 * @param {string} type The message type. | |
| 307 * @param {Object} message The parsed extension message data. | |
| 308 * @override {remoting.ProtocolExtension} | |
| 309 */ | |
| 310 remoting.AppRemoting.prototype.onExtensionMessage = function(type, message) { | |
| 311 switch (type) { | |
| 312 | |
| 313 case 'openURL': | |
| 314 // URL requests from the hosted app are untrusted, so disallow anything | |
| 315 // other than HTTP or HTTPS. | |
| 316 var url = base.getStringAttr(message, 'url'); | |
| 317 if (url.indexOf('http:') != 0 && url.indexOf('https:') != 0) { | |
| 318 console.error('Bad URL: ' + url); | |
| 319 } else { | |
| 320 window.open(url); | |
| 321 } | |
| 322 return true; | |
| 323 | |
| 324 case 'onWindowRemoved': | |
| 325 var id = base.getNumberAttr(message, 'id'); | |
| 326 this.windowActivationMenu_.remove(id); | |
| 327 return true; | |
| 328 | |
| 329 case 'onWindowAdded': | |
| 330 var id = base.getNumberAttr(message, 'id'); | |
| 331 var title = base.getStringAttr(message, 'title'); | |
| 332 this.windowActivationMenu_.add(id, title); | |
| 333 return true; | |
| 334 | |
| 335 case 'onAllWindowsMinimized': | |
| 336 chrome.app.window.current().minimize(); | |
| 337 return true; | |
| 338 | |
| 339 case 'setKeyboardLayouts': | |
| 340 var supportedLayouts = base.getArrayAttr(message, 'supportedLayouts'); | |
| 341 var currentLayout = base.getStringAttr(message, 'currentLayout'); | |
| 342 console.log('Current host keyboard layout: ' + currentLayout); | |
| 343 console.log('Supported host keyboard layouts: ' + supportedLayouts); | |
| 344 this.keyboardLayoutsMenu_.setLayouts(supportedLayouts, currentLayout); | |
| 345 return true; | |
| 346 | |
| 347 case 'pingResponse': | |
| 348 var then = base.getNumberAttr(message, 'timestamp'); | |
| 349 var now = new Date().getTime(); | |
| 350 this.contextMenu_.updateConnectionRTT(now - then); | |
| 351 return true; | |
| 352 } | |
| 353 | |
| 354 return false; | |
| 355 }; | |
| 356 | |
| 357 /** | |
| 358 * Timer callback to send the access token to the host. | |
| 359 * @param {function(string, string)} sendExtensionMessage | |
| 360 * @private | |
| 361 */ | |
| 362 remoting.AppRemoting.prototype.sendGoogleDriveAccessToken_ | |
| 363 = function(sendExtensionMessage) { | |
| 364 var googleDriveScopes = [ | |
| 365 'https://docs.google.com/feeds/', | |
| 366 'https://www.googleapis.com/auth/drive' | |
| 367 ]; | |
| 368 remoting.identity.getNewToken(googleDriveScopes).then( | |
| 369 function(/** string */ token){ | |
| 370 sendExtensionMessage('accessToken', token); | |
| 371 }).catch(remoting.Error.handler(function(/** remoting.Error */ error) { | |
| 372 console.log('Failed to refresh access token: ' + error.toString()); | |
| 373 })); | |
| 374 }; | |
| 375 | |
| 376 /** | |
| 377 * @return {string} | |
| 378 * @private | |
| 379 */ | |
| 380 remoting.AppRemoting.prototype.runApplicationUrl_ = function() { | |
| 381 return remoting.settings.APP_REMOTING_API_BASE_URL + '/applications/' + | |
| 382 remoting.settings.getAppRemotingApplicationId() + '/run'; | |
| 383 }; | |
| OLD | NEW |