[Webapp Refactor] remoting.ConnectedView

This CL implements a basic UX control for a connected remote session.
DesktopRemoting will augment the functionality that it required through
DesktopConnectedView.

BUG=465878

Review URL: https://codereview.chromium.org/1001503006

Cr-Commit-Position: refs/heads/master@{#320200}
diff --git a/remoting/remoting_webapp_files.gypi b/remoting/remoting_webapp_files.gypi
index ceeff21..f787f38 100644
--- a/remoting/remoting_webapp_files.gypi
+++ b/remoting/remoting_webapp_files.gypi
@@ -141,6 +141,7 @@
       'webapp/crd/js/client_plugin_host_desktop_impl.js',
       'webapp/crd/js/client_session.js',
       'webapp/crd/js/clipboard.js',
+      'webapp/crd/js/connected_view.js',
       'webapp/crd/js/credentials_provider.js',
       'webapp/crd/js/desktop_connected_view.js',
       'webapp/crd/js/host_desktop.js',
diff --git a/remoting/webapp/crd/js/client_session.js b/remoting/webapp/crd/js/client_session.js
index 4f4b8e3..6ac117c4 100644
--- a/remoting/webapp/crd/js/client_session.js
+++ b/remoting/webapp/crd/js/client_session.js
@@ -390,8 +390,7 @@
 remoting.ClientSession.prototype.onConnectionStatusUpdate =
     function(status, error) {
   if (status == remoting.ClientSession.State.CONNECTED) {
-    remoting.desktopConnectedView.updateClientSessionUi_(this);
-
+    remoting.desktopConnectedView.onConnected();
   } else if (status == remoting.ClientSession.State.FAILED) {
     switch (error) {
       case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
diff --git a/remoting/webapp/crd/js/connected_view.js b/remoting/webapp/crd/js/connected_view.js
new file mode 100644
index 0000000..26387bb4
--- /dev/null
+++ b/remoting/webapp/crd/js/connected_view.js
@@ -0,0 +1,179 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview
+ * Implements a basic UX control for a connected remoting session.
+ */
+
+/** @suppress {duplicate} */
+var remoting = remoting || {};
+
+(function() {
+
+'use strict';
+
+/**
+ * True to enable mouse lock.
+ * This is currently disabled because the current client plugin does not
+ * properly handle mouse lock and delegated large cursors at the same time.
+ * This should be re-enabled (by removing this flag) once a version of
+ * the plugin that supports both has reached Chrome Stable channel.
+ * (crbug.com/429322).
+ *
+ * @type {boolean}
+ */
+remoting.enableMouseLock = false;
+
+/**
+ * @param {remoting.ClientPlugin} plugin
+ * @param {HTMLElement} viewportElement
+ * @param {HTMLElement} cursorElement
+ *
+ * @constructor
+ * @implements {base.Disposable}
+ */
+remoting.ConnectedView = function(plugin, viewportElement, cursorElement) {
+  /** @private */
+  this.viewportElement_ = viewportElement;
+
+  /** @private */
+  this.plugin_ = plugin;
+
+  /** private */
+  this.cursor_ = new remoting.ConnectedView.Cursor(
+      plugin, viewportElement, cursorElement);
+
+  var pluginElement = plugin.element();
+
+  /** private */
+  this.disposables_ = new base.Disposables(
+    this.cursor_,
+    new base.DomEventHook(pluginElement, 'focus',
+                          this.onPluginGotFocus_.bind(this), false),
+    new base.DomEventHook(pluginElement, 'blur',
+                          this.onPluginLostFocus_.bind(this), false),
+    new base.DomEventHook(document, 'visibilitychange',
+                          this.onVisibilityChanged_.bind(this), false)
+  );
+
+  // TODO(wez): Only allow mouse lock if the app has the pointerLock permission.
+  // Enable automatic mouse-lock.
+  if (remoting.enableMouseLock &&
+      this.plugin_.hasFeature(remoting.ClientPlugin.Feature.ALLOW_MOUSE_LOCK)) {
+    this.plugin_.allowMouseLock();
+  }
+
+  pluginElement.focus();
+};
+
+/**
+ * @return {void} Nothing.
+ */
+remoting.ConnectedView.prototype.dispose = function() {
+  base.dispose(this.disposables_);
+  this.disposables_ = null;
+  this.cursorRender_ = null;
+  this.plugin_ = null;
+};
+
+/**
+ * Called when the app window is hidden.
+ * @return {void} Nothing.
+ * @private
+ */
+remoting.ConnectedView.prototype.onVisibilityChanged_ = function() {
+  var pause = document.hidden;
+  this.plugin_.pauseVideo(pause);
+  this.plugin_.pauseAudio(pause);
+};
+
+/**
+ * Callback that the plugin invokes to indicate when the connection is
+ * ready.
+ *
+ * @param {boolean} ready True if the connection is ready.
+ */
+remoting.ConnectedView.prototype.onConnectionReady = function(ready) {
+  this.viewportElement_.classList.toggle('session-client-inactive', !ready);
+};
+
+/**
+ * Callback function called when the plugin element gets focus.
+ * @private
+ */
+remoting.ConnectedView.prototype.onPluginGotFocus_ = function() {
+  remoting.clipboard.initiateToHost();
+};
+
+/**
+ * Callback function called when the plugin element loses focus.
+ * @private
+ */
+remoting.ConnectedView.prototype.onPluginLostFocus_ = function() {
+  // Release all keys to prevent them becoming 'stuck down' on the host.
+  this.plugin_.releaseAllKeys();
+
+  // Focus should stay on the element, not (for example) the toolbar.
+  // Due to crbug.com/246335, we can't restore the focus immediately,
+  // otherwise the plugin gets confused about whether or not it has focus.
+  window.setTimeout(
+      this.plugin_.element().focus.bind(this.plugin_.element()), 0);
+};
+
+/**
+ * @param {remoting.ClientPlugin} plugin
+ * @param {HTMLElement} viewportElement
+ * @param {HTMLElement} cursorElement
+ *
+ * @constructor
+ * @implements {base.Disposable}
+ */
+remoting.ConnectedView.Cursor = function(
+    plugin, viewportElement, cursorElement) {
+  /** @private */
+  this.plugin_ = plugin;
+  /** @private */
+  this.cursorElement_ = cursorElement;
+  /** @private */
+  this.eventHook_ = new base.DomEventHook(
+      viewportElement, 'mousemove', this.onMouseMoved_.bind(this), true);
+
+  this.plugin_.setMouseCursorHandler(this.onCursorChanged_.bind(this));
+};
+
+remoting.ConnectedView.Cursor.prototype.dispose = function() {
+  base.dispose(this.eventHook_);
+  this.eventHook_ = null;
+  this.plugin_.setMouseCursorHandler(
+      /** function(string, string, number) */ base.doNothing);
+  this.plugin_ = null;
+};
+
+/**
+ * @param {string} url
+ * @param {number} hotspotX
+ * @param {number} hotspotY
+ * @private
+ */
+remoting.ConnectedView.Cursor.prototype.onCursorChanged_ = function(
+    url, hotspotX, hotspotY) {
+  this.cursorElement_.hidden = !url;
+  if (url) {
+    this.cursorElement_.style.marginLeft = '-' + hotspotX + 'px';
+    this.cursorElement_.style.marginTop = '-' + hotspotY + 'px';
+    this.cursorElement_.src = url;
+  }
+};
+
+/**
+ * @param {Event} event
+ * @private
+ */
+remoting.ConnectedView.Cursor.prototype.onMouseMoved_ = function(event) {
+  this.cursorElement_.style.top = event.offsetY + 'px';
+  this.cursorElement_.style.left = event.offsetX + 'px';
+};
+
+})();
diff --git a/remoting/webapp/crd/js/desktop_connected_view.js b/remoting/webapp/crd/js/desktop_connected_view.js
index 1c1c236..96a08ed 100644
--- a/remoting/webapp/crd/js/desktop_connected_view.js
+++ b/remoting/webapp/crd/js/desktop_connected_view.js
@@ -13,20 +13,7 @@
 var remoting = remoting || {};
 
 /**
- * True to enable mouse lock.
- * This is currently disabled because the current client plugin does not
- * properly handle mouse lock and delegated large cursors at the same time.
- * This should be re-enabled (by removing this flag) once a version of
- * the plugin that supports both has reached Chrome Stable channel.
- * (crbug.com/429322).
- *
- * @type {boolean}
- */
-remoting.enableMouseLock = false;
-
-/**
  * @param {remoting.ClientPlugin} plugin
- * @param {remoting.ClientSession} session
  * @param {HTMLElement} container
  * @param {remoting.Host} host
  * @param {remoting.DesktopConnectedView.Mode} mode The mode of this connection.
@@ -34,10 +21,10 @@
  *     when the client doesn't define any.
  * @constructor
  * @extends {base.EventSourceImpl}
+ * @implements {base.Disposable}
  */
-remoting.DesktopConnectedView = function(plugin, session, container, host, mode,
+remoting.DesktopConnectedView = function(plugin, container, host, mode,
                                          defaultRemapKeys) {
-  this.session_ = session;
 
   /** @private {HTMLElement} */
   this.container_ = container;
@@ -54,31 +41,15 @@
   /** @private {string} */
   this.defaultRemapKeys_ = defaultRemapKeys;
 
-  /** @private */
-  this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
-  /** @private */
-  this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
   /** @private {Element} */
   this.debugRegionContainer_ =
       this.container_.querySelector('.debug-region-container');
 
-  /** @private {Element} */
-  this.mouseCursorOverlay_ =
-      this.container_.querySelector('.mouse-cursor-overlay');
-
   /** @private {remoting.DesktopViewport} */
   this.viewport_ = null;
 
-  /** @type {Element} */
-  var img = this.mouseCursorOverlay_;
-  /**
-   * @param {Event} event
-   * @private
-   */
-  this.updateMouseCursorPosition_ = function(event) {
-    img.style.top = event.offsetY + 'px';
-    img.style.left = event.offsetX + 'px';
-  };
+  /** private {remoting.ConnectedView} */
+  this.view_ = null;
 
   /** @private {remoting.VideoFrameRecorder} */
   this.videoFrameRecorder_ = null;
@@ -89,6 +60,27 @@
   this.setupPlugin_();
 };
 
+/** @return {void} Nothing. */
+remoting.DesktopConnectedView.prototype.dispose = function() {
+  if (remoting.windowFrame) {
+    remoting.windowFrame.setDesktopConnectedView(null);
+  }
+  if (remoting.toolbar) {
+    remoting.toolbar.setDesktopConnectedView(null);
+  }
+  if (remoting.optionsMenu) {
+    remoting.optionsMenu.setDesktopConnectedView(null);
+  }
+
+  document.body.classList.remove('connected');
+
+  base.dispose(this.eventHooks_);
+  this.eventHooks_ = null;
+
+  base.dispose(this.viewport_);
+  this.viewport_ = null;
+};
+
 // The mode of this session.
 /** @enum {number} */
 remoting.DesktopConnectedView.Mode = {
@@ -170,15 +162,6 @@
   if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) {
     this.applyRemapKeys_(true);
   }
-
-  // TODO(wez): Only allow mouse lock if the app has the pointerLock permission.
-  // Enable automatic mouse-lock.
-  if (remoting.enableMouseLock &&
-      this.plugin_.hasFeature(remoting.ClientPlugin.Feature.ALLOW_MOUSE_LOCK)) {
-    this.plugin_.allowMouseLock();
-  }
-
-  this.plugin_.setMouseCursorHandler(this.updateMouseCursorImage_.bind(this));
 };
 
 /**
@@ -194,111 +177,47 @@
 };
 
 /**
- * Called when the app window is hidden.
- * @return {void} Nothing.
- */
-remoting.DesktopConnectedView.prototype.onVisibilityChanged_ = function() {
-  this.pauseVideo(document.hidden);
-};
-
-/**
  * Callback that the plugin invokes to indicate when the connection is
  * ready.
  *
  * @param {boolean} ready True if the connection is ready.
  */
 remoting.DesktopConnectedView.prototype.onConnectionReady = function(ready) {
-  if (!ready) {
-    this.container_.classList.add('session-client-inactive');
-  } else {
-    this.container_.classList.remove('session-client-inactive');
+  if (this.view_) {
+    this.view_.onConnectionReady(ready);
   }
 };
 
-/**
- * Deletes the <embed> element from the container, without sending a
- * session_terminate request.  This is to be called when the session was
- * disconnected by the Host.
- *
- * @return {void} Nothing.
- */
-remoting.DesktopConnectedView.prototype.removePlugin = function() {
-  if (this.plugin_) {
-    this.plugin_.element().removeEventListener(
-        'focus', this.callPluginGotFocus_, false);
-    this.plugin_.element().removeEventListener(
-        'blur', this.callPluginLostFocus_, false);
-    this.plugin_ = null;
+remoting.DesktopConnectedView.prototype.onConnected = function() {
+  document.body.classList.add('connected');
+
+  this.view_ = new remoting.ConnectedView(
+      this.plugin_, this.container_,
+      this.container_.querySelector('.mouse-cursor-overlay'));
+
+  var scrollerElement = document.getElementById('scroller');
+  this.viewport_ = new remoting.DesktopViewport(
+      scrollerElement || document.body,
+      this.plugin_.hostDesktop(),
+      this.host_.options);
+
+  if (remoting.windowFrame) {
+    remoting.windowFrame.setDesktopConnectedView(this);
+  }
+  if (remoting.toolbar) {
+    remoting.toolbar.setDesktopConnectedView(this);
+  }
+  if (remoting.optionsMenu) {
+    remoting.optionsMenu.setDesktopConnectedView(this);
   }
 
-  this.updateClientSessionUi_(null);
-};
-
-/**
- * @param {remoting.ClientSession} clientSession The active session, or null if
- *     there is no connection.
- */
-remoting.DesktopConnectedView.prototype.updateClientSessionUi_ = function(
-    clientSession) {
-  if (clientSession === null) {
-    if (remoting.windowFrame) {
-      remoting.windowFrame.setDesktopConnectedView(null);
-    }
-    if (remoting.toolbar) {
-      remoting.toolbar.setDesktopConnectedView(null);
-    }
-    if (remoting.optionsMenu) {
-      remoting.optionsMenu.setDesktopConnectedView(null);
-    }
-
-    document.body.classList.remove('connected');
-    this.container_.removeEventListener(
-        'mousemove', this.updateMouseCursorPosition_, true);
-    base.dispose(this.eventHooks_);
-    this.eventHooks_ = null;
-    base.dispose(this.viewport_);
-    this.viewport_ = null;
-  } else {
-    var scrollerElement = document.getElementById('scroller');
-    this.viewport_ = new remoting.DesktopViewport(
-        scrollerElement || document.body,
-        this.plugin_.hostDesktop(),
-        this.host_.options);
-    if (remoting.windowFrame) {
-      remoting.windowFrame.setDesktopConnectedView(this);
-    }
-    if (remoting.toolbar) {
-      remoting.toolbar.setDesktopConnectedView(this);
-    }
-    if (remoting.optionsMenu) {
-      remoting.optionsMenu.setDesktopConnectedView(this);
-    }
-
-    document.body.classList.add('connected');
-    this.container_.addEventListener(
-        'mousemove', this.updateMouseCursorPosition_, true);
-    // Activate full-screen related UX.
-    this.setFocusHandlers_();
-    this.eventHooks_ = new base.Disposables(
-      new base.DomEventHook(window, 'resize', this.onResize_.bind(this), false),
-      new base.DomEventHook(document, 'visibilitychange',
-                            this.onVisibilityChanged_.bind(this), false),
-      new remoting.Fullscreen.EventHook(this.onFullScreenChanged_.bind(this))
-    );
-    this.onFullScreenChanged_(remoting.fullscreen.isActive());
-  }
-};
-
-/**
- * Constrains the focus to the plugin element.
- * @private
- */
-remoting.DesktopConnectedView.prototype.setFocusHandlers_ = function() {
-  this.plugin_.element().addEventListener(
-      'focus', this.callPluginGotFocus_, false);
-  this.plugin_.element().addEventListener(
-      'blur', this.callPluginLostFocus_, false);
-  this.plugin_.element().focus();
+  // Activate full-screen related UX.
+  this.eventHooks_ = new base.Disposables(
+    this.view_,
+    new base.DomEventHook(window, 'resize', this.onResize_.bind(this), false),
+    new remoting.Fullscreen.EventHook(this.onFullScreenChanged_.bind(this))
+  );
+  this.onFullScreenChanged_(remoting.fullscreen.isActive());
 };
 
 /**
@@ -341,45 +260,6 @@
 };
 
 /**
- * Callback function called when the plugin element gets focus.
- */
-remoting.DesktopConnectedView.prototype.pluginGotFocus_ = function() {
-  remoting.clipboard.initiateToHost();
-};
-
-/**
- * Callback function called when the plugin element loses focus.
- */
-remoting.DesktopConnectedView.prototype.pluginLostFocus_ = function() {
-  if (this.plugin_) {
-    // Release all keys to prevent them becoming 'stuck down' on the host.
-    this.plugin_.releaseAllKeys();
-    if (this.plugin_.element()) {
-      // Focus should stay on the element, not (for example) the toolbar.
-      // Due to crbug.com/246335, we can't restore the focus immediately,
-      // otherwise the plugin gets confused about whether or not it has focus.
-      window.setTimeout(
-          this.plugin_.element().focus.bind(this.plugin_.element()), 0);
-    }
-  }
-};
-
-/**
- * @param {string} url
- * @param {number} hotspotX
- * @param {number} hotspotY
- */
-remoting.DesktopConnectedView.prototype.updateMouseCursorImage_ =
-    function(url, hotspotX, hotspotY) {
-  this.mouseCursorOverlay_.hidden = !url;
-  if (url) {
-    this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px';
-    this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px';
-    this.mouseCursorOverlay_.src = url;
-  }
-};
-
-/**
  * Sets and stores the key remapping setting for the current host.
  *
  * @param {string} remappings Comma separated list of key remappings.
@@ -469,30 +349,6 @@
   this.sendKeyCombination_([0x070046]);
 };
 
-/**
- * Requests that the host pause or resume video updates.
- *
- * @param {boolean} pause True to pause video, false to resume.
- * @return {void} Nothing.
- */
-remoting.DesktopConnectedView.prototype.pauseVideo = function(pause) {
-  if (this.plugin_) {
-    this.plugin_.pauseVideo(pause);
-  }
-};
-
-/**
- * Requests that the host pause or resume audio.
- *
- * @param {boolean} pause True to pause audio, false to resume.
- * @return {void} Nothing.
- */
-remoting.DesktopConnectedView.prototype.pauseAudio = function(pause) {
-  if (this.plugin_) {
-    this.plugin_.pauseAudio(pause)
-  }
-};
-
 remoting.DesktopConnectedView.prototype.initVideoFrameRecorder = function() {
   this.videoFrameRecorder_ = new remoting.VideoFrameRecorder(this.plugin_);
 };
diff --git a/remoting/webapp/crd/js/session_connector_impl.js b/remoting/webapp/crd/js/session_connector_impl.js
index 89250f4..9fa1770 100644
--- a/remoting/webapp/crd/js/session_connector_impl.js
+++ b/remoting/webapp/crd/js/session_connector_impl.js
@@ -356,9 +356,8 @@
   remoting.clientSession = this.clientSession_;
 
   this.connectedView_ = new remoting.DesktopConnectedView(
-      this.plugin_, this.clientSession_, this.clientContainer_, this.host_,
-      this.connectionMode_,
-      this.defaultRemapKeys_);
+      this.plugin_, this.clientContainer_, this.host_,
+      this.connectionMode_, this.defaultRemapKeys_);
   remoting.desktopConnectedView = this.connectedView_;
 
   this.clientSession_.logHostOfflineErrors(this.logHostOfflineErrors_);
@@ -388,15 +387,11 @@
   this.clientSession_ = null;
   remoting.clientSession = null;
 
-  if (this.connectedView_) {
-    this.connectedView_.removePlugin();
-  }
+  base.dispose(this.connectedView_);
   this.connectedView_ = null;
   remoting.desktopConnectedView = null;
 
-  if (this.plugin_) {
-    this.plugin_.dispose();
-  }
+  base.dispose(this.plugin_);
   this.plugin_ = null;
 };