Javascript  |  327行  |  10.5 KB

// 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,
 * representing connections to the webapp and hangout, respectively.
 *
 * Connection is always initiated from Hangouts by calling
 *   var port = chrome.runtime.connect({name:'it2me.helper.hangout'}, extId).
 *   port.postMessage('hello')
 * If the webapp is not installed, |port.onDisconnect| will fire.
 * If the webapp is installed, Hangouts will receive a hello response with the
 * list of supported features.
 *
 *   Hangout                     It2MeHelperChannel        Chrome Remote Desktop
 *      |-----runtime.connect() ------>|                                |
 *      |--------hello message-------->|                                |
 *      |                              |<-----helloResponse message-----|
 *      |-------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 = {
  HELLO: 'hello',
  HELLO_RESPONSE: 'helloResponse',
  CONNECT: 'connect',
  DISCONNECT: 'disconnect',
  ERROR: 'error'
};

/** @enum {string} */
remoting.It2MeHelperChannel.Features = {
  REMOTE_ASSISTANCE: 'remoteAssistance'
};

/** @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;
      case MessageTypes.HELLO:
        this.hangoutPort_.postMessage({
          method: MessageTypes.HELLO_RESPONSE,
          supportedFeatures: base.values(remoting.It2MeHelperChannel.Features)
        });
        return true;
    }
    throw new Error('Unknown message method=' + message.method);
  } catch(e) {
    var error = /** @type {Error} */ e;
    this.sendErrorResponse_(this.hangoutPort_, 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;
    this.sendErrorResponse_(this.webappPort_, 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;
  }
};

/**
 * @param {chrome.runtime.Port} port
 * @param {string|Error} error
 * @param {?{method:string, data:Object.<string,*>}=} opt_incomingMessage
 * @private
 */
remoting.It2MeHelperChannel.prototype.sendErrorResponse_ =
    function(port, error, opt_incomingMessage) {
  if (error instanceof Error) {
    error = error.message;
  }

  console.error('Error responding to message method:' +
                (opt_incomingMessage ? opt_incomingMessage.method : 'null') +
                ' error:' + error);
  port.postMessage({
    method: remoting.It2MeHelperChannel.HangoutMessageTypes.ERROR,
    message: error,
    request: opt_incomingMessage
  });
};