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
3// found in the LICENSE file.
4
5/**
6 * @fileoverview
7 *
8 * It2MeHelperChannel relays messages between Hangouts and Chrome Remote Desktop
9 * (webapp) for the helper (the Hangouts participant who is giving remote
10 * assistance).
11 *
12 * It runs in the background page and contains two chrome.runtime.Port objects,
13 * representing connections to the webapp and hangout, respectively.
14 *
15 * Connection is always initiated from Hangouts by calling
16 *   var port = chrome.runtime.connect({name:'it2me.helper.hangout'}, extId).
17 *   port.postMessage('hello')
18 * If the webapp is not installed, |port.onDisconnect| will fire.
19 * If the webapp is installed, Hangouts will receive a hello response with the
20 * list of supported features.
21 *
22 *   Hangout                     It2MeHelperChannel        Chrome Remote Desktop
23 *      |-----runtime.connect() ------>|                                |
24 *      |--------hello message-------->|                                |
25 *      |                              |<-----helloResponse message-----|
26 *      |-------connect message------->|                                |
27 *      |                              |-------appLauncher.launch()---->|
28 *      |                              |<------runtime.connect()------- |
29 *      |                              |<-----sessionStateChanged------ |
30 *      |<----sessionStateChanged------|                                |
31 *
32 * Disconnection can be initiated from either side:
33 * 1. In the normal flow initiated from hangout
34 *    Hangout                    It2MeHelperChannel        Chrome Remote Desktop
35 *       |-----disconnect message------>|                               |
36 *       |<-sessionStateChanged(CLOSED)-|                               |
37 *       |                              |-----appLauncher.close()------>|
38 *
39 * 2. In the normal flow initiated from webapp
40 *    Hangout                    It2MeHelperChannel        Chrome Remote Desktop
41 *       |                              |<-sessionStateChanged(CLOSED)--|
42 *       |                              |<--------port.disconnect()-----|
43 *       |<--------port.disconnect()----|                               |
44 *
45 * 2. If hangout crashes
46 *    Hangout                    It2MeHelperChannel        Chrome Remote Desktop
47 *       |---------port.disconnect()--->|                               |
48 *       |                              |--------port.disconnect()----->|
49 *       |                              |------appLauncher.close()----->|
50 *
51 * 3. If webapp crashes
52 *    Hangout                    It2MeHelperChannel        Chrome Remote Desktop
53 *       |                              |<-------port.disconnect()------|
54 *       |<-sessionStateChanged(FAILED)-|                               |
55 *       |<--------port.disconnect()----|                               |
56 */
57
58'use strict';
59
60/** @suppress {duplicate} */
61var remoting = remoting || {};
62
63/**
64 * @param {remoting.AppLauncher} appLauncher
65 * @param {chrome.runtime.Port} hangoutPort Represents an active connection to
66 *     Hangouts.
67 * @param {function(remoting.It2MeHelperChannel)} onDisconnectCallback Callback
68 *     to notify when the connection is torn down.  IT2MeService uses this
69 *     callback to dispose of the channel object.
70 * @constructor
71 */
72remoting.It2MeHelperChannel =
73    function(appLauncher, hangoutPort, onDisconnectCallback) {
74
75  /**
76   * @type {remoting.AppLauncher}
77   * @private
78   */
79  this.appLauncher_ = appLauncher;
80
81  /**
82   * @type {chrome.runtime.Port}
83   * @private
84   */
85  this.hangoutPort_ = hangoutPort;
86
87  /**
88   * @type {chrome.runtime.Port}
89   * @private
90   */
91  this.webappPort_ = null;
92
93  /**
94   * @type {string}
95   * @private
96   */
97  this.instanceId_ = '';
98
99  /**
100   * @type {remoting.ClientSession.State}
101   * @private
102   */
103  this.sessionState_ = remoting.ClientSession.State.CONNECTING;
104
105  /**
106   * @type {?function(remoting.It2MeHelperChannel)}
107   * @private
108   */
109  this.onDisconnectCallback_ = onDisconnectCallback;
110
111  this.onWebappMessageRef_ = this.onWebappMessage_.bind(this);
112  this.onWebappDisconnectRef_ = this.onWebappDisconnect_.bind(this);
113  this.onHangoutMessageRef_ = this.onHangoutMessage_.bind(this);
114  this.onHangoutDisconnectRef_ = this.onHangoutDisconnect_.bind(this);
115};
116
117/** @enum {string} */
118remoting.It2MeHelperChannel.HangoutMessageTypes = {
119  HELLO: 'hello',
120  HELLO_RESPONSE: 'helloResponse',
121  CONNECT: 'connect',
122  DISCONNECT: 'disconnect',
123  ERROR: 'error'
124};
125
126/** @enum {string} */
127remoting.It2MeHelperChannel.Features = {
128  REMOTE_ASSISTANCE: 'remoteAssistance'
129};
130
131/** @enum {string} */
132remoting.It2MeHelperChannel.WebappMessageTypes = {
133  SESSION_STATE_CHANGED: 'sessionStateChanged'
134};
135
136remoting.It2MeHelperChannel.prototype.init = function() {
137  this.hangoutPort_.onMessage.addListener(this.onHangoutMessageRef_);
138  this.hangoutPort_.onDisconnect.addListener(this.onHangoutDisconnectRef_);
139};
140
141/** @return {string} */
142remoting.It2MeHelperChannel.prototype.instanceId = function() {
143  return this.instanceId_;
144};
145
146/**
147 * @param {{method:string, data:Object.<string,*>}} message
148 * @return {boolean} whether the message is handled or not.
149 * @private
150 */
151remoting.It2MeHelperChannel.prototype.onHangoutMessage_ = function(message) {
152  try {
153    var MessageTypes = remoting.It2MeHelperChannel.HangoutMessageTypes;
154    switch (message.method) {
155      case MessageTypes.CONNECT:
156        this.launchWebapp_(message);
157        return true;
158      case MessageTypes.DISCONNECT:
159        this.closeWebapp_(message);
160        return true;
161      case MessageTypes.HELLO:
162        this.hangoutPort_.postMessage({
163          method: MessageTypes.HELLO_RESPONSE,
164          supportedFeatures: base.values(remoting.It2MeHelperChannel.Features)
165        });
166        return true;
167    }
168    throw new Error('Unknown message method=' + message.method);
169  } catch(e) {
170    var error = /** @type {Error} */ e;
171    this.sendErrorResponse_(this.hangoutPort_, error, message);
172  }
173  return false;
174};
175
176/**
177 * Disconnect the existing connection to the helpee.
178 *
179 * @param {{method:string, data:Object.<string,*>}} message
180 * @private
181 */
182remoting.It2MeHelperChannel.prototype.closeWebapp_ =
183    function(message) {
184  // TODO(kelvinp): Closing the v2 app currently doesn't disconnect the IT2me
185  // session (crbug.com/402137), so send an explicit notification to Hangouts.
186  this.sessionState_ = remoting.ClientSession.State.CLOSED;
187  this.hangoutPort_.postMessage({
188    method: 'sessionStateChanged',
189    state: this.sessionState_
190  });
191  this.appLauncher_.close(this.instanceId_);
192};
193
194/**
195 * Launches the web app.
196 *
197 * @param {{method:string, data:Object.<string,*>}} message
198 * @private
199 */
200remoting.It2MeHelperChannel.prototype.launchWebapp_ =
201    function(message) {
202  var accessCode = getStringAttr(message, 'accessCode');
203  if (!accessCode) {
204    throw new Error('Access code is missing');
205  }
206
207  // Launch the webapp.
208  this.appLauncher_.launch({
209    mode: 'hangout',
210    accessCode: accessCode
211  }).then(
212    /**
213     * @this {remoting.It2MeHelperChannel}
214     * @param {string} instanceId
215     */
216    function(instanceId){
217      this.instanceId_ = instanceId;
218    }.bind(this));
219};
220
221/**
222 * @private
223 */
224remoting.It2MeHelperChannel.prototype.onHangoutDisconnect_ = function() {
225  this.appLauncher_.close(this.instanceId_);
226  this.unhookPorts_();
227};
228
229/**
230 * @param {chrome.runtime.Port} port The port represents a connection to the
231 *     webapp.
232 * @param {string} id The id of the tab or window that is hosting the webapp.
233 */
234remoting.It2MeHelperChannel.prototype.onWebappConnect = function(port, id) {
235  base.debug.assert(id === this.instanceId_);
236  base.debug.assert(this.hangoutPort_ !== null);
237
238  // Hook listeners.
239  port.onMessage.addListener(this.onWebappMessageRef_);
240  port.onDisconnect.addListener(this.onWebappDisconnectRef_);
241  this.webappPort_ = port;
242};
243
244/** @param {chrome.runtime.Port} port The webapp port. */
245remoting.It2MeHelperChannel.prototype.onWebappDisconnect_ = function(port) {
246  // If the webapp port got disconnected while the session is still connected,
247  // treat it as an error.
248  var States = remoting.ClientSession.State;
249  if (this.sessionState_ === States.CONNECTING ||
250      this.sessionState_ === States.CONNECTED) {
251    this.sessionState_ = States.FAILED;
252    this.hangoutPort_.postMessage({
253      method: 'sessionStateChanged',
254      state: this.sessionState_
255    });
256  }
257  this.unhookPorts_();
258};
259
260/**
261 * @param {{method:string, data:Object.<string,*>}} message
262 * @private
263 */
264remoting.It2MeHelperChannel.prototype.onWebappMessage_ = function(message) {
265  try {
266    console.log('It2MeHelperChannel id=' + this.instanceId_ +
267                ' incoming message method=' + message.method);
268    var MessageTypes = remoting.It2MeHelperChannel.WebappMessageTypes;
269    switch (message.method) {
270      case MessageTypes.SESSION_STATE_CHANGED:
271        var state = getNumberAttr(message, 'state');
272        this.sessionState_ =
273            /** @type {remoting.ClientSession.State} */ state;
274        this.hangoutPort_.postMessage(message);
275        return true;
276    }
277    throw new Error('Unknown message method=' + message.method);
278  } catch(e) {
279    var error = /** @type {Error} */ e;
280    this.sendErrorResponse_(this.webappPort_, error, message);
281  }
282  return false;
283};
284
285remoting.It2MeHelperChannel.prototype.unhookPorts_ = function() {
286  if (this.webappPort_) {
287    this.webappPort_.onMessage.removeListener(this.onWebappMessageRef_);
288    this.webappPort_.onDisconnect.removeListener(this.onWebappDisconnectRef_);
289    this.webappPort_.disconnect();
290    this.webappPort_ = null;
291  }
292
293  if (this.hangoutPort_) {
294    this.hangoutPort_.onMessage.removeListener(this.onHangoutMessageRef_);
295    this.hangoutPort_.onDisconnect.removeListener(this.onHangoutDisconnectRef_);
296    this.hangoutPort_.disconnect();
297    this.hangoutPort_ = null;
298  }
299
300  if (this.onDisconnectCallback_) {
301    this.onDisconnectCallback_(this);
302    this.onDisconnectCallback_  = null;
303  }
304};
305
306/**
307 * @param {chrome.runtime.Port} port
308 * @param {string|Error} error
309 * @param {?{method:string, data:Object.<string,*>}=} opt_incomingMessage
310 * @private
311 */
312remoting.It2MeHelperChannel.prototype.sendErrorResponse_ =
313    function(port, error, opt_incomingMessage) {
314  if (error instanceof Error) {
315    error = error.message;
316  }
317
318  console.error('Error responding to message method:' +
319                (opt_incomingMessage ? opt_incomingMessage.method : 'null') +
320                ' error:' + error);
321  port.postMessage({
322    method: remoting.It2MeHelperChannel.HangoutMessageTypes.ERROR,
323    message: error,
324    request: opt_incomingMessage
325  });
326};
327