Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / remoting / webapp / base / js / client_session.js
blobc8449c744c55ccb848de4fa66842052546c3b6eb
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, e.g. delivers incoming/outgoing signaling
12  * messages.
13  *
14  * This class should not access the plugin directly, instead it should
15  * do it through ClientPlugin class which abstracts plugin version
16  * differences.
17  */
19 'use strict';
21 /** @suppress {duplicate} */
22 var remoting = remoting || {};
24 /**
25  * @param {remoting.ClientPlugin} plugin
26  * @param {remoting.SignalStrategy} signalStrategy Signal strategy.
27  * @param {boolean} useApiaryForLogging
28  * @param {remoting.ClientSession.EventHandler} listener
29  *
30  * @constructor
31  * @extends {base.EventSourceImpl}
32  * @implements {base.Disposable}
33  * @implements {remoting.ClientPlugin.ConnectionEventHandler}
34  */
35 remoting.ClientSession = function(
36     plugin, signalStrategy, useApiaryForLogging, listener) {
37   base.inherits(this, base.EventSourceImpl);
39   /** @private */
40   this.state_ = remoting.ClientSession.State.INITIALIZING;
42   /** @private {!remoting.Error} */
43   this.error_ = remoting.Error.none();
45   /** @private {remoting.Host} */
46   this.host_ = null;
48   /** @private {remoting.CredentialsProvider} */
49   this.credentialsProvider_ = null;
51   /** @private */
52   this.sessionId_ = '';
54   /** @private */
55   this.listener_ = listener;
57   /** @private */
58   this.hasReceivedFrame_ = false;
60   /** @private {remoting.Logger} */
61   this.logger_ = this.createLogger_(useApiaryForLogging, signalStrategy);
63   /** @private */
64   this.signalStrategy_ = signalStrategy;
66   var state = this.signalStrategy_.getState();
67   console.assert(state == remoting.SignalStrategy.State.CONNECTED,
68                  'ClientSession ctor called in state ' + state + '.');
69   this.signalStrategy_.setIncomingStanzaCallback(
70       this.onIncomingMessage_.bind(this));
72   /** @private {remoting.FormatIq} */
73   this.iqFormatter_ = null;
75   /**
76    * Allow host-offline error reporting to be suppressed in situations where it
77    * would not be useful, for example, when using a cached host JID.
78    *
79    * @type {boolean} @private
80    */
81   this.logHostOfflineErrors_ = true;
83   /** @private {remoting.ClientPlugin} */
84   this.plugin_ = plugin;
85   plugin.setConnectionEventHandler(this);
87   /** @private  */
88   this.connectedDisposables_ = new base.Disposables();
90   this.defineEvents(Object.keys(remoting.ClientSession.Events));
93 /** @enum {string} */
94 remoting.ClientSession.Events = {
95   videoChannelStateChanged: 'videoChannelStateChanged'
98 /**
99  * @interface
100  * [START]-------> [onConnected] ------> [onDisconnected]
101  *    |
102  *    |-----> [OnConnectionFailed]
104  */
105 remoting.ClientSession.EventHandler = function() {};
108  * Called when the connection failed before it is connected.
110  * @param {!remoting.Error} error
111  */
112 remoting.ClientSession.EventHandler.prototype.onConnectionFailed =
113     function(error) {};
116  * Called when a new session has been connected.  The |connectionInfo| will be
117  * valid until onDisconnected() is called.
119  * @param {!remoting.ConnectionInfo} connectionInfo
120  */
121 remoting.ClientSession.EventHandler.prototype.onConnected =
122     function(connectionInfo) {};
125  * Called when the current session has been disconnected.
127  * @param {!remoting.Error} reason Reason that the session is disconnected.
128  *     Set to remoting.Error.none() if there is no error.
129  */
130 remoting.ClientSession.EventHandler.prototype.onDisconnected =
131     function(reason) {};
133 // Note that the positive values in both of these enums are copied directly
134 // from connection_to_host.h and must be kept in sync. Code in
135 // chromoting_instance.cc converts the C++ enums into strings that must match
136 // the names given here.
137 // The negative values represent state transitions that occur within the
138 // web-app that have no corresponding plugin state transition.
139 /** @enum {number} */
140 remoting.ClientSession.State = {
141   CONNECTION_CANCELED: -3,  // Connection closed (gracefully) before connecting.
142   CONNECTION_DROPPED: -2,  // Succeeded, but subsequently closed with an error.
143   CREATED: -1,
144   UNKNOWN: 0,
145   INITIALIZING: 1,
146   CONNECTING: 2,
147   AUTHENTICATED: 3,
148   CONNECTED: 4,
149   CLOSED: 5,
150   FAILED: 6
154  * @param {string} state The state name.
155  * @return {remoting.ClientSession.State} The session state enum value.
156  */
157 remoting.ClientSession.State.fromString = function(state) {
158   if (!remoting.ClientSession.State.hasOwnProperty(state)) {
159     throw "Invalid ClientSession.State: " + state;
160   }
161   return remoting.ClientSession.State[state];
164 /** @enum {number} */
165 remoting.ClientSession.ConnectionError = {
166   UNKNOWN: -1,
167   NONE: 0,
168   HOST_IS_OFFLINE: 1,
169   SESSION_REJECTED: 2,
170   INCOMPATIBLE_PROTOCOL: 3,
171   NETWORK_FAILURE: 4,
172   HOST_OVERLOAD: 5
176  * @param {string} error The connection error name.
177  * @return {remoting.ClientSession.ConnectionError} The connection error enum.
178  */
179 remoting.ClientSession.ConnectionError.fromString = function(error) {
180   if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
181     console.error('Unexpected ClientSession.ConnectionError string: ', error);
182     return remoting.ClientSession.ConnectionError.UNKNOWN;
183   }
184   return remoting.ClientSession.ConnectionError[error];
188  * Type used for performance statistics collected by the plugin.
189  * @constructor
190  */
191 remoting.ClientSession.PerfStats = function() {};
192 /** @type {number} */
193 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
194 /** @type {number} */
195 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
196 /** @type {number} */
197 remoting.ClientSession.PerfStats.prototype.captureLatency;
198 /** @type {number} */
199 remoting.ClientSession.PerfStats.prototype.encodeLatency;
200 /** @type {number} */
201 remoting.ClientSession.PerfStats.prototype.decodeLatency;
202 /** @type {number} */
203 remoting.ClientSession.PerfStats.prototype.renderLatency;
204 /** @type {number} */
205 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
207 // Keys for connection statistics.
208 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
209 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
210 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
211 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
212 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
213 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
214 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
217  * Set of capabilities for which hasCapability() can be used to test.
219  * @enum {string}
220  */
221 remoting.ClientSession.Capability = {
222   // When enabled this capability causes the client to send its screen
223   // resolution to the host once connection has been established. See
224   // this.plugin_.notifyClientResolution().
225   SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
227   // Let the host know that we're interested in knowing whether or not it
228   // rate limits desktop-resize requests.
229   // TODO(kelvinp): This has been supported since M-29.  Currently we only have
230   // <1000 users on M-29 or below. Remove this and the capability on the host.
231   RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
233   // Indicates native touch input support. If the host does not support
234   // touch then the client will let Chrome synthesize mouse events from touch
235   // input, for compatibility with non-touch-aware systems.
236   TOUCH_EVENTS: 'touchEvents',
238   // Indicates that host/client supports Google Drive integration, and that the
239   // client should send to the host the OAuth tokens to be used by Google Drive
240   // on the host.
241   GOOGLE_DRIVE: 'googleDrive',
243   // Indicates that the client supports the video frame-recording extension.
244   VIDEO_RECORDER: 'videoRecorder',
246   // Indicates that the client supports 'cast'ing the video stream to a
247   // cast-enabled device.
248   CAST: 'casting',
252  * Connects to |host| using |credentialsProvider| as the credentails.
254  * @param {remoting.Host} host
255  * @param {remoting.CredentialsProvider} credentialsProvider
256  */
257 remoting.ClientSession.prototype.connect = function(host, credentialsProvider) {
258   this.host_ = host;
259   this.credentialsProvider_ = credentialsProvider;
260   this.iqFormatter_ =
261       new remoting.FormatIq(this.signalStrategy_.getJid(), host.jabberId);
262   this.plugin_.connect(this.host_, this.signalStrategy_.getJid(),
263                        credentialsProvider);
267  * Disconnect the current session with a particular |error|.  The session will
268  * raise a |stateChanged| event in response to it.  The caller should then call
269  * dispose() to remove and destroy the <embed> element.
271  * @param {!remoting.Error} error The reason for the disconnection.  Use
272  *    remoting.Error.none() if there is no error.
273  * @return {void} Nothing.
274  */
275 remoting.ClientSession.prototype.disconnect = function(error) {
276   if (this.isFinished()) {
277     // Do not send the session-terminate Iq if disconnect() is already called or
278     // if it is initiated by the host.
279     return;
280   }
282   this.sendIq_(
283       '<cli:iq ' +
284           'to="' + this.host_.jabberId + '" ' +
285           'type="set" ' +
286           'id="session-terminate" ' +
287           'xmlns:cli="jabber:client">' +
288         '<jingle ' +
289             'xmlns="urn:xmpp:jingle:1" ' +
290             'action="session-terminate" ' +
291             'sid="' + this.sessionId_ + '">' +
292           '<reason><success/></reason>' +
293         '</jingle>' +
294       '</cli:iq>');
296   var state = error.isNone() ?
297                   remoting.ClientSession.State.CLOSED :
298                   remoting.ClientSession.State.FAILED;
299   this.error_ = error;
300   this.setState_(state);
304  * Deletes the <embed> element from the container and disconnects.
306  * @return {void} Nothing.
307  */
308 remoting.ClientSession.prototype.dispose = function() {
309   base.dispose(this.connectedDisposables_);
310   this.connectedDisposables_ = null;
311   base.dispose(this.plugin_);
312   this.plugin_ = null;
316  * @return {remoting.ClientSession.State} The current state.
317  */
318 remoting.ClientSession.prototype.getState = function() {
319   return this.state_;
323  * @return {remoting.Logger}.
324  */
325 remoting.ClientSession.prototype.getLogger = function() {
326   return this.logger_;
330  * @return {!remoting.Error} The current error code.
331  */
332 remoting.ClientSession.prototype.getError = function() {
333   return this.error_;
337  * Drop the session when the computer is suspended for more than
338  * |suspendDurationInMS|.
340  * @param {number} suspendDurationInMS maximum duration of suspension allowed
341  *     before the session will be dropped.
342  */
343 remoting.ClientSession.prototype.dropSessionOnSuspend = function(
344     suspendDurationInMS) {
345   if (this.state_ !== remoting.ClientSession.State.CONNECTED) {
346     console.error('The session is not connected.');
347     return;
348   }
350   var suspendDetector = new remoting.SuspendDetector(suspendDurationInMS);
351   this.connectedDisposables_.add(
352       suspendDetector,
353       new base.EventHook(
354           suspendDetector, remoting.SuspendDetector.Events.resume,
355           this.disconnect.bind(
356               this, new remoting.Error(remoting.Error.Tag.CLIENT_SUSPENDED))));
360  * Called when the client receives its first frame.
362  * @return {void} Nothing.
363  */
364 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
365   this.hasReceivedFrame_ = true;
369  * @return {boolean} Whether the client has received a video buffer.
370  */
371 remoting.ClientSession.prototype.hasReceivedFrame = function() {
372   return this.hasReceivedFrame_;
376  * @param {boolean} useApiary
377  * @param {remoting.SignalStrategy} signalStrategy
378  * @return {remoting.Logger}
380  * @private
381  */
382 remoting.ClientSession.prototype.createLogger_ = function(
383     useApiary, signalStrategy) {
384   if (useApiary) {
385     var logger = new remoting.SessionLogger(
386       remoting.ChromotingEvent.Role.CLIENT,
387       remoting.TelemetryEventWriter.Client.write
388     );
389     signalStrategy.sendConnectionSetupResults(logger);
390     return logger;
391   } else {
392     return new remoting.LogToServer(signalStrategy);
393   }
397  * Sends a signaling message.
399  * @param {string} message XML string of IQ stanza to send to server.
400  * @return {void} Nothing.
401  * @private
402  */
403 remoting.ClientSession.prototype.sendIq_ = function(message) {
404   // Extract the session id, so we can close the session later.
405   var parser = new DOMParser();
406   var iqNode = parser.parseFromString(message, 'text/xml').firstChild;
407   var jingleNode = iqNode.firstChild;
408   if (jingleNode) {
409     var action = jingleNode.getAttribute('action');
410     if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
411       this.sessionId_ = jingleNode.getAttribute('sid');
412     }
413   }
415   console.log(base.timestamp() + this.iqFormatter_.prettifySendIq(message));
416   if (this.signalStrategy_.getState() !=
417       remoting.SignalStrategy.State.CONNECTED) {
418     console.log("Message above is dropped because signaling is not connected.");
419     return;
420   }
422   this.signalStrategy_.sendMessage(message);
426  * @param {string} message XML string of IQ stanza to send to server.
427  */
428 remoting.ClientSession.prototype.onOutgoingIq = function(message) {
429   this.sendIq_(message);
433  * @param {string} msg
434  */
435 remoting.ClientSession.prototype.onDebugMessage = function(msg) {
436   console.log('plugin: ' + msg.trimRight());
440  * @param {Element} message
441  * @private
442  */
443 remoting.ClientSession.prototype.onIncomingMessage_ = function(message) {
444   if (!this.plugin_) {
445     return;
446   }
447   var formatted = new XMLSerializer().serializeToString(message);
448   console.log(base.timestamp() +
449               this.iqFormatter_.prettifyReceiveIq(formatted));
450   this.plugin_.onIncomingIq(formatted);
454  * Callback that the plugin invokes to indicate that the connection
455  * status has changed.
457  * @param {remoting.ClientSession.State} status The plugin's status.
458  * @param {remoting.ClientSession.ConnectionError} error The plugin's error
459  *        state, if any.
460  */
461 remoting.ClientSession.prototype.onConnectionStatusUpdate =
462     function(status, error) {
463   if (status == remoting.ClientSession.State.FAILED) {
464     switch (error) {
465       case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
466         this.error_ = new remoting.Error(
467             remoting.Error.Tag.HOST_IS_OFFLINE);
468         break;
469       case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
470         this.error_ = new remoting.Error(
471             remoting.Error.Tag.INVALID_ACCESS_CODE);
472         break;
473       case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
474         this.error_ = new remoting.Error(
475             remoting.Error.Tag.INCOMPATIBLE_PROTOCOL);
476         break;
477       case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
478         this.error_ = new remoting.Error(
479             remoting.Error.Tag.P2P_FAILURE);
480         break;
481       case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
482         this.error_ = new remoting.Error(
483             remoting.Error.Tag.HOST_OVERLOAD);
484         break;
485       default:
486         this.error_ = remoting.Error.unexpected();
487     }
488   }
489   this.setState_(status);
493  * Callback that the plugin invokes to indicate that the connection type for
494  * a channel has changed.
496  * @param {string} channel The channel name.
497  * @param {string} connectionType The new connection type.
498  * @private
499  */
500 remoting.ClientSession.prototype.onRouteChanged =
501     function(channel, connectionType) {
502   console.log('plugin: Channel ' + channel + ' using ' +
503               connectionType + ' connection.');
504   this.logger_.setConnectionType(connectionType);
508  * Callback that the plugin invokes to indicate when the connection is
509  * ready.
511  * @param {boolean} ready True if the connection is ready.
512  */
513 remoting.ClientSession.prototype.onConnectionReady = function(ready) {
514   // TODO(jamiewalch): Currently, the logic for determining whether or not the
515   // connection is available is based solely on whether or not any video frames
516   // have been received recently. which leads to poor UX on slow connections.
517   // Re-enable this once crbug.com/435315 has been fixed.
518   var ignoreVideoChannelState = true;
519   if (ignoreVideoChannelState) {
520     console.log('Video channel ' + (ready ? '' : 'not ') + 'ready.');
521     return;
522   }
524   this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
525                   ready);
528 /** @return {boolean} */
529 remoting.ClientSession.prototype.isFinished = function() {
530   var finishedStates = [
531     remoting.ClientSession.State.CLOSED,
532     remoting.ClientSession.State.FAILED,
533     remoting.ClientSession.State.CONNECTION_CANCELED,
534     remoting.ClientSession.State.CONNECTION_DROPPED
535   ];
536   return finishedStates.indexOf(this.getState()) !== -1;
539  * @param {remoting.ClientSession.State} newState The new state for the session.
540  * @return {void} Nothing.
541  * @private
542  */
543 remoting.ClientSession.prototype.setState_ = function(newState) {
544   // If we are at a finished state, ignore further state changes.
545   if (this.isFinished()) {
546     return;
547   }
549   var oldState = this.state_;
550   this.state_ = this.translateState_(oldState, newState);
552   if (newState == remoting.ClientSession.State.CONNECTED) {
553     this.connectedDisposables_.add(
554         new base.RepeatingTimer(this.reportStatistics.bind(this), 1000));
555     if (this.plugin_.hasCapability(
556           remoting.ClientSession.Capability.TOUCH_EVENTS)) {
557       this.plugin_.enableTouchEvents(true);
558     }
559   } else if (this.isFinished()) {
560     base.dispose(this.connectedDisposables_);
561     this.connectedDisposables_ = null;
562   }
564   this.notifyStateChanges_(oldState, this.state_);
565   this.logger_.logClientSessionStateChange(this.state_, this.error_);
569  * @param {remoting.ClientSession.State} oldState The new state for the session.
570  * @param {remoting.ClientSession.State} newState The new state for the session.
571  * @private
572  */
573 remoting.ClientSession.prototype.notifyStateChanges_ =
574     function(oldState, newState) {
575   /** @type {remoting.Error} */
576   var error;
577   switch (this.state_) {
578     case remoting.ClientSession.State.CONNECTED:
579       console.log('Connection established.');
580       var connectionInfo = new remoting.ConnectionInfo(
581           this.host_, this.credentialsProvider_, this, this.plugin_);
582       this.listener_.onConnected(connectionInfo);
583       break;
585     case remoting.ClientSession.State.CONNECTING:
586       remoting.identity.getEmail().then(function(/** string */ email) {
587         console.log('Connecting as ' + email);
588       });
589       break;
591     case remoting.ClientSession.State.AUTHENTICATED:
592       console.log('Connection authenticated.');
593       break;
595     case remoting.ClientSession.State.INITIALIZING:
596       console.log('Connection initializing .');
597       break;
599     case remoting.ClientSession.State.CLOSED:
600       console.log('Connection closed.');
601       this.listener_.onDisconnected(remoting.Error.none());
602       break;
604     case remoting.ClientSession.State.CONNECTION_CANCELED:
605     case remoting.ClientSession.State.FAILED:
606       error = this.getError();
607       if (!error.isNone()) {
608         console.error('Connection failed: ' + error.toString());
609       }
610       this.listener_.onConnectionFailed(error);
611       break;
613     case remoting.ClientSession.State.CONNECTION_DROPPED:
614       error = this.getError();
615       console.error('Connection dropped: ' + error.toString());
616       this.listener_.onDisconnected(error);
617       break;
619     default:
620       console.error('Unexpected client plugin state: ' + newState);
621   }
625  * @param {remoting.ClientSession.State} previous
626  * @param {remoting.ClientSession.State} current
627  * @return {remoting.ClientSession.State}
628  * @private
629  */
630 remoting.ClientSession.prototype.translateState_ = function(previous, current) {
631   var State = remoting.ClientSession.State;
632   if (previous == State.CONNECTING || previous == State.AUTHENTICATED) {
633     if (current == State.CLOSED) {
634       return remoting.ClientSession.State.CONNECTION_CANCELED;
635     } else if (current == State.FAILED &&
636         this.error_.hasTag(remoting.Error.Tag.HOST_IS_OFFLINE) &&
637         !this.logHostOfflineErrors_) {
638       // The application requested host-offline errors to be suppressed, for
639       // example, because this connection attempt is using a cached host JID.
640       console.log('Suppressing host-offline error.');
641       return State.CONNECTION_CANCELED;
642     }
643   } else if (previous == State.CONNECTED && current == State.FAILED) {
644     return State.CONNECTION_DROPPED;
645   }
646   return current;
649 /** @private */
650 remoting.ClientSession.prototype.reportStatistics = function() {
651   this.logger_.logStatistics(this.plugin_.getPerfStats());
655  * Enable or disable logging of connection errors due to a host being offline.
656  * For example, if attempting a connection using a cached JID, host-offline
657  * errors should not be logged because the JID will be refreshed and the
658  * connection retried.
660  * @param {boolean} enable True to log host-offline errors; false to suppress.
661  */
662 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
663   this.logHostOfflineErrors_ = enable;