Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / crd / js / client_plugin_impl.js
blob2d3efe07343b627ab93abf176d487a31aa835e72
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.
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  */
14 'use strict';
16 /** @suppress {duplicate} */
17 var remoting = remoting || {};
19 /** @constructor */
20 remoting.ClientPluginMessage = function() {
21   /** @type {string} */
22   this.method = '';
24   /** @type {Object<string,*>} */
25   this.data = {};
28 /**
29  * @param {Element} container The container for the embed element.
30  * @param {function(string, string):boolean} onExtensionMessage The handler for
31  *     protocol extension messages. Returns true if a message is recognized;
32  *     false otherwise.
33  * @param {Array<string>} requiredCapabilities The set of capabilties that the
34  *     session must support for this application.
35  * @constructor
36  * @implements {remoting.ClientPlugin}
37  */
38 remoting.ClientPluginImpl = function(container, onExtensionMessage,
39                                      requiredCapabilities) {
40   this.plugin_ = remoting.ClientPluginImpl.createPluginElement_();
41   this.plugin_.id = 'session-client-plugin';
42   container.appendChild(this.plugin_);
44   this.onExtensionMessage_ = onExtensionMessage;
45   /**
46    * @type {Array<string>}
47    * @private
48    */
49   this.requiredCapabilities_ = requiredCapabilities;
51   /**
52    * @param {string} iq The Iq stanza received from the host.
53    * @private
54    */
55   this.onOutgoingIqHandler_ = function (iq) {};
56   /**
57    * @param {string} message Log message.
58    * @private
59    */
60   this.onDebugMessageHandler_ = function (message) {};
61   /**
62    * @param {number} state The connection state.
63    * @param {number} error The error code, if any.
64    * @private
65    */
66   this.onConnectionStatusUpdateHandler_ = function(state, error) {};
68   /**
69    * @param {string} channel The channel name.
70    * @param {string} connectionType The connection type.
71    * @private
72    */
73   this.onRouteChangedHandler_ = function(channel, connectionType) {};
75   /**
76    * @param {boolean} ready Connection ready state.
77    * @private
78    */
79   this.onConnectionReadyHandler_ = function(ready) {};
81   /**
82    * @param {string} tokenUrl Token-request URL, received from the host.
83    * @param {string} hostPublicKey Public key for the host.
84    * @param {string} scope OAuth scope to request the token for.
85    * @private
86    */
87   this.fetchThirdPartyTokenHandler_ = function(
88     tokenUrl, hostPublicKey, scope) {};
89   /**
90    * @param {!Array<string>} capabilities The negotiated capabilities.
91    * @private
92    */
93   this.onSetCapabilitiesHandler_ = function (capabilities) {};
94   /** @private */
95   this.fetchPinHandler_ = function (supportsPairing) {};
96   /**
97    * @param {string} data Remote gnubbyd data.
98    * @private
99    */
100   this.onGnubbyAuthHandler_ = function(data) {};
101   /**
102    * @param {string} url
103    * @param {number} hotspotX
104    * @param {number} hotspotY
105    * @private
106    */
107   this.updateMouseCursorImage_ = function(url, hotspotX, hotspotY) {};
108   /**
109    * @param {string} data Remote cast extension message.
110    * @private
111    */
112   this.onCastExtensionHandler_ = function(data) {};
113   /** @private {?function({rects:Array<Array<number>>}):void} */
114   this.debugRegionHandler_ = null;
116   /**
117    * @type {number}
118    * @private
119    */
120   this.pluginApiVersion_ = -1;
121   /**
122    * @type {Array<string>}
123    * @private
124    */
125   this.pluginApiFeatures_ = [];
126   /**
127    * @type {number}
128    * @private
129    */
130   this.pluginApiMinVersion_ = -1;
131   /**
132    * @type {!Array<string>}
133    * @private
134    */
135   this.capabilities_ = [];
136   /**
137    * @type {boolean}
138    * @private
139    */
140   this.helloReceived_ = false;
141   /**
142    * @type {function(boolean)|null}
143    * @private
144    */
145   this.onInitializedCallback_ = null;
146   /**
147    * @type {function(string, string):void}
148    * @private
149    */
150   this.onPairingComplete_ = function(clientId, sharedSecret) {};
151   /**
152    * @type {remoting.ClientSession.PerfStats}
153    * @private
154    */
155   this.perfStats_ = new remoting.ClientSession.PerfStats();
157   /** @type {remoting.ClientPluginImpl} */
158   var that = this;
159   /** @param {Event} event Message event from the plugin. */
160   this.plugin_.addEventListener('message', function(event) {
161       that.handleMessage_(
162           /** @type {remoting.ClientPluginMessage} */ (event.data));
163     }, false);
165   if (remoting.settings.CLIENT_PLUGIN_TYPE == 'native') {
166     window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
167   }
169   this.hostDesktop_ = new remoting.ClientPlugin.HostDesktopImpl(
170       this, this.postMessage_.bind(this));
174  * Creates plugin element without adding it to a container.
176  * @return {HTMLEmbedElement} Plugin element
177  */
178 remoting.ClientPluginImpl.createPluginElement_ = function() {
179   var plugin =
180       /** @type {HTMLEmbedElement} */ (document.createElement('embed'));
181   if (remoting.settings.CLIENT_PLUGIN_TYPE == 'pnacl') {
182     plugin.src = 'remoting_client_pnacl.nmf';
183     plugin.type = 'application/x-pnacl';
184   } else if (remoting.settings.CLIENT_PLUGIN_TYPE == 'nacl') {
185     plugin.src = 'remoting_client_nacl.nmf';
186     plugin.type = 'application/x-nacl';
187   } else {
188     plugin.src = 'about://none';
189     plugin.type = 'application/vnd.chromium.remoting-viewer';
190   }
191   plugin.width = '0';
192   plugin.height = '0';
193   plugin.tabIndex = 0;  // Required, otherwise focus() doesn't work.
194   return plugin;
198  * Chromoting session API version (for this javascript).
199  * This is compared with the plugin API version to verify that they are
200  * compatible.
202  * @const
203  * @private
204  */
205 remoting.ClientPluginImpl.prototype.API_VERSION_ = 6;
208  * The oldest API version that we support.
209  * This will differ from the |API_VERSION_| if we maintain backward
210  * compatibility with older API versions.
212  * @const
213  * @private
214  */
215 remoting.ClientPluginImpl.prototype.API_MIN_VERSION_ = 5;
218  * @param {function(string):void} handler
219  */
220 remoting.ClientPluginImpl.prototype.setOnOutgoingIqHandler = function(handler) {
221   this.onOutgoingIqHandler_ = handler;
225  * @param {function(string):void} handler
226  */
227 remoting.ClientPluginImpl.prototype.setOnDebugMessageHandler =
228     function(handler) {
229   this.onDebugMessageHandler_ = handler;
233  * @param {function(number, number):void} handler
234  */
235 remoting.ClientPluginImpl.prototype.setConnectionStatusUpdateHandler =
236     function(handler) {
237   this.onConnectionStatusUpdateHandler_ = handler;
241  * @param {function(string, string):void} handler
242  */
243 remoting.ClientPluginImpl.prototype.setRouteChangedHandler = function(handler) {
244   this.onRouteChangedHandler_ =  handler;
248  * @param {function(boolean):void} handler
249  */
250 remoting.ClientPluginImpl.prototype.setConnectionReadyHandler =
251     function(handler) {
252   this.onConnectionReadyHandler_ = handler;
256  * @param {function(!Array<string>):void} handler
257  */
258 remoting.ClientPluginImpl.prototype.setCapabilitiesHandler = function(handler) {
259   this.onSetCapabilitiesHandler_ = handler;
263  * @param {function(string):void} handler
264  */
265 remoting.ClientPluginImpl.prototype.setGnubbyAuthHandler = function(handler) {
266   this.onGnubbyAuthHandler_ = handler;
270  * @param {function(string):void} handler
271  */
272 remoting.ClientPluginImpl.prototype.setCastExtensionHandler =
273     function(handler) {
274   this.onCastExtensionHandler_ = handler;
278  * @param {function(string, number, number):void} handler
279  */
280 remoting.ClientPluginImpl.prototype.setMouseCursorHandler = function(handler) {
281   this.updateMouseCursorImage_ = handler;
285  * @param {function(string, string, string):void} handler
286  */
287 remoting.ClientPluginImpl.prototype.setFetchThirdPartyTokenHandler =
288     function(handler) {
289   this.fetchThirdPartyTokenHandler_ = handler;
293  * @param {function(boolean):void} handler
294  */
295 remoting.ClientPluginImpl.prototype.setFetchPinHandler = function(handler) {
296   this.fetchPinHandler_ = handler;
300  * @param {?function({rects:Array<Array<number>>}):void} handler
301  */
302 remoting.ClientPluginImpl.prototype.setDebugDirtyRegionHandler =
303     function(handler) {
304   this.debugRegionHandler_ = handler;
305   this.plugin_.postMessage(JSON.stringify(
306       { method: 'enableDebugRegion', data: { enable: handler != null } }));
310  * @param {string|remoting.ClientPluginMessage}
311  *    rawMessage Message from the plugin.
312  * @private
313  */
314 remoting.ClientPluginImpl.prototype.handleMessage_ = function(rawMessage) {
315   var message =
316       /** @type {remoting.ClientPluginMessage} */
317       ((typeof(rawMessage) == 'string') ? base.jsonParseSafe(rawMessage)
318                                         : rawMessage);
319   if (!message || !('method' in message) || !('data' in message)) {
320     console.error('Received invalid message from the plugin:', rawMessage);
321     return;
322   }
324   try {
325     this.handleMessageMethod_(message);
326   } catch(/** @type {*} */ e) {
327     console.error(e);
328   }
332  * @param {remoting.ClientPluginMessage}
333  *    message Parsed message from the plugin.
334  * @private
335  */
336 remoting.ClientPluginImpl.prototype.handleMessageMethod_ = function(message) {
337   /**
338    * Splits a string into a list of words delimited by spaces.
339    * @param {string} str String that should be split.
340    * @return {!Array<string>} List of words.
341    */
342   var tokenize = function(str) {
343     /** @type {Array<string>} */
344     var tokens = str.match(/\S+/g);
345     return tokens ? tokens : [];
346   };
348   if (message.method == 'hello') {
349     // Resize in case we had to enlarge it to support click-to-play.
350     this.hidePluginForClickToPlay_();
351     this.pluginApiVersion_ = getNumberAttr(message.data, 'apiVersion');
352     this.pluginApiMinVersion_ = getNumberAttr(message.data, 'apiMinVersion');
354     if (this.pluginApiVersion_ >= 7) {
355       this.pluginApiFeatures_ =
356           tokenize(getStringAttr(message.data, 'apiFeatures'));
358       // Negotiate capabilities.
359       /** @type {!Array<string>} */
360       var supportedCapabilities = [];
361       if ('supportedCapabilities' in message.data) {
362         supportedCapabilities =
363             tokenize(getStringAttr(message.data, 'supportedCapabilities'));
364       }
365       // At the moment the webapp does not recognize any of
366       // 'requestedCapabilities' capabilities (so they all should be disabled)
367       // and do not care about any of 'supportedCapabilities' capabilities (so
368       // they all can be enabled).
369       // All the required capabilities (specified by the app) are added to this.
370       this.capabilities_ = supportedCapabilities.concat(
371           this.requiredCapabilities_);
372     } else if (this.pluginApiVersion_ >= 6) {
373       this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
374     } else {
375       this.pluginApiFeatures_ = ['highQualityScaling'];
376     }
377     this.helloReceived_ = true;
378     if (this.onInitializedCallback_ != null) {
379       this.onInitializedCallback_(true);
380       this.onInitializedCallback_ = null;
381     }
383   } else if (message.method == 'sendOutgoingIq') {
384     this.onOutgoingIqHandler_(getStringAttr(message.data, 'iq'));
386   } else if (message.method == 'logDebugMessage') {
387     this.onDebugMessageHandler_(getStringAttr(message.data, 'message'));
389   } else if (message.method == 'onConnectionStatus') {
390     var state = remoting.ClientSession.State.fromString(
391         getStringAttr(message.data, 'state'));
392     var error = remoting.ClientSession.ConnectionError.fromString(
393         getStringAttr(message.data, 'error'));
394     this.onConnectionStatusUpdateHandler_(state, error);
396   } else if (message.method == 'onRouteChanged') {
397     var channel = getStringAttr(message.data, 'channel');
398     var connectionType = getStringAttr(message.data, 'connectionType');
399     this.onRouteChangedHandler_(channel, connectionType);
401   } else if (message.method == 'onDesktopSize') {
402     this.hostDesktop_.onSizeUpdated(message);
403   } else if (message.method == 'onDesktopShape') {
404     this.hostDesktop_.onShapeUpdated(message);
405   } else if (message.method == 'onPerfStats') {
406     // Return value is ignored. These calls will throw an error if the value
407     // is not a number.
408     getNumberAttr(message.data, 'videoBandwidth');
409     getNumberAttr(message.data, 'videoFrameRate');
410     getNumberAttr(message.data, 'captureLatency');
411     getNumberAttr(message.data, 'encodeLatency');
412     getNumberAttr(message.data, 'decodeLatency');
413     getNumberAttr(message.data, 'renderLatency');
414     getNumberAttr(message.data, 'roundtripLatency');
415     this.perfStats_ =
416         /** @type {remoting.ClientSession.PerfStats} */ (message.data);
418   } else if (message.method == 'injectClipboardItem') {
419     var mimetype = getStringAttr(message.data, 'mimeType');
420     var item = getStringAttr(message.data, 'item');
421     if (remoting.clipboard) {
422       remoting.clipboard.fromHost(mimetype, item);
423     }
425   } else if (message.method == 'onFirstFrameReceived') {
426     if (remoting.clientSession) {
427       remoting.clientSession.onFirstFrameReceived();
428     }
430   } else if (message.method == 'onConnectionReady') {
431     var ready = getBooleanAttr(message.data, 'ready');
432     this.onConnectionReadyHandler_(ready);
434   } else if (message.method == 'fetchPin') {
435     // The pairingSupported value in the dictionary indicates whether both
436     // client and host support pairing. If the client doesn't support pairing,
437     // then the value won't be there at all, so give it a default of false.
438     var pairingSupported = getBooleanAttr(message.data, 'pairingSupported',
439                                           false)
440     this.fetchPinHandler_(pairingSupported);
442   } else if (message.method == 'setCapabilities') {
443     /** @type {!Array<string>} */
444     var capabilities = tokenize(getStringAttr(message.data, 'capabilities'));
445     this.onSetCapabilitiesHandler_(capabilities);
447   } else if (message.method == 'fetchThirdPartyToken') {
448     var tokenUrl = getStringAttr(message.data, 'tokenUrl');
449     var hostPublicKey = getStringAttr(message.data, 'hostPublicKey');
450     var scope = getStringAttr(message.data, 'scope');
451     this.fetchThirdPartyTokenHandler_(tokenUrl, hostPublicKey, scope);
453   } else if (message.method == 'pairingResponse') {
454     var clientId = getStringAttr(message.data, 'clientId');
455     var sharedSecret = getStringAttr(message.data, 'sharedSecret');
456     this.onPairingComplete_(clientId, sharedSecret);
458   } else if (message.method == 'extensionMessage') {
459     var extMsgType = getStringAttr(message.data, 'type');
460     var extMsgData = getStringAttr(message.data, 'data');
461     switch (extMsgType) {
462       case 'gnubby-auth':
463         this.onGnubbyAuthHandler_(extMsgData);
464         break;
465       case 'test-echo-reply':
466         console.log('Got echo reply: ' + extMsgData);
467         break;
468       case 'cast_message':
469         this.onCastExtensionHandler_(extMsgData);
470         break;
471       default:
472         this.onExtensionMessage_(extMsgType, extMsgData);
473         break;
474     }
476   } else if (message.method == 'unsetCursorShape') {
477     this.updateMouseCursorImage_('', 0, 0);
479   } else if (message.method == 'setCursorShape') {
480     var width = getNumberAttr(message.data, 'width');
481     var height = getNumberAttr(message.data, 'height');
482     var hotspotX = getNumberAttr(message.data, 'hotspotX');
483     var hotspotY = getNumberAttr(message.data, 'hotspotY');
484     var srcArrayBuffer = getObjectAttr(message.data, 'data');
486     var canvas =
487         /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
488     canvas.width = width;
489     canvas.height = height;
491     var context =
492         /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
493     var imageData = context.getImageData(0, 0, width, height);
494     base.debug.assert(srcArrayBuffer instanceof ArrayBuffer);
495     var src = new Uint8Array(/** @type {ArrayBuffer} */(srcArrayBuffer));
496     var dest = imageData.data;
497     for (var i = 0; i < /** @type {number} */(dest.length); i += 4) {
498       dest[i] = src[i + 2];
499       dest[i + 1] = src[i + 1];
500       dest[i + 2] = src[i];
501       dest[i + 3] = src[i + 3];
502     }
504     context.putImageData(imageData, 0, 0);
505     this.updateMouseCursorImage_(canvas.toDataURL(), hotspotX, hotspotY);
507   } else if (message.method == 'onDebugRegion') {
508     if (this.debugRegionHandler_) {
509       this.debugRegionHandler_(
510           /** @type {{rects: Array<(Array<number>)>}} **/(message.data));
511     }
513   }
517  * Deletes the plugin.
518  */
519 remoting.ClientPluginImpl.prototype.dispose = function() {
520   if (this.plugin_) {
521     this.plugin_.parentNode.removeChild(this.plugin_);
522     this.plugin_ = null;
523   }
527  * @return {HTMLEmbedElement} HTML element that corresponds to the plugin.
528  */
529 remoting.ClientPluginImpl.prototype.element = function() {
530   return this.plugin_;
534  * @param {function(boolean): void} onDone
535  */
536 remoting.ClientPluginImpl.prototype.initialize = function(onDone) {
537   if (this.helloReceived_) {
538     onDone(true);
539   } else {
540     this.onInitializedCallback_ = onDone;
541   }
545  * @return {boolean} True if the plugin and web-app versions are compatible.
546  */
547 remoting.ClientPluginImpl.prototype.isSupportedVersion = function() {
548   if (!this.helloReceived_) {
549     console.error(
550         "isSupportedVersion() is called before the plugin is initialized.");
551     return false;
552   }
553   return this.API_VERSION_ >= this.pluginApiMinVersion_ &&
554       this.pluginApiVersion_ >= this.API_MIN_VERSION_;
558  * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
559  * @return {boolean} True if the plugin supports the named feature.
560  */
561 remoting.ClientPluginImpl.prototype.hasFeature = function(feature) {
562   if (!this.helloReceived_) {
563     console.error(
564         "hasFeature() is called before the plugin is initialized.");
565     return false;
566   }
567   return this.pluginApiFeatures_.indexOf(feature) > -1;
571  * @return {boolean} True if the plugin supports the injectKeyEvent API.
572  */
573 remoting.ClientPluginImpl.prototype.isInjectKeyEventSupported = function() {
574   return this.pluginApiVersion_ >= 6;
578  * @param {string} iq Incoming IQ stanza.
579  */
580 remoting.ClientPluginImpl.prototype.onIncomingIq = function(iq) {
581   if (this.plugin_ && this.plugin_.postMessage) {
582     this.plugin_.postMessage(JSON.stringify(
583         { method: 'incomingIq', data: { iq: iq } }));
584   } else {
585     // plugin.onIq may not be set after the plugin has been shut
586     // down. Particularly this happens when we receive response to
587     // session-terminate stanza.
588     console.warn('plugin.onIq is not set so dropping incoming message.');
589   }
593  * @param {string} hostJid The jid of the host to connect to.
594  * @param {string} hostPublicKey The base64 encoded version of the host's
595  *     public key.
596  * @param {string} localJid Local jid.
597  * @param {string} sharedSecret The access code for IT2Me or the PIN
598  *     for Me2Me.
599  * @param {string} authenticationMethods Comma-separated list of
600  *     authentication methods the client should attempt to use.
601  * @param {string} authenticationTag A host-specific tag to mix into
602  *     authentication hashes.
603  * @param {string} clientPairingId For paired Me2Me connections, the
604  *     pairing id for this client, as issued by the host.
605  * @param {string} clientPairedSecret For paired Me2Me connections, the
606  *     paired secret for this client, as issued by the host.
607  */
608 remoting.ClientPluginImpl.prototype.connect = function(
609     hostJid, hostPublicKey, localJid, sharedSecret,
610     authenticationMethods, authenticationTag,
611     clientPairingId, clientPairedSecret) {
612   var keyFilter = '';
613   if (remoting.platformIsMac()) {
614     keyFilter = 'mac';
615   } else if (remoting.platformIsChromeOS()) {
616     keyFilter = 'cros';
617   }
618   // Use PPB_VideoDecoder API only in Chrome 42 and above. It is broken in
619   // previous versions of Chrome, see http://crbug.com/447403 .
620   // Currently PPAPI doesn't provide a way for plugins to check the Chrome
621   // version, so this check needs to be in the webapp.
622   var enableVideoDecodeRenderer =
623       parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10) >= 42;
624   this.plugin_.postMessage(JSON.stringify(
625       { method: 'delegateLargeCursors', data: {} }));
626   this.plugin_.postMessage(JSON.stringify(
627     { method: 'connect', data: {
628         hostJid: hostJid,
629         hostPublicKey: hostPublicKey,
630         localJid: localJid,
631         sharedSecret: sharedSecret,
632         authenticationMethods: authenticationMethods,
633         authenticationTag: authenticationTag,
634         capabilities: this.capabilities_.join(" "),
635         clientPairingId: clientPairingId,
636         clientPairedSecret: clientPairedSecret,
637         keyFilter: keyFilter,
638         enableVideoDecodeRenderer: enableVideoDecodeRenderer
639       }
640     }));
644  * Release all currently pressed keys.
645  */
646 remoting.ClientPluginImpl.prototype.releaseAllKeys = function() {
647   this.plugin_.postMessage(JSON.stringify(
648       { method: 'releaseAllKeys', data: {} }));
652  * Send a key event to the host.
654  * @param {number} usbKeycode The USB-style code of the key to inject.
655  * @param {boolean} pressed True to inject a key press, False for a release.
656  */
657 remoting.ClientPluginImpl.prototype.injectKeyEvent =
658     function(usbKeycode, pressed) {
659   this.plugin_.postMessage(JSON.stringify(
660       { method: 'injectKeyEvent', data: {
661           'usbKeycode': usbKeycode,
662           'pressed': pressed}
663       }));
667  * Remap one USB keycode to another in all subsequent key events.
669  * @param {number} fromKeycode The USB-style code of the key to remap.
670  * @param {number} toKeycode The USB-style code to remap the key to.
671  */
672 remoting.ClientPluginImpl.prototype.remapKey =
673     function(fromKeycode, toKeycode) {
674   this.plugin_.postMessage(JSON.stringify(
675       { method: 'remapKey', data: {
676           'fromKeycode': fromKeycode,
677           'toKeycode': toKeycode}
678       }));
682  * Enable/disable redirection of the specified key to the web-app.
684  * @param {number} keycode The USB-style code of the key.
685  * @param {Boolean} trap True to enable trapping, False to disable.
686  */
687 remoting.ClientPluginImpl.prototype.trapKey = function(keycode, trap) {
688   this.plugin_.postMessage(JSON.stringify(
689       { method: 'trapKey', data: {
690           'keycode': keycode,
691           'trap': trap}
692       }));
696  * Returns an associative array with a set of stats for this connecton.
698  * @return {remoting.ClientSession.PerfStats} The connection statistics.
699  */
700 remoting.ClientPluginImpl.prototype.getPerfStats = function() {
701   return this.perfStats_;
705  * Sends a clipboard item to the host.
707  * @param {string} mimeType The MIME type of the clipboard item.
708  * @param {string} item The clipboard item.
709  */
710 remoting.ClientPluginImpl.prototype.sendClipboardItem =
711     function(mimeType, item) {
712   if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM))
713     return;
714   this.plugin_.postMessage(JSON.stringify(
715       { method: 'sendClipboardItem',
716         data: { mimeType: mimeType, item: item }}));
720  * Notifies the host that the client has the specified size and pixel density.
722  * @param {number} width The available client width in DIPs.
723  * @param {number} height The available client height in DIPs.
724  * @param {number} device_scale The number of device pixels per DIP.
725  */
726 remoting.ClientPluginImpl.prototype.notifyClientResolution =
727     function(width, height, device_scale) {
728   this.hostDesktop_.resize(width, height, device_scale);
732  * Requests that the host pause or resume sending video updates.
734  * @param {boolean} pause True to suspend video updates, false otherwise.
735  */
736 remoting.ClientPluginImpl.prototype.pauseVideo =
737     function(pause) {
738   if (this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
739     this.plugin_.postMessage(JSON.stringify(
740         { method: 'videoControl', data: { pause: pause }}));
741   } else if (this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) {
742     this.plugin_.postMessage(JSON.stringify(
743         { method: 'pauseVideo', data: { pause: pause }}));
744   }
748  * Requests that the host pause or resume sending audio updates.
750  * @param {boolean} pause True to suspend audio updates, false otherwise.
751  */
752 remoting.ClientPluginImpl.prototype.pauseAudio =
753     function(pause) {
754   if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) {
755     return;
756   }
757   this.plugin_.postMessage(JSON.stringify(
758       { method: 'pauseAudio', data: { pause: pause }}));
762  * Requests that the host configure the video codec for lossless encode.
764  * @param {boolean} wantLossless True to request lossless encoding.
765  */
766 remoting.ClientPluginImpl.prototype.setLosslessEncode =
767     function(wantLossless) {
768   if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
769     return;
770   }
771   this.plugin_.postMessage(JSON.stringify(
772       { method: 'videoControl', data: { losslessEncode: wantLossless }}));
776  * Requests that the host configure the video codec for lossless color.
778  * @param {boolean} wantLossless True to request lossless color.
779  */
780 remoting.ClientPluginImpl.prototype.setLosslessColor =
781     function(wantLossless) {
782   if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
783     return;
784   }
785   this.plugin_.postMessage(JSON.stringify(
786       { method: 'videoControl', data: { losslessColor: wantLossless }}));
790  * Called when a PIN is obtained from the user.
792  * @param {string} pin The PIN.
793  */
794 remoting.ClientPluginImpl.prototype.onPinFetched =
795     function(pin) {
796   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
797     return;
798   }
799   this.plugin_.postMessage(JSON.stringify(
800       { method: 'onPinFetched', data: { pin: pin }}));
804  * Tells the plugin to ask for the PIN asynchronously.
805  */
806 remoting.ClientPluginImpl.prototype.useAsyncPinDialog =
807     function() {
808   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
809     return;
810   }
811   this.plugin_.postMessage(JSON.stringify(
812       { method: 'useAsyncPinDialog', data: {} }));
816  * Allows automatic mouse-lock.
817  */
818 remoting.ClientPluginImpl.prototype.allowMouseLock = function() {
819   this.plugin_.postMessage(JSON.stringify(
820       { method: 'allowMouseLock', data: {} }));
824  * Sets the third party authentication token and shared secret.
826  * @param {string} token The token received from the token URL.
827  * @param {string} sharedSecret Shared secret received from the token URL.
828  */
829 remoting.ClientPluginImpl.prototype.onThirdPartyTokenFetched = function(
830     token, sharedSecret) {
831   this.plugin_.postMessage(JSON.stringify(
832     { method: 'onThirdPartyTokenFetched',
833       data: { token: token, sharedSecret: sharedSecret}}));
837  * Request pairing with the host for PIN-less authentication.
839  * @param {string} clientName The human-readable name of the client.
840  * @param {function(string, string):void} onDone, Callback to receive the
841  *     client id and shared secret when they are available.
842  */
843 remoting.ClientPluginImpl.prototype.requestPairing =
844     function(clientName, onDone) {
845   if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) {
846     return;
847   }
848   this.onPairingComplete_ = onDone;
849   this.plugin_.postMessage(JSON.stringify(
850       { method: 'requestPairing', data: { clientName: clientName } }));
854  * Send an extension message to the host.
856  * @param {string} type The message type.
857  * @param {string} message The message payload.
858  */
859 remoting.ClientPluginImpl.prototype.sendClientMessage =
860     function(type, message) {
861   if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) {
862     return;
863   }
864   this.plugin_.postMessage(JSON.stringify(
865       { method: 'extensionMessage',
866         data: { type: type, data: message } }));
870 remoting.ClientPluginImpl.prototype.hostDesktop = function() {
871   return this.hostDesktop_;
875  * If we haven't yet received a "hello" message from the plugin, change its
876  * size so that the user can confirm it if click-to-play is enabled, or can
877  * see the "this plugin is disabled" message if it is actually disabled.
878  * @private
879  */
880 remoting.ClientPluginImpl.prototype.showPluginForClickToPlay_ = function() {
881   if (!this.helloReceived_) {
882     var width = 200;
883     var height = 200;
884     this.plugin_.style.width = width + 'px';
885     this.plugin_.style.height = height + 'px';
886     // Center the plugin just underneath the "Connnecting..." dialog.
887     var dialog = document.getElementById('client-dialog');
888     var dialogRect = dialog.getBoundingClientRect();
889     this.plugin_.style.top = (dialogRect.bottom + 16) + 'px';
890     this.plugin_.style.left = (window.innerWidth - width) / 2 + 'px';
891     this.plugin_.style.position = 'fixed';
892   }
896  * Undo the CSS rules needed to make the plugin clickable for click-to-play.
897  * @private
898  */
899 remoting.ClientPluginImpl.prototype.hidePluginForClickToPlay_ = function() {
900   this.plugin_.style.width = '';
901   this.plugin_.style.height = '';
902   this.plugin_.style.top = '';
903   this.plugin_.style.left = '';
904   this.plugin_.style.position = '';
908  * Callback passed to submodules to post a message to the plugin.
910  * @param {Object} message
911  * @private
912  */
913 remoting.ClientPluginImpl.prototype.postMessage_ = function(message) {
914   if (this.plugin_ && this.plugin_.postMessage) {
915     this.plugin_.postMessage(JSON.stringify(message));
916   }
920  * @constructor
921  * @implements {remoting.ClientPluginFactory}
922  */
923 remoting.DefaultClientPluginFactory = function() {};
926  * @param {Element} container
927  * @param {function(string, string):boolean} onExtensionMessage
928  * @param {Array<string>} requiredCapabilities
929  * @return {remoting.ClientPlugin}
930  */
931 remoting.DefaultClientPluginFactory.prototype.createPlugin =
932     function(container, onExtensionMessage, requiredCapabilities) {
933   return new remoting.ClientPluginImpl(container, onExtensionMessage,
934                                        requiredCapabilities);
937 remoting.DefaultClientPluginFactory.prototype.preloadPlugin = function() {
938   if (remoting.settings.CLIENT_PLUGIN_TYPE != 'pnacl') {
939     return;
940   }
942   var plugin = remoting.ClientPluginImpl.createPluginElement_();
943   plugin.addEventListener(
944       'loadend', function() { document.body.removeChild(plugin); }, false);
945   document.body.appendChild(plugin);