diff options
author | kelvinp@chromium.org <kelvinp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-08-14 12:42:28 +0000 |
---|---|---|
committer | kelvinp@chromium.org <kelvinp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-08-14 12:44:38 +0000 |
commit | 23005b6b310879f5877e2411cc7aec74480861dc (patch) | |
tree | 949ad16391059d26d271675489fd08f7b89a8178 | |
parent | b8e70ea6025cdb426a5514ee58179cf417ade1be (diff) | |
download | chromium_src-23005b6b310879f5877e2411cc7aec74480861dc.zip chromium_src-23005b6b310879f5877e2411cc7aec74480861dc.tar.gz chromium_src-23005b6b310879f5877e2411cc7aec74480861dc.tar.bz2 |
Hangouts remote desktop part III - It2MeService
This CL
- Introduces an It2MeService component that listens to incoming connection requests between Hangouts and the webapp and establish a channel between them.
- It enables launching an IT2Me helper session from Hangouts
Review URL: https://codereview.chromium.org/468693002
Cr-Commit-Position: refs/heads/master@{#289538}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@289538 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | remoting/remoting_test.gypi | 6 | ||||
-rw-r--r-- | remoting/remoting_webapp_files.gypi | 9 | ||||
-rw-r--r-- | remoting/webapp/background/app_launcher.js | 2 | ||||
-rw-r--r-- | remoting/webapp/background/background.js | 24 | ||||
-rw-r--r-- | remoting/webapp/background/it2me_helper_channel.js | 290 | ||||
-rw-r--r-- | remoting/webapp/background/it2me_service.js | 184 | ||||
-rw-r--r-- | remoting/webapp/client_screen.js | 8 | ||||
-rw-r--r-- | remoting/webapp/hangout_session.js | 14 | ||||
-rw-r--r-- | remoting/webapp/js_proto/chrome_proto.js | 2 | ||||
-rw-r--r-- | remoting/webapp/js_proto/dom_proto.js | 4 | ||||
-rw-r--r-- | remoting/webapp/manifest.json.jinja2 | 14 | ||||
-rw-r--r-- | remoting/webapp/remoting.js | 49 | ||||
-rw-r--r-- | remoting/webapp/unittests/chrome_mocks.js | 50 | ||||
-rw-r--r-- | remoting/webapp/unittests/it2me_helper_channel_unittest.js | 179 | ||||
-rw-r--r-- | remoting/webapp/unittests/it2me_service_unittest.js | 145 |
15 files changed, 954 insertions, 26 deletions
diff --git a/remoting/remoting_test.gypi b/remoting/remoting_test.gypi index 03292a4..6227b3a 100644 --- a/remoting/remoting_test.gypi +++ b/remoting/remoting_test.gypi @@ -327,7 +327,7 @@ 'destination': '<(output_dir)', 'files': [ '<@(webapp_js_files)', - '<@(remoting_webapp_unittest_cases)', + '<@(remoting_webapp_unittest_js_files)', '<@(remoting_webapp_unittest_additional_files)' ], }, @@ -339,7 +339,7 @@ 'webapp/build-html.py', '<(remoting_webapp_unittest_template_main)', '<@(webapp_js_files)', - '<@(remoting_webapp_unittest_cases)' + '<@(remoting_webapp_unittest_js_files)' ], 'outputs': [ '<(output_dir)/unittest.html', @@ -353,7 +353,7 @@ # instrumentedjs flag or else GYP will ignore the files in the # exclude list. '--exclude-js', '<@(remoting_webapp_unittest_exclude_files)', - '--js', '<@(remoting_webapp_unittest_cases)', + '--js', '<@(remoting_webapp_unittest_js_files)', '--instrument-js', '<@(webapp_js_files)', ], }, diff --git a/remoting/remoting_webapp_files.gypi b/remoting/remoting_webapp_files.gypi index 7bfa958..0491067 100644 --- a/remoting/remoting_webapp_files.gypi +++ b/remoting/remoting_webapp_files.gypi @@ -139,11 +139,14 @@ 'webapp/event_handlers.js', ], # The unit test cases for the webapp - 'remoting_webapp_unittest_cases': [ + 'remoting_webapp_unittest_js_files': [ 'webapp/js_proto/chrome_proto.js', + 'webapp/unittests/chrome_mocks.js', 'webapp/unittests/base_unittest.js', 'webapp/unittests/l10n_unittest.js', 'webapp/unittests/menu_button_unittest.js', + 'webapp/unittests/it2me_helper_channel_unittest.js', + 'webapp/unittests/it2me_service_unittest.js' ], 'remoting_webapp_unittest_additional_files': [ 'webapp/menu_button.css', @@ -177,7 +180,9 @@ 'webapp/client_session.js', 'webapp/typecheck.js', 'webapp/background/app_launcher.js', - 'webapp/background/background.js' + 'webapp/background/background.js', + 'webapp/background/it2me_helper_channel.js', + 'webapp/background/it2me_service.js', ], # The JavaScript files required by wcs_sandbox.html. diff --git a/remoting/webapp/background/app_launcher.js b/remoting/webapp/background/app_launcher.js index d1fc57c..b506a6c 100644 --- a/remoting/webapp/background/app_launcher.js +++ b/remoting/webapp/background/app_launcher.js @@ -61,7 +61,7 @@ remoting.V1AppLauncher.prototype.launch = function(opt_launchArgs) { if (!tab) { reject(new Error(chrome.runtime.lastError.message)); } else { - resolve(tab.id); + resolve(String(tab.id)); } }); }); diff --git a/remoting/webapp/background/background.js b/remoting/webapp/background/background.js index 5f26788..28c2010 100644 --- a/remoting/webapp/background/background.js +++ b/remoting/webapp/background/background.js @@ -43,6 +43,29 @@ function initializeAppV2(appLauncher) { ); } +/** + * The background service is responsible for listening to incoming connection + * requests from Hangouts and the webapp. + * + * @param {remoting.AppLauncher} appLauncher + */ +function initializeBackgroundService(appLauncher) { + function initializeIt2MeService() { + /** @type {remoting.It2MeService} */ + remoting.it2meService = new remoting.It2MeService(appLauncher); + remoting.it2meService.init(); + } + + chrome.runtime.onSuspend.addListener(function() { + base.debug.assert(remoting.it2meService != null); + remoting.it2meService.dispose(); + remoting.it2meService = null; + }); + + chrome.runtime.onSuspendCanceled.addListener(initializeIt2MeService); + initializeIt2MeService(); +} + function main() { /** @type {remoting.AppLauncher} */ var appLauncher = new remoting.V1AppLauncher(); @@ -50,6 +73,7 @@ function main() { appLauncher = new remoting.V2AppLauncher(); initializeAppV2(appLauncher); } + initializeBackgroundService(appLauncher); } window.addEventListener('load', main, false); diff --git a/remoting/webapp/background/it2me_helper_channel.js b/remoting/webapp/background/it2me_helper_channel.js new file mode 100644 index 0000000..e25c443 --- /dev/null +++ b/remoting/webapp/background/it2me_helper_channel.js @@ -0,0 +1,290 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview + * + * It2MeHelperChannel relays messages between Hangouts and Chrome Remote Desktop + * (webapp) for the helper (the Hangouts participant who is giving remote + * assistance). + * + * It runs in the background page and contains two chrome.runtime.Port objects, + * respresenting connections to the webapp and hangout, respectively. + * + * Connection is always initiated from Hangouts. + * + * Hangout It2MeHelperChannel Chrome Remote Desktop + * |-----runtime.connect() ------>| | + * |------connect message-------->| | + * | |-------appLauncher.launch()---->| + * | |<------runtime.connect()------- | + * | |<-----sessionStateChanged------ | + * |<----sessionStateChanged------| | + * + * Disconnection can be initiated from either side: + * 1. In the normal flow initiated from hangout + * Hangout It2MeHelperChannel Chrome Remote Desktop + * |-----disconnect message------>| | + * |<-sessionStateChanged(CLOSED)-| | + * | |-----appLauncher.close()------>| + * + * 2. In the normal flow initiated from webapp + * Hangout It2MeHelperChannel Chrome Remote Desktop + * | |<-sessionStateChanged(CLOSED)--| + * | |<--------port.disconnect()-----| + * |<--------port.disconnect()----| | + * + * 2. If hangout crashes + * Hangout It2MeHelperChannel Chrome Remote Desktop + * |---------port.disconnect()--->| | + * | |--------port.disconnect()----->| + * | |------appLauncher.close()----->| + * + * 3. If webapp crashes + * Hangout It2MeHelperChannel Chrome Remote Desktop + * | |<-------port.disconnect()------| + * |<-sessionStateChanged(FAILED)-| | + * |<--------port.disconnect()----| | + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** + * @param {remoting.AppLauncher} appLauncher + * @param {chrome.runtime.Port} hangoutPort Represents an active connection to + * Hangouts. + * @param {function(remoting.It2MeHelperChannel)} onDisconnectCallback Callback + * to notify when the connection is torn down. IT2MeService uses this + * callback to dispose of the channel object. + * @constructor + */ +remoting.It2MeHelperChannel = + function(appLauncher, hangoutPort, onDisconnectCallback) { + + /** + * @type {remoting.AppLauncher} + * @private + */ + this.appLauncher_ = appLauncher; + + /** + * @type {chrome.runtime.Port} + * @private + */ + this.hangoutPort_ = hangoutPort; + + /** + * @type {chrome.runtime.Port} + * @private + */ + this.webappPort_ = null; + + /** + * @type {string} + * @private + */ + this.instanceId_ = ''; + + /** + * @type {remoting.ClientSession.State} + * @private + */ + this.sessionState_ = remoting.ClientSession.State.CONNECTING; + + /** + * @type {?function(remoting.It2MeHelperChannel)} + * @private + */ + this.onDisconnectCallback_ = onDisconnectCallback; + + this.onWebappMessageRef_ = this.onWebappMessage_.bind(this); + this.onWebappDisconnectRef_ = this.onWebappDisconnect_.bind(this); + this.onHangoutMessageRef_ = this.onHangoutMessage_.bind(this); + this.onHangoutDisconnectRef_ = this.onHangoutDisconnect_.bind(this); +}; + +/** @enum {string} */ +remoting.It2MeHelperChannel.HangoutMessageTypes = { + CONNECT: 'connect', + DISCONNECT: 'disconnect' +}; + +/** @enum {string} */ +remoting.It2MeHelperChannel.WebappMessageTypes = { + SESSION_STATE_CHANGED: 'sessionStateChanged' +}; + +remoting.It2MeHelperChannel.prototype.init = function() { + this.hangoutPort_.onMessage.addListener(this.onHangoutMessageRef_); + this.hangoutPort_.onDisconnect.addListener(this.onHangoutDisconnectRef_); +}; + +/** @return {string} */ +remoting.It2MeHelperChannel.prototype.instanceId = function() { + return this.instanceId_; +}; + +/** + * @param {{method:string, data:Object.<string,*>}} message + * @return {boolean} whether the message is handled or not. + * @private + */ +remoting.It2MeHelperChannel.prototype.onHangoutMessage_ = function(message) { + try { + var MessageTypes = remoting.It2MeHelperChannel.HangoutMessageTypes; + switch (message.method) { + case MessageTypes.CONNECT: + this.launchWebapp_(message); + return true; + case MessageTypes.DISCONNECT: + this.closeWebapp_(message); + return true; + } + } catch(e) { + var error = /** @type {Error} */ e; + console.error(error); + this.hangoutPort_.postMessage({ + method: message.method + 'Response', + error: error.message + }); + } + return false; +}; + +/** + * Disconnect the existing connection to the helpee. + * + * @param {{method:string, data:Object.<string,*>}} message + * @private + */ +remoting.It2MeHelperChannel.prototype.closeWebapp_ = + function(message) { + // TODO(kelvinp): Closing the v2 app currently doesn't disconnect the IT2me + // session (crbug.com/402137), so send an explicit notification to Hangouts. + this.sessionState_ = remoting.ClientSession.State.CLOSED; + this.hangoutPort_.postMessage({ + method: 'sessionStateChanged', + state: this.sessionState_ + }); + this.appLauncher_.close(this.instanceId_); +}; + +/** + * Launches the web app. + * + * @param {{method:string, data:Object.<string,*>}} message + * @private + */ +remoting.It2MeHelperChannel.prototype.launchWebapp_ = + function(message) { + var accessCode = getStringAttr(message, 'accessCode'); + if (!accessCode) { + throw new Error('Access code is missing'); + } + + // Launch the webapp. + this.appLauncher_.launch({ + mode: 'hangout', + accessCode: accessCode + }).then( + /** + * @this {remoting.It2MeHelperChannel} + * @param {string} instanceId + */ + function(instanceId){ + this.instanceId_ = instanceId; + }.bind(this)); +}; + +/** + * @private + */ +remoting.It2MeHelperChannel.prototype.onHangoutDisconnect_ = function() { + this.appLauncher_.close(this.instanceId_); + this.unhookPorts_(); +}; + +/** + * @param {chrome.runtime.Port} port The port represents a connection to the + * webapp. + * @param {string} id The id of the tab or window that is hosting the webapp. + */ +remoting.It2MeHelperChannel.prototype.onWebappConnect = function(port, id) { + base.debug.assert(id === this.instanceId_); + base.debug.assert(this.hangoutPort_ !== null); + + // Hook listeners. + port.onMessage.addListener(this.onWebappMessageRef_); + port.onDisconnect.addListener(this.onWebappDisconnectRef_); + this.webappPort_ = port; +}; + +/** @param {chrome.runtime.Port} port The webapp port. */ +remoting.It2MeHelperChannel.prototype.onWebappDisconnect_ = function(port) { + // If the webapp port got disconnected while the session is still connected, + // treat it as an error. + var States = remoting.ClientSession.State; + if (this.sessionState_ === States.CONNECTING || + this.sessionState_ === States.CONNECTED) { + this.sessionState_ = States.FAILED; + this.hangoutPort_.postMessage({ + method: 'sessionStateChanged', + state: this.sessionState_ + }); + } + this.unhookPorts_(); +}; + +/** + * @param {{method:string, data:Object.<string,*>}} message + * @private + */ +remoting.It2MeHelperChannel.prototype.onWebappMessage_ = function(message) { + try { + console.log('It2MeHelperChannel id=' + this.instanceId_ + + ' incoming message method=' + message.method); + var MessageTypes = remoting.It2MeHelperChannel.WebappMessageTypes; + switch (message.method) { + case MessageTypes.SESSION_STATE_CHANGED: + var state = getNumberAttr(message, 'state'); + this.sessionState_ = + /** @type {remoting.ClientSession.State} */ state; + this.hangoutPort_.postMessage(message); + return true; + } + throw new Error('Unknown message method=' + message.method); + } catch(e) { + var error = /** @type {Error} */ e; + console.error(error); + this.webappPort_.postMessage({ + method: message.method + 'Response', + error: error.message + }); + } + return false; +}; + +remoting.It2MeHelperChannel.prototype.unhookPorts_ = function() { + if (this.webappPort_) { + this.webappPort_.onMessage.removeListener(this.onWebappMessageRef_); + this.webappPort_.onDisconnect.removeListener(this.onWebappDisconnectRef_); + this.webappPort_.disconnect(); + this.webappPort_ = null; + } + + if (this.hangoutPort_) { + this.hangoutPort_.onMessage.removeListener(this.onHangoutMessageRef_); + this.hangoutPort_.onDisconnect.removeListener(this.onHangoutDisconnectRef_); + this.hangoutPort_.disconnect(); + this.hangoutPort_ = null; + } + + if (this.onDisconnectCallback_) { + this.onDisconnectCallback_(this); + this.onDisconnectCallback_ = null; + } +}; diff --git a/remoting/webapp/background/it2me_service.js b/remoting/webapp/background/it2me_service.js new file mode 100644 index 0000000..9656864 --- /dev/null +++ b/remoting/webapp/background/it2me_service.js @@ -0,0 +1,184 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview + * It2MeService listens to incoming connections requests from Hangouts + * and the webapp and creates a It2MeHelperChannel between them. + * It supports multiple helper sessions, but only a single helpee. + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** + * @param {remoting.AppLauncher} appLauncher + * @constructor + */ +remoting.It2MeService = function(appLauncher) { + /** + * @type {remoting.AppLauncher} + * @private + */ + this.appLauncher_ = appLauncher; + + /** + * @type {Array.<remoting.It2MeHelperChannel>} + * @private + */ + this.helpers_ = []; + + /** @private */ + this.helpee_ = null; + + this.onWebappConnectRef_ = this.onWebappConnect_.bind(this); + this.onMessageExternalRef_ = this.onMessageExternal_.bind(this); + this.onConnectExternalRef_ = this.onConnectExternal_.bind(this); +}; + +/** @enum {string} */ +remoting.It2MeService.ConnectionTypes = { + HELPER_HANGOUT: 'it2me.helper.hangout', + HELPEE_HANGOUT: 'it2me.helpee.hangout', + HELPER_WEBAPP: 'it2me.helper.webapp' +}; + +/** + * Starts listening to external connection from Hangouts and the webapp. + */ +remoting.It2MeService.prototype.init = function() { + chrome.runtime.onConnect.addListener(this.onWebappConnectRef_); + chrome.runtime.onMessageExternal.addListener(this.onMessageExternalRef_); + chrome.runtime.onConnectExternal.addListener(this.onConnectExternalRef_); +}; + +remoting.It2MeService.prototype.dispose = function() { + chrome.runtime.onConnect.removeListener(this.onWebappConnectRef_); + chrome.runtime.onMessageExternal.removeListener( + this.onMessageExternalRef_); + chrome.runtime.onConnectExternal.removeListener( + this.onConnectExternalRef_); +}; + +/** + * This function is called when a runtime message is received from an external + * web page (hangout) or extension. + * + * @param {{method:string, data:Object.<string,*>}} message + * @param {chrome.runtime.MessageSender} sender + * @param {function(*):void} sendResponse + * @private + */ +remoting.It2MeService.prototype.onMessageExternal_ = + function(message, sender, sendResponse) { + try { + var method = message.method; + if (method == 'hello') { + // The hello message is used by hangouts to detect whether the app is + // installed and what features are supported. + sendResponse({ + method: 'helloResponse', + supportedFeatures: ['it2me'] + }); + return true; + } + throw new Error('Unknown method: ' + method); + } catch (e) { + var error = /** @type {Error} */ e; + console.error(error); + sendResponse({ + method: message.method + 'Response', + error: error.message + }); + } + return false; +}; + +/** + * This function is called when Hangouts connects via chrome.runtime.connect. + * Only web pages that are white-listed in the manifest are allowed to connect. + * + * @param {chrome.runtime.Port} port + * @private + */ +remoting.It2MeService.prototype.onConnectExternal_ = function(port) { + var ConnectionTypes = remoting.It2MeService.ConnectionTypes; + try { + switch (port.name) { + case ConnectionTypes.HELPER_HANGOUT: + this.handleExternalHelperConnection_(port); + return true; + default: + throw new Error('Unsupported port - ' + port.name); + } + } catch (e) { + var error = /**@type {Error} */ e; + console.error(error); + port.disconnect(); + } + return false; +}; + +/** + * @param {chrome.runtime.Port} port + * @private + */ +remoting.It2MeService.prototype.onWebappConnect_ = function(port) { + try { + console.log('Incoming helper connection from webapp.'); + + // The senderId (tabId or windowId) of the webapp is embedded in the port + // name with the format port_name@senderId. + var parts = port.name.split('@'); + var portName = parts[0]; + var senderId = parts[1]; + var ConnectionTypes = remoting.It2MeService.ConnectionTypes; + if (portName === ConnectionTypes.HELPER_WEBAPP && senderId !== undefined) { + for (var i = 0; i < this.helpers_.length; i++) { + var helper = this.helpers_[i]; + if (helper.instanceId() === senderId) { + helper.onWebappConnect(port, senderId); + return; + } + } + } + throw new Error('No matching hangout connection found for ' + port.name); + } catch (e) { + var error = /** @type {Error} */ e; + console.error(error); + port.disconnect(); + } +}; + +/** + * @param {remoting.It2MeHelperChannel} helper + */ +remoting.It2MeService.prototype.onHelperChannelDisconnected = function(helper) { + for (var i = 0; i < this.helpers_.length; i++) { + if (helper === this.helpers_[i]) { + this.helpers_.splice(i, 1); + } + } +}; + +/** + * @param {chrome.runtime.Port} port + * @private + */ +remoting.It2MeService.prototype.handleExternalHelperConnection_ = + function(port) { + if (this.helpee_) { + console.error( + 'Cannot start a helper session while a helpee session is in process.'); + port.disconnect(); + } + + console.log('Incoming helper connection from Hangouts'); + var helper = new remoting.It2MeHelperChannel( + this.appLauncher_, port, this.onHelperChannelDisconnected.bind(this)); + helper.init(); + this.helpers_.push(helper); +}; diff --git a/remoting/webapp/client_screen.js b/remoting/webapp/client_screen.js index f1f0bac..61270fe 100644 --- a/remoting/webapp/client_screen.js +++ b/remoting/webapp/client_screen.js @@ -137,8 +137,8 @@ function showConnectError_(errorTag) { if (mode == remoting.ClientSession.Mode.IT2ME) { remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME); remoting.hangoutSessionEvents.raiseEvent( - remoting.hangoutSessionEvents.sessionStateChanged, - remoting.ClientSession.State.FAILED + remoting.hangoutSessionEvents.sessionStateChanged, + remoting.ClientSession.State.FAILED ); } else { remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME); @@ -325,8 +325,8 @@ remoting.onConnected = function(clientSession) { remoting.clipboard.startSession(); updateStatistics_(); remoting.hangoutSessionEvents.raiseEvent( - remoting.hangoutSessionEvents.sessionStateChanged, - remoting.ClientSession.State.CONNECTED + remoting.hangoutSessionEvents.sessionStateChanged, + remoting.ClientSession.State.CONNECTED ); if (remoting.connector.pairingRequested) { /** diff --git a/remoting/webapp/hangout_session.js b/remoting/webapp/hangout_session.js index 58c1804..bfcda86 100644 --- a/remoting/webapp/hangout_session.js +++ b/remoting/webapp/hangout_session.js @@ -17,17 +17,25 @@ var remoting = remoting || {}; /** * @constructor + * @param {string} senderId id of the current tab or window. */ -remoting.HangoutSession = function() { +remoting.HangoutSession = function(senderId) { /** * @private * @type {chrome.runtime.Port} */ this.port_ = null; + + /** + * @private + * @type {string} + */ + this.senderId_ = senderId; }; remoting.HangoutSession.prototype.init = function() { - this.port_ = chrome.runtime.connect({name: 'it2me.helper.webapp'}); + var portName = 'it2me.helper.webapp@' + this.senderId_; + this.port_ = chrome.runtime.connect({name: portName}); remoting.hangoutSessionEvents.addEventListener( remoting.hangoutSessionEvents.sessionStateChanged, @@ -44,6 +52,8 @@ remoting.HangoutSession.prototype.onSessionStateChanged_ = function(state) { } catch (e) { // postMessage will throw an exception if the port is disconnected. // We can safely ignore this exception. + var error = /** @type {Error} */ e; + console.error(error); } finally { if (state === State.FAILED || state === State.CLOSED) { // close the current window diff --git a/remoting/webapp/js_proto/chrome_proto.js b/remoting/webapp/js_proto/chrome_proto.js index 455af5e..107cf54 100644 --- a/remoting/webapp/js_proto/chrome_proto.js +++ b/remoting/webapp/js_proto/chrome_proto.js @@ -60,6 +60,8 @@ chrome.runtime = { /** @type {chrome.Event} */ onSuspend: null, /** @type {chrome.Event} */ + onSuspendCanceled: null, + /** @type {chrome.Event} */ onConnect: null, /** @type {chrome.Event} */ onConnectExternal: null, diff --git a/remoting/webapp/js_proto/dom_proto.js b/remoting/webapp/js_proto/dom_proto.js index d7f475d..8eaa5ce 100644 --- a/remoting/webapp/js_proto/dom_proto.js +++ b/remoting/webapp/js_proto/dom_proto.js @@ -184,8 +184,8 @@ MediaSource.prototype.addSourceBuffer = function(format) {} var Promise = function (init) {}; /** - * @param {function(*=) : (Promise|void)} onFulfill - * @param {function(*=) : (Promise|void)} onReject + * @param {function(?=) : (Promise|void)} onFulfill + * @param {function(?=) : (Promise|void)=} onReject * @return {Promise} */ Promise.prototype.then = function (onFulfill, onReject) {}; diff --git a/remoting/webapp/manifest.json.jinja2 b/remoting/webapp/manifest.json.jinja2 index a5efc1f..4d15da4 100644 --- a/remoting/webapp/manifest.json.jinja2 +++ b/remoting/webapp/manifest.json.jinja2 @@ -14,9 +14,15 @@ {% else %} "background": { "page": "background.html" - } + } {% endif %} }, +{% if webapp_type == 'v1' %} + "background": { + "page": "background.html", + "persistent": false + }, +{% endif %} "icons": { "128": "chromoting128.webp", "48": "chromoting48.webp", @@ -54,7 +60,11 @@ "pages": [ "wcs_sandbox.html" ] }, {% endif %} - + "externally_connectable": { + "matches": [ + "https://*.talkgadget.google.com/*" + ] + }, "permissions": [ "{{ OAUTH2_ACCOUNTS_HOST }}/*", "{{ OAUTH2_API_BASE_URL }}/*", diff --git a/remoting/webapp/remoting.js b/remoting/webapp/remoting.js index c02a48b..f5691c1 100644 --- a/remoting/webapp/remoting.js +++ b/remoting/webapp/remoting.js @@ -133,23 +133,52 @@ remoting.init = function() { button.disabled = true; } + /** + * @return {Promise} A promise that resolves to the id of the current + * containing tab/window. + */ + var getCurrentId = function () { + if (remoting.isAppsV2) { + return Promise.resolve(chrome.app.window.current().id); + } + + /** + * @param {function(*=):void} resolve + * @param {function(*=):void} reject + */ + return new Promise(function(resolve, reject) { + /** @param {chrome.Tab} tab */ + chrome.tabs.getCurrent(function(tab){ + if (tab) { + resolve(String(tab.id)); + } + reject('Cannot retrieve the current tab.'); + }); + }); + }; + var onLoad = function() { // Parse URL parameters. var urlParams = getUrlParameters_(); if ('mode' in urlParams) { - if (urlParams['mode'] == 'me2me') { + if (urlParams['mode'] === 'me2me') { var hostId = urlParams['hostId']; remoting.connectMe2Me(hostId); return; - } else if (urlParams['mode'] == 'hangout') { - var accessCode = urlParams['accessCode']; - remoting.ensureSessionConnector_(); - remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); - remoting.connector.connectIT2Me(accessCode); - - document.body.classList.add('hangout-remote-desktop'); - var hangoutSession = new remoting.HangoutSession(); - hangoutSession.init(); + } else if (urlParams['mode'] === 'hangout') { + /** @param {*} id */ + getCurrentId().then(function(id) { + /** @type {string} */ + var accessCode = urlParams['accessCode']; + remoting.ensureSessionConnector_(); + remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); + remoting.connector.connectIT2Me(accessCode); + + document.body.classList.add('hangout-remote-desktop'); + var senderId = /** @type {string} */ String(id); + var hangoutSession = new remoting.HangoutSession(senderId); + hangoutSession.init(); + }); return; } } diff --git a/remoting/webapp/unittests/chrome_mocks.js b/remoting/webapp/unittests/chrome_mocks.js new file mode 100644 index 0000000..e10f081 --- /dev/null +++ b/remoting/webapp/unittests/chrome_mocks.js @@ -0,0 +1,50 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file contains various mock objects for the chrome platform to make +// unit testing easier. + +(function(scope){ + +var chromeMocks = {}; + +chromeMocks.Event = function() { + this.listeners_ = []; +}; + +chromeMocks.Event.prototype.addListener = function(callback) { + this.listeners_.push(callback); +}; + +chromeMocks.Event.prototype.removeListener = function(callback) { + for (var i = 0; i < this.listeners_.length; i++) { + if (this.listeners_[i] === callback) { + this.listeners_.splice(i, 1); + break; + } + } +}; + +chromeMocks.Event.prototype.mock$fire = function(data) { + this.listeners_.forEach(function(listener){ + listener(data); + }); +}; + +chromeMocks.runtime = {}; + +chromeMocks.runtime.Port = function() { + this.onMessage = new chromeMocks.Event(); + this.onDisconnect = new chromeMocks.Event(); + + this.name = ''; + this.sender = null; +}; + +chromeMocks.runtime.Port.prototype.disconnect = function() {}; +chromeMocks.runtime.Port.prototype.postMessage = function() {}; + +scope.chromeMocks = chromeMocks; + +})(window); diff --git a/remoting/webapp/unittests/it2me_helper_channel_unittest.js b/remoting/webapp/unittests/it2me_helper_channel_unittest.js new file mode 100644 index 0000000..b5511c3 --- /dev/null +++ b/remoting/webapp/unittests/it2me_helper_channel_unittest.js @@ -0,0 +1,179 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +(function() { + +'use strict'; + +var appLauncher = null; +var hangoutPort = null; +var webappPort = null; +var helperChannel = null; +var disconnectCallback = null; + +module('It2MeHelperChannel', { + setup: function() { + // App Launcher. + appLauncher = { + launch: function () { + return promiseResolveSynchronous('tabId'); + }, + close: function () {} + }; + appLauncher.launch = sinon.spy(appLauncher, 'launch'); + appLauncher.close = sinon.spy(appLauncher, 'close'); + + // HangoutPort. + hangoutPort = new chromeMocks.runtime.Port(); + hangoutPort.postMessage = sinon.spy(hangoutPort, 'postMessage'); + hangoutPort.disconnect = sinon.spy(hangoutPort, 'disconnect'); + + // WebappPort. + webappPort = new chromeMocks.runtime.Port(); + webappPort.sender = { + tab : { + id : 'tabId' + } + }; + webappPort.postMessage = sinon.spy(webappPort, 'postMessage'); + webappPort.disconnect = sinon.spy(webappPort, 'disconnect'); + + // disconnect callback + disconnectCallback = sinon.spy(); + + // HelperChannel. + helperChannel = new remoting.It2MeHelperChannel( + appLauncher, hangoutPort, disconnectCallback); + helperChannel.init(); + hangoutPort.onMessage.mock$fire({ + method: remoting.It2MeHelperChannel.HangoutMessageTypes.CONNECT, + accessCode: "123412341234" + }); + }, +}); + +function promiseResolveSynchronous(value) { + return { + then: function(callback) { + callback('tabId'); + } + }; +} + +test('onHangoutMessage_(|connect|) should launch the webapp', + function() { + sinon.assert.called(appLauncher.launch); + QUnit.equal(helperChannel.instanceId(), 'tabId'); +}); + +test('onWebappMessage() should forward messages to hangout', function() { + // Execute. + helperChannel.onWebappConnect(webappPort); + webappPort.onMessage.mock$fire({ + method:'sessionStateChanged', + state:remoting.ClientSession.State.CONNECTING + }); + webappPort.onMessage.mock$fire({ + method:'sessionStateChanged', + state:remoting.ClientSession.State.CONNECTED + }); + + // Verify events are forwarded. + sinon.assert.calledWith(hangoutPort.postMessage, { + method:'sessionStateChanged', + state:remoting.ClientSession.State.CONNECTING + }); + + sinon.assert.calledWith(hangoutPort.postMessage, { + method:'sessionStateChanged', + state:remoting.ClientSession.State.CONNECTED + }); +}); + +test('should notify hangout when the webapp crashes', function() { + // Execute. + helperChannel.onWebappConnect(webappPort); + webappPort.onDisconnect.mock$fire(); + + // Verify events are forwarded. + sinon.assert.calledWith(hangoutPort.postMessage, { + method:'sessionStateChanged', + state: remoting.ClientSession.State.FAILED + }); + sinon.assert.called(hangoutPort.disconnect); + sinon.assert.calledOnce(disconnectCallback); +}); + +test('should notify hangout when the session is ended', function() { + // Execute. + helperChannel.onWebappConnect(webappPort); + webappPort.onMessage.mock$fire({ + method:'sessionStateChanged', + state:remoting.ClientSession.State.CLOSED + }); + + webappPort.onDisconnect.mock$fire(); + + // Verify events are forwarded. + sinon.assert.calledWith(hangoutPort.postMessage, { + method:'sessionStateChanged', + state:remoting.ClientSession.State.CLOSED + }); + sinon.assert.called(hangoutPort.disconnect); + sinon.assert.calledOnce(disconnectCallback); +}); + +test('should notify hangout when the session has error', function() { + helperChannel.onWebappConnect(webappPort); + webappPort.onMessage.mock$fire({ + method:'sessionStateChanged', + state:remoting.ClientSession.State.FAILED + }); + + webappPort.onDisconnect.mock$fire(); + + // Verify events are forwarded. + sinon.assert.calledWith(hangoutPort.postMessage, { + method:'sessionStateChanged', + state:remoting.ClientSession.State.FAILED + }); + sinon.assert.called(hangoutPort.disconnect); + sinon.assert.calledOnce(disconnectCallback); +}); + + +test('onHangoutMessages_(disconnect) should close the webapp', function() { + // Execute. + helperChannel.onWebappConnect(webappPort); + hangoutPort.onMessage.mock$fire({ + method: remoting.It2MeHelperChannel.HangoutMessageTypes.DISCONNECT + }); + + sinon.assert.calledOnce(appLauncher.close); + + // Webapp will respond by disconnecting the port + webappPort.onDisconnect.mock$fire(); + + // Verify events are forwarded. + sinon.assert.calledWith(hangoutPort.postMessage, { + method:'sessionStateChanged', + state:remoting.ClientSession.State.CLOSED + }); + sinon.assert.called(webappPort.disconnect); + sinon.assert.called(hangoutPort.disconnect); +}); + +test('should close the webapp when hangout crashes', function() { + // Execute. + helperChannel.onWebappConnect(webappPort); + hangoutPort.onDisconnect.mock$fire(); + + sinon.assert.calledOnce(appLauncher.close); + sinon.assert.calledOnce(disconnectCallback); + + sinon.assert.called(hangoutPort.disconnect); + sinon.assert.called(webappPort.disconnect); +}); + +})(); diff --git a/remoting/webapp/unittests/it2me_service_unittest.js b/remoting/webapp/unittests/it2me_service_unittest.js new file mode 100644 index 0000000..a592426 --- /dev/null +++ b/remoting/webapp/unittests/it2me_service_unittest.js @@ -0,0 +1,145 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +(function() { + +'use strict'; + +var appLauncher = null; +var hangoutPort = null; +var webappPort = null; +var it2meService = null; + +function createPort(name, senderId) { + var port = new chromeMocks.runtime.Port(); + port.name = (senderId) ? name +'@' + senderId : name; + port.postMessage = sinon.spy(port, 'postMessage'); + port.disconnect = sinon.spy(port, 'disconnect'); + + return port; +} + +function promiseResolveSynchronous(value) { + return { + then: function(callback) { + callback(value); + } + }; +} + +module('It2MeService', { + setup: function() { + // App Launcher. + appLauncher = { + launch: function () { + return promiseResolveSynchronous('tabId'); + }, + close: function () {} + }; + // HangoutPort. + hangoutPort = createPort('it2me.helper.hangout'); + it2meService = new remoting.It2MeService(appLauncher); + it2meService.onConnectExternal_(hangoutPort); + webappPort = createPort('it2me.helper.webapp', 'tabId'); + } +}); + +test('should establish a channel two way channel when the webapp connects', + function() { + // Hangout ---- connect ----> It2MeService. + hangoutPort.onMessage.mock$fire({ + method: 'connect', + accessCode: "123412341234" + }); + + // Webapp ---- connect ----> It2MeService. + it2meService.onWebappConnect_(webappPort); + + // Webapp ---- sessionStateChanged ----> It2MeService. + webappPort.onMessage.mock$fire({ + method: 'sessionStateChanged', + state: remoting.ClientSession.State.CONNECTED + }); + + // verify that hangout can receive message events. + sinon.assert.calledWith(hangoutPort.postMessage, { + method: 'sessionStateChanged', + state: remoting.ClientSession.State.CONNECTED + }); + + hangoutPort.onDisconnect.mock$fire(); + QUnit.equal(it2meService.helpers_.length, 0); +}); + +test('should handle multiple helper connections', function() { + // Hangout ---- connect ----> It2MeService. + hangoutPort.onMessage.mock$fire({ + method: 'connect', + accessCode: "123412341234" + }); + + // Hangout2 ---- connect ----> It2MeService. + var hangoutPort2 = createPort('it2me.helper.hangout'); + it2meService.onConnectExternal_(hangoutPort2); + + appLauncher.launch = function () { + return promiseResolveSynchronous('tabId2'); + }; + + hangoutPort2.onMessage.mock$fire({ + method: 'connect', + accessCode: "123412341234" + }); + + it2meService.onWebappConnect_(webappPort); + + var webappPort2 = createPort('it2me.helper.webapp', 'tabId2'); + it2meService.onWebappConnect_(webappPort2); + + webappPort.onMessage.mock$fire({ + method: 'sessionStateChanged', + state: remoting.ClientSession.State.CONNECTED + }); + + // verify that hangout can receive message events from webapp 1 + sinon.assert.calledWith(hangoutPort.postMessage, { + method: 'sessionStateChanged', + state: remoting.ClientSession.State.CONNECTED + }); + + webappPort2.onMessage.mock$fire({ + method: 'sessionStateChanged', + state: remoting.ClientSession.State.CLOSED + }); + + // verify that hangout can receive message events from webapp 2. + sinon.assert.calledWith(hangoutPort2.postMessage, { + method: 'sessionStateChanged', + state: remoting.ClientSession.State.CLOSED + }); +}); + +test('should reject unknown connection', function() { + it2meService.onWebappConnect_(webappPort); + sinon.assert.called(webappPort.disconnect); + + var randomPort = createPort('unsupported.port.name'); + it2meService.onConnectExternal_(randomPort); + sinon.assert.called(randomPort.disconnect); +}); + +test('messageExternal("hello") should return supportedFeatures', function() { + var response = null; + function callback(msg) { + response = msg; + } + + it2meService.onMessageExternal_({ + method: 'hello' + }, null, callback); + + QUnit.ok(response.supportedFeatures instanceof Array); +}); + +})();
\ No newline at end of file |