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} */
17var remoting = remoting || {};
18
19/**
20 * @param {Element} container The container for the embed element.
21 * @param {function(string, string):boolean} onExtensionMessage The handler for
22 *     protocol extension messages. Returns true if a message is recognized;
23 *     false otherwise.
24 * @constructor
25 * @implements {remoting.ClientPlugin}
26 */
27remoting.ClientPluginImpl = function(container, onExtensionMessage) {
28  this.plugin_ = remoting.ClientPluginImpl.createPluginElement_();
29  this.plugin_.id = 'session-client-plugin';
30  container.appendChild(this.plugin_);
31
32  this.onExtensionMessage_ = onExtensionMessage;
33
34  /** @private */
35  this.desktopWidth_ = 0;
36  /** @private */
37  this.desktopHeight_ = 0;
38  /** @private */
39  this.desktopXDpi_ = 96;
40  /** @private */
41  this.desktopYDpi_ = 96;
42
43  /**
44   * @param {string} iq The Iq stanza received from the host.
45   * @private
46   */
47  this.onOutgoingIqHandler_ = function (iq) {};
48  /**
49   * @param {string} message Log message.
50   * @private
51   */
52  this.onDebugMessageHandler_ = function (message) {};
53  /**
54   * @param {number} state The connection state.
55   * @param {number} error The error code, if any.
56   * @private
57   */
58  this.onConnectionStatusUpdateHandler_ = function(state, error) {};
59  /**
60   * @param {boolean} ready Connection ready state.
61   * @private
62   */
63  this.onConnectionReadyHandler_ = function(ready) {};
64
65  /**
66   * @param {string} tokenUrl Token-request URL, received from the host.
67   * @param {string} hostPublicKey Public key for the host.
68   * @param {string} scope OAuth scope to request the token for.
69   * @private
70   */
71  this.fetchThirdPartyTokenHandler_ = function(
72    tokenUrl, hostPublicKey, scope) {};
73  /** @private */
74  this.onDesktopSizeUpdateHandler_ = function () {};
75  /**
76   * @param {!Array.<string>} capabilities The negotiated capabilities.
77   * @private
78   */
79  this.onSetCapabilitiesHandler_ = function (capabilities) {};
80  /** @private */
81  this.fetchPinHandler_ = function (supportsPairing) {};
82  /**
83   * @param {string} data Remote gnubbyd data.
84   * @private
85   */
86  this.onGnubbyAuthHandler_ = function(data) {};
87  /**
88   * @param {string} url
89   * @param {number} hotspotX
90   * @param {number} hotspotY
91   * @private
92   */
93  this.updateMouseCursorImage_ = function(url, hotspotX, hotspotY) {};
94
95  /**
96   * @param {string} data Remote cast extension message.
97   * @private
98   */
99  this.onCastExtensionHandler_ = function(data) {};
100
101  /**
102   * @type {remoting.MediaSourceRenderer}
103   * @private
104   */
105  this.mediaSourceRenderer_ = null;
106
107  /**
108   * @type {number}
109   * @private
110   */
111  this.pluginApiVersion_ = -1;
112  /**
113   * @type {Array.<string>}
114   * @private
115   */
116  this.pluginApiFeatures_ = [];
117  /**
118   * @type {number}
119   * @private
120   */
121  this.pluginApiMinVersion_ = -1;
122  /**
123   * @type {!Array.<string>}
124   * @private
125   */
126  this.capabilities_ = [];
127  /**
128   * @type {boolean}
129   * @private
130   */
131  this.helloReceived_ = false;
132  /**
133   * @type {function(boolean)|null}
134   * @private
135   */
136  this.onInitializedCallback_ = null;
137  /**
138   * @type {function(string, string):void}
139   * @private
140   */
141  this.onPairingComplete_ = function(clientId, sharedSecret) {};
142  /**
143   * @type {remoting.ClientSession.PerfStats}
144   * @private
145   */
146  this.perfStats_ = new remoting.ClientSession.PerfStats();
147
148  /** @type {remoting.ClientPluginImpl} */
149  var that = this;
150  /** @param {Event} event Message event from the plugin. */
151  this.plugin_.addEventListener('message', function(event) {
152      that.handleMessage_(event.data);
153    }, false);
154
155  if (remoting.settings.CLIENT_PLUGIN_TYPE == 'native') {
156    window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
157  }
158};
159
160/**
161 * Creates plugin element without adding it to a container.
162 *
163 * @return {remoting.ViewerPlugin} Plugin element
164 */
165remoting.ClientPluginImpl.createPluginElement_ = function() {
166  var plugin = /** @type {remoting.ViewerPlugin} */
167      document.createElement('embed');
168  if (remoting.settings.CLIENT_PLUGIN_TYPE == 'pnacl') {
169    plugin.src = 'remoting_client_pnacl.nmf';
170    plugin.type = 'application/x-pnacl';
171  } else if (remoting.settings.CLIENT_PLUGIN_TYPE == 'nacl') {
172    plugin.src = 'remoting_client_nacl.nmf';
173    plugin.type = 'application/x-nacl';
174  } else {
175    plugin.src = 'about://none';
176    plugin.type = 'application/vnd.chromium.remoting-viewer';
177  }
178  plugin.width = 0;
179  plugin.height = 0;
180  plugin.tabIndex = 0;  // Required, otherwise focus() doesn't work.
181  return plugin;
182}
183
184/**
185 * Chromoting session API version (for this javascript).
186 * This is compared with the plugin API version to verify that they are
187 * compatible.
188 *
189 * @const
190 * @private
191 */
192remoting.ClientPluginImpl.prototype.API_VERSION_ = 6;
193
194/**
195 * The oldest API version that we support.
196 * This will differ from the |API_VERSION_| if we maintain backward
197 * compatibility with older API versions.
198 *
199 * @const
200 * @private
201 */
202remoting.ClientPluginImpl.prototype.API_MIN_VERSION_ = 5;
203
204/**
205 * @param {function(string):void} handler
206 */
207remoting.ClientPluginImpl.prototype.setOnOutgoingIqHandler = function(handler) {
208  this.onOutgoingIqHandler_ = handler;
209};
210
211/**
212 * @param {function(string):void} handler
213 */
214remoting.ClientPluginImpl.prototype.setOnDebugMessageHandler =
215    function(handler) {
216  this.onDebugMessageHandler_ = handler;
217};
218
219/**
220 * @param {function(number, number):void} handler
221 */
222remoting.ClientPluginImpl.prototype.setConnectionStatusUpdateHandler =
223    function(handler) {
224  this.onConnectionStatusUpdateHandler_ = handler;
225};
226
227/**
228 * @param {function(boolean):void} handler
229 */
230remoting.ClientPluginImpl.prototype.setConnectionReadyHandler =
231    function(handler) {
232  this.onConnectionReadyHandler_ = handler;
233};
234
235/**
236 * @param {function():void} handler
237 */
238remoting.ClientPluginImpl.prototype.setDesktopSizeUpdateHandler =
239    function(handler) {
240  this.onDesktopSizeUpdateHandler_ = handler;
241};
242
243/**
244 * @param {function(!Array.<string>):void} handler
245 */
246remoting.ClientPluginImpl.prototype.setCapabilitiesHandler = function(handler) {
247  this.onSetCapabilitiesHandler_ = handler;
248};
249
250/**
251 * @param {function(string):void} handler
252 */
253remoting.ClientPluginImpl.prototype.setGnubbyAuthHandler = function(handler) {
254  this.onGnubbyAuthHandler_ = handler;
255};
256
257/**
258 * @param {function(string):void} handler
259 */
260remoting.ClientPluginImpl.prototype.setCastExtensionHandler =
261    function(handler) {
262  this.onCastExtensionHandler_ = handler;
263};
264
265/**
266 * @param {function(string, number, number):void} handler
267 */
268remoting.ClientPluginImpl.prototype.setMouseCursorHandler = function(handler) {
269  this.updateMouseCursorImage_ = handler;
270};
271
272/**
273 * @param {function(string, string, string):void} handler
274 */
275remoting.ClientPluginImpl.prototype.setFetchThirdPartyTokenHandler =
276    function(handler) {
277  this.fetchThirdPartyTokenHandler_ = handler;
278};
279
280/**
281 * @param {function(boolean):void} handler
282 */
283remoting.ClientPluginImpl.prototype.setFetchPinHandler = function(handler) {
284  this.fetchPinHandler_ = handler;
285};
286
287/**
288 * @param {string|{method:string, data:Object.<string,*>}}
289 *    rawMessage Message from the plugin.
290 * @private
291 */
292remoting.ClientPluginImpl.prototype.handleMessage_ = function(rawMessage) {
293  var message =
294      /** @type {{method:string, data:Object.<string,*>}} */
295      ((typeof(rawMessage) == 'string') ? jsonParseSafe(rawMessage)
296                                        : rawMessage);
297  if (!message || !('method' in message) || !('data' in message)) {
298    console.error('Received invalid message from the plugin:', rawMessage);
299    return;
300  }
301
302  try {
303    this.handleMessageMethod_(message);
304  } catch(e) {
305    console.error(/** @type {*} */ (e));
306  }
307}
308
309/**
310 * @param {{method:string, data:Object.<string,*>}}
311 *    message Parsed message from the plugin.
312 * @private
313 */
314remoting.ClientPluginImpl.prototype.handleMessageMethod_ = function(message) {
315  /**
316   * Splits a string into a list of words delimited by spaces.
317   * @param {string} str String that should be split.
318   * @return {!Array.<string>} List of words.
319   */
320  var tokenize = function(str) {
321    /** @type {Array.<string>} */
322    var tokens = str.match(/\S+/g);
323    return tokens ? tokens : [];
324  };
325
326  if (message.method == 'hello') {
327    // Resize in case we had to enlarge it to support click-to-play.
328    this.hidePluginForClickToPlay_();
329    this.pluginApiVersion_ = getNumberAttr(message.data, 'apiVersion');
330    this.pluginApiMinVersion_ = getNumberAttr(message.data, 'apiMinVersion');
331
332    if (this.pluginApiVersion_ >= 7) {
333      this.pluginApiFeatures_ =
334          tokenize(getStringAttr(message.data, 'apiFeatures'));
335
336      // Negotiate capabilities.
337
338      /** @type {!Array.<string>} */
339      var requestedCapabilities = [];
340      if ('requestedCapabilities' in message.data) {
341        requestedCapabilities =
342            tokenize(getStringAttr(message.data, 'requestedCapabilities'));
343      }
344
345      /** @type {!Array.<string>} */
346      var supportedCapabilities = [];
347      if ('supportedCapabilities' in message.data) {
348        supportedCapabilities =
349            tokenize(getStringAttr(message.data, 'supportedCapabilities'));
350      }
351
352      // At the moment the webapp does not recognize any of
353      // 'requestedCapabilities' capabilities (so they all should be disabled)
354      // and do not care about any of 'supportedCapabilities' capabilities (so
355      // they all can be enabled).
356      this.capabilities_ = supportedCapabilities;
357
358      // Let the host know that the webapp can be requested to always send
359      // the client's dimensions.
360      this.capabilities_.push(
361          remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION);
362
363      // Let the host know that we're interested in knowing whether or not
364      // it rate-limits desktop-resize requests.
365      this.capabilities_.push(
366          remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS);
367
368      // Let the host know that we can use the video framerecording extension.
369      this.capabilities_.push(
370          remoting.ClientSession.Capability.VIDEO_RECORDER);
371
372      // Let the host know that we can support casting of the screen.
373      // TODO(aiguha): Add this capability based on a gyp/command-line flag,
374      // rather than by default.
375      this.capabilities_.push(
376          remoting.ClientSession.Capability.CAST);
377
378    } else if (this.pluginApiVersion_ >= 6) {
379      this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
380    } else {
381      this.pluginApiFeatures_ = ['highQualityScaling'];
382    }
383    this.helloReceived_ = true;
384    if (this.onInitializedCallback_ != null) {
385      this.onInitializedCallback_(true);
386      this.onInitializedCallback_ = null;
387    }
388
389  } else if (message.method == 'sendOutgoingIq') {
390    this.onOutgoingIqHandler_(getStringAttr(message.data, 'iq'));
391
392  } else if (message.method == 'logDebugMessage') {
393    this.onDebugMessageHandler_(getStringAttr(message.data, 'message'));
394
395  } else if (message.method == 'onConnectionStatus') {
396    var state = remoting.ClientSession.State.fromString(
397        getStringAttr(message.data, 'state'))
398    var error = remoting.ClientSession.ConnectionError.fromString(
399        getStringAttr(message.data, 'error'));
400    this.onConnectionStatusUpdateHandler_(state, error);
401
402  } else if (message.method == 'onDesktopSize') {
403    this.desktopWidth_ = getNumberAttr(message.data, 'width');
404    this.desktopHeight_ = getNumberAttr(message.data, 'height');
405    this.desktopXDpi_ = getNumberAttr(message.data, 'x_dpi', 96);
406    this.desktopYDpi_ = getNumberAttr(message.data, 'y_dpi', 96);
407    this.onDesktopSizeUpdateHandler_();
408
409  } else if (message.method == 'onPerfStats') {
410    // Return value is ignored. These calls will throw an error if the value
411    // is not a number.
412    getNumberAttr(message.data, 'videoBandwidth');
413    getNumberAttr(message.data, 'videoFrameRate');
414    getNumberAttr(message.data, 'captureLatency');
415    getNumberAttr(message.data, 'encodeLatency');
416    getNumberAttr(message.data, 'decodeLatency');
417    getNumberAttr(message.data, 'renderLatency');
418    getNumberAttr(message.data, 'roundtripLatency');
419    this.perfStats_ =
420        /** @type {remoting.ClientSession.PerfStats} */ message.data;
421
422  } else if (message.method == 'injectClipboardItem') {
423    var mimetype = getStringAttr(message.data, 'mimeType');
424    var item = getStringAttr(message.data, 'item');
425    if (remoting.clipboard) {
426      remoting.clipboard.fromHost(mimetype, item);
427    }
428
429  } else if (message.method == 'onFirstFrameReceived') {
430    if (remoting.clientSession) {
431      remoting.clientSession.onFirstFrameReceived();
432    }
433
434  } else if (message.method == 'onConnectionReady') {
435    var ready = getBooleanAttr(message.data, 'ready');
436    this.onConnectionReadyHandler_(ready);
437
438  } else if (message.method == 'fetchPin') {
439    // The pairingSupported value in the dictionary indicates whether both
440    // client and host support pairing. If the client doesn't support pairing,
441    // then the value won't be there at all, so give it a default of false.
442    var pairingSupported = getBooleanAttr(message.data, 'pairingSupported',
443                                          false)
444    this.fetchPinHandler_(pairingSupported);
445
446  } else if (message.method == 'setCapabilities') {
447    /** @type {!Array.<string>} */
448    var capabilities = tokenize(getStringAttr(message.data, 'capabilities'));
449    this.onSetCapabilitiesHandler_(capabilities);
450
451  } else if (message.method == 'fetchThirdPartyToken') {
452    var tokenUrl = getStringAttr(message.data, 'tokenUrl');
453    var hostPublicKey = getStringAttr(message.data, 'hostPublicKey');
454    var scope = getStringAttr(message.data, 'scope');
455    this.fetchThirdPartyTokenHandler_(tokenUrl, hostPublicKey, scope);
456
457  } else if (message.method == 'pairingResponse') {
458    var clientId = getStringAttr(message.data, 'clientId');
459    var sharedSecret = getStringAttr(message.data, 'sharedSecret');
460    this.onPairingComplete_(clientId, sharedSecret);
461
462  } else if (message.method == 'extensionMessage') {
463    var extMsgType = getStringAttr(message.data, 'type');
464    var extMsgData = getStringAttr(message.data, 'data');
465    switch (extMsgType) {
466      case 'gnubby-auth':
467        this.onGnubbyAuthHandler_(extMsgData);
468        break;
469      case 'test-echo-reply':
470        console.log('Got echo reply: ' + extMsgData);
471        break;
472      case 'cast_message':
473        this.onCastExtensionHandler_(extMsgData);
474        break;
475      default:
476        this.onExtensionMessage_(extMsgType, extMsgData);
477        break;
478    }
479
480  } else if (message.method == 'mediaSourceReset') {
481    if (!this.mediaSourceRenderer_) {
482      console.error('Unexpected mediaSourceReset.');
483      return;
484    }
485    this.mediaSourceRenderer_.reset(getStringAttr(message.data, 'format'))
486
487  } else if (message.method == 'mediaSourceData') {
488    if (!(message.data['buffer'] instanceof ArrayBuffer)) {
489      console.error('Invalid mediaSourceData message:', message.data);
490      return;
491    }
492    if (!this.mediaSourceRenderer_) {
493      console.error('Unexpected mediaSourceData.');
494      return;
495    }
496    // keyframe flag may be absent from the message.
497    var keyframe = !!message.data['keyframe'];
498    this.mediaSourceRenderer_.onIncomingData(
499        (/** @type {ArrayBuffer} */ message.data['buffer']), keyframe);
500
501  } else if (message.method == 'unsetCursorShape') {
502    this.updateMouseCursorImage_('', 0, 0);
503
504  } else if (message.method == 'setCursorShape') {
505    var width = getNumberAttr(message.data, 'width');
506    var height = getNumberAttr(message.data, 'height');
507    var hotspotX = getNumberAttr(message.data, 'hotspotX');
508    var hotspotY = getNumberAttr(message.data, 'hotspotY');
509    var srcArrayBuffer = getObjectAttr(message.data, 'data');
510
511    var canvas =
512        /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
513    canvas.width = width;
514    canvas.height = height;
515
516    var context =
517        /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
518    var imageData = context.getImageData(0, 0, width, height);
519    base.debug.assert(srcArrayBuffer instanceof ArrayBuffer);
520    var src = new Uint8Array(/** @type {ArrayBuffer} */(srcArrayBuffer));
521    var dest = imageData.data;
522    for (var i = 0; i < /** @type {number} */(dest.length); i += 4) {
523      dest[i] = src[i + 2];
524      dest[i + 1] = src[i + 1];
525      dest[i + 2] = src[i];
526      dest[i + 3] = src[i + 3];
527    }
528
529    context.putImageData(imageData, 0, 0);
530    this.updateMouseCursorImage_(canvas.toDataURL(), hotspotX, hotspotY);
531  }
532};
533
534/**
535 * Deletes the plugin.
536 */
537remoting.ClientPluginImpl.prototype.dispose = function() {
538  if (this.plugin_) {
539    this.plugin_.parentNode.removeChild(this.plugin_);
540    this.plugin_ = null;
541  }
542};
543
544/**
545 * @return {HTMLEmbedElement} HTML element that corresponds to the plugin.
546 */
547remoting.ClientPluginImpl.prototype.element = function() {
548  return this.plugin_;
549};
550
551/**
552 * @param {function(boolean): void} onDone
553 */
554remoting.ClientPluginImpl.prototype.initialize = function(onDone) {
555  if (this.helloReceived_) {
556    onDone(true);
557  } else {
558    this.onInitializedCallback_ = onDone;
559  }
560};
561
562/**
563 * @return {boolean} True if the plugin and web-app versions are compatible.
564 */
565remoting.ClientPluginImpl.prototype.isSupportedVersion = function() {
566  if (!this.helloReceived_) {
567    console.error(
568        "isSupportedVersion() is called before the plugin is initialized.");
569    return false;
570  }
571  return this.API_VERSION_ >= this.pluginApiMinVersion_ &&
572      this.pluginApiVersion_ >= this.API_MIN_VERSION_;
573};
574
575/**
576 * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
577 * @return {boolean} True if the plugin supports the named feature.
578 */
579remoting.ClientPluginImpl.prototype.hasFeature = function(feature) {
580  if (!this.helloReceived_) {
581    console.error(
582        "hasFeature() is called before the plugin is initialized.");
583    return false;
584  }
585  return this.pluginApiFeatures_.indexOf(feature) > -1;
586};
587
588/**
589 * @return {boolean} True if the plugin supports the injectKeyEvent API.
590 */
591remoting.ClientPluginImpl.prototype.isInjectKeyEventSupported = function() {
592  return this.pluginApiVersion_ >= 6;
593};
594
595/**
596 * @param {string} iq Incoming IQ stanza.
597 */
598remoting.ClientPluginImpl.prototype.onIncomingIq = function(iq) {
599  if (this.plugin_ && this.plugin_.postMessage) {
600    this.plugin_.postMessage(JSON.stringify(
601        { method: 'incomingIq', data: { iq: iq } }));
602  } else {
603    // plugin.onIq may not be set after the plugin has been shut
604    // down. Particularly this happens when we receive response to
605    // session-terminate stanza.
606    console.warn('plugin.onIq is not set so dropping incoming message.');
607  }
608};
609
610/**
611 * @param {string} hostJid The jid of the host to connect to.
612 * @param {string} hostPublicKey The base64 encoded version of the host's
613 *     public key.
614 * @param {string} localJid Local jid.
615 * @param {string} sharedSecret The access code for IT2Me or the PIN
616 *     for Me2Me.
617 * @param {string} authenticationMethods Comma-separated list of
618 *     authentication methods the client should attempt to use.
619 * @param {string} authenticationTag A host-specific tag to mix into
620 *     authentication hashes.
621 * @param {string} clientPairingId For paired Me2Me connections, the
622 *     pairing id for this client, as issued by the host.
623 * @param {string} clientPairedSecret For paired Me2Me connections, the
624 *     paired secret for this client, as issued by the host.
625 */
626remoting.ClientPluginImpl.prototype.connect = function(
627    hostJid, hostPublicKey, localJid, sharedSecret,
628    authenticationMethods, authenticationTag,
629    clientPairingId, clientPairedSecret) {
630  var keyFilter = '';
631  if (remoting.platformIsMac()) {
632    keyFilter = 'mac';
633  } else if (remoting.platformIsChromeOS()) {
634    keyFilter = 'cros';
635  }
636  this.plugin_.postMessage(JSON.stringify(
637      { method: 'delegateLargeCursors', data: {} }));
638  this.plugin_.postMessage(JSON.stringify(
639    { method: 'connect', data: {
640        hostJid: hostJid,
641        hostPublicKey: hostPublicKey,
642        localJid: localJid,
643        sharedSecret: sharedSecret,
644        authenticationMethods: authenticationMethods,
645        authenticationTag: authenticationTag,
646        capabilities: this.capabilities_.join(" "),
647        clientPairingId: clientPairingId,
648        clientPairedSecret: clientPairedSecret,
649        keyFilter: keyFilter
650      }
651    }));
652};
653
654/**
655 * Release all currently pressed keys.
656 */
657remoting.ClientPluginImpl.prototype.releaseAllKeys = function() {
658  this.plugin_.postMessage(JSON.stringify(
659      { method: 'releaseAllKeys', data: {} }));
660};
661
662/**
663 * Send a key event to the host.
664 *
665 * @param {number} usbKeycode The USB-style code of the key to inject.
666 * @param {boolean} pressed True to inject a key press, False for a release.
667 */
668remoting.ClientPluginImpl.prototype.injectKeyEvent =
669    function(usbKeycode, pressed) {
670  this.plugin_.postMessage(JSON.stringify(
671      { method: 'injectKeyEvent', data: {
672          'usbKeycode': usbKeycode,
673          'pressed': pressed}
674      }));
675};
676
677/**
678 * Remap one USB keycode to another in all subsequent key events.
679 *
680 * @param {number} fromKeycode The USB-style code of the key to remap.
681 * @param {number} toKeycode The USB-style code to remap the key to.
682 */
683remoting.ClientPluginImpl.prototype.remapKey =
684    function(fromKeycode, toKeycode) {
685  this.plugin_.postMessage(JSON.stringify(
686      { method: 'remapKey', data: {
687          'fromKeycode': fromKeycode,
688          'toKeycode': toKeycode}
689      }));
690};
691
692/**
693 * Enable/disable redirection of the specified key to the web-app.
694 *
695 * @param {number} keycode The USB-style code of the key.
696 * @param {Boolean} trap True to enable trapping, False to disable.
697 */
698remoting.ClientPluginImpl.prototype.trapKey = function(keycode, trap) {
699  this.plugin_.postMessage(JSON.stringify(
700      { method: 'trapKey', data: {
701          'keycode': keycode,
702          'trap': trap}
703      }));
704};
705
706/**
707 * Returns an associative array with a set of stats for this connecton.
708 *
709 * @return {remoting.ClientSession.PerfStats} The connection statistics.
710 */
711remoting.ClientPluginImpl.prototype.getPerfStats = function() {
712  return this.perfStats_;
713};
714
715/**
716 * Sends a clipboard item to the host.
717 *
718 * @param {string} mimeType The MIME type of the clipboard item.
719 * @param {string} item The clipboard item.
720 */
721remoting.ClientPluginImpl.prototype.sendClipboardItem =
722    function(mimeType, item) {
723  if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM))
724    return;
725  this.plugin_.postMessage(JSON.stringify(
726      { method: 'sendClipboardItem',
727        data: { mimeType: mimeType, item: item }}));
728};
729
730/**
731 * Notifies the host that the client has the specified size and pixel density.
732 *
733 * @param {number} width The available client width in DIPs.
734 * @param {number} height The available client height in DIPs.
735 * @param {number} device_scale The number of device pixels per DIP.
736 */
737remoting.ClientPluginImpl.prototype.notifyClientResolution =
738    function(width, height, device_scale) {
739  if (this.hasFeature(remoting.ClientPlugin.Feature.NOTIFY_CLIENT_RESOLUTION)) {
740    var dpi = Math.floor(device_scale * 96);
741    this.plugin_.postMessage(JSON.stringify(
742        { method: 'notifyClientResolution',
743          data: { width: Math.floor(width * device_scale),
744                  height: Math.floor(height * device_scale),
745                  x_dpi: dpi, y_dpi: dpi }}));
746  }
747};
748
749/**
750 * Requests that the host pause or resume sending video updates.
751 *
752 * @param {boolean} pause True to suspend video updates, false otherwise.
753 */
754remoting.ClientPluginImpl.prototype.pauseVideo =
755    function(pause) {
756  if (this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
757    this.plugin_.postMessage(JSON.stringify(
758        { method: 'videoControl', data: { pause: pause }}));
759  } else if (this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) {
760    this.plugin_.postMessage(JSON.stringify(
761        { method: 'pauseVideo', data: { pause: pause }}));
762  }
763};
764
765/**
766 * Requests that the host pause or resume sending audio updates.
767 *
768 * @param {boolean} pause True to suspend audio updates, false otherwise.
769 */
770remoting.ClientPluginImpl.prototype.pauseAudio =
771    function(pause) {
772  if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) {
773    return;
774  }
775  this.plugin_.postMessage(JSON.stringify(
776      { method: 'pauseAudio', data: { pause: pause }}));
777};
778
779/**
780 * Requests that the host configure the video codec for lossless encode.
781 *
782 * @param {boolean} wantLossless True to request lossless encoding.
783 */
784remoting.ClientPluginImpl.prototype.setLosslessEncode =
785    function(wantLossless) {
786  if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
787    return;
788  }
789  this.plugin_.postMessage(JSON.stringify(
790      { method: 'videoControl', data: { losslessEncode: wantLossless }}));
791};
792
793/**
794 * Requests that the host configure the video codec for lossless color.
795 *
796 * @param {boolean} wantLossless True to request lossless color.
797 */
798remoting.ClientPluginImpl.prototype.setLosslessColor =
799    function(wantLossless) {
800  if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
801    return;
802  }
803  this.plugin_.postMessage(JSON.stringify(
804      { method: 'videoControl', data: { losslessColor: wantLossless }}));
805};
806
807/**
808 * Called when a PIN is obtained from the user.
809 *
810 * @param {string} pin The PIN.
811 */
812remoting.ClientPluginImpl.prototype.onPinFetched =
813    function(pin) {
814  if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
815    return;
816  }
817  this.plugin_.postMessage(JSON.stringify(
818      { method: 'onPinFetched', data: { pin: pin }}));
819};
820
821/**
822 * Tells the plugin to ask for the PIN asynchronously.
823 */
824remoting.ClientPluginImpl.prototype.useAsyncPinDialog =
825    function() {
826  if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
827    return;
828  }
829  this.plugin_.postMessage(JSON.stringify(
830      { method: 'useAsyncPinDialog', data: {} }));
831};
832
833/**
834 * Sets the third party authentication token and shared secret.
835 *
836 * @param {string} token The token received from the token URL.
837 * @param {string} sharedSecret Shared secret received from the token URL.
838 */
839remoting.ClientPluginImpl.prototype.onThirdPartyTokenFetched = function(
840    token, sharedSecret) {
841  this.plugin_.postMessage(JSON.stringify(
842    { method: 'onThirdPartyTokenFetched',
843      data: { token: token, sharedSecret: sharedSecret}}));
844};
845
846/**
847 * Request pairing with the host for PIN-less authentication.
848 *
849 * @param {string} clientName The human-readable name of the client.
850 * @param {function(string, string):void} onDone, Callback to receive the
851 *     client id and shared secret when they are available.
852 */
853remoting.ClientPluginImpl.prototype.requestPairing =
854    function(clientName, onDone) {
855  if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) {
856    return;
857  }
858  this.onPairingComplete_ = onDone;
859  this.plugin_.postMessage(JSON.stringify(
860      { method: 'requestPairing', data: { clientName: clientName } }));
861};
862
863/**
864 * Send an extension message to the host.
865 *
866 * @param {string} type The message type.
867 * @param {string} message The message payload.
868 */
869remoting.ClientPluginImpl.prototype.sendClientMessage =
870    function(type, message) {
871  if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) {
872    return;
873  }
874  this.plugin_.postMessage(JSON.stringify(
875      { method: 'extensionMessage',
876        data: { type: type, data: message } }));
877
878};
879
880/**
881 * Request MediaStream-based rendering.
882 *
883 * @param {remoting.MediaSourceRenderer} mediaSourceRenderer
884 */
885remoting.ClientPluginImpl.prototype.enableMediaSourceRendering =
886    function(mediaSourceRenderer) {
887  if (!this.hasFeature(remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
888    return;
889  }
890  this.mediaSourceRenderer_ = mediaSourceRenderer;
891  this.plugin_.postMessage(JSON.stringify(
892      { method: 'enableMediaSourceRendering', data: {} }));
893};
894
895remoting.ClientPluginImpl.prototype.getDesktopWidth = function() {
896  return this.desktopWidth_;
897}
898
899remoting.ClientPluginImpl.prototype.getDesktopHeight = function() {
900  return this.desktopHeight_;
901}
902
903remoting.ClientPluginImpl.prototype.getDesktopXDpi = function() {
904  return this.desktopXDpi_;
905}
906
907remoting.ClientPluginImpl.prototype.getDesktopYDpi = function() {
908  return this.desktopYDpi_;
909}
910
911/**
912 * If we haven't yet received a "hello" message from the plugin, change its
913 * size so that the user can confirm it if click-to-play is enabled, or can
914 * see the "this plugin is disabled" message if it is actually disabled.
915 * @private
916 */
917remoting.ClientPluginImpl.prototype.showPluginForClickToPlay_ = function() {
918  if (!this.helloReceived_) {
919    var width = 200;
920    var height = 200;
921    this.plugin_.style.width = width + 'px';
922    this.plugin_.style.height = height + 'px';
923    // Center the plugin just underneath the "Connnecting..." dialog.
924    var dialog = document.getElementById('client-dialog');
925    var dialogRect = dialog.getBoundingClientRect();
926    this.plugin_.style.top = (dialogRect.bottom + 16) + 'px';
927    this.plugin_.style.left = (window.innerWidth - width) / 2 + 'px';
928    this.plugin_.style.position = 'fixed';
929  }
930};
931
932/**
933 * Undo the CSS rules needed to make the plugin clickable for click-to-play.
934 * @private
935 */
936remoting.ClientPluginImpl.prototype.hidePluginForClickToPlay_ = function() {
937  this.plugin_.style.width = '';
938  this.plugin_.style.height = '';
939  this.plugin_.style.top = '';
940  this.plugin_.style.left = '';
941  this.plugin_.style.position = '';
942};
943
944
945/**
946 * @constructor
947 * @implements {remoting.ClientPluginFactory}
948 */
949remoting.DefaultClientPluginFactory = function() {};
950
951/**
952 * @param {Element} container
953 * @param {function(string, string):boolean} onExtensionMessage
954 * @return {remoting.ClientPlugin}
955 */
956remoting.DefaultClientPluginFactory.prototype.createPlugin =
957    function(container, onExtensionMessage) {
958  return new remoting.ClientPluginImpl(container, onExtensionMessage);
959};
960
961remoting.DefaultClientPluginFactory.prototype.preloadPlugin = function() {
962  if (remoting.settings.CLIENT_PLUGIN_TYPE != 'pnacl') {
963    return;
964  }
965
966  var plugin = remoting.ClientPluginImpl.createPluginElement_();
967  plugin.addEventListener(
968      'loadend', function() { document.body.removeChild(plugin); }, false);
969  document.body.appendChild(plugin);
970};
971