Fix Crashes in the New Autofill UI
[chromium-blink-merge.git] / remoting / webapp / client_plugin_async.js
blobe27983ccaaf58e490155d48bb0bc794ba5050c5c
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 /**
20  * @param {remoting.ViewerPlugin} plugin The plugin embed element.
21  * @constructor
22  * @implements {remoting.ClientPlugin}
23  */
24 remoting.ClientPluginAsync = function(plugin) {
25   this.plugin = plugin;
27   this.desktopWidth = 0;
28   this.desktopHeight = 0;
29   this.desktopXDpi = 96;
30   this.desktopYDpi = 96;
32   /** @param {string} iq The Iq stanza received from the host. */
33   this.onOutgoingIqHandler = function (iq) {};
34   /** @param {string} message Log message. */
35   this.onDebugMessageHandler = function (message) {};
36   /**
37    * @param {number} state The connection state.
38    * @param {number} error The error code, if any.
39    */
40   this.onConnectionStatusUpdateHandler = function(state, error) {};
41   /** @param {boolean} ready Connection ready state. */
42   this.onConnectionReadyHandler = function(ready) {};
43   this.onDesktopSizeUpdateHandler = function () {};
45   /** @type {number} */
46   this.pluginApiVersion_ = -1;
47   /** @type {Array.<string>} */
48   this.pluginApiFeatures_ = [];
49   /** @type {number} */
50   this.pluginApiMinVersion_ = -1;
51   /** @type {boolean} */
52   this.helloReceived_ = false;
53   /** @type {function(boolean)|null} */
54   this.onInitializedCallback_ = null;
56   /** @type {remoting.ClientSession.PerfStats} */
57   this.perfStats_ = new remoting.ClientSession.PerfStats();
59   /** @type {remoting.ClientPluginAsync} */
60   var that = this;
61   /** @param {Event} event Message event from the plugin. */
62   this.plugin.addEventListener('message', function(event) {
63       that.handleMessage_(event.data);
64     }, false);
65   window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
68 /**
69  * Chromoting session API version (for this javascript).
70  * This is compared with the plugin API version to verify that they are
71  * compatible.
72  *
73  * @const
74  * @private
75  */
76 remoting.ClientPluginAsync.prototype.API_VERSION_ = 6;
78 /**
79  * The oldest API version that we support.
80  * This will differ from the |API_VERSION_| if we maintain backward
81  * compatibility with older API versions.
82  *
83  * @const
84  * @private
85  */
86 remoting.ClientPluginAsync.prototype.API_MIN_VERSION_ = 5;
88 /**
89  * @param {string} messageStr Message from the plugin.
90  */
91 remoting.ClientPluginAsync.prototype.handleMessage_ = function(messageStr) {
92   var message = /** @type {{method:string, data:Object.<string,string>}} */
93       jsonParseSafe(messageStr);
95   if (!message || !('method' in message) || !('data' in message)) {
96     console.error('Received invalid message from the plugin: ' + messageStr);
97     return;
98   }
100   if (message.method == 'hello') {
101     // Reset the size in case we had to enlarge it to support click-to-play.
102     this.plugin.width = 0;
103     this.plugin.height = 0;
104     if (typeof message.data['apiVersion'] != 'number' ||
105         typeof message.data['apiMinVersion'] != 'number') {
106       console.error('Received invalid hello message: ' + messageStr);
107       return;
108     }
109     this.pluginApiVersion_ = /** @type {number} */ message.data['apiVersion'];
110     if (this.pluginApiVersion_ >= 7) {
111       if (typeof message.data['apiFeatures'] != 'string') {
112         console.error('Received invalid hello message: ' + messageStr);
113         return;
114       }
115       this.pluginApiFeatures_ =
116           /** @type {Array.<string>} */ message.data['apiFeatures'].split(' ');
117     } else if (this.pluginApiVersion_ >= 6) {
118       this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
119     } else {
120       this.pluginApiFeatures_ = ['highQualityScaling'];
121     }
122     this.pluginApiMinVersion_ =
123         /** @type {number} */ message.data['apiMinVersion'];
124     this.helloReceived_ = true;
125     if (this.onInitializedCallback_ != null) {
126       this.onInitializedCallback_(true);
127       this.onInitializedCallback_ = null;
128     }
129   } else if (message.method == 'sendOutgoingIq') {
130     if (typeof message.data['iq'] != 'string') {
131       console.error('Received invalid sendOutgoingIq message: ' + messageStr);
132       return;
133     }
134     this.onOutgoingIqHandler(message.data['iq']);
135   } else if (message.method == 'logDebugMessage') {
136     if (typeof message.data['message'] != 'string') {
137       console.error('Received invalid logDebugMessage message: ' + messageStr);
138       return;
139     }
140     this.onDebugMessageHandler(message.data['message']);
141   } else if (message.method == 'onConnectionStatus') {
142     if (typeof message.data['state'] != 'string' ||
143         !(message.data['state'] in remoting.ClientSession.State) ||
144         typeof message.data['error'] != 'string') {
145       console.error('Received invalid onConnectionState message: ' +
146                     messageStr);
147       return;
148     }
150     /** @type {remoting.ClientSession.State} */
151     var state = remoting.ClientSession.State[message.data['state']];
152     var error;
153     if (message.data['error'] in remoting.ClientSession.ConnectionError) {
154       error = /** @type {remoting.ClientSession.ConnectionError} */
155           remoting.ClientSession.ConnectionError[message.data['error']];
156     } else {
157       error = remoting.ClientSession.ConnectionError.UNKNOWN;
158     }
160     this.onConnectionStatusUpdateHandler(state, error);
161   } else if (message.method == 'onDesktopSize') {
162     if (typeof message.data['width'] != 'number' ||
163         typeof message.data['height'] != 'number') {
164       console.error('Received invalid onDesktopSize message: ' + messageStr);
165       return;
166     }
167     this.desktopWidth = /** @type {number} */ message.data['width'];
168     this.desktopHeight = /** @type {number} */ message.data['height'];
169     this.desktopXDpi = (typeof message.data['x_dpi'] == 'number') ?
170         /** @type {number} */ (message.data['x_dpi']) : 96;
171     this.desktopYDpi = (typeof message.data['y_dpi'] == 'number') ?
172         /** @type {number} */ (message.data['y_dpi']) : 96;
173     this.onDesktopSizeUpdateHandler();
174   } else if (message.method == 'onPerfStats') {
175     if (typeof message.data['videoBandwidth'] != 'number' ||
176         typeof message.data['videoFrameRate'] != 'number' ||
177         typeof message.data['captureLatency'] != 'number' ||
178         typeof message.data['encodeLatency'] != 'number' ||
179         typeof message.data['decodeLatency'] != 'number' ||
180         typeof message.data['renderLatency'] != 'number' ||
181         typeof message.data['roundtripLatency'] != 'number') {
182       console.error('Received incorrect onPerfStats message: ' + messageStr);
183       return;
184     }
185     this.perfStats_ =
186         /** @type {remoting.ClientSession.PerfStats} */ message.data;
187   } else if (message.method == 'injectClipboardItem') {
188     if (typeof message.data['mimeType'] != 'string' ||
189         typeof message.data['item'] != 'string') {
190       console.error('Received incorrect injectClipboardItem message.');
191       return;
192     }
193     if (remoting.clipboard) {
194       remoting.clipboard.fromHost(message.data['mimeType'],
195                                   message.data['item']);
196     }
197   } else if (message.method == 'onFirstFrameReceived') {
198     if (remoting.clientSession) {
199       remoting.clientSession.onFirstFrameReceived();
200     }
201   } else if (message.method == 'onConnectionReady') {
202     if (typeof message.data['ready'] != 'boolean') {
203       console.error('Received incorrect onConnectionReady message.');
204       return;
205     }
206     var ready = /** @type {boolean} */ message.data['ready'];
207     this.onConnectionReadyHandler(ready);
208   }
212  * Deletes the plugin.
213  */
214 remoting.ClientPluginAsync.prototype.cleanup = function() {
215   this.plugin.parentNode.removeChild(this.plugin);
219  * @return {HTMLEmbedElement} HTML element that correspods to the plugin.
220  */
221 remoting.ClientPluginAsync.prototype.element = function() {
222   return this.plugin;
226  * @param {function(boolean): void} onDone
227  */
228 remoting.ClientPluginAsync.prototype.initialize = function(onDone) {
229   if (this.helloReceived_) {
230     onDone(true);
231   } else {
232     this.onInitializedCallback_ = onDone;
233   }
237  * @return {boolean} True if the plugin and web-app versions are compatible.
238  */
239 remoting.ClientPluginAsync.prototype.isSupportedVersion = function() {
240   if (!this.helloReceived_) {
241     console.error(
242         "isSupportedVersion() is called before the plugin is initialized.");
243     return false;
244   }
245   return this.API_VERSION_ >= this.pluginApiMinVersion_ &&
246       this.pluginApiVersion_ >= this.API_MIN_VERSION_;
250  * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
251  * @return {boolean} True if the plugin supports the named feature.
252  */
253 remoting.ClientPluginAsync.prototype.hasFeature = function(feature) {
254   if (!this.helloReceived_) {
255     console.error(
256         "hasFeature() is called before the plugin is initialized.");
257     return false;
258   }
259   return this.pluginApiFeatures_.indexOf(feature) > -1;
263  * @return {boolean} True if the plugin supports the injectKeyEvent API.
264  */
265 remoting.ClientPluginAsync.prototype.isInjectKeyEventSupported = function() {
266   return this.pluginApiVersion_ >= 6;
270  * @param {string} iq Incoming IQ stanza.
271  */
272 remoting.ClientPluginAsync.prototype.onIncomingIq = function(iq) {
273   if (this.plugin && this.plugin.postMessage) {
274     this.plugin.postMessage(JSON.stringify(
275         { method: 'incomingIq', data: { iq: iq } }));
276   } else {
277     // plugin.onIq may not be set after the plugin has been shut
278     // down. Particularly this happens when we receive response to
279     // session-terminate stanza.
280     console.warn('plugin.onIq is not set so dropping incoming message.');
281   }
285  * @param {string} hostJid The jid of the host to connect to.
286  * @param {string} hostPublicKey The base64 encoded version of the host's
287  *     public key.
288  * @param {string} localJid Local jid.
289  * @param {string} sharedSecret The access code for IT2Me or the PIN
290  *     for Me2Me.
291  * @param {string} authenticationMethods Comma-separated list of
292  *     authentication methods the client should attempt to use.
293  * @param {string} authenticationTag A host-specific tag to mix into
294  *     authentication hashes.
295  */
296 remoting.ClientPluginAsync.prototype.connect = function(
297     hostJid, hostPublicKey, localJid, sharedSecret,
298     authenticationMethods, authenticationTag) {
299   this.plugin.postMessage(JSON.stringify(
300     { method: 'connect', data: {
301         hostJid: hostJid,
302         hostPublicKey: hostPublicKey,
303         localJid: localJid,
304         sharedSecret: sharedSecret,
305         authenticationMethods: authenticationMethods,
306         authenticationTag: authenticationTag
307       }
308     }));
312  * Release all currently pressed keys.
313  */
314 remoting.ClientPluginAsync.prototype.releaseAllKeys = function() {
315   this.plugin.postMessage(JSON.stringify(
316       { method: 'releaseAllKeys', data: {} }));
320  * Send a key event to the host.
322  * @param {number} usbKeycode The USB-style code of the key to inject.
323  * @param {boolean} pressed True to inject a key press, False for a release.
324  */
325 remoting.ClientPluginAsync.prototype.injectKeyEvent =
326     function(usbKeycode, pressed) {
327   this.plugin.postMessage(JSON.stringify(
328       { method: 'injectKeyEvent', data: {
329           'usbKeycode': usbKeycode,
330           'pressed': pressed}
331       }));
335  * Remap one USB keycode to another in all subsequent key events.
337  * @param {number} fromKeycode The USB-style code of the key to remap.
338  * @param {number} toKeycode The USB-style code to remap the key to.
339  */
340 remoting.ClientPluginAsync.prototype.remapKey =
341     function(fromKeycode, toKeycode) {
342   this.plugin.postMessage(JSON.stringify(
343       { method: 'remapKey', data: {
344           'fromKeycode': fromKeycode,
345           'toKeycode': toKeycode}
346       }));
350  * Returns an associative array with a set of stats for this connecton.
352  * @return {remoting.ClientSession.PerfStats} The connection statistics.
353  */
354 remoting.ClientPluginAsync.prototype.getPerfStats = function() {
355   return this.perfStats_;
359  * Sends a clipboard item to the host.
361  * @param {string} mimeType The MIME type of the clipboard item.
362  * @param {string} item The clipboard item.
363  */
364 remoting.ClientPluginAsync.prototype.sendClipboardItem =
365     function(mimeType, item) {
366   if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM))
367     return;
368   this.plugin.postMessage(JSON.stringify(
369       { method: 'sendClipboardItem',
370         data: { mimeType: mimeType, item: item }}));
374  * Notifies the host that the client has the specified dimensions.
376  * @param {number} width The available client width.
377  * @param {number} height The available client height.
378  */
379 remoting.ClientPluginAsync.prototype.notifyClientDimensions =
380     function(width, height) {
381   if (!this.hasFeature(remoting.ClientPlugin.Feature.NOTIFY_CLIENT_DIMENSIONS))
382     return;
383   this.plugin.postMessage(JSON.stringify(
384       { method: 'notifyClientDimensions',
385         data: { width: width, height: height }}));
389  * Requests that the host pause or resume sending video updates.
391  * @param {boolean} pause True to suspend video updates, false otherwise.
392  */
393 remoting.ClientPluginAsync.prototype.pauseVideo =
394     function(pause) {
395   if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO))
396     return;
397   this.plugin.postMessage(JSON.stringify(
398       { method: 'pauseVideo', data: { pause: pause }}));
402  * Requests that the host pause or resume sending audio updates.
404  * @param {boolean} pause True to suspend audio updates, false otherwise.
405  */
406 remoting.ClientPluginAsync.prototype.pauseAudio =
407     function(pause) {
408   if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO))
409     return;
410   this.plugin.postMessage(JSON.stringify(
411       { method: 'pauseAudio', data: { pause: pause }}));
415  * If we haven't yet received a "hello" message from the plugin, change its
416  * size so that the user can confirm it if click-to-play is enabled, or can
417  * see the "this plugin is disabled" message if it is actually disabled.
418  * @private
419  */
420 remoting.ClientPluginAsync.prototype.showPluginForClickToPlay_ = function() {
421   if (!this.helloReceived_) {
422     var width = 200;
423     var height = 200;
424     this.plugin.width = width;
425     this.plugin.height = height;
426     // Center the plugin just underneath the "Connnecting..." dialog.
427     var parentNode = this.plugin.parentNode;
428     var dialog = document.getElementById('client-dialog');
429     var dialogRect = dialog.getBoundingClientRect();
430     parentNode.style.top = (dialogRect.bottom + 16) + 'px';
431     parentNode.style.left = (window.innerWidth - width) / 2 + 'px';
432   }