[Chromoting] Move app-specific code out of remoting.js
[chromium-blink-merge.git] / remoting / webapp / crd / js / host_setup_dialog.js
blob40cc7d5c1067c49ec8cb5cb17c814994e96cae35
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 'use strict';
7 /** @suppress {duplicate} */
8 var remoting = remoting || {};
10 /**
11  * @param {Array<remoting.HostSetupFlow.State>} sequence Sequence of
12  *     steps for the flow.
13  * @constructor
14  */
15 remoting.HostSetupFlow = function(sequence) {
16   this.sequence_ = sequence;
17   this.currentStep_ = 0;
18   this.state_ = sequence[0];
19   this.pin = '';
20   this.consent = false;
23 /** @enum {number} */
24 remoting.HostSetupFlow.State = {
25   NONE: 0,
27   // Dialog states.
28   ASK_PIN: 1,
30   // Prompts the user to install the host package.
31   INSTALL_HOST: 2,
33   // Processing states.
34   STARTING_HOST: 3,
35   UPDATING_PIN: 4,
36   STOPPING_HOST: 5,
38   // Done states.
39   HOST_STARTED: 6,
40   UPDATED_PIN: 7,
41   HOST_STOPPED: 8,
43   // Failure states.
44   REGISTRATION_FAILED: 9,
45   START_HOST_FAILED: 10,
46   UPDATE_PIN_FAILED: 11,
47   STOP_HOST_FAILED: 12
50 /** @return {remoting.HostSetupFlow.State} Current state of the flow. */
51 remoting.HostSetupFlow.prototype.getState = function() {
52   return this.state_;
55 remoting.HostSetupFlow.prototype.switchToNextStep = function() {
56   if (this.state_ == remoting.HostSetupFlow.State.NONE) {
57     return;
58   }
60   if (this.currentStep_ < this.sequence_.length - 1) {
61     this.currentStep_ += 1;
62     this.state_ = this.sequence_[this.currentStep_];
63   } else {
64     this.state_ = remoting.HostSetupFlow.State.NONE;
65   }
68 /**
69  * @param {!remoting.Error} error
70  */
71 remoting.HostSetupFlow.prototype.switchToErrorState = function(error) {
72   if (error.hasTag(remoting.Error.Tag.CANCELLED)) {
73     // Stop the setup flow if user rejected one of the actions.
74     this.state_ = remoting.HostSetupFlow.State.NONE;
75   } else {
76     // Current step failed, so switch to corresponding error state.
77     if (this.state_ == remoting.HostSetupFlow.State.STARTING_HOST) {
78       if (error.hasTag(remoting.Error.Tag.REGISTRATION_FAILED)) {
79         this.state_ = remoting.HostSetupFlow.State.REGISTRATION_FAILED;
80       } else {
81         this.state_ = remoting.HostSetupFlow.State.START_HOST_FAILED;
82       }
83     } else if (this.state_ == remoting.HostSetupFlow.State.UPDATING_PIN) {
84       this.state_ = remoting.HostSetupFlow.State.UPDATE_PIN_FAILED;
85     } else if (this.state_ == remoting.HostSetupFlow.State.STOPPING_HOST) {
86       this.state_ = remoting.HostSetupFlow.State.STOP_HOST_FAILED;
87     } else {
88       // TODO(sergeyu): Add other error states and use them here.
89       this.state_ = remoting.HostSetupFlow.State.START_HOST_FAILED;
90     }
91   }
94 /**
95  * @param {remoting.HostController} hostController The HostController
96  *     responsible for the host daemon.
97  * @param {function(!remoting.Error)} onError Function to call when an error
98  *     occurs.
99  * @constructor
100  */
101 remoting.HostSetupDialog = function(hostController, onError) {
102   this.hostController_ = hostController;
103   this.onError_ = onError;
105   this.pinEntry_ = document.getElementById('daemon-pin-entry');
106   this.pinConfirm_ = document.getElementById('daemon-pin-confirm');
107   this.pinErrorDiv_ = document.getElementById('daemon-pin-error-div');
108   this.pinErrorMessage_ = document.getElementById('daemon-pin-error-message');
110   /** @type {remoting.HostSetupFlow} */
111   this.flow_ = new remoting.HostSetupFlow([remoting.HostSetupFlow.State.NONE]);
113   /** @type {remoting.HostSetupDialog} */
114   var that = this;
115   /** @param {Event} event The event. */
116   var onPinSubmit = function(event) {
117     event.preventDefault();
118     that.onPinSubmit_();
119   };
120   var onPinConfirmFocus = function() {
121     that.validatePin_();
122   };
124   var form = document.getElementById('ask-pin-form');
125   form.addEventListener('submit', onPinSubmit, false);
126   /** @param {Event} event The event. */
127   var onDaemonPinEntryKeyPress = function(event) {
128     if (event.which == 13) {
129       document.getElementById('daemon-pin-confirm').focus();
130       event.preventDefault();
131     }
132   };
133   /** @param {Event} event A keypress event. */
134   var noDigitsInPin = function(event) {
135     if (event.which == 13) {
136       return;  // Otherwise the "submit" action can't be triggered by Enter.
137     }
138     if ((event.which >= 48) && (event.which <= 57)) {
139       return;
140     }
141     event.preventDefault();
142   };
143   this.pinEntry_.addEventListener('keypress', onDaemonPinEntryKeyPress, false);
144   this.pinEntry_.addEventListener('keypress', noDigitsInPin, false);
145   this.pinConfirm_.addEventListener('focus', onPinConfirmFocus, false);
146   this.pinConfirm_.addEventListener('keypress', noDigitsInPin, false);
148   this.usageStats_ = document.getElementById('usagestats-consent');
149   this.usageStatsCheckbox_ = /** @type {HTMLInputElement} */
150       (document.getElementById('usagestats-consent-checkbox'));
154  * Show the dialog in order to get a PIN prior to starting the daemon. When the
155  * user clicks OK, the dialog shows a spinner until the daemon has started.
157  * @return {void} Nothing.
158  */
159 remoting.HostSetupDialog.prototype.showForStart = function() {
160   /** @type {remoting.HostSetupDialog} */
161   var that = this;
163   /**
164    * @param {remoting.HostController.State} state
165    */
166   var onState = function(state) {
167     // Although we don't need an access token in order to start the host,
168     // using callWithToken here ensures consistent error handling in the
169     // case where the refresh token is invalid.
170     remoting.identity.getToken().then(
171         that.showForStartWithToken_.bind(that, state),
172         remoting.Error.handler(that.onError_));
173   };
175   this.hostController_.getLocalHostState(onState);
179  * @param {remoting.HostController.State} state The current state of the local
180  *     host.
181  * @param {string} token The OAuth2 token.
182  * @private
183  */
184 remoting.HostSetupDialog.prototype.showForStartWithToken_ =
185     function(state, token) {
186   /** @type {remoting.HostSetupDialog} */
187   var that = this;
189   /**
190    * @param {boolean} supported True if crash dump reporting is supported by
191    *     the host.
192    * @param {boolean} allowed True if crash dump reporting is allowed.
193    * @param {boolean} set_by_policy True if crash dump reporting is controlled
194    *     by policy.
195    */
196   function onGetConsent(supported, allowed, set_by_policy) {
197     // Hide the usage stats check box if it is not supported or the policy
198     // doesn't allow usage stats collection.
199     var checkBoxLabel = that.usageStats_.querySelector('.checkbox-label');
200     that.usageStats_.hidden = !supported || (set_by_policy && !allowed);
201     that.usageStatsCheckbox_.checked = allowed;
203     that.usageStatsCheckbox_.disabled = set_by_policy;
204     checkBoxLabel.classList.toggle('disabled', set_by_policy);
206     if (set_by_policy) {
207       that.usageStats_.title = l10n.getTranslationOrError(
208           /*i18n-content*/ 'SETTING_MANAGED_BY_POLICY');
209     } else {
210       that.usageStats_.title = '';
211     }
212   }
214   /** @param {!remoting.Error} error */
215   function onError(error) {
216     console.error('Error getting consent status: ' + error.toString());
217   }
219   this.usageStats_.hidden = true;
220   this.usageStatsCheckbox_.checked = false;
222   // Prevent user from ticking the box until the current consent status is
223   // known.
224   this.usageStatsCheckbox_.disabled = true;
226   this.hostController_.getConsent(onGetConsent, onError);
228   var flow = [
229       remoting.HostSetupFlow.State.INSTALL_HOST,
230       remoting.HostSetupFlow.State.ASK_PIN,
231       remoting.HostSetupFlow.State.STARTING_HOST,
232       remoting.HostSetupFlow.State.HOST_STARTED];
234   var installed =
235       state != remoting.HostController.State.NOT_INSTALLED &&
236       state != remoting.HostController.State.UNKNOWN;
238   // Skip the installation step when the host is already installed.
239   if (installed) {
240     flow.shift();
241   }
243   this.startNewFlow_(flow);
247  * Show the dialog in order to change the PIN associated with a running daemon.
249  * @return {void} Nothing.
250  */
251 remoting.HostSetupDialog.prototype.showForPin = function() {
252   this.usageStats_.hidden = true;
253   this.startNewFlow_(
254       [remoting.HostSetupFlow.State.ASK_PIN,
255        remoting.HostSetupFlow.State.UPDATING_PIN,
256        remoting.HostSetupFlow.State.UPDATED_PIN]);
260  * Show the dialog in order to stop the daemon.
262  * @return {void} Nothing.
263  */
264 remoting.HostSetupDialog.prototype.showForStop = function() {
265   // TODO(sergeyu): Add another step to unregister the host, crubg.com/121146 .
266   this.startNewFlow_(
267       [remoting.HostSetupFlow.State.STOPPING_HOST,
268        remoting.HostSetupFlow.State.HOST_STOPPED]);
272  * @return {void} Nothing.
273  */
274 remoting.HostSetupDialog.prototype.hide = function() {
275   remoting.setMode(remoting.AppMode.HOME);
279  * Starts new flow with the specified sequence of steps.
280  * @param {Array<remoting.HostSetupFlow.State>} sequence Sequence of steps.
281  * @private
282  */
283 remoting.HostSetupDialog.prototype.startNewFlow_ = function(sequence) {
284   this.flow_ = new remoting.HostSetupFlow(sequence);
285   this.pinEntry_.value = '';
286   this.pinConfirm_.value = '';
287   this.pinErrorDiv_.hidden = true;
288   this.updateState_();
292  * Updates current UI mode according to the current state of the setup
293  * flow and start the action corresponding to the current step (if
294  * any).
295  * @private
296  */
297 remoting.HostSetupDialog.prototype.updateState_ = function() {
298   remoting.updateLocalHostState();
300   /** @param {string} tag1
301    *  @param {string=} opt_tag2 */
302   function showDoneMessage(tag1, opt_tag2) {
303     var messageDiv = document.getElementById('host-setup-done-message');
304     l10n.localizeElementFromTag(messageDiv, tag1);
305     messageDiv = document.getElementById('host-setup-done-message-2');
306     if (opt_tag2) {
307       l10n.localizeElementFromTag(messageDiv, opt_tag2);
308     } else {
309       messageDiv.innerText = '';
310     }
311     remoting.setMode(remoting.AppMode.HOST_SETUP_DONE);
312   }
313   /** @param {string} tag */
314   function showErrorMessage(tag) {
315     var errorDiv = document.getElementById('host-setup-error-message');
316     l10n.localizeElementFromTag(errorDiv, tag);
317     remoting.setMode(remoting.AppMode.HOST_SETUP_ERROR);
318   }
320   var state = this.flow_.getState();
321   if (state == remoting.HostSetupFlow.State.NONE) {
322     this.hide();
323   } else if (state == remoting.HostSetupFlow.State.ASK_PIN) {
324     remoting.setMode(remoting.AppMode.HOST_SETUP_ASK_PIN);
325   } else if (state == remoting.HostSetupFlow.State.INSTALL_HOST) {
326     this.installHost_();
327   } else if (state == remoting.HostSetupFlow.State.STARTING_HOST) {
328     remoting.showSetupProcessingMessage(/*i18n-content*/'HOST_SETUP_STARTING');
329     this.startHost_();
330   } else if (state == remoting.HostSetupFlow.State.UPDATING_PIN) {
331     remoting.showSetupProcessingMessage(
332         /*i18n-content*/'HOST_SETUP_UPDATING_PIN');
333     this.updatePin_();
334   } else if (state == remoting.HostSetupFlow.State.STOPPING_HOST) {
335     remoting.showSetupProcessingMessage(/*i18n-content*/'HOST_SETUP_STOPPING');
336     this.stopHost_();
337   } else if (state == remoting.HostSetupFlow.State.HOST_STARTED) {
338     // TODO(jamiewalch): Only display the second string if the computer's power
339     // management settings indicate that it's necessary.
340     showDoneMessage(/*i18n-content*/'HOST_SETUP_STARTED',
341                     /*i18n-content*/'HOST_SETUP_STARTED_DISABLE_SLEEP');
342   } else if (state == remoting.HostSetupFlow.State.UPDATED_PIN) {
343     showDoneMessage(/*i18n-content*/'HOST_SETUP_UPDATED_PIN');
344   } else if (state == remoting.HostSetupFlow.State.HOST_STOPPED) {
345     showDoneMessage(/*i18n-content*/'HOST_SETUP_STOPPED');
346   } else if (state == remoting.HostSetupFlow.State.REGISTRATION_FAILED) {
347     showErrorMessage(/*i18n-content*/'ERROR_HOST_REGISTRATION_FAILED');
348   } else if (state == remoting.HostSetupFlow.State.START_HOST_FAILED) {
349     showErrorMessage(/*i18n-content*/'HOST_SETUP_HOST_FAILED');
350   } else if (state == remoting.HostSetupFlow.State.UPDATE_PIN_FAILED) {
351     showErrorMessage(/*i18n-content*/'HOST_SETUP_UPDATE_PIN_FAILED');
352   } else if (state == remoting.HostSetupFlow.State.STOP_HOST_FAILED) {
353     showErrorMessage(/*i18n-content*/'HOST_SETUP_STOP_FAILED');
354   }
358  * Shows the prompt that asks the user to install the host.
359  */
360 remoting.HostSetupDialog.prototype.installHost_ = function() {
361   /** @type {remoting.HostSetupDialog} */
362   var that = this;
363   /** @type {remoting.HostSetupFlow} */
364   var flow = this.flow_;
366   /** @param {!remoting.Error} error */
367   var onError = function(error) {
368     flow.switchToErrorState(error);
369     that.updateState_();
370   };
372   var onDone = function() {
373     that.hostController_.getLocalHostState(onHostState);
374   };
376   /** @param {remoting.HostController.State} state */
377   var onHostState = function(state) {
378     var installed =
379         state != remoting.HostController.State.NOT_INSTALLED &&
380         state != remoting.HostController.State.UNKNOWN;
382     if (installed) {
383       that.flow_.switchToNextStep();
384       that.updateState_();
385     } else {
386       // Prompt the user again if the host is not installed.
387       hostInstallDialog.tryAgain();
388     }
389   };
391   /** @type {remoting.HostInstallDialog} */
392   var hostInstallDialog = new remoting.HostInstallDialog();
393   hostInstallDialog.show(onDone, onError);
397  * Registers and starts the host.
398  */
399 remoting.HostSetupDialog.prototype.startHost_ = function() {
400   /** @type {remoting.HostSetupDialog} */
401   var that = this;
402   /** @type {remoting.HostSetupFlow} */
403   var flow = this.flow_;
405   /** @return {boolean} */
406   function isFlowActive() {
407     if (flow !== that.flow_ ||
408         flow.getState() != remoting.HostSetupFlow.State.STARTING_HOST) {
409       console.error('Host setup was interrupted when starting the host');
410       return false;
411     }
412     return true;
413   }
415   function onHostStarted() {
416     if (isFlowActive()) {
417       flow.switchToNextStep();
418       that.updateState_();
419     }
420   }
422   /** @param {!remoting.Error} error */
423   function onError(error) {
424     if (isFlowActive()) {
425       flow.switchToErrorState(error);
426       that.updateState_();
427     }
428   }
430   this.hostController_.start(this.flow_.pin, this.flow_.consent, onHostStarted,
431                              onError);
434 remoting.HostSetupDialog.prototype.updatePin_ = function() {
435   /** @type {remoting.HostSetupDialog} */
436   var that = this;
437   /** @type {remoting.HostSetupFlow} */
438   var flow = this.flow_;
440   /** @return {boolean} */
441   function isFlowActive() {
442     if (flow !== that.flow_ ||
443         flow.getState() != remoting.HostSetupFlow.State.UPDATING_PIN) {
444       console.error('Host setup was interrupted when updating PIN');
445       return false;
446     }
447     return true;
448   }
450   function onPinUpdated() {
451     if (isFlowActive()) {
452       flow.switchToNextStep();
453       that.updateState_();
454     }
455   }
457   /** @param {!remoting.Error} error */
458   function onError(error) {
459     if (isFlowActive()) {
460       flow.switchToErrorState(error);
461       that.updateState_();
462     }
463   }
465   this.hostController_.updatePin(flow.pin, onPinUpdated, onError);
469  * Stops the host.
470  */
471 remoting.HostSetupDialog.prototype.stopHost_ = function() {
472   /** @type {remoting.HostSetupDialog} */
473   var that = this;
474   /** @type {remoting.HostSetupFlow} */
475   var flow = this.flow_;
477   /** @return {boolean} */
478   function isFlowActive() {
479     if (flow !== that.flow_ ||
480         flow.getState() != remoting.HostSetupFlow.State.STOPPING_HOST) {
481       console.error('Host setup was interrupted when stopping the host');
482       return false;
483     }
484     return true;
485   }
487   function onHostStopped() {
488     if (isFlowActive()) {
489       flow.switchToNextStep();
490       that.updateState_();
491     }
492   }
494   /** @param {!remoting.Error} error */
495   function onError(error) {
496     if (isFlowActive()) {
497       flow.switchToErrorState(error);
498       that.updateState_();
499     }
500   }
502   this.hostController_.stop(onHostStopped, onError);
506  * Validates the PIN and shows an error message if it's invalid.
507  * @return {boolean} true if the PIN is valid, false otherwise.
508  * @private
509  */
510 remoting.HostSetupDialog.prototype.validatePin_ = function() {
511   var pin = this.pinEntry_.value;
512   var pinIsValid = remoting.HostSetupDialog.validPin_(pin);
513   if (!pinIsValid) {
514     l10n.localizeElementFromTag(
515         this.pinErrorMessage_, /*i18n-content*/'INVALID_PIN');
516   }
517   this.pinErrorDiv_.hidden = pinIsValid;
518   return pinIsValid;
521 /** @private */
522 remoting.HostSetupDialog.prototype.onPinSubmit_ = function() {
523   if (this.flow_.getState() != remoting.HostSetupFlow.State.ASK_PIN) {
524     console.error('PIN submitted in an invalid state', this.flow_.getState());
525     return;
526   }
527   var pin1 = this.pinEntry_.value;
528   var pin2 = this.pinConfirm_.value;
529   if (pin1 != pin2) {
530     l10n.localizeElementFromTag(
531         this.pinErrorMessage_, /*i18n-content*/'PINS_NOT_EQUAL');
532     this.pinErrorDiv_.hidden = false;
533     this.prepareForPinEntry_();
534     return;
535   }
536   if (!this.validatePin_()) {
537     this.prepareForPinEntry_();
538     return;
539   }
540   this.flow_.pin = pin1;
541   this.flow_.consent = !this.usageStats_.hidden &&
542       this.usageStatsCheckbox_.checked;
543   this.flow_.switchToNextStep();
544   this.updateState_();
547 /** @private */
548 remoting.HostSetupDialog.prototype.prepareForPinEntry_ = function() {
549   this.pinEntry_.value = '';
550   this.pinConfirm_.value = '';
551   this.pinEntry_.focus();
555  * Returns whether a PIN is valid.
557  * @private
558  * @param {string} pin A PIN.
559  * @return {boolean} Whether the PIN is valid.
560  */
561 remoting.HostSetupDialog.validPin_ = function(pin) {
562   if (pin.length < 6) {
563     return false;
564   }
565   for (var i = 0; i < pin.length; i++) {
566     var c = pin.charAt(i);
567     if ((c < '0') || (c > '9')) {
568       return false;
569     }
570   }
571   return true;
574 /** @type {remoting.HostSetupDialog} */
575 remoting.hostSetupDialog = null;