Home | History | Annotate | Download | only in webapp
      1 // Copyright (c) 2012 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  * Class that wraps low-level details of interacting with the client plugin.
      8  *
      9  * This abstracts a <embed> element and controls the plugin which does
     10  * the actual remoting work. It also handles differences between
     11  * client plugins versions when it is necessary.
     12  */
     13 
     14 'use strict';
     15 
     16 /** @suppress {duplicate} */
     17 var remoting = remoting || {};
     18 
     19 /**
     20  * @param {remoting.ViewerPlugin} plugin The plugin embed element.
     21  * @constructor
     22  * @implements {remoting.ClientPlugin}
     23  */
     24 remoting.ClientPluginAsync = function(plugin) {
     25   this.plugin = plugin;
     26 
     27   this.desktopWidth = 0;
     28   this.desktopHeight = 0;
     29   this.desktopXDpi = 96;
     30   this.desktopYDpi = 96;
     31 
     32   /** @param {string} iq The Iq stanza received from the host. */
     33   this.onOutgoingIqHandler = function (iq) {};
     34   /** @param {string} message Log message. */
     35   this.onDebugMessageHandler = function (message) {};
     36   /**
     37    * @param {number} state The connection state.
     38    * @param {number} error The error code, if any.
     39    */
     40   this.onConnectionStatusUpdateHandler = function(state, error) {};
     41   /** @param {boolean} ready Connection ready state. */
     42   this.onConnectionReadyHandler = function(ready) {};
     43   /**
     44    * @param {string} tokenUrl Token-request URL, received from the host.
     45    * @param {string} hostPublicKey Public key for the host.
     46    * @param {string} scope OAuth scope to request the token for.
     47    */
     48   this.fetchThirdPartyTokenHandler = function(
     49     tokenUrl, hostPublicKey, scope) {};
     50   this.onDesktopSizeUpdateHandler = function () {};
     51   /** @param {!Array.<string>} capabilities The negotiated capabilities. */
     52   this.onSetCapabilitiesHandler = function (capabilities) {};
     53   this.fetchPinHandler = function (supportsPairing) {};
     54 
     55   /** @type {number} */
     56   this.pluginApiVersion_ = -1;
     57   /** @type {Array.<string>} */
     58   this.pluginApiFeatures_ = [];
     59   /** @type {number} */
     60   this.pluginApiMinVersion_ = -1;
     61   /** @type {!Array.<string>} */
     62   this.capabilities_ = [];
     63   /** @type {boolean} */
     64   this.helloReceived_ = false;
     65   /** @type {function(boolean)|null} */
     66   this.onInitializedCallback_ = null;
     67   /** @type {function(string, string):void} */
     68   this.onPairingComplete_ = function(clientId, sharedSecret) {};
     69   /** @type {remoting.ClientSession.PerfStats} */
     70   this.perfStats_ = new remoting.ClientSession.PerfStats();
     71 
     72   /** @type {remoting.ClientPluginAsync} */
     73   var that = this;
     74   /** @param {Event} event Message event from the plugin. */
     75   this.plugin.addEventListener('message', function(event) {
     76       that.handleMessage_(event.data);
     77     }, false);
     78   window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
     79 };
     80 
     81 /**
     82  * Chromoting session API version (for this javascript).
     83  * This is compared with the plugin API version to verify that they are
     84  * compatible.
     85  *
     86  * @const
     87  * @private
     88  */
     89 remoting.ClientPluginAsync.prototype.API_VERSION_ = 6;
     90 
     91 /**
     92  * The oldest API version that we support.
     93  * This will differ from the |API_VERSION_| if we maintain backward
     94  * compatibility with older API versions.
     95  *
     96  * @const
     97  * @private
     98  */
     99 remoting.ClientPluginAsync.prototype.API_MIN_VERSION_ = 5;
    100 
    101 /**
    102  * @param {string} messageStr Message from the plugin.
    103  */
    104 remoting.ClientPluginAsync.prototype.handleMessage_ = function(messageStr) {
    105   var message = /** @type {{method:string, data:Object.<string,string>}} */
    106       jsonParseSafe(messageStr);
    107 
    108   if (!message || !('method' in message) || !('data' in message)) {
    109     console.error('Received invalid message from the plugin: ' + messageStr);
    110     return;
    111   }
    112 
    113   /**
    114    * Splits a string into a list of words delimited by spaces.
    115    * @param {string} str String that should be split.
    116    * @return {!Array.<string>} List of words.
    117    */
    118   var tokenize = function(str) {
    119     /** @type {Array.<string>} */
    120     var tokens = str.match(/\S+/g);
    121     return tokens ? tokens : [];
    122   };
    123 
    124   if (message.method == 'hello') {
    125     // Reset the size in case we had to enlarge it to support click-to-play.
    126     this.plugin.width = 0;
    127     this.plugin.height = 0;
    128     if (typeof message.data['apiVersion'] != 'number' ||
    129         typeof message.data['apiMinVersion'] != 'number') {
    130       console.error('Received invalid hello message: ' + messageStr);
    131       return;
    132     }
    133     this.pluginApiVersion_ = /** @type {number} */ message.data['apiVersion'];
    134 
    135     if (this.pluginApiVersion_ >= 7) {
    136       if (typeof message.data['apiFeatures'] != 'string') {
    137         console.error('Received invalid hello message: ' + messageStr);
    138         return;
    139       }
    140       this.pluginApiFeatures_ =
    141           /** @type {Array.<string>} */ tokenize(message.data['apiFeatures']);
    142 
    143       // Negotiate capabilities.
    144 
    145       /** @type {!Array.<string>} */
    146       var requestedCapabilities = [];
    147       if ('requestedCapabilities' in message.data) {
    148         if (typeof message.data['requestedCapabilities'] != 'string') {
    149           console.error('Received invalid hello message: ' + messageStr);
    150           return;
    151         }
    152         requestedCapabilities = tokenize(message.data['requestedCapabilities']);
    153       }
    154 
    155       /** @type {!Array.<string>} */
    156       var supportedCapabilities = [];
    157       if ('supportedCapabilities' in message.data) {
    158         if (typeof message.data['supportedCapabilities'] != 'string') {
    159           console.error('Received invalid hello message: ' + messageStr);
    160           return;
    161         }
    162         supportedCapabilities = tokenize(message.data['supportedCapabilities']);
    163       }
    164 
    165       // At the moment the webapp does not recognize any of
    166       // 'requestedCapabilities' capabilities (so they all should be disabled)
    167       // and do not care about any of 'supportedCapabilities' capabilities (so
    168       // they all can be enabled).
    169       this.capabilities_ = supportedCapabilities;
    170 
    171       // Let the host know that the webapp can be requested to always send
    172       // the client's dimensions.
    173       this.capabilities_.push(
    174           remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION);
    175 
    176       // Let the host know that we're interested in knowing whether or not
    177       // it rate-limits desktop-resize requests.
    178       this.capabilities_.push(
    179           remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS);
    180     } else if (this.pluginApiVersion_ >= 6) {
    181       this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
    182     } else {
    183       this.pluginApiFeatures_ = ['highQualityScaling'];
    184     }
    185     this.pluginApiMinVersion_ =
    186         /** @type {number} */ message.data['apiMinVersion'];
    187     this.helloReceived_ = true;
    188     if (this.onInitializedCallback_ != null) {
    189       this.onInitializedCallback_(true);
    190       this.onInitializedCallback_ = null;
    191     }
    192   } else if (message.method == 'sendOutgoingIq') {
    193     if (typeof message.data['iq'] != 'string') {
    194       console.error('Received invalid sendOutgoingIq message: ' + messageStr);
    195       return;
    196     }
    197     this.onOutgoingIqHandler(message.data['iq']);
    198   } else if (message.method == 'logDebugMessage') {
    199     if (typeof message.data['message'] != 'string') {
    200       console.error('Received invalid logDebugMessage message: ' + messageStr);
    201       return;
    202     }
    203     this.onDebugMessageHandler(message.data['message']);
    204   } else if (message.method == 'onConnectionStatus') {
    205     if (typeof message.data['state'] != 'string' ||
    206         !remoting.ClientSession.State.hasOwnProperty(message.data['state']) ||
    207         typeof message.data['error'] != 'string') {
    208       console.error('Received invalid onConnectionState message: ' +
    209                     messageStr);
    210       return;
    211     }
    212 
    213     /** @type {remoting.ClientSession.State} */
    214     var state = remoting.ClientSession.State[message.data['state']];
    215     var error;
    216     if (remoting.ClientSession.ConnectionError.hasOwnProperty(
    217         message.data['error'])) {
    218       error = /** @type {remoting.ClientSession.ConnectionError} */
    219           remoting.ClientSession.ConnectionError[message.data['error']];
    220     } else {
    221       error = remoting.ClientSession.ConnectionError.UNKNOWN;
    222     }
    223 
    224     this.onConnectionStatusUpdateHandler(state, error);
    225   } else if (message.method == 'onDesktopSize') {
    226     if (typeof message.data['width'] != 'number' ||
    227         typeof message.data['height'] != 'number') {
    228       console.error('Received invalid onDesktopSize message: ' + messageStr);
    229       return;
    230     }
    231     this.desktopWidth = /** @type {number} */ message.data['width'];
    232     this.desktopHeight = /** @type {number} */ message.data['height'];
    233     this.desktopXDpi = (typeof message.data['x_dpi'] == 'number') ?
    234         /** @type {number} */ (message.data['x_dpi']) : 96;
    235     this.desktopYDpi = (typeof message.data['y_dpi'] == 'number') ?
    236         /** @type {number} */ (message.data['y_dpi']) : 96;
    237     this.onDesktopSizeUpdateHandler();
    238   } else if (message.method == 'onPerfStats') {
    239     if (typeof message.data['videoBandwidth'] != 'number' ||
    240         typeof message.data['videoFrameRate'] != 'number' ||
    241         typeof message.data['captureLatency'] != 'number' ||
    242         typeof message.data['encodeLatency'] != 'number' ||
    243         typeof message.data['decodeLatency'] != 'number' ||
    244         typeof message.data['renderLatency'] != 'number' ||
    245         typeof message.data['roundtripLatency'] != 'number') {
    246       console.error('Received incorrect onPerfStats message: ' + messageStr);
    247       return;
    248     }
    249     this.perfStats_ =
    250         /** @type {remoting.ClientSession.PerfStats} */ message.data;
    251   } else if (message.method == 'injectClipboardItem') {
    252     if (typeof message.data['mimeType'] != 'string' ||
    253         typeof message.data['item'] != 'string') {
    254       console.error('Received incorrect injectClipboardItem message.');
    255       return;
    256     }
    257     if (remoting.clipboard) {
    258       remoting.clipboard.fromHost(message.data['mimeType'],
    259                                   message.data['item']);
    260     }
    261   } else if (message.method == 'onFirstFrameReceived') {
    262     if (remoting.clientSession) {
    263       remoting.clientSession.onFirstFrameReceived();
    264     }
    265   } else if (message.method == 'onConnectionReady') {
    266     if (typeof message.data['ready'] != 'boolean') {
    267       console.error('Received incorrect onConnectionReady message.');
    268       return;
    269     }
    270     var ready = /** @type {boolean} */ message.data['ready'];
    271     this.onConnectionReadyHandler(ready);
    272   } else if (message.method == 'fetchPin') {
    273     // The pairingSupported value in the dictionary indicates whether both
    274     // client and host support pairing. If the client doesn't support pairing,
    275     // then the value won't be there at all, so give it a default of false.
    276     /** @type {boolean} */
    277     var pairingSupported = false;
    278     if ('pairingSupported' in message.data) {
    279       pairingSupported =
    280           /** @type {boolean} */ message.data['pairingSupported'];
    281       if (typeof pairingSupported != 'boolean') {
    282         console.error('Received incorrect fetchPin message.');
    283         return;
    284       }
    285     }
    286     this.fetchPinHandler(pairingSupported);
    287   } else if (message.method == 'setCapabilities') {
    288     if (typeof message.data['capabilities'] != 'string') {
    289       console.error('Received incorrect setCapabilities message.');
    290       return;
    291     }
    292 
    293     /** @type {!Array.<string>} */
    294     var capabilities = tokenize(message.data['capabilities']);
    295     this.onSetCapabilitiesHandler(capabilities);
    296   } else if (message.method == 'fetchThirdPartyToken') {
    297     if (typeof message.data['tokenUrl'] != 'string' ||
    298         typeof message.data['hostPublicKey'] != 'string' ||
    299         typeof message.data['scope'] != 'string') {
    300       console.error('Received incorrect fetchThirdPartyToken message.');
    301       return;
    302     }
    303     var tokenUrl = /** @type {string} */ message.data['tokenUrl'];
    304     var hostPublicKey =
    305         /** @type {string} */ message.data['hostPublicKey'];
    306     var scope = /** @type {string} */ message.data['scope'];
    307     this.fetchThirdPartyTokenHandler(tokenUrl, hostPublicKey, scope);
    308   } else if (message.method == 'pairingResponse') {
    309     var clientId = /** @type {string} */ message.data['clientId'];
    310     var sharedSecret = /** @type {string} */ message.data['sharedSecret'];
    311     if (typeof clientId != 'string' || typeof sharedSecret != 'string') {
    312       console.error('Received incorrect pairingResponse message.');
    313       return;
    314     }
    315     this.onPairingComplete_(clientId, sharedSecret);
    316   } else if (message.method == 'extensionMessage') {
    317     // No messages currently supported.
    318     console.log('Unexpected message received: ' +
    319                 message.data.type + ': ' + message.data.data);
    320   }
    321 };
    322 
    323 /**
    324  * Deletes the plugin.
    325  */
    326 remoting.ClientPluginAsync.prototype.cleanup = function() {
    327   this.plugin.parentNode.removeChild(this.plugin);
    328 };
    329 
    330 /**
    331  * @return {HTMLEmbedElement} HTML element that correspods to the plugin.
    332  */
    333 remoting.ClientPluginAsync.prototype.element = function() {
    334   return this.plugin;
    335 };
    336 
    337 /**
    338  * @param {function(boolean): void} onDone
    339  */
    340 remoting.ClientPluginAsync.prototype.initialize = function(onDone) {
    341   if (this.helloReceived_) {
    342     onDone(true);
    343   } else {
    344     this.onInitializedCallback_ = onDone;
    345   }
    346 };
    347 
    348 /**
    349  * @return {boolean} True if the plugin and web-app versions are compatible.
    350  */
    351 remoting.ClientPluginAsync.prototype.isSupportedVersion = function() {
    352   if (!this.helloReceived_) {
    353     console.error(
    354         "isSupportedVersion() is called before the plugin is initialized.");
    355     return false;
    356   }
    357   return this.API_VERSION_ >= this.pluginApiMinVersion_ &&
    358       this.pluginApiVersion_ >= this.API_MIN_VERSION_;
    359 };
    360 
    361 /**
    362  * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
    363  * @return {boolean} True if the plugin supports the named feature.
    364  */
    365 remoting.ClientPluginAsync.prototype.hasFeature = function(feature) {
    366   if (!this.helloReceived_) {
    367     console.error(
    368         "hasFeature() is called before the plugin is initialized.");
    369     return false;
    370   }
    371   return this.pluginApiFeatures_.indexOf(feature) > -1;
    372 };
    373 
    374 /**
    375  * @return {boolean} True if the plugin supports the injectKeyEvent API.
    376  */
    377 remoting.ClientPluginAsync.prototype.isInjectKeyEventSupported = function() {
    378   return this.pluginApiVersion_ >= 6;
    379 };
    380 
    381 /**
    382  * @param {string} iq Incoming IQ stanza.
    383  */
    384 remoting.ClientPluginAsync.prototype.onIncomingIq = function(iq) {
    385   if (this.plugin && this.plugin.postMessage) {
    386     this.plugin.postMessage(JSON.stringify(
    387         { method: 'incomingIq', data: { iq: iq } }));
    388   } else {
    389     // plugin.onIq may not be set after the plugin has been shut
    390     // down. Particularly this happens when we receive response to
    391     // session-terminate stanza.
    392     console.warn('plugin.onIq is not set so dropping incoming message.');
    393   }
    394 };
    395 
    396 /**
    397  * @param {string} hostJid The jid of the host to connect to.
    398  * @param {string} hostPublicKey The base64 encoded version of the host's
    399  *     public key.
    400  * @param {string} localJid Local jid.
    401  * @param {string} sharedSecret The access code for IT2Me or the PIN
    402  *     for Me2Me.
    403  * @param {string} authenticationMethods Comma-separated list of
    404  *     authentication methods the client should attempt to use.
    405  * @param {string} authenticationTag A host-specific tag to mix into
    406  *     authentication hashes.
    407  * @param {string} clientPairingId For paired Me2Me connections, the
    408  *     pairing id for this client, as issued by the host.
    409  * @param {string} clientPairedSecret For paired Me2Me connections, the
    410  *     paired secret for this client, as issued by the host.
    411  */
    412 remoting.ClientPluginAsync.prototype.connect = function(
    413     hostJid, hostPublicKey, localJid, sharedSecret,
    414     authenticationMethods, authenticationTag,
    415     clientPairingId, clientPairedSecret) {
    416   this.plugin.postMessage(JSON.stringify(
    417     { method: 'connect', data: {
    418         hostJid: hostJid,
    419         hostPublicKey: hostPublicKey,
    420         localJid: localJid,
    421         sharedSecret: sharedSecret,
    422         authenticationMethods: authenticationMethods,
    423         authenticationTag: authenticationTag,
    424         capabilities: this.capabilities_.join(" "),
    425         clientPairingId: clientPairingId,
    426         clientPairedSecret: clientPairedSecret
    427       }
    428     }));
    429 };
    430 
    431 /**
    432  * Release all currently pressed keys.
    433  */
    434 remoting.ClientPluginAsync.prototype.releaseAllKeys = function() {
    435   this.plugin.postMessage(JSON.stringify(
    436       { method: 'releaseAllKeys', data: {} }));
    437 };
    438 
    439 /**
    440  * Send a key event to the host.
    441  *
    442  * @param {number} usbKeycode The USB-style code of the key to inject.
    443  * @param {boolean} pressed True to inject a key press, False for a release.
    444  */
    445 remoting.ClientPluginAsync.prototype.injectKeyEvent =
    446     function(usbKeycode, pressed) {
    447   this.plugin.postMessage(JSON.stringify(
    448       { method: 'injectKeyEvent', data: {
    449           'usbKeycode': usbKeycode,
    450           'pressed': pressed}
    451       }));
    452 };
    453 
    454 /**
    455  * Remap one USB keycode to another in all subsequent key events.
    456  *
    457  * @param {number} fromKeycode The USB-style code of the key to remap.
    458  * @param {number} toKeycode The USB-style code to remap the key to.
    459  */
    460 remoting.ClientPluginAsync.prototype.remapKey =
    461     function(fromKeycode, toKeycode) {
    462   this.plugin.postMessage(JSON.stringify(
    463       { method: 'remapKey', data: {
    464           'fromKeycode': fromKeycode,
    465           'toKeycode': toKeycode}
    466       }));
    467 };
    468 
    469 /**
    470  * Enable/disable redirection of the specified key to the web-app.
    471  *
    472  * @param {number} keycode The USB-style code of the key.
    473  * @param {Boolean} trap True to enable trapping, False to disable.
    474  */
    475 remoting.ClientPluginAsync.prototype.trapKey = function(keycode, trap) {
    476   this.plugin.postMessage(JSON.stringify(
    477       { method: 'trapKey', data: {
    478           'keycode': keycode,
    479           'trap': trap}
    480       }));
    481 };
    482 
    483 /**
    484  * Returns an associative array with a set of stats for this connecton.
    485  *
    486  * @return {remoting.ClientSession.PerfStats} The connection statistics.
    487  */
    488 remoting.ClientPluginAsync.prototype.getPerfStats = function() {
    489   return this.perfStats_;
    490 };
    491 
    492 /**
    493  * Sends a clipboard item to the host.
    494  *
    495  * @param {string} mimeType The MIME type of the clipboard item.
    496  * @param {string} item The clipboard item.
    497  */
    498 remoting.ClientPluginAsync.prototype.sendClipboardItem =
    499     function(mimeType, item) {
    500   if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM))
    501     return;
    502   this.plugin.postMessage(JSON.stringify(
    503       { method: 'sendClipboardItem',
    504         data: { mimeType: mimeType, item: item }}));
    505 };
    506 
    507 /**
    508  * Notifies the host that the client has the specified size and pixel density.
    509  *
    510  * @param {number} width The available client width in DIPs.
    511  * @param {number} height The available client height in DIPs.
    512  * @param {number} device_scale The number of device pixels per DIP.
    513  */
    514 remoting.ClientPluginAsync.prototype.notifyClientResolution =
    515     function(width, height, device_scale) {
    516   if (this.hasFeature(remoting.ClientPlugin.Feature.NOTIFY_CLIENT_RESOLUTION)) {
    517     var dpi = device_scale * 96;
    518     this.plugin.postMessage(JSON.stringify(
    519         { method: 'notifyClientResolution',
    520           data: { width: width * device_scale,
    521                   height: height * device_scale,
    522                   x_dpi: dpi, y_dpi: dpi }}));
    523   } else if (this.hasFeature(
    524                  remoting.ClientPlugin.Feature.NOTIFY_CLIENT_DIMENSIONS)) {
    525     this.plugin.postMessage(JSON.stringify(
    526         { method: 'notifyClientDimensions',
    527           data: { width: width, height: height }}));
    528   }
    529 };
    530 
    531 /**
    532  * Requests that the host pause or resume sending video updates.
    533  *
    534  * @param {boolean} pause True to suspend video updates, false otherwise.
    535  */
    536 remoting.ClientPluginAsync.prototype.pauseVideo =
    537     function(pause) {
    538   if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO))
    539     return;
    540   this.plugin.postMessage(JSON.stringify(
    541       { method: 'pauseVideo', data: { pause: pause }}));
    542 };
    543 
    544 /**
    545  * Requests that the host pause or resume sending audio updates.
    546  *
    547  * @param {boolean} pause True to suspend audio updates, false otherwise.
    548  */
    549 remoting.ClientPluginAsync.prototype.pauseAudio =
    550     function(pause) {
    551   if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO))
    552     return;
    553   this.plugin.postMessage(JSON.stringify(
    554       { method: 'pauseAudio', data: { pause: pause }}));
    555 };
    556 
    557 /**
    558  * Called when a PIN is obtained from the user.
    559  *
    560  * @param {string} pin The PIN.
    561  */
    562 remoting.ClientPluginAsync.prototype.onPinFetched =
    563     function(pin) {
    564   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
    565     return;
    566   }
    567   this.plugin.postMessage(JSON.stringify(
    568       { method: 'onPinFetched', data: { pin: pin }}));
    569 };
    570 
    571 /**
    572  * Tells the plugin to ask for the PIN asynchronously.
    573  */
    574 remoting.ClientPluginAsync.prototype.useAsyncPinDialog =
    575     function() {
    576   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
    577     return;
    578   }
    579   this.plugin.postMessage(JSON.stringify(
    580       { method: 'useAsyncPinDialog', data: {} }));
    581 };
    582 
    583 /**
    584  * Sets the third party authentication token and shared secret.
    585  *
    586  * @param {string} token The token received from the token URL.
    587  * @param {string} sharedSecret Shared secret received from the token URL.
    588  */
    589 remoting.ClientPluginAsync.prototype.onThirdPartyTokenFetched = function(
    590     token, sharedSecret) {
    591   this.plugin.postMessage(JSON.stringify(
    592     { method: 'onThirdPartyTokenFetched',
    593       data: { token: token, sharedSecret: sharedSecret}}));
    594 };
    595 
    596 /**
    597  * Request pairing with the host for PIN-less authentication.
    598  *
    599  * @param {string} clientName The human-readable name of the client.
    600  * @param {function(string, string):void} onDone, Callback to receive the
    601  *     client id and shared secret when they are available.
    602  */
    603 remoting.ClientPluginAsync.prototype.requestPairing =
    604     function(clientName, onDone) {
    605   if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) {
    606     return;
    607   }
    608   this.onPairingComplete_ = onDone;
    609   this.plugin.postMessage(JSON.stringify(
    610       { method: 'requestPairing', data: { clientName: clientName } }));
    611 };
    612 
    613 /**
    614  * Send an extension message to the host.
    615  *
    616  * @param {string} type The message type.
    617  * @param {Object} message The message payload.
    618  */
    619 remoting.ClientPluginAsync.prototype.sendClientMessage =
    620     function(type, message) {
    621   if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) {
    622     return;
    623   }
    624   this.plugin.postMessage(JSON.stringify(
    625     { method: 'extensionMessage',
    626       data: { type: type, data: JSON.stringify(message) } }));
    627 
    628 };
    629 
    630 /**
    631  * If we haven't yet received a "hello" message from the plugin, change its
    632  * size so that the user can confirm it if click-to-play is enabled, or can
    633  * see the "this plugin is disabled" message if it is actually disabled.
    634  * @private
    635  */
    636 remoting.ClientPluginAsync.prototype.showPluginForClickToPlay_ = function() {
    637   if (!this.helloReceived_) {
    638     var width = 200;
    639     var height = 200;
    640     this.plugin.width = width;
    641     this.plugin.height = height;
    642     // Center the plugin just underneath the "Connnecting..." dialog.
    643     var parentNode = this.plugin.parentNode;
    644     var dialog = document.getElementById('client-dialog');
    645     var dialogRect = dialog.getBoundingClientRect();
    646     parentNode.style.top = (dialogRect.bottom + 16) + 'px';
    647     parentNode.style.left = (window.innerWidth - width) / 2 + 'px';
    648   }
    649 };
    650