Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / crd / js / desktop_connected_view.js
blobdde6821a4e0735a6fd3f3d0443f87ac0fd8af49c
1 // Copyright 2015 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 user-facing aspects of the client session.
8  */
10 'use strict';
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
15 /**
16  * True to enable mouse lock.
17  * This is currently disabled because the current client plugin does not
18  * properly handle mouse lock and delegated large cursors at the same time.
19  * This should be re-enabled (by removing this flag) once a version of
20  * the plugin that supports both has reached Chrome Stable channel.
21  * (crbug.com/429322).
22  *
23  * @type {boolean}
24  */
25 remoting.enableMouseLock = false;
27 /**
28  * @param {remoting.ClientSession} session
29  * @param {HTMLElement} container
30  * @param {remoting.Host} host
31  * @param {remoting.DesktopConnectedView.Mode} mode The mode of this connection.
32  * @param {string} defaultRemapKeys The default set of remap keys, to use
33  *     when the client doesn't define any.
34  * @param {function(remoting.Error, remoting.ClientPlugin): void} onInitialized
35  * @constructor
36  * @extends {base.EventSourceImpl}
37  */
38 remoting.DesktopConnectedView = function(session, container, host, mode,
39                                          defaultRemapKeys, onInitialized) {
40   this.session_ = session;
42   /** @type {HTMLElement} @private */
43   this.container_ = container;
45   /** @type {remoting.ClientPlugin} @private */
46   this.plugin_ = null;
48   /** @private */
49   this.host_ = host;
51   /** @private */
52   this.mode_ = mode;
54   /** @type {string} @private */
55   this.defaultRemapKeys_ = defaultRemapKeys;
57   /**
58    * Called when the UI is finished initializing.
59    * @type {function(remoting.Error, remoting.ClientPlugin):void}
60    */
61   this.onInitialized_ = onInitialized;
63   /** @type {function(boolean=):void} @private */
64   this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
66   /** @private */
67   this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
68   /** @private */
69   this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
70   /** @type {Element} @private */
71   this.debugRegionContainer_ =
72       this.container_.querySelector('.debug-region-container');
74   /** @type {Element} @private */
75   this.mouseCursorOverlay_ =
76       this.container_.querySelector('.mouse-cursor-overlay');
78   /** @private {remoting.DesktopViewport} */
79   this.viewport_ = null;
81   /** @type {Element} */
82   var img = this.mouseCursorOverlay_;
83   /** @param {Event} event @private */
84   this.updateMouseCursorPosition_ = function(event) {
85     img.style.top = event.offsetY + 'px';
86     img.style.left = event.offsetX + 'px';
87   };
89   /** @type {remoting.VideoFrameRecorder} @private */
90   this.videoFrameRecorder_ = null;
93 // The mode of this session.
94 /** @enum {number} */
95 remoting.DesktopConnectedView.Mode = {
96   IT2ME: 0,
97   ME2ME: 1,
98   APP_REMOTING: 2
101 // Keys for per-host settings.
102 remoting.DesktopConnectedView.KEY_REMAP_KEYS = 'remapKeys';
103 remoting.DesktopConnectedView.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
104 remoting.DesktopConnectedView.KEY_SHRINK_TO_FIT = 'shrinkToFit';
105 remoting.DesktopConnectedView.KEY_DESKTOP_SCALE = 'desktopScale';
108  * Get host display name.
110  * @return {string}
111  */
112 remoting.DesktopConnectedView.prototype.getHostDisplayName = function() {
113   return this.host_.hostName;
117  * @return {remoting.DesktopConnectedView.Mode} The current state.
118  */
119 remoting.DesktopConnectedView.prototype.getMode = function() {
120   return this.mode_;
124  * @return {boolean} True if shrink-to-fit is enabled; false otherwise.
125  */
126 remoting.DesktopConnectedView.prototype.getShrinkToFit = function() {
127   if (this.viewport_) {
128     return this.viewport_.getShrinkToFit();
129   }
130   return false;
134  * @return {boolean} True if resize-to-client is enabled; false otherwise.
135  */
136 remoting.DesktopConnectedView.prototype.getResizeToClient = function() {
137   if (this.viewport_) {
138     return this.viewport_.getResizeToClient();
139   }
140   return false;
144  * @return {Element} The element that should host the plugin.
145  * @private
146  */
147 remoting.DesktopConnectedView.prototype.getPluginContainer_ = function() {
148   return this.container_.querySelector('.client-plugin-container');
151 /** @return {remoting.DesktopViewport} */
152 remoting.DesktopConnectedView.prototype.getViewportForTesting = function() {
153   return this.viewport_;
157  * Adds <embed> element to the UI container and readies the session object.
159  * @param {function(string, string):boolean} onExtensionMessage The handler for
160  *     protocol extension messages. Returns true if a message is recognized;
161  *     false otherwise.
162  * @param {Array<string>} requiredCapabilities A list of capabilities
163  *     required by this application.
164  */
165 remoting.DesktopConnectedView.prototype.createPluginAndConnect =
166     function(onExtensionMessage, requiredCapabilities) {
167   this.plugin_ = remoting.ClientPlugin.factory.createPlugin(
168       this.getPluginContainer_(),
169       onExtensionMessage, requiredCapabilities);
170   var that = this;
171   this.host_.options.load().then(function(){
172     that.plugin_.initialize(that.onPluginInitialized_.bind(that));
173   });
177  * @param {boolean} initialized
178  */
179 remoting.DesktopConnectedView.prototype.onPluginInitialized_ = function(
180     initialized) {
181   if (!initialized) {
182     console.error('ERROR: remoting plugin not loaded');
183     this.onInitialized_(remoting.Error.MISSING_PLUGIN, this.plugin_);
184     return;
185   }
187   if (!this.plugin_.isSupportedVersion()) {
188     this.onInitialized_(remoting.Error.BAD_PLUGIN_VERSION, this.plugin_);
189     return;
190   }
192   // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
193   // and the Ctrl-Alt-Del button only in Me2Me mode.
194   if (!this.plugin_.hasFeature(
195           remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
196     var sendKeysElement = document.getElementById('send-keys-menu');
197     sendKeysElement.hidden = true;
198   } else if (this.mode_ != remoting.DesktopConnectedView.Mode.ME2ME &&
199       this.mode_ != remoting.DesktopConnectedView.Mode.APP_REMOTING) {
200     var sendCadElement = document.getElementById('send-ctrl-alt-del');
201     sendCadElement.hidden = true;
202   }
204   // Apply customized key remappings if the plugin supports remapKeys.
205   if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) {
206     this.applyRemapKeys_(true);
207   }
209   // TODO(wez): Only allow mouse lock if the app has the pointerLock permission.
210   // Enable automatic mouse-lock.
211   if (remoting.enableMouseLock &&
212       this.plugin_.hasFeature(remoting.ClientPlugin.Feature.ALLOW_MOUSE_LOCK)) {
213     this.plugin_.allowMouseLock();
214   }
216   this.plugin_.setMouseCursorHandler(this.updateMouseCursorImage_.bind(this));
218   this.onInitialized_(remoting.Error.NONE, this.plugin_);
222  * This is a callback that gets called when the window is resized.
224  * @return {void} Nothing.
225  */
226 remoting.DesktopConnectedView.prototype.onResize = function() {
227   if (this.viewport_) {
228     this.viewport_.onResize();
229   }
233  * Callback that the plugin invokes to indicate when the connection is
234  * ready.
236  * @param {boolean} ready True if the connection is ready.
237  */
238 remoting.DesktopConnectedView.prototype.onConnectionReady = function(ready) {
239   if (!ready) {
240     this.container_.classList.add('session-client-inactive');
241   } else {
242     this.container_.classList.remove('session-client-inactive');
243   }
247  * Deletes the <embed> element from the container, without sending a
248  * session_terminate request.  This is to be called when the session was
249  * disconnected by the Host.
251  * @return {void} Nothing.
252  */
253 remoting.DesktopConnectedView.prototype.removePlugin = function() {
254   if (this.plugin_) {
255     this.plugin_.element().removeEventListener(
256         'focus', this.callPluginGotFocus_, false);
257     this.plugin_.element().removeEventListener(
258         'blur', this.callPluginLostFocus_, false);
259     this.plugin_.dispose();
260     this.plugin_ = null;
261   }
263   this.updateClientSessionUi_(null);
267  * @param {remoting.ClientSession} clientSession The active session, or null if
268  *     there is no connection.
269  */
270 remoting.DesktopConnectedView.prototype.updateClientSessionUi_ = function(
271     clientSession) {
272   if (clientSession == null) {
273     if (remoting.windowFrame) {
274       remoting.windowFrame.setDesktopConnectedView(null);
275     }
276     if (remoting.toolbar) {
277       remoting.toolbar.setDesktopConnectedView(null);
278     }
279     if (remoting.optionsMenu) {
280       remoting.optionsMenu.setDesktopConnectedView(null);
281     }
283     document.body.classList.remove('connected');
284     this.container_.removeEventListener(
285         'mousemove', this.updateMouseCursorPosition_, true);
286     // Stop listening for full-screen events.
287     remoting.fullscreen.removeListener(this.callOnFullScreenChanged_);
289     base.dispose(this.viewport_);
290     this.viewport_ = null;
291   } else {
292     var scrollerElement = document.getElementById('scroller');
293     this.viewport_ = new remoting.DesktopViewport(
294         scrollerElement || document.body,
295         this.plugin_.hostDesktop(),
296         this.host_.options);
297     if (remoting.windowFrame) {
298       remoting.windowFrame.setDesktopConnectedView(this);
299     }
300     if (remoting.toolbar) {
301       remoting.toolbar.setDesktopConnectedView(this);
302     }
303     if (remoting.optionsMenu) {
304       remoting.optionsMenu.setDesktopConnectedView(this);
305     }
307     document.body.classList.add('connected');
308     this.container_.addEventListener(
309         'mousemove', this.updateMouseCursorPosition_, true);
311     // Activate full-screen related UX.
312     remoting.fullscreen.addListener(this.callOnFullScreenChanged_);
313     this.onFullScreenChanged_(remoting.fullscreen.isActive());
314     this.setFocusHandlers_();
315   }
319  * Constrains the focus to the plugin element.
320  * @private
321  */
322 remoting.DesktopConnectedView.prototype.setFocusHandlers_ = function() {
323   this.plugin_.element().addEventListener(
324       'focus', this.callPluginGotFocus_, false);
325   this.plugin_.element().addEventListener(
326       'blur', this.callPluginLostFocus_, false);
327   this.plugin_.element().focus();
331  * Set the shrink-to-fit and resize-to-client flags and save them if this is
332  * a Me2Me connection.
334  * @param {boolean} shrinkToFit True if the remote desktop should be scaled
335  *     down if it is larger than the client window; false if scroll-bars
336  *     should be added in this case.
337  * @param {boolean} resizeToClient True if window resizes should cause the
338  *     host to attempt to resize its desktop to match the client window size;
339  *     false to disable this behaviour for subsequent window resizes--the
340  *     current host desktop size is not restored in this case.
341  * @return {void} Nothing.
342  */
343 remoting.DesktopConnectedView.prototype.setScreenMode =
344     function(shrinkToFit, resizeToClient) {
345   this.viewport_.setScreenMode(shrinkToFit, resizeToClient);
349  * Called when the full-screen status has changed, either via the
350  * remoting.Fullscreen class, or via a system event such as the Escape key
352  * @param {boolean=} fullscreen True if the app is entering full-screen mode;
353  *     false if it is leaving it.
354  * @private
355  */
356 remoting.DesktopConnectedView.prototype.onFullScreenChanged_ = function (
357     fullscreen) {
358   if (this.viewport_) {
359     this.viewport_.enableBumpScroll(Boolean(fullscreen));
360   }
364  * Callback function called when the plugin element gets focus.
365  */
366 remoting.DesktopConnectedView.prototype.pluginGotFocus_ = function() {
367   remoting.clipboard.initiateToHost();
371  * Callback function called when the plugin element loses focus.
372  */
373 remoting.DesktopConnectedView.prototype.pluginLostFocus_ = function() {
374   if (this.plugin_) {
375     // Release all keys to prevent them becoming 'stuck down' on the host.
376     this.plugin_.releaseAllKeys();
377     if (this.plugin_.element()) {
378       // Focus should stay on the element, not (for example) the toolbar.
379       // Due to crbug.com/246335, we can't restore the focus immediately,
380       // otherwise the plugin gets confused about whether or not it has focus.
381       window.setTimeout(
382           this.plugin_.element().focus.bind(this.plugin_.element()), 0);
383     }
384   }
388  * @param {string} url
389  * @param {number} hotspotX
390  * @param {number} hotspotY
391  */
392 remoting.DesktopConnectedView.prototype.updateMouseCursorImage_ =
393     function(url, hotspotX, hotspotY) {
394   this.mouseCursorOverlay_.hidden = !url;
395   if (url) {
396     this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px';
397     this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px';
398     this.mouseCursorOverlay_.src = url;
399   }
403  * Sets and stores the key remapping setting for the current host.
405  * @param {string} remappings Comma separated list of key remappings.
406  */
407 remoting.DesktopConnectedView.prototype.setRemapKeys = function(remappings) {
408   // Cancel any existing remappings and apply the new ones.
409   this.applyRemapKeys_(false);
410   this.host_.options.remapKeys = remappings;
411   this.applyRemapKeys_(true);
413   // Save the new remapping setting.
414   this.host_.options.save();
418  * Applies the configured key remappings to the session, or resets them.
420  * @param {boolean} apply True to apply remappings, false to cancel them.
421  */
422 remoting.DesktopConnectedView.prototype.applyRemapKeys_ = function(apply) {
423   var remapKeys = this.host_.options.remapKeys;
424   if (remapKeys == '') {
425     remapKeys = this.defaultRemapKeys_;
426     if (remapKeys == '') {
427       return;
428     }
429   }
431   var remappings = remapKeys.split(',');
432   for (var i = 0; i < remappings.length; ++i) {
433     var keyCodes = remappings[i].split('>');
434     if (keyCodes.length != 2) {
435       console.log('bad remapKey: ' + remappings[i]);
436       continue;
437     }
438     var fromKey = parseInt(keyCodes[0], 0);
439     var toKey = parseInt(keyCodes[1], 0);
440     if (!fromKey || !toKey) {
441       console.log('bad remapKey code: ' + remappings[i]);
442       continue;
443     }
444     if (apply) {
445       console.log('remapKey 0x' + fromKey.toString(16) +
446                   '>0x' + toKey.toString(16));
447       this.plugin_.remapKey(fromKey, toKey);
448     } else {
449       console.log('cancel remapKey 0x' + fromKey.toString(16));
450       this.plugin_.remapKey(fromKey, fromKey);
451     }
452   }
456  * Sends a key combination to the remoting client, by sending down events for
457  * the given keys, followed by up events in reverse order.
459  * @param {Array<number>} keys Key codes to be sent.
460  * @return {void} Nothing.
461  * @private
462  */
463 remoting.DesktopConnectedView.prototype.sendKeyCombination_ = function(keys) {
464   for (var i = 0; i < keys.length; i++) {
465     this.plugin_.injectKeyEvent(keys[i], true);
466   }
467   for (var i = 0; i < keys.length; i++) {
468     this.plugin_.injectKeyEvent(keys[i], false);
469   }
473  * Sends a Ctrl-Alt-Del sequence to the remoting client.
475  * @return {void} Nothing.
476  */
477 remoting.DesktopConnectedView.prototype.sendCtrlAltDel = function() {
478   console.log('Sending Ctrl-Alt-Del.');
479   this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
483  * Sends a Print Screen keypress to the remoting client.
485  * @return {void} Nothing.
486  */
487 remoting.DesktopConnectedView.prototype.sendPrintScreen = function() {
488   console.log('Sending Print Screen.');
489   this.sendKeyCombination_([0x070046]);
493  * Requests that the host pause or resume video updates.
495  * @param {boolean} pause True to pause video, false to resume.
496  * @return {void} Nothing.
497  */
498 remoting.DesktopConnectedView.prototype.pauseVideo = function(pause) {
499   if (this.plugin_) {
500     this.plugin_.pauseVideo(pause);
501   }
505  * Requests that the host pause or resume audio.
507  * @param {boolean} pause True to pause audio, false to resume.
508  * @return {void} Nothing.
509  */
510 remoting.DesktopConnectedView.prototype.pauseAudio = function(pause) {
511   if (this.plugin_) {
512     this.plugin_.pauseAudio(pause)
513   }
516 remoting.DesktopConnectedView.prototype.initVideoFrameRecorder = function() {
517   this.videoFrameRecorder_ = new remoting.VideoFrameRecorder(this.plugin_);
521  * Returns true if the ClientSession can record video frames to a file.
522  * @return {boolean}
523  */
524 remoting.DesktopConnectedView.prototype.canRecordVideo = function() {
525   return !!this.videoFrameRecorder_;
529  * Returns true if the ClientSession is currently recording video frames.
530  * @return {boolean}
531  */
532 remoting.DesktopConnectedView.prototype.isRecordingVideo = function() {
533   if (!this.videoFrameRecorder_) {
534     return false;
535   }
536   return this.videoFrameRecorder_.isRecording();
540  * Starts or stops recording of video frames.
541  */
542 remoting.DesktopConnectedView.prototype.startStopRecording = function() {
543   if (this.videoFrameRecorder_) {
544     this.videoFrameRecorder_.startStopRecording();
545   }
549  * Handles protocol extension messages.
550  * @param {string} type Type of extension message.
551  * @param {Object} message The parsed extension message data.
552  * @return {boolean} True if the message was recognized, false otherwise.
553  */
554 remoting.DesktopConnectedView.prototype.handleExtensionMessage =
555     function(type, message) {
556   if (this.videoFrameRecorder_) {
557     return this.videoFrameRecorder_.handleMessage(type, message);
558   }
559   return false;
563  * Handles dirty region debug messages.
565  * @param {{rects:Array<Array<number>>}} region Dirty region of the latest
566  *     frame.
567  */
568 remoting.DesktopConnectedView.prototype.handleDebugRegion = function(region) {
569   while (this.debugRegionContainer_.firstChild) {
570     this.debugRegionContainer_.removeChild(
571         this.debugRegionContainer_.firstChild);
572   }
573   if (region.rects) {
574     var rects = region.rects;
575     for (var i = 0; i < rects.length; ++i) {
576       var rect = document.createElement('div');
577       rect.classList.add('debug-region-rect');
578       rect.style.left = rects[i][0] + 'px';
579       rect.style.top = rects[i][1] +'px';
580       rect.style.width = rects[i][2] +'px';
581       rect.style.height = rects[i][3] + 'px';
582       this.debugRegionContainer_.appendChild(rect);
583     }
584   }