summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkelvinp@chromium.org <kelvinp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-08-14 12:42:28 +0000
committerkelvinp@chromium.org <kelvinp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-08-14 12:44:38 +0000
commit23005b6b310879f5877e2411cc7aec74480861dc (patch)
tree949ad16391059d26d271675489fd08f7b89a8178
parentb8e70ea6025cdb426a5514ee58179cf417ade1be (diff)
downloadchromium_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.gypi6
-rw-r--r--remoting/remoting_webapp_files.gypi9
-rw-r--r--remoting/webapp/background/app_launcher.js2
-rw-r--r--remoting/webapp/background/background.js24
-rw-r--r--remoting/webapp/background/it2me_helper_channel.js290
-rw-r--r--remoting/webapp/background/it2me_service.js184
-rw-r--r--remoting/webapp/client_screen.js8
-rw-r--r--remoting/webapp/hangout_session.js14
-rw-r--r--remoting/webapp/js_proto/chrome_proto.js2
-rw-r--r--remoting/webapp/js_proto/dom_proto.js4
-rw-r--r--remoting/webapp/manifest.json.jinja214
-rw-r--r--remoting/webapp/remoting.js49
-rw-r--r--remoting/webapp/unittests/chrome_mocks.js50
-rw-r--r--remoting/webapp/unittests/it2me_helper_channel_unittest.js179
-rw-r--r--remoting/webapp/unittests/it2me_service_unittest.js145
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