Upstream version 11.39.266.0
[platform/framework/web/crosswalk.git] / src / remoting / webapp / client_plugin_impl.js
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 {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  */
27 remoting.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  */
165 remoting.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  */
192 remoting.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  */
202 remoting.ClientPluginImpl.prototype.API_MIN_VERSION_ = 5;
203
204 /**
205  * @param {function(string):void} handler
206  */
207 remoting.ClientPluginImpl.prototype.setOnOutgoingIqHandler = function(handler) {
208   this.onOutgoingIqHandler_ = handler;
209 };
210
211 /**
212  * @param {function(string):void} handler
213  */
214 remoting.ClientPluginImpl.prototype.setOnDebugMessageHandler =
215     function(handler) {
216   this.onDebugMessageHandler_ = handler;
217 };
218
219 /**
220  * @param {function(number, number):void} handler
221  */
222 remoting.ClientPluginImpl.prototype.setConnectionStatusUpdateHandler =
223     function(handler) {
224   this.onConnectionStatusUpdateHandler_ = handler;
225 };
226
227 /**
228  * @param {function(boolean):void} handler
229  */
230 remoting.ClientPluginImpl.prototype.setConnectionReadyHandler =
231     function(handler) {
232   this.onConnectionReadyHandler_ = handler;
233 };
234
235 /**
236  * @param {function():void} handler
237  */
238 remoting.ClientPluginImpl.prototype.setDesktopSizeUpdateHandler =
239     function(handler) {
240   this.onDesktopSizeUpdateHandler_ = handler;
241 };
242
243 /**
244  * @param {function(!Array.<string>):void} handler
245  */
246 remoting.ClientPluginImpl.prototype.setCapabilitiesHandler = function(handler) {
247   this.onSetCapabilitiesHandler_ = handler;
248 };
249
250 /**
251  * @param {function(string):void} handler
252  */
253 remoting.ClientPluginImpl.prototype.setGnubbyAuthHandler = function(handler) {
254   this.onGnubbyAuthHandler_ = handler;
255 };
256
257 /**
258  * @param {function(string):void} handler
259  */
260 remoting.ClientPluginImpl.prototype.setCastExtensionHandler =
261     function(handler) {
262   this.onCastExtensionHandler_ = handler;
263 };
264
265 /**
266  * @param {function(string, number, number):void} handler
267  */
268 remoting.ClientPluginImpl.prototype.setMouseCursorHandler = function(handler) {
269   this.updateMouseCursorImage_ = handler;
270 };
271
272 /**
273  * @param {function(string, string, string):void} handler
274  */
275 remoting.ClientPluginImpl.prototype.setFetchThirdPartyTokenHandler =
276     function(handler) {
277   this.fetchThirdPartyTokenHandler_ = handler;
278 };
279
280 /**
281  * @param {function(boolean):void} handler
282  */
283 remoting.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  */
292 remoting.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  */
314 remoting.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  */
537 remoting.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  */
547 remoting.ClientPluginImpl.prototype.element = function() {
548   return this.plugin_;
549 };
550
551 /**
552  * @param {function(boolean): void} onDone
553  */
554 remoting.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  */
565 remoting.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  */
579 remoting.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  */
591 remoting.ClientPluginImpl.prototype.isInjectKeyEventSupported = function() {
592   return this.pluginApiVersion_ >= 6;
593 };
594
595 /**
596  * @param {string} iq Incoming IQ stanza.
597  */
598 remoting.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  */
626 remoting.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  */
657 remoting.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  */
668 remoting.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  */
683 remoting.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  */
698 remoting.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  */
711 remoting.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  */
721 remoting.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  */
737 remoting.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  */
754 remoting.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  */
770 remoting.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  */
784 remoting.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  */
798 remoting.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  */
812 remoting.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  */
824 remoting.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  */
839 remoting.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  */
853 remoting.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  */
869 remoting.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  */
885 remoting.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
895 remoting.ClientPluginImpl.prototype.getDesktopWidth = function() {
896   return this.desktopWidth_;
897 }
898
899 remoting.ClientPluginImpl.prototype.getDesktopHeight = function() {
900   return this.desktopHeight_;
901 }
902
903 remoting.ClientPluginImpl.prototype.getDesktopXDpi = function() {
904   return this.desktopXDpi_;
905 }
906
907 remoting.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  */
917 remoting.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  */
936 remoting.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  */
949 remoting.DefaultClientPluginFactory = function() {};
950
951 /**
952  * @param {Element} container
953  * @param {function(string, string):boolean} onExtensionMessage
954  * @return {remoting.ClientPlugin}
955  */
956 remoting.DefaultClientPluginFactory.prototype.createPlugin =
957     function(container, onExtensionMessage) {
958   return new remoting.ClientPluginImpl(container, onExtensionMessage);
959 };
960
961 remoting.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 };