// Copyright (c) 2012 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. 'use strict'; /** @suppress {duplicate} */ var remoting = remoting || {}; /** * @param {Array.} sequence Sequence of * steps for the flow. * @constructor */ remoting.HostSetupFlow = function(sequence) { this.sequence_ = sequence; this.currentStep_ = 0; this.state_ = sequence[0]; this.pin = ''; this.consent = false; }; /** @enum {number} */ remoting.HostSetupFlow.State = { NONE: 0, // Dialog states. ASK_PIN: 1, // Used on Mac OS X to prompt the user to manually install a .dmg package. INSTALL_HOST: 2, // Processing states. STARTING_HOST: 3, UPDATING_PIN: 4, STOPPING_HOST: 5, // Done states. HOST_STARTED: 6, UPDATED_PIN: 7, HOST_STOPPED: 8, // Failure states. REGISTRATION_FAILED: 9, START_HOST_FAILED: 10, UPDATE_PIN_FAILED: 11, STOP_HOST_FAILED: 12 }; /** @return {remoting.HostSetupFlow.State} Current state of the flow. */ remoting.HostSetupFlow.prototype.getState = function() { return this.state_; }; remoting.HostSetupFlow.prototype.switchToNextStep = function() { if (this.state_ == remoting.HostSetupFlow.State.NONE) { return; } if (this.currentStep_ < this.sequence_.length - 1) { this.currentStep_ += 1; this.state_ = this.sequence_[this.currentStep_]; } else { this.state_ = remoting.HostSetupFlow.State.NONE; } }; /** * @param {remoting.Error} error */ remoting.HostSetupFlow.prototype.switchToErrorState = function(error) { if (error == remoting.Error.CANCELLED) { // Stop the setup flow if user rejected one of the actions. this.state_ = remoting.HostSetupFlow.State.NONE; } else { // Current step failed, so switch to corresponding error state. if (this.state_ == remoting.HostSetupFlow.State.STARTING_HOST) { if (error == remoting.Error.REGISTRATION_FAILED) { this.state_ = remoting.HostSetupFlow.State.REGISTRATION_FAILED; } else { this.state_ = remoting.HostSetupFlow.State.START_HOST_FAILED; } } else if (this.state_ == remoting.HostSetupFlow.State.UPDATING_PIN) { this.state_ = remoting.HostSetupFlow.State.UPDATE_PIN_FAILED; } else if (this.state_ == remoting.HostSetupFlow.State.STOPPING_HOST) { this.state_ = remoting.HostSetupFlow.State.STOP_HOST_FAILED; } else { // TODO(sergeyu): Add other error states and use them here. this.state_ = remoting.HostSetupFlow.State.START_HOST_FAILED; } } }; /** * @param {remoting.HostController} hostController The HostController * responsible for the host daemon. * @constructor */ remoting.HostSetupDialog = function(hostController) { this.hostController_ = hostController; this.pinEntry_ = document.getElementById('daemon-pin-entry'); this.pinConfirm_ = document.getElementById('daemon-pin-confirm'); this.pinErrorDiv_ = document.getElementById('daemon-pin-error-div'); this.pinErrorMessage_ = document.getElementById('daemon-pin-error-message'); this.continueInstallButton_ = document.getElementById( 'host-config-install-continue'); this.cancelInstallButton_ = document.getElementById( 'host-config-install-dismiss'); this.retryInstallButton_ = document.getElementById( 'host-config-install-retry'); this.continueInstallButton_.addEventListener( 'click', this.onInstallDialogOk.bind(this), false); this.cancelInstallButton_.addEventListener( 'click', this.hide.bind(this), false); this.retryInstallButton_.addEventListener( 'click', this.onInstallDialogRetry.bind(this), false); /** @type {remoting.HostSetupFlow} */ this.flow_ = new remoting.HostSetupFlow([remoting.HostSetupFlow.State.NONE]); /** @type {remoting.HostSetupDialog} */ var that = this; /** @param {Event} event The event. */ var onPinSubmit = function(event) { event.preventDefault(); that.onPinSubmit_(); }; var onPinConfirmFocus = function() { that.validatePin_(); }; var form = document.getElementById('ask-pin-form'); form.addEventListener('submit', onPinSubmit, false); /** @param {Event} event The event. */ var onDaemonPinEntryKeyPress = function(event) { if (event.which == 13) { document.getElementById('daemon-pin-confirm').focus(); event.preventDefault(); } }; /** @param {Event} event A keypress event. */ var noDigitsInPin = function(event) { if (event.which == 13) { return; // Otherwise the "submit" action can't be triggered by Enter. } if ((event.which >= 48) && (event.which <= 57)) { return; } event.preventDefault(); }; this.pinEntry_.addEventListener('keypress', onDaemonPinEntryKeyPress, false); this.pinEntry_.addEventListener('keypress', noDigitsInPin, false); this.pinConfirm_.addEventListener('focus', onPinConfirmFocus, false); this.pinConfirm_.addEventListener('keypress', noDigitsInPin, false); this.usageStats_ = document.getElementById('usagestats-consent'); this.usageStatsCheckbox_ = /** @type {HTMLInputElement} */ document.getElementById('usagestats-consent-checkbox'); }; /** * Show the dialog in order to get a PIN prior to starting the daemon. When the * user clicks OK, the dialog shows a spinner until the daemon has started. * * @return {void} Nothing. */ remoting.HostSetupDialog.prototype.showForStart = function() { /** @type {remoting.HostSetupDialog} */ var that = this; /** * @param {remoting.HostController.State} state */ var onState = function(state) { // Although we don't need an access token in order to start the host, // using callWithToken here ensures consistent error handling in the // case where the refresh token is invalid. remoting.identity.callWithToken( that.showForStartWithToken_.bind(that, state), remoting.showErrorMessage); }; this.hostController_.getLocalHostState(onState); }; /** * @param {remoting.HostController.State} state The current state of the local * host. * @param {string} token The OAuth2 token. * @private */ remoting.HostSetupDialog.prototype.showForStartWithToken_ = function(state, token) { /** @type {remoting.HostSetupDialog} */ var that = this; /** * @param {boolean} supported True if crash dump reporting is supported by * the host. * @param {boolean} allowed True if crash dump reporting is allowed. * @param {boolean} set_by_policy True if crash dump reporting is controlled * by policy. */ function onGetConsent(supported, allowed, set_by_policy) { that.usageStats_.hidden = !supported; that.usageStatsCheckbox_.checked = allowed; that.usageStatsCheckbox_.disabled = set_by_policy; } /** @param {remoting.Error} error */ function onError(error) { console.error('Error getting consent status: ' + error); } this.usageStats_.hidden = false; this.usageStatsCheckbox_.checked = false; // Prevent user from ticking the box until the current consent status is // known. this.usageStatsCheckbox_.disabled = true; this.hostController_.getConsent(onGetConsent, onError); var flow = [ remoting.HostSetupFlow.State.ASK_PIN, remoting.HostSetupFlow.State.STARTING_HOST, remoting.HostSetupFlow.State.HOST_STARTED]; var installed = state != remoting.HostController.State.NOT_INSTALLED && state != remoting.HostController.State.INSTALLING; if (navigator.platform.indexOf('Mac') != -1 && !installed) { flow.unshift(remoting.HostSetupFlow.State.INSTALL_HOST); } this.startNewFlow_(flow); }; /** * Show the dialog in order to change the PIN associated with a running daemon. * * @return {void} Nothing. */ remoting.HostSetupDialog.prototype.showForPin = function() { this.usageStats_.hidden = true; this.startNewFlow_( [remoting.HostSetupFlow.State.ASK_PIN, remoting.HostSetupFlow.State.UPDATING_PIN, remoting.HostSetupFlow.State.UPDATED_PIN]); }; /** * Show the dialog in order to stop the daemon. * * @return {void} Nothing. */ remoting.HostSetupDialog.prototype.showForStop = function() { // TODO(sergeyu): Add another step to unregister the host, crubg.com/121146 . this.startNewFlow_( [remoting.HostSetupFlow.State.STOPPING_HOST, remoting.HostSetupFlow.State.HOST_STOPPED]); }; /** * @return {void} Nothing. */ remoting.HostSetupDialog.prototype.hide = function() { remoting.setMode(remoting.AppMode.HOME); }; /** * Starts new flow with the specified sequence of steps. * @param {Array.} sequence Sequence of steps. * @private */ remoting.HostSetupDialog.prototype.startNewFlow_ = function(sequence) { this.flow_ = new remoting.HostSetupFlow(sequence); this.pinEntry_.value = ''; this.pinConfirm_.value = ''; this.pinErrorDiv_.hidden = true; this.updateState_(); }; /** * Updates current UI mode according to the current state of the setup * flow and start the action corresponding to the current step (if * any). * @private */ remoting.HostSetupDialog.prototype.updateState_ = function() { remoting.updateLocalHostState(); /** @param {string} tag */ function showProcessingMessage(tag) { var messageDiv = document.getElementById('host-setup-processing-message'); l10n.localizeElementFromTag(messageDiv, tag); remoting.setMode(remoting.AppMode.HOST_SETUP_PROCESSING); } /** @param {string} tag1 * @param {string=} opt_tag2 */ function showDoneMessage(tag1, opt_tag2) { var messageDiv = document.getElementById('host-setup-done-message'); l10n.localizeElementFromTag(messageDiv, tag1); messageDiv = document.getElementById('host-setup-done-message-2'); if (opt_tag2) { l10n.localizeElementFromTag(messageDiv, opt_tag2); } else { messageDiv.innerText = ''; } remoting.setMode(remoting.AppMode.HOST_SETUP_DONE); } /** @param {string} tag */ function showErrorMessage(tag) { var errorDiv = document.getElementById('host-setup-error-message'); l10n.localizeElementFromTag(errorDiv, tag); remoting.setMode(remoting.AppMode.HOST_SETUP_ERROR); } var state = this.flow_.getState(); if (state == remoting.HostSetupFlow.State.NONE) { this.hide(); } else if (state == remoting.HostSetupFlow.State.ASK_PIN) { remoting.setMode(remoting.AppMode.HOST_SETUP_ASK_PIN); } else if (state == remoting.HostSetupFlow.State.INSTALL_HOST) { remoting.setMode(remoting.AppMode.HOST_SETUP_INSTALL); window.location = 'https://dl.google.com/chrome-remote-desktop/chromeremotedesktop.dmg'; } else if (state == remoting.HostSetupFlow.State.STARTING_HOST) { showProcessingMessage(/*i18n-content*/'HOST_SETUP_STARTING'); this.startHost_(); } else if (state == remoting.HostSetupFlow.State.UPDATING_PIN) { showProcessingMessage(/*i18n-content*/'HOST_SETUP_UPDATING_PIN'); this.updatePin_(); } else if (state == remoting.HostSetupFlow.State.STOPPING_HOST) { showProcessingMessage(/*i18n-content*/'HOST_SETUP_STOPPING'); this.stopHost_(); } else if (state == remoting.HostSetupFlow.State.HOST_STARTED) { // TODO(jamiewalch): Only display the second string if the computer's power // management settings indicate that it's necessary. showDoneMessage(/*i18n-content*/'HOST_SETUP_STARTED', /*i18n-content*/'HOST_SETUP_STARTED_DISABLE_SLEEP'); } else if (state == remoting.HostSetupFlow.State.UPDATED_PIN) { showDoneMessage(/*i18n-content*/'HOST_SETUP_UPDATED_PIN'); } else if (state == remoting.HostSetupFlow.State.HOST_STOPPED) { showDoneMessage(/*i18n-content*/'HOST_SETUP_STOPPED'); } else if (state == remoting.HostSetupFlow.State.REGISTRATION_FAILED) { showErrorMessage(/*i18n-content*/'ERROR_HOST_REGISTRATION_FAILED'); } else if (state == remoting.HostSetupFlow.State.START_HOST_FAILED) { showErrorMessage(/*i18n-content*/'HOST_SETUP_HOST_FAILED'); } else if (state == remoting.HostSetupFlow.State.UPDATE_PIN_FAILED) { showErrorMessage(/*i18n-content*/'HOST_SETUP_UPDATE_PIN_FAILED'); } else if (state == remoting.HostSetupFlow.State.STOP_HOST_FAILED) { showErrorMessage(/*i18n-content*/'HOST_SETUP_STOP_FAILED'); } }; /** * Registers and starts the host. */ remoting.HostSetupDialog.prototype.startHost_ = function() { /** @type {remoting.HostSetupDialog} */ var that = this; /** @type {remoting.HostSetupFlow} */ var flow = this.flow_; /** @return {boolean} */ function isFlowActive() { if (flow !== that.flow_ || flow.getState() != remoting.HostSetupFlow.State.STARTING_HOST) { console.error('Host setup was interrupted when starting the host'); return false; } return true; } function onHostStarted() { if (isFlowActive()) { flow.switchToNextStep(); that.updateState_(); } } /** @param {remoting.Error} error */ function onError(error) { if (isFlowActive()) { flow.switchToErrorState(error); that.updateState_(); } } this.hostController_.start(this.flow_.pin, this.flow_.consent, onHostStarted, onError); }; remoting.HostSetupDialog.prototype.updatePin_ = function() { /** @type {remoting.HostSetupDialog} */ var that = this; /** @type {remoting.HostSetupFlow} */ var flow = this.flow_; /** @return {boolean} */ function isFlowActive() { if (flow !== that.flow_ || flow.getState() != remoting.HostSetupFlow.State.UPDATING_PIN) { console.error('Host setup was interrupted when updating PIN'); return false; } return true; } function onPinUpdated() { if (isFlowActive()) { flow.switchToNextStep(); that.updateState_(); } } /** @param {remoting.Error} error */ function onError(error) { if (isFlowActive()) { flow.switchToErrorState(error); that.updateState_(); } } this.hostController_.updatePin(flow.pin, onPinUpdated, onError); }; /** * Stops the host. */ remoting.HostSetupDialog.prototype.stopHost_ = function() { /** @type {remoting.HostSetupDialog} */ var that = this; /** @type {remoting.HostSetupFlow} */ var flow = this.flow_; /** @return {boolean} */ function isFlowActive() { if (flow !== that.flow_ || flow.getState() != remoting.HostSetupFlow.State.STOPPING_HOST) { console.error('Host setup was interrupted when stopping the host'); return false; } return true; } function onHostStopped() { if (isFlowActive()) { flow.switchToNextStep(); that.updateState_(); } } /** @param {remoting.Error} error */ function onError(error) { if (isFlowActive()) { flow.switchToErrorState(error); that.updateState_(); } } this.hostController_.stop(onHostStopped, onError); }; /** * Validates the PIN and shows an error message if it's invalid. * @return {boolean} true if the PIN is valid, false otherwise. * @private */ remoting.HostSetupDialog.prototype.validatePin_ = function() { var pin = this.pinEntry_.value; var pinIsValid = remoting.HostSetupDialog.validPin_(pin); if (!pinIsValid) { l10n.localizeElementFromTag( this.pinErrorMessage_, /*i18n-content*/'INVALID_PIN'); } this.pinErrorDiv_.hidden = pinIsValid; return pinIsValid; }; /** @private */ remoting.HostSetupDialog.prototype.onPinSubmit_ = function() { if (this.flow_.getState() != remoting.HostSetupFlow.State.ASK_PIN) { console.error('PIN submitted in an invalid state', this.flow_.getState()); return; } var pin1 = this.pinEntry_.value; var pin2 = this.pinConfirm_.value; if (pin1 != pin2) { l10n.localizeElementFromTag( this.pinErrorMessage_, /*i18n-content*/'PINS_NOT_EQUAL'); this.pinErrorDiv_.hidden = false; this.prepareForPinEntry_(); return; } if (!this.validatePin_()) { this.prepareForPinEntry_(); return; } this.flow_.pin = pin1; this.flow_.consent = !this.usageStats_.hidden && this.usageStatsCheckbox_.checked; this.flow_.switchToNextStep(); this.updateState_(); }; /** @private */ remoting.HostSetupDialog.prototype.prepareForPinEntry_ = function() { this.pinEntry_.value = ''; this.pinConfirm_.value = ''; this.pinEntry_.focus(); }; /** * Returns whether a PIN is valid. * * @private * @param {string} pin A PIN. * @return {boolean} Whether the PIN is valid. */ remoting.HostSetupDialog.validPin_ = function(pin) { if (pin.length < 6) { return false; } for (var i = 0; i < pin.length; i++) { var c = pin.charAt(i); if ((c < '0') || (c > '9')) { return false; } } return true; }; /** * @return {void} Nothing. */ remoting.HostSetupDialog.prototype.onInstallDialogOk = function() { this.continueInstallButton_.disabled = true; this.cancelInstallButton_.disabled = true; /** @type {remoting.HostSetupDialog} */ var that = this; /** @param {remoting.HostController.State} state */ var onHostState = function(state) { that.continueInstallButton_.disabled = false; that.cancelInstallButton_.disabled = false; var installed = state != remoting.HostController.State.NOT_INSTALLED && state != remoting.HostController.State.INSTALLING; if (installed) { that.flow_.switchToNextStep(); that.updateState_(); } else { remoting.setMode(remoting.AppMode.HOST_SETUP_INSTALL_PENDING); } }; this.hostController_.getLocalHostState(onHostState); }; /** * @return {void} Nothing. */ remoting.HostSetupDialog.prototype.onInstallDialogRetry = function() { remoting.setMode(remoting.AppMode.HOST_SETUP_INSTALL); }; /** @type {remoting.HostSetupDialog} */ remoting.hostSetupDialog = null;