Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / crd / js / client_session.js
blobdc2a2a9ccf96949f794959a288869591d9753092
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 handling creation and teardown of a remoting client session.
8  *
9  * The ClientSession class controls lifetime of the client plugin
10  * object and provides the plugin with the functionality it needs to
11  * establish connection. Specifically it:
12  *  - Delivers incoming/outgoing signaling messages,
13  *  - Adjusts plugin size and position when destop resolution changes,
14  *
15  * This class should not access the plugin directly, instead it should
16  * do it through ClientPlugin class which abstracts plugin version
17  * differences.
18  */
20 'use strict';
22 /** @suppress {duplicate} */
23 var remoting = remoting || {};
25 /**
26  * Interval that determines how often the web-app should send a new access token
27  * to the host.
28  *
29  * @const
30  * @type {number}
31  */
32 remoting.ACCESS_TOKEN_RESEND_INTERVAL_MS = 15 * 60 * 1000;
34 /**
35  * @param {remoting.Host} host The host to connect to.
36  * @param {remoting.SignalStrategy} signalStrategy Signal strategy.
37  * @param {HTMLElement} container Container element for the client view.
38  * @param {string} accessCode The IT2Me access code. Blank for Me2Me.
39  * @param {function(boolean, function(string): void): void} fetchPin
40  *     Called by Me2Me connections when a PIN needs to be obtained
41  *     interactively.
42  * @param {function(string, string, string,
43  *                  function(string, string): void): void}
44  *     fetchThirdPartyToken Called by Me2Me connections when a third party
45  *     authentication token must be obtained.
46  * @param {string} authenticationMethods Comma-separated list of
47  *     authentication methods the client should attempt to use.
48  * @param {remoting.DesktopConnectedView.Mode} mode The mode of this connection.
49  * @param {string} clientPairingId For paired Me2Me connections, the
50  *     pairing id for this client, as issued by the host.
51  * @param {string} clientPairedSecret For paired Me2Me connections, the
52  *     paired secret for this client, as issued by the host.
53  * @param {string} defaultRemapKeys The default set of remap keys, to use
54  *     when the client doesn't define any.
55  * @constructor
56  * @extends {base.EventSourceImpl}
57  */
58 remoting.ClientSession = function(host, signalStrategy, container, accessCode,
59                                   fetchPin, fetchThirdPartyToken,
60                                   authenticationMethods, mode, clientPairingId,
61                                   clientPairedSecret, defaultRemapKeys) {
62   /** @private */
63   this.state_ = remoting.ClientSession.State.CREATED;
65   /** @private */
66   this.error_ = remoting.Error.NONE;
68   /** @private */
69   this.host_ = host;
70   /** @private */
71   this.accessCode_ = accessCode;
72   /** @private */
73   this.fetchPin_ = fetchPin;
74   /** @private */
75   this.fetchThirdPartyToken_ = fetchThirdPartyToken;
76   /** @private */
77   this.authenticationMethods_ = authenticationMethods;
78   /** @private */
79   this.clientPairingId_ = clientPairingId;
80   /** @private */
81   this.clientPairedSecret_ = clientPairedSecret;
83   /** @private */
84   this.uiHandler_ = new remoting.DesktopConnectedView(
85       this, container, this.host_, mode, defaultRemapKeys,
86       this.onPluginInitialized_.bind(this));
87   remoting.desktopConnectedView = this.uiHandler_;
89   /** @private */
90   this.sessionId_ = '';
91   /** @type {remoting.ClientPlugin}
92     * @private */
93   this.plugin_ = null;
94   /** @private */
95   this.hasReceivedFrame_ = false;
96   this.logToServer = new remoting.LogToServer(signalStrategy, mode);
98   /** @private */
99   this.signalStrategy_ = signalStrategy;
100   base.debug.assert(this.signalStrategy_.getState() ==
101                     remoting.SignalStrategy.State.CONNECTED);
102   this.signalStrategy_.setIncomingStanzaCallback(
103       this.onIncomingMessage_.bind(this));
104   remoting.formatIq.setJids(this.signalStrategy_.getJid(), host.jabberId);
106   /**
107    * Allow host-offline error reporting to be suppressed in situations where it
108    * would not be useful, for example, when using a cached host JID.
109    *
110    * @type {boolean} @private
111    */
112   this.logHostOfflineErrors_ = true;
114   /** @type {remoting.GnubbyAuthHandler} @private */
115   this.gnubbyAuthHandler_ = null;
117   /** @type {remoting.CastExtensionHandler} @private */
118   this.castExtensionHandler_ = null;
120   this.defineEvents(Object.keys(remoting.ClientSession.Events));
123 base.extend(remoting.ClientSession, base.EventSourceImpl);
125 /** @enum {string} */
126 remoting.ClientSession.Events = {
127   stateChanged: 'stateChanged',
128   videoChannelStateChanged: 'videoChannelStateChanged',
131 // Note that the positive values in both of these enums are copied directly
132 // from chromoting_scriptable_object.h and must be kept in sync. The negative
133 // values represent state transitions that occur within the web-app that have
134 // no corresponding plugin state transition.
135 /** @enum {number} */
136 remoting.ClientSession.State = {
137   CONNECTION_CANCELED: -3,  // Connection closed (gracefully) before connecting.
138   CONNECTION_DROPPED: -2,  // Succeeded, but subsequently closed with an error.
139   CREATED: -1,
140   UNKNOWN: 0,
141   CONNECTING: 1,
142   INITIALIZING: 2,
143   CONNECTED: 3,
144   CLOSED: 4,
145   FAILED: 5
149  * @param {string} state The state name.
150  * @return {remoting.ClientSession.State} The session state enum value.
151  */
152 remoting.ClientSession.State.fromString = function(state) {
153   if (!remoting.ClientSession.State.hasOwnProperty(state)) {
154     throw "Invalid ClientSession.State: " + state;
155   }
156   return remoting.ClientSession.State[state];
160   @param {remoting.ClientSession.State} current
161   @param {remoting.ClientSession.State} previous
162   @constructor
164 remoting.ClientSession.StateEvent = function(current, previous) {
165   /** @type {remoting.ClientSession.State} */
166   this.previous = previous
168   /** @type {remoting.ClientSession.State} */
169   this.current = current;
172 /** @enum {number} */
173 remoting.ClientSession.ConnectionError = {
174   UNKNOWN: -1,
175   NONE: 0,
176   HOST_IS_OFFLINE: 1,
177   SESSION_REJECTED: 2,
178   INCOMPATIBLE_PROTOCOL: 3,
179   NETWORK_FAILURE: 4,
180   HOST_OVERLOAD: 5
184  * @param {string} error The connection error name.
185  * @return {remoting.ClientSession.ConnectionError} The connection error enum.
186  */
187 remoting.ClientSession.ConnectionError.fromString = function(error) {
188   if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
189     console.error('Unexpected ClientSession.ConnectionError string: ', error);
190     return remoting.ClientSession.ConnectionError.UNKNOWN;
191   }
192   return remoting.ClientSession.ConnectionError[error];
196  * Type used for performance statistics collected by the plugin.
197  * @constructor
198  */
199 remoting.ClientSession.PerfStats = function() {};
200 /** @type {number} */
201 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
202 /** @type {number} */
203 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
204 /** @type {number} */
205 remoting.ClientSession.PerfStats.prototype.captureLatency;
206 /** @type {number} */
207 remoting.ClientSession.PerfStats.prototype.encodeLatency;
208 /** @type {number} */
209 remoting.ClientSession.PerfStats.prototype.decodeLatency;
210 /** @type {number} */
211 remoting.ClientSession.PerfStats.prototype.renderLatency;
212 /** @type {number} */
213 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
215 // Keys for connection statistics.
216 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
217 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
218 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
219 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
220 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
221 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
222 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
225  * Set of capabilities for which hasCapability() can be used to test.
227  * @enum {string}
228  */
229 remoting.ClientSession.Capability = {
230   // When enabled this capability causes the client to send its screen
231   // resolution to the host once connection has been established. See
232   // this.plugin_.notifyClientResolution().
233   SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
235   // Let the host know that we're interested in knowing whether or not it
236   // rate limits desktop-resize requests.
237   // TODO(kelvinp): This has been supported since M-29.  Currently we only have
238   // <1000 users on M-29 or below. Remove this and the capability on the host.
239   RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
241   // Indicates that host/client supports Google Drive integration, and that the
242   // client should send to the host the OAuth tokens to be used by Google Drive
243   // on the host.
244   GOOGLE_DRIVE: "googleDrive",
246   // Indicates that the client supports the video frame-recording extension.
247   VIDEO_RECORDER: 'videoRecorder',
249   // Indicates that the client supports 'cast'ing the video stream to a
250   // cast-enabled device.
251   CAST: 'casting',
255  * The set of capabilities negotiated between the client and host.
256  * @type {Array<string>}
257  * @private
258  */
259 remoting.ClientSession.prototype.capabilities_ = null;
262  * @param {remoting.ClientSession.Capability} capability The capability to test
263  *     for.
264  * @return {boolean} True if the capability has been negotiated between
265  *     the client and host.
266  */
267 remoting.ClientSession.prototype.hasCapability = function(capability) {
268   if (this.capabilities_ == null)
269     return false;
271   return this.capabilities_.indexOf(capability) > -1;
275  * Adds <embed> element to the UI container and readies the session object.
277  * @param {function(string, string):boolean} onExtensionMessage The handler for
278  *     protocol extension messages. Returns true if a message is recognized;
279  *     false otherwise.
280  * @param {Array<string>} requiredCapabilities A list of capabilities
281  *     required by this application.
282  */
283 remoting.ClientSession.prototype.createPluginAndConnect =
284     function(onExtensionMessage, requiredCapabilities) {
285   this.uiHandler_.createPluginAndConnect(onExtensionMessage,
286                                          requiredCapabilities);
290  * @param {remoting.Error} error
291  * @param {remoting.ClientPlugin} plugin
292  */
293 remoting.ClientSession.prototype.onPluginInitialized_ = function(
294     error, plugin) {
295   if (error != remoting.Error.NONE) {
296     this.resetWithError_(error);
297   }
299   this.plugin_ = plugin;
300   plugin.setOnOutgoingIqHandler(this.sendIq_.bind(this));
301   plugin.setOnDebugMessageHandler(this.onDebugMessage_.bind(this));
303   plugin.setConnectionStatusUpdateHandler(
304       this.onConnectionStatusUpdate_.bind(this));
305   plugin.setRouteChangedHandler(this.onRouteChanged_.bind(this));
306   plugin.setConnectionReadyHandler(this.onConnectionReady_.bind(this));
307   plugin.setCapabilitiesHandler(this.onSetCapabilities_.bind(this));
308   plugin.setGnubbyAuthHandler(
309       this.processGnubbyAuthMessage_.bind(this));
310   plugin.setCastExtensionHandler(
311       this.processCastExtensionMessage_.bind(this));
313   this.initiateConnection_();
317  * @param {remoting.Error} error
318  */
319 remoting.ClientSession.prototype.resetWithError_ = function(error) {
320   this.signalStrategy_.setIncomingStanzaCallback(null);
321   this.removePlugin();
322   this.error_ = error;
323   this.setState_(remoting.ClientSession.State.FAILED);
327  * Deletes the <embed> element from the container, without sending a
328  * session_terminate request.  This is to be called when the session was
329  * disconnected by the Host.
331  * @return {void} Nothing.
332  */
333 remoting.ClientSession.prototype.removePlugin = function() {
334   this.uiHandler_.removePlugin();
335   this.plugin_ = null;
339  * Disconnect the current session with a particular |error|.  The session will
340  * raise a |stateChanged| event in response to it.  The caller should then call
341  * |cleanup| to remove and destroy the <embed> element.
343  * @param {remoting.Error} error The reason for the disconnection.  Use
344  *    remoting.Error.NONE if there is no error.
345  * @return {void} Nothing.
346  */
347 remoting.ClientSession.prototype.disconnect = function(error) {
348   var state = (error == remoting.Error.NONE) ?
349                   remoting.ClientSession.State.CLOSED :
350                   remoting.ClientSession.State.FAILED;
352   // The plugin won't send a state change notification, so we explicitly log
353   // the fact that the connection has closed.
354   this.logToServer.logClientSessionStateChange(state, error);
355   this.error_ = error;
356   this.setState_(state);
360  * Deletes the <embed> element from the container and disconnects.
362  * @return {void} Nothing.
363  */
364 remoting.ClientSession.prototype.cleanup = function() {
365   this.sendIq_(
366       '<cli:iq ' +
367           'to="' + this.host_.jabberId + '" ' +
368           'type="set" ' +
369           'id="session-terminate" ' +
370           'xmlns:cli="jabber:client">' +
371         '<jingle ' +
372             'xmlns="urn:xmpp:jingle:1" ' +
373             'action="session-terminate" ' +
374             'sid="' + this.sessionId_ + '">' +
375           '<reason><success/></reason>' +
376         '</jingle>' +
377       '</cli:iq>');
378   this.removePlugin();
382  * @return {remoting.ClientSession.State} The current state.
383  */
384 remoting.ClientSession.prototype.getState = function() {
385   return this.state_;
389  * @return {remoting.Error} The current error code.
390  */
391 remoting.ClientSession.prototype.getError = function() {
392   return this.error_;
396  * Called when the client receives its first frame.
398  * @return {void} Nothing.
399  */
400 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
401   this.hasReceivedFrame_ = true;
405  * @return {boolean} Whether the client has received a video buffer.
406  */
407 remoting.ClientSession.prototype.hasReceivedFrame = function() {
408   return this.hasReceivedFrame_;
412  * Sends a signaling message.
414  * @param {string} message XML string of IQ stanza to send to server.
415  * @return {void} Nothing.
416  * @private
417  */
418 remoting.ClientSession.prototype.sendIq_ = function(message) {
419   // Extract the session id, so we can close the session later.
420   var parser = new DOMParser();
421   var iqNode = parser.parseFromString(message, 'text/xml').firstChild;
422   var jingleNode = iqNode.firstChild;
423   if (jingleNode) {
424     var action = jingleNode.getAttribute('action');
425     if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
426       this.sessionId_ = jingleNode.getAttribute('sid');
427     }
428   }
430   console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(message));
431   if (this.signalStrategy_.getState() !=
432       remoting.SignalStrategy.State.CONNECTED) {
433     console.log("Message above is dropped because signaling is not connected.");
434     return;
435   }
437   this.signalStrategy_.sendMessage(message);
441  * @param {string} msg
442  * @private
443  */
444 remoting.ClientSession.prototype.onDebugMessage_ = function(msg) {
445   console.log('plugin: ' + msg.trimRight());
449  * @param {Element} message
450  * @private
451  */
452 remoting.ClientSession.prototype.onIncomingMessage_ = function(message) {
453   if (!this.plugin_) {
454     return;
455   }
456   var formatted = new XMLSerializer().serializeToString(message);
457   console.log(remoting.timestamp(),
458               remoting.formatIq.prettifyReceiveIq(formatted));
459   this.plugin_.onIncomingIq(formatted);
463  * @private
464  */
465 remoting.ClientSession.prototype.initiateConnection_ = function() {
466   /** @type {remoting.ClientSession} */
467   var that = this;
469   /** @param {string} sharedSecret Shared secret. */
470   function onSharedSecretReceived(sharedSecret) {
471     that.plugin_.connect(that.host_.jabberId, that.host_.publicKey,
472                          that.signalStrategy_.getJid(), sharedSecret,
473                          that.authenticationMethods_, that.host_.hostId,
474                          that.clientPairingId_, that.clientPairedSecret_);
475   }
477   this.getSharedSecret_(onSharedSecretReceived);
481  * Gets shared secret to be used for connection.
483  * @param {function(string)} callback Callback called with the shared secret.
484  * @return {void} Nothing.
485  * @private
486  */
487 remoting.ClientSession.prototype.getSharedSecret_ = function(callback) {
488   /** @type remoting.ClientSession */
489   var that = this;
490   if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) {
491     /** @type{function(string, string, string): void} */
492     var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) {
493       that.fetchThirdPartyToken_(
494           tokenUrl, hostPublicKey, scope,
495           that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_));
496     };
497     this.plugin_.setFetchThirdPartyTokenHandler(fetchThirdPartyToken);
498   }
499   if (this.accessCode_) {
500     // Shared secret was already supplied before connecting (It2Me case).
501     callback(this.accessCode_);
502   } else if (this.plugin_.hasFeature(
503       remoting.ClientPlugin.Feature.ASYNC_PIN)) {
504     // Plugin supports asynchronously asking for the PIN.
505     this.plugin_.useAsyncPinDialog();
506     /** @param {boolean} pairingSupported */
507     var fetchPin = function(pairingSupported) {
508       that.fetchPin_(pairingSupported,
509                      that.plugin_.onPinFetched.bind(that.plugin_));
510     };
511     this.plugin_.setFetchPinHandler(fetchPin);
512     callback('');
513   } else {
514     // Clients that don't support asking for a PIN asynchronously also don't
515     // support pairing, so request the PIN now without offering to remember it.
516     this.fetchPin_(false, callback);
517   }
521  * Callback that the plugin invokes to indicate that the connection
522  * status has changed.
524  * @param {number} status The plugin's status.
525  * @param {number} error The plugin's error state, if any.
526  * @private
527  */
528 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
529     function(status, error) {
530   if (status == remoting.ClientSession.State.CONNECTED) {
531     this.uiHandler_.updateClientSessionUi_(this);
533   } else if (status == remoting.ClientSession.State.FAILED) {
534     switch (error) {
535       case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
536         this.error_ = remoting.Error.HOST_IS_OFFLINE;
537         break;
538       case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
539         this.error_ = remoting.Error.INVALID_ACCESS_CODE;
540         break;
541       case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
542         this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL;
543         break;
544       case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
545         this.error_ = remoting.Error.P2P_FAILURE;
546         break;
547       case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
548         this.error_ = remoting.Error.HOST_OVERLOAD;
549         break;
550       default:
551         this.error_ = remoting.Error.UNEXPECTED;
552     }
553   }
554   this.setState_(/** @type {remoting.ClientSession.State} */ (status));
558  * Callback that the plugin invokes to indicate that the connection type for
559  * a channel has changed.
561  * @param {string} channel The channel name.
562  * @param {string} connectionType The new connection type.
563  * @private
564  */
565 remoting.ClientSession.prototype.onRouteChanged_ =
566     function(channel, connectionType) {
567   console.log('plugin: Channel ' + channel + ' using ' +
568               connectionType + ' connection.');
569   this.logToServer.setConnectionType(connectionType);
573  * Callback that the plugin invokes to indicate when the connection is
574  * ready.
576  * @param {boolean} ready True if the connection is ready.
577  * @private
578  */
579 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
580   // TODO(jamiewalch): Currently, the logic for determining whether or not the
581   // connection is available is based solely on whether or not any video frames
582   // have been received recently. which leads to poor UX on slow connections.
583   // Re-enable this once crbug.com/435315 has been fixed.
584   var ignoreVideoChannelState = true;
585   if (ignoreVideoChannelState) {
586     console.log('Video channel ' + (ready ? '' : 'not ') + 'ready.');
587     return;
588   }
590   this.uiHandler_.onConnectionReady(ready);
592   this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
593                   ready);
597  * Called when the client-host capabilities negotiation is complete.
598  * TODO(kelvinp): Move this function out of ClientSession.
600  * @param {!Array<string>} capabilities The set of capabilities negotiated
601  *     between the client and host.
602  * @return {void} Nothing.
603  * @private
604  */
605 remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
606   if (this.capabilities_ != null) {
607     console.error('onSetCapabilities_() is called more than once');
608     return;
609   }
611   this.capabilities_ = capabilities;
612   if (this.hasCapability(remoting.ClientSession.Capability.GOOGLE_DRIVE)) {
613     this.sendGoogleDriveAccessToken_();
614   }
615   if (this.hasCapability(
616       remoting.ClientSession.Capability.VIDEO_RECORDER)) {
617     this.uiHandler_.initVideoFrameRecorder();
618   }
622  * @param {remoting.ClientSession.State} newState The new state for the session.
623  * @return {void} Nothing.
624  * @private
625  */
626 remoting.ClientSession.prototype.setState_ = function(newState) {
627   var oldState = this.state_;
628   this.state_ = newState;
629   var state = this.state_;
630   if (oldState == remoting.ClientSession.State.CONNECTING) {
631     if (this.state_ == remoting.ClientSession.State.CLOSED) {
632       state = remoting.ClientSession.State.CONNECTION_CANCELED;
633     } else if (this.state_ == remoting.ClientSession.State.FAILED &&
634         this.error_ == remoting.Error.HOST_IS_OFFLINE &&
635         !this.logHostOfflineErrors_) {
636       // The application requested host-offline errors to be suppressed, for
637       // example, because this connection attempt is using a cached host JID.
638       console.log('Suppressing host-offline error.');
639       state = remoting.ClientSession.State.CONNECTION_CANCELED;
640     }
641   } else if (oldState == remoting.ClientSession.State.CONNECTED &&
642              this.state_ == remoting.ClientSession.State.FAILED) {
643     state = remoting.ClientSession.State.CONNECTION_DROPPED;
644   }
645   this.logToServer.logClientSessionStateChange(state, this.error_);
646   if (this.state_ == remoting.ClientSession.State.CONNECTED) {
647     this.createGnubbyAuthHandler_();
648     this.createCastExtensionHandler_();
649   }
651   this.raiseEvent(remoting.ClientSession.Events.stateChanged,
652     new remoting.ClientSession.StateEvent(newState, oldState)
653   );
657  * Returns an associative array with a set of stats for this connection.
659  * @return {remoting.ClientSession.PerfStats} The connection statistics.
660  */
661 remoting.ClientSession.prototype.getPerfStats = function() {
662   return this.plugin_.getPerfStats();
666  * Logs statistics.
668  * @param {remoting.ClientSession.PerfStats} stats
669  */
670 remoting.ClientSession.prototype.logStatistics = function(stats) {
671   this.logToServer.logStatistics(stats);
675  * Enable or disable logging of connection errors due to a host being offline.
676  * For example, if attempting a connection using a cached JID, host-offline
677  * errors should not be logged because the JID will be refreshed and the
678  * connection retried.
680  * @param {boolean} enable True to log host-offline errors; false to suppress.
681  */
682 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
683   this.logHostOfflineErrors_ = enable;
687  * Request pairing with the host for PIN-less authentication.
689  * @param {string} clientName The human-readable name of the client.
690  * @param {function(string, string):void} onDone Callback to receive the
691  *     client id and shared secret when they are available.
692  */
693 remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) {
694   if (this.plugin_) {
695     this.plugin_.requestPairing(clientName, onDone);
696   }
700  * Sends a clipboard item to the host.
702  * @param {string} mimeType The MIME type of the clipboard item.
703  * @param {string} item The clipboard item.
704  */
705 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
706   if (!this.plugin_)
707     return;
708   this.plugin_.sendClipboardItem(mimeType, item);
712  * Sends an extension message to the host.
714  * @param {string} type The message type.
715  * @param {string} message The message payload.
716  */
717 remoting.ClientSession.prototype.sendClientMessage = function(type, message) {
718   if (!this.plugin_)
719     return;
720   this.plugin_.sendClientMessage(type, message);
724  * Send a gnubby-auth extension message to the host.
725  * @param {Object} data The gnubby-auth message data.
726  */
727 remoting.ClientSession.prototype.sendGnubbyAuthMessage = function(data) {
728   if (!this.plugin_)
729     return;
730   this.plugin_.sendClientMessage('gnubby-auth', JSON.stringify(data));
734  * Process a remote gnubby auth request.
735  * @param {string} data Remote gnubby request data.
736  * @private
737  */
738 remoting.ClientSession.prototype.processGnubbyAuthMessage_ = function(data) {
739   if (this.gnubbyAuthHandler_) {
740     try {
741       this.gnubbyAuthHandler_.onMessage(data);
742     } catch (/** @type {*} */ err) {
743       console.error('Failed to process gnubby message: ', err);
744     }
745   } else {
746     console.error('Received unexpected gnubby message');
747   }
751  * Create a gnubby auth handler and inform the host that gnubby auth is
752  * supported.
753  * @private
754  */
755 remoting.ClientSession.prototype.createGnubbyAuthHandler_ = function() {
756   if (this.uiHandler_.getMode() == remoting.DesktopConnectedView.Mode.ME2ME) {
757     this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler(this);
758     // TODO(psj): Move to more generic capabilities mechanism.
759     this.sendGnubbyAuthMessage({'type': 'control', 'option': 'auth-v1'});
760   }
764  * Timer callback to send the access token to the host.
765  * @private
766  */
767 remoting.ClientSession.prototype.sendGoogleDriveAccessToken_ = function() {
768   if (this.state_ != remoting.ClientSession.State.CONNECTED) {
769     return;
770   }
771   /** @type {remoting.ClientSession} */
772   var that = this;
774   /** @param {string} token */
775   var sendToken = function(token) {
776     remoting.clientSession.sendClientMessage('accessToken', token);
777   };
778   /** @param {remoting.Error} error */
779   var sendError = function(error) {
780     console.log('Failed to refresh access token: ' + error);
781   };
782   remoting.identity.getNewToken().
783       then(sendToken).
784       catch(remoting.Error.handler(sendError));
785   window.setTimeout(this.sendGoogleDriveAccessToken_.bind(this),
786                     remoting.ACCESS_TOKEN_RESEND_INTERVAL_MS);
790  * Send a Cast extension message to the host.
791  * @param {Object} data The cast message data.
792  */
793 remoting.ClientSession.prototype.sendCastExtensionMessage = function(data) {
794   if (!this.plugin_)
795     return;
796   this.plugin_.sendClientMessage('cast_message', JSON.stringify(data));
800  * Process a remote Cast extension message from the host.
801  * @param {string} data Remote cast extension data message.
802  * @private
803  */
804 remoting.ClientSession.prototype.processCastExtensionMessage_ = function(data) {
805   if (this.castExtensionHandler_) {
806     try {
807       this.castExtensionHandler_.onMessage(data);
808     } catch (/** @type {*} */ err) {
809       console.error('Failed to process cast message: ', err);
810     }
811   } else {
812     console.error('Received unexpected cast message');
813   }
817  * Create a CastExtensionHandler and inform the host that cast extension
818  * is supported.
819  * @private
820  */
821 remoting.ClientSession.prototype.createCastExtensionHandler_ = function() {
822   if (remoting.app.hasCapability(remoting.ClientSession.Capability.CAST) &&
823       this.uiHandler_.getMode() == remoting.DesktopConnectedView.Mode.ME2ME) {
824     this.castExtensionHandler_ = new remoting.CastExtensionHandler(this);
825   }
829  * Handles protocol extension messages.
830  * @param {string} type Type of extension message.
831  * @param {Object} message The parsed extension message data.
832  * @return {boolean} True if the message was recognized, false otherwise.
833  */
834 remoting.ClientSession.prototype.handleExtensionMessage =
835     function(type, message) {
836   if (this.uiHandler_.handleExtensionMessage(type, message)) {
837     return true;
838   }
839   return false;
843  * Enables or disables rendering of dirty regions for debugging.
844  * @param {boolean} enable True to enable rendering.
845  */
846 remoting.ClientSession.prototype.enableDebugRegion = function(enable) {
847   if (enable) {
848     this.plugin_.setDebugDirtyRegionHandler(
849         this.uiHandler_.handleDebugRegion.bind(this.uiHandler_));
850   } else {
851     this.plugin_.setDebugDirtyRegionHandler(null);
852   }