Home | History | Annotate | Download | only in login
      1 // Copyright 2014 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.
      4 
      5 /**
      6  * @fileoverview Display manager for WebUI OOBE and login.
      7  */
      8 
      9 // TODO(xiyuan): Find a better to share those constants.
     10 /** @const */ var SCREEN_OOBE_NETWORK = 'connect';
     11 /** @const */ var SCREEN_OOBE_HID_DETECTION = 'hid-detection';
     12 /** @const */ var SCREEN_OOBE_EULA = 'eula';
     13 /** @const */ var SCREEN_OOBE_UPDATE = 'update';
     14 /** @const */ var SCREEN_OOBE_RESET = 'reset';
     15 /** @const */ var SCREEN_OOBE_ENROLLMENT = 'oauth-enrollment';
     16 /** @const */ var SCREEN_OOBE_KIOSK_ENABLE = 'kiosk-enable';
     17 /** @const */ var SCREEN_OOBE_AUTO_ENROLLMENT_CHECK = 'auto-enrollment-check';
     18 /** @const */ var SCREEN_GAIA_SIGNIN = 'gaia-signin';
     19 /** @const */ var SCREEN_ACCOUNT_PICKER = 'account-picker';
     20 /** @const */ var SCREEN_USER_IMAGE_PICKER = 'user-image';
     21 /** @const */ var SCREEN_ERROR_MESSAGE = 'error-message';
     22 /** @const */ var SCREEN_TPM_ERROR = 'tpm-error-message';
     23 /** @const */ var SCREEN_PASSWORD_CHANGED = 'password-changed';
     24 /** @const */ var SCREEN_CREATE_MANAGED_USER_FLOW =
     25     'managed-user-creation';
     26 /** @const */ var SCREEN_APP_LAUNCH_SPLASH = 'app-launch-splash';
     27 /** @const */ var SCREEN_CONFIRM_PASSWORD = 'confirm-password';
     28 /** @const */ var SCREEN_FATAL_ERROR = 'fatal-error';
     29 /** @const */ var SCREEN_KIOSK_ENABLE = 'kiosk-enable';
     30 /** @const */ var SCREEN_TERMS_OF_SERVICE = 'terms-of-service';
     31 /** @const */ var SCREEN_WRONG_HWID = 'wrong-hwid';
     32 
     33 /* Accelerator identifiers. Must be kept in sync with webui_login_view.cc. */
     34 /** @const */ var ACCELERATOR_CANCEL = 'cancel';
     35 /** @const */ var ACCELERATOR_ENROLLMENT = 'enrollment';
     36 /** @const */ var ACCELERATOR_KIOSK_ENABLE = 'kiosk_enable';
     37 /** @const */ var ACCELERATOR_VERSION = 'version';
     38 /** @const */ var ACCELERATOR_RESET = 'reset';
     39 /** @const */ var ACCELERATOR_FOCUS_PREV = 'focus_prev';
     40 /** @const */ var ACCELERATOR_FOCUS_NEXT = 'focus_next';
     41 /** @const */ var ACCELERATOR_DEVICE_REQUISITION = 'device_requisition';
     42 /** @const */ var ACCELERATOR_DEVICE_REQUISITION_REMORA =
     43     'device_requisition_remora';
     44 /** @const */ var ACCELERATOR_APP_LAUNCH_BAILOUT = 'app_launch_bailout';
     45 /** @const */ var ACCELERATOR_APP_LAUNCH_NETWORK_CONFIG =
     46     'app_launch_network_config';
     47 
     48 /* Signin UI state constants. Used to control header bar UI. */
     49 /** @const */ var SIGNIN_UI_STATE = {
     50   HIDDEN: 0,
     51   GAIA_SIGNIN: 1,
     52   ACCOUNT_PICKER: 2,
     53   WRONG_HWID_WARNING: 3,
     54   MANAGED_USER_CREATION_FLOW: 4,
     55   SAML_PASSWORD_CONFIRM: 5,
     56 };
     57 
     58 /* Possible UI states of the error screen. */
     59 /** @const */ var ERROR_SCREEN_UI_STATE = {
     60   UNKNOWN: 'ui-state-unknown',
     61   UPDATE: 'ui-state-update',
     62   SIGNIN: 'ui-state-signin',
     63   MANAGED_USER_CREATION_FLOW: 'ui-state-locally-managed',
     64   KIOSK_MODE: 'ui-state-kiosk-mode',
     65   LOCAL_STATE_ERROR: 'ui-state-local-state-error',
     66   AUTO_ENROLLMENT_ERROR: 'ui-state-auto-enrollment-error',
     67   ROLLBACK_ERROR: 'ui-state-rollback-error'
     68 };
     69 
     70 /* Possible types of UI. */
     71 /** @const */ var DISPLAY_TYPE = {
     72   UNKNOWN: 'unknown',
     73   OOBE: 'oobe',
     74   LOGIN: 'login',
     75   LOCK: 'lock',
     76   USER_ADDING: 'user-adding',
     77   APP_LAUNCH_SPLASH: 'app-launch-splash',
     78   DESKTOP_USER_MANAGER: 'login-add-user'
     79 };
     80 
     81 cr.define('cr.ui.login', function() {
     82   var Bubble = cr.ui.Bubble;
     83 
     84   /**
     85    * Maximum time in milliseconds to wait for step transition to finish.
     86    * The value is used as the duration for ensureTransitionEndEvent below.
     87    * It needs to be inline with the step screen transition duration time
     88    * defined in css file. The current value in css is 200ms. To avoid emulated
     89    * webkitTransitionEnd fired before real one, 250ms is used.
     90    * @const
     91    */
     92   var MAX_SCREEN_TRANSITION_DURATION = 250;
     93 
     94   /**
     95    * Groups of screens (screen IDs) that should have the same dimensions.
     96    * @type Array.<Array.<string>>
     97    * @const
     98    */
     99   var SCREEN_GROUPS = [[SCREEN_OOBE_NETWORK,
    100                         SCREEN_OOBE_EULA,
    101                         SCREEN_OOBE_UPDATE,
    102                         SCREEN_OOBE_AUTO_ENROLLMENT_CHECK]
    103                       ];
    104   /**
    105    * Group of screens (screen IDs) where factory-reset screen invocation is
    106    * available.
    107    * @type Array.<string>
    108    * @const
    109    */
    110   var RESET_AVAILABLE_SCREEN_GROUP = [
    111     SCREEN_OOBE_NETWORK,
    112     SCREEN_OOBE_EULA,
    113     SCREEN_OOBE_UPDATE,
    114     SCREEN_OOBE_ENROLLMENT,
    115     SCREEN_OOBE_AUTO_ENROLLMENT_CHECK,
    116     SCREEN_GAIA_SIGNIN,
    117     SCREEN_ACCOUNT_PICKER,
    118     SCREEN_KIOSK_ENABLE,
    119     SCREEN_ERROR_MESSAGE,
    120     SCREEN_USER_IMAGE_PICKER,
    121     SCREEN_TPM_ERROR,
    122     SCREEN_PASSWORD_CHANGED,
    123     SCREEN_TERMS_OF_SERVICE,
    124     SCREEN_WRONG_HWID,
    125     SCREEN_CONFIRM_PASSWORD,
    126     SCREEN_FATAL_ERROR
    127   ];
    128 
    129   /**
    130    * Group of screens (screen IDs) that are not participating in
    131    * left-current-right animation.
    132    * @type Array.<string>
    133    * @const
    134    */
    135   var NOT_ANIMATED_SCREEN_GROUP = [
    136     SCREEN_OOBE_RESET
    137   ];
    138 
    139 
    140   /**
    141    * OOBE screens group index.
    142    */
    143   var SCREEN_GROUP_OOBE = 0;
    144 
    145   /**
    146    * Constructor a display manager that manages initialization of screens,
    147    * transitions, error messages display.
    148    *
    149    * @constructor
    150    */
    151   function DisplayManager() {
    152   }
    153 
    154   DisplayManager.prototype = {
    155     /**
    156      * Registered screens.
    157      */
    158     screens_: [],
    159 
    160     /**
    161      * Current OOBE step, index in the screens array.
    162      * @type {number}
    163      */
    164     currentStep_: 0,
    165 
    166     /**
    167      * Whether version label can be toggled by ACCELERATOR_VERSION.
    168      * @type {boolean}
    169      */
    170     allowToggleVersion_: false,
    171 
    172     /**
    173      * Whether keyboard navigation flow is enforced.
    174      * @type {boolean}
    175      */
    176     forceKeyboardFlow_: false,
    177 
    178     /**
    179      * Whether virtual keyboard is shown.
    180      * @type {boolean}
    181      */
    182     virtualKeyboardShown_: false,
    183 
    184     /**
    185      * Virtual keyboard width.
    186      * @type {number}
    187      */
    188     virtualKeyboardWidth_: 0,
    189 
    190     /**
    191      * Virtual keyboard height.
    192      * @type {number}
    193      */
    194     virtualKeyboardHeight_: 0,
    195 
    196     /**
    197      * Type of UI.
    198      * @type {string}
    199      */
    200     displayType_: DISPLAY_TYPE.UNKNOWN,
    201 
    202     /**
    203      * Error message (bubble) was shown. This is checked in tests.
    204      */
    205     errorMessageWasShownForTesting_: false,
    206 
    207     get displayType() {
    208       return this.displayType_;
    209     },
    210 
    211     set displayType(displayType) {
    212       this.displayType_ = displayType;
    213       document.documentElement.setAttribute('screen', displayType);
    214     },
    215 
    216     get newKioskUI() {
    217       return loadTimeData.getString('newKioskUI') == 'on';
    218     },
    219 
    220     /**
    221      * Returns dimensions of screen exluding header bar.
    222      * @type {Object}
    223      */
    224     get clientAreaSize() {
    225       var container = $('outer-container');
    226       return {width: container.offsetWidth, height: container.offsetHeight};
    227     },
    228 
    229     /**
    230      * Gets current screen element.
    231      * @type {HTMLElement}
    232      */
    233     get currentScreen() {
    234       return $(this.screens_[this.currentStep_]);
    235     },
    236 
    237     /**
    238      * Hides/shows header (Shutdown/Add User/Cancel buttons).
    239      * @param {boolean} hidden Whether header is hidden.
    240      */
    241     get headerHidden() {
    242       return $('login-header-bar').hidden;
    243     },
    244 
    245     set headerHidden(hidden) {
    246       $('login-header-bar').hidden = hidden;
    247     },
    248 
    249     /**
    250      * Virtual keyboard state (hidden/shown).
    251      * @param {boolean} hidden Whether keyboard is shown.
    252      */
    253     get virtualKeyboardShown() {
    254       return this.virtualKeyboardShown_;
    255     },
    256 
    257     set virtualKeyboardShown(shown) {
    258       this.virtualKeyboardShown_ = shown;
    259     },
    260 
    261     /**
    262      * Sets the current size of the virtual keyboard.
    263      * @param {number} width keyboard width
    264      * @param {number} height keyboard height
    265      */
    266     setVirtualKeyboardSize: function(width, height) {
    267       this.virtualKeyboardWidth_ = width;
    268       this.virtualKeyboardHeight_ = height;
    269     },
    270 
    271     /**
    272      * Sets the current size of the client area (display size).
    273      * @param {number} width client area width
    274      * @param {number} height client area height
    275      */
    276     setClientAreaSize: function(width, height) {
    277       var clientArea = $('outer-container');
    278       var bottom = parseInt(window.getComputedStyle(clientArea).bottom);
    279       clientArea.style.minHeight = cr.ui.toCssPx(height - bottom);
    280     },
    281 
    282     /**
    283      * Toggles background of main body between transparency and solid.
    284      * @param {boolean} solid Whether to show a solid background.
    285      */
    286     set solidBackground(solid) {
    287       if (solid)
    288         document.body.classList.add('solid');
    289       else
    290         document.body.classList.remove('solid');
    291     },
    292 
    293     /**
    294      * Forces keyboard based OOBE navigation.
    295      * @param {boolean} value True if keyboard navigation flow is forced.
    296      */
    297     set forceKeyboardFlow(value) {
    298       this.forceKeyboardFlow_ = value;
    299       if (value) {
    300         keyboard.initializeKeyboardFlow();
    301         cr.ui.DropDown.enableKeyboardFlow();
    302         for (var i = 0; i < this.screens_.length; ++i) {
    303           var screen = $(this.screens_[i]);
    304           if (screen.enableKeyboardFlow)
    305             screen.enableKeyboardFlow();
    306         }
    307       }
    308     },
    309 
    310     /**
    311      * Shows/hides version labels.
    312      * @param {boolean} show Whether labels should be visible by default. If
    313      *     false, visibility can be toggled by ACCELERATOR_VERSION.
    314      */
    315     showVersion: function(show) {
    316       $('version-labels').hidden = !show;
    317       this.allowToggleVersion_ = !show;
    318     },
    319 
    320     /**
    321      * Handle accelerators.
    322      * @param {string} name Accelerator name.
    323      */
    324     handleAccelerator: function(name) {
    325       var currentStepId = this.screens_[this.currentStep_];
    326       if (name == ACCELERATOR_CANCEL) {
    327         if (this.currentScreen.cancel) {
    328           this.currentScreen.cancel();
    329         }
    330       } else if (name == ACCELERATOR_ENROLLMENT) {
    331         if (currentStepId == SCREEN_GAIA_SIGNIN ||
    332             currentStepId == SCREEN_ACCOUNT_PICKER) {
    333           chrome.send('toggleEnrollmentScreen');
    334         } else if (currentStepId == SCREEN_OOBE_NETWORK ||
    335                    currentStepId == SCREEN_OOBE_EULA) {
    336           // In this case update check will be skipped and OOBE will
    337           // proceed straight to enrollment screen when EULA is accepted.
    338           chrome.send('skipUpdateEnrollAfterEula');
    339         } else if (currentStepId == SCREEN_OOBE_ENROLLMENT) {
    340           // This accelerator is also used to manually cancel auto-enrollment.
    341           if (this.currentScreen.cancelAutoEnrollment)
    342             this.currentScreen.cancelAutoEnrollment();
    343         }
    344       } else if (name == ACCELERATOR_KIOSK_ENABLE) {
    345         if (currentStepId == SCREEN_GAIA_SIGNIN ||
    346             currentStepId == SCREEN_ACCOUNT_PICKER) {
    347           chrome.send('toggleKioskEnableScreen');
    348         }
    349       } else if (name == ACCELERATOR_VERSION) {
    350         if (this.allowToggleVersion_)
    351           $('version-labels').hidden = !$('version-labels').hidden;
    352       } else if (name == ACCELERATOR_RESET) {
    353         if (RESET_AVAILABLE_SCREEN_GROUP.indexOf(currentStepId) != -1)
    354           chrome.send('toggleResetScreen');
    355       } else if (name == ACCELERATOR_DEVICE_REQUISITION) {
    356         if (this.isOobeUI())
    357           this.showDeviceRequisitionPrompt_();
    358       } else if (name == ACCELERATOR_DEVICE_REQUISITION_REMORA) {
    359         if (this.isOobeUI())
    360           this.showDeviceRequisitionRemoraPrompt_();
    361       } else if (name == ACCELERATOR_APP_LAUNCH_BAILOUT) {
    362         if (currentStepId == SCREEN_APP_LAUNCH_SPLASH)
    363           chrome.send('cancelAppLaunch');
    364       } else if (name == ACCELERATOR_APP_LAUNCH_NETWORK_CONFIG) {
    365         if (currentStepId == SCREEN_APP_LAUNCH_SPLASH)
    366           chrome.send('networkConfigRequest');
    367       }
    368 
    369       if (!this.forceKeyboardFlow_)
    370         return;
    371 
    372       // Handle special accelerators for keyboard enhanced navigation flow.
    373       if (name == ACCELERATOR_FOCUS_PREV)
    374         keyboard.raiseKeyFocusPrevious(document.activeElement);
    375       else if (name == ACCELERATOR_FOCUS_NEXT)
    376         keyboard.raiseKeyFocusNext(document.activeElement);
    377     },
    378 
    379     /**
    380      * Appends buttons to the button strip.
    381      * @param {Array.<HTMLElement>} buttons Array with the buttons to append.
    382      * @param {string} screenId Id of the screen that buttons belong to.
    383      */
    384     appendButtons_: function(buttons, screenId) {
    385       if (buttons) {
    386         var buttonStrip = $(screenId + '-controls');
    387         if (buttonStrip) {
    388           for (var i = 0; i < buttons.length; ++i)
    389             buttonStrip.appendChild(buttons[i]);
    390         }
    391       }
    392     },
    393 
    394     /**
    395      * Disables or enables control buttons on the specified screen.
    396      * @param {HTMLElement} screen Screen which controls should be affected.
    397      * @param {boolean} disabled Whether to disable controls.
    398      */
    399     disableButtons_: function(screen, disabled) {
    400       var buttons = document.querySelectorAll(
    401           '#' + screen.id + '-controls button:not(.preserve-disabled-state)');
    402       for (var i = 0; i < buttons.length; ++i) {
    403         buttons[i].disabled = disabled;
    404       }
    405     },
    406 
    407     screenIsAnimated_: function(screenId) {
    408       return NOT_ANIMATED_SCREEN_GROUP.indexOf(screenId) != -1;
    409     },
    410 
    411     /**
    412      * Updates a step's css classes to reflect left, current, or right position.
    413      * @param {number} stepIndex step index.
    414      * @param {string} state one of 'left', 'current', 'right'.
    415      */
    416     updateStep_: function(stepIndex, state) {
    417       var stepId = this.screens_[stepIndex];
    418       var step = $(stepId);
    419       var header = $('header-' + stepId);
    420       var states = ['left', 'right', 'current'];
    421       for (var i = 0; i < states.length; ++i) {
    422         if (states[i] != state) {
    423           step.classList.remove(states[i]);
    424           header.classList.remove(states[i]);
    425         }
    426       }
    427 
    428       step.classList.add(state);
    429       header.classList.add(state);
    430     },
    431 
    432     /**
    433      * Switches to the next OOBE step.
    434      * @param {number} nextStepIndex Index of the next step.
    435      */
    436     toggleStep_: function(nextStepIndex, screenData) {
    437       var currentStepId = this.screens_[this.currentStep_];
    438       var nextStepId = this.screens_[nextStepIndex];
    439       var oldStep = $(currentStepId);
    440       var newStep = $(nextStepId);
    441       var newHeader = $('header-' + nextStepId);
    442 
    443       // Disable controls before starting animation.
    444       this.disableButtons_(oldStep, true);
    445 
    446       if (oldStep.onBeforeHide)
    447         oldStep.onBeforeHide();
    448 
    449       $('oobe').className = nextStepId;
    450 
    451       // Need to do this before calling newStep.onBeforeShow() so that new step
    452       // is back in DOM tree and has correct offsetHeight / offsetWidth.
    453       newStep.hidden = false;
    454 
    455       if (newStep.onBeforeShow)
    456         newStep.onBeforeShow(screenData);
    457 
    458       newStep.classList.remove('hidden');
    459 
    460       if (this.isOobeUI() &&
    461           this.screenIsAnimated_(nextStepId) &&
    462           this.screenIsAnimated_(currentStepId)) {
    463         // Start gliding animation for OOBE steps.
    464         if (nextStepIndex > this.currentStep_) {
    465           for (var i = this.currentStep_; i < nextStepIndex; ++i)
    466             this.updateStep_(i, 'left');
    467           this.updateStep_(nextStepIndex, 'current');
    468         } else if (nextStepIndex < this.currentStep_) {
    469           for (var i = this.currentStep_; i > nextStepIndex; --i)
    470             this.updateStep_(i, 'right');
    471           this.updateStep_(nextStepIndex, 'current');
    472         }
    473       } else {
    474         // Start fading animation for login display or reset screen.
    475         oldStep.classList.add('faded');
    476         newStep.classList.remove('faded');
    477         if (!this.screenIsAnimated_(nextStepId)) {
    478           newStep.classList.remove('left');
    479           newStep.classList.remove('right');
    480         }
    481       }
    482 
    483       this.disableButtons_(newStep, false);
    484 
    485       // Adjust inner container height based on new step's height.
    486       this.updateScreenSize(newStep);
    487 
    488       if (newStep.onAfterShow)
    489         newStep.onAfterShow(screenData);
    490 
    491       // Workaround for gaia and network screens.
    492       // Due to other origin iframe and long ChromeVox focusing correspondingly
    493       // passive aria-label title is not pronounced.
    494       // Gaia hack can be removed on fixed crbug.com/316726.
    495       if (nextStepId == SCREEN_GAIA_SIGNIN) {
    496         newStep.setAttribute(
    497             'aria-label',
    498             loadTimeData.getString('signinScreenTitle'));
    499       } else if (nextStepId == SCREEN_OOBE_NETWORK) {
    500         newStep.setAttribute(
    501             'aria-label',
    502             loadTimeData.getString('networkScreenAccessibleTitle'));
    503       }
    504 
    505       // Default control to be focused (if specified).
    506       var defaultControl = newStep.defaultControl;
    507 
    508       var outerContainer = $('outer-container');
    509       var innerContainer = $('inner-container');
    510       var isOOBE = this.isOobeUI();
    511       if (this.currentStep_ != nextStepIndex &&
    512           !oldStep.classList.contains('hidden')) {
    513         if (oldStep.classList.contains('animated')) {
    514           innerContainer.classList.add('animation');
    515           oldStep.addEventListener('webkitTransitionEnd', function f(e) {
    516             oldStep.removeEventListener('webkitTransitionEnd', f);
    517             if (oldStep.classList.contains('faded') ||
    518                 oldStep.classList.contains('left') ||
    519                 oldStep.classList.contains('right')) {
    520               innerContainer.classList.remove('animation');
    521               oldStep.classList.add('hidden');
    522               if (!isOOBE)
    523                 oldStep.hidden = true;
    524             }
    525             // Refresh defaultControl. It could have changed.
    526             var defaultControl = newStep.defaultControl;
    527             if (defaultControl)
    528               defaultControl.focus();
    529           });
    530           ensureTransitionEndEvent(oldStep, MAX_SCREEN_TRANSITION_DURATION);
    531         } else {
    532           oldStep.classList.add('hidden');
    533           oldStep.hidden = true;
    534           if (defaultControl)
    535             defaultControl.focus();
    536         }
    537       } else {
    538         // First screen on OOBE launch.
    539         if (this.isOobeUI() && innerContainer.classList.contains('down')) {
    540           innerContainer.classList.remove('down');
    541           innerContainer.addEventListener(
    542               'webkitTransitionEnd', function f(e) {
    543                 innerContainer.removeEventListener('webkitTransitionEnd', f);
    544                 outerContainer.classList.remove('down');
    545                 $('progress-dots').classList.remove('down');
    546                 chrome.send('loginVisible', ['oobe']);
    547                 // Refresh defaultControl. It could have changed.
    548                 var defaultControl = newStep.defaultControl;
    549                 if (defaultControl)
    550                   defaultControl.focus();
    551               });
    552           ensureTransitionEndEvent(innerContainer,
    553                                    MAX_SCREEN_TRANSITION_DURATION);
    554         } else {
    555           if (defaultControl)
    556             defaultControl.focus();
    557           chrome.send('loginVisible', ['oobe']);
    558         }
    559       }
    560       this.currentStep_ = nextStepIndex;
    561 
    562       $('step-logo').hidden = newStep.classList.contains('no-logo');
    563 
    564       chrome.send('updateCurrentScreen', [this.currentScreen.id]);
    565     },
    566 
    567     /**
    568      * Make sure that screen is initialized and decorated.
    569      * @param {Object} screen Screen params dict, e.g. {id: screenId, data: {}}.
    570      */
    571     preloadScreen: function(screen) {
    572       var screenEl = $(screen.id);
    573       if (screenEl.deferredDecorate !== undefined) {
    574         screenEl.deferredDecorate();
    575         delete screenEl.deferredDecorate;
    576       }
    577     },
    578 
    579     /**
    580      * Show screen of given screen id.
    581      * @param {Object} screen Screen params dict, e.g. {id: screenId, data: {}}.
    582      */
    583     showScreen: function(screen) {
    584       var screenId = screen.id;
    585 
    586       // Make sure the screen is decorated.
    587       this.preloadScreen(screen);
    588 
    589       if (screen.data !== undefined && screen.data.disableAddUser)
    590         DisplayManager.updateAddUserButtonStatus(true);
    591 
    592 
    593       // Show sign-in screen instead of account picker if pod row is empty.
    594       if (screenId == SCREEN_ACCOUNT_PICKER && $('pod-row').pods.length == 0) {
    595         // Manually hide 'add-user' header bar, because of the case when
    596         // 'Cancel' button is used on the offline login page.
    597         $('add-user-header-bar-item').hidden = true;
    598         Oobe.showSigninUI(true);
    599         return;
    600       }
    601 
    602       var data = screen.data;
    603       var index = this.getScreenIndex_(screenId);
    604       if (index >= 0)
    605         this.toggleStep_(index, data);
    606     },
    607 
    608     /**
    609      * Gets index of given screen id in screens_.
    610      * @param {string} screenId Id of the screen to look up.
    611      * @private
    612      */
    613     getScreenIndex_: function(screenId) {
    614       for (var i = 0; i < this.screens_.length; ++i) {
    615         if (this.screens_[i] == screenId)
    616           return i;
    617       }
    618       return -1;
    619     },
    620 
    621     /**
    622      * Register an oobe screen.
    623      * @param {Element} el Decorated screen element.
    624      */
    625     registerScreen: function(el) {
    626       var screenId = el.id;
    627       this.screens_.push(screenId);
    628 
    629       var header = document.createElement('span');
    630       header.id = 'header-' + screenId;
    631       header.textContent = el.header ? el.header : '';
    632       header.className = 'header-section';
    633       $('header-sections').appendChild(header);
    634 
    635       var dot = document.createElement('div');
    636       dot.id = screenId + '-dot';
    637       dot.className = 'progdot';
    638       var progressDots = $('progress-dots');
    639       if (progressDots)
    640         progressDots.appendChild(dot);
    641 
    642       this.appendButtons_(el.buttons, screenId);
    643     },
    644 
    645     /**
    646      * Updates inner container size based on the size of the current screen and
    647      * other screens in the same group.
    648      * Should be executed on screen change / screen size change.
    649      * @param {!HTMLElement} screen Screen that is being shown.
    650      */
    651     updateScreenSize: function(screen) {
    652       // Have to reset any previously predefined screen size first
    653       // so that screen contents would define it instead.
    654       $('inner-container').style.height = '';
    655       $('inner-container').style.width = '';
    656       screen.style.width = '';
    657       screen.style.height = '';
    658 
    659      $('outer-container').classList.toggle(
    660         'fullscreen', screen.classList.contains('fullscreen'));
    661 
    662       var width = screen.getPreferredSize().width;
    663       var height = screen.getPreferredSize().height;
    664       for (var i = 0, screenGroup; screenGroup = SCREEN_GROUPS[i]; i++) {
    665         if (screenGroup.indexOf(screen.id) != -1) {
    666           // Set screen dimensions to maximum dimensions within this group.
    667           for (var j = 0, screen2; screen2 = $(screenGroup[j]); j++) {
    668             width = Math.max(width, screen2.getPreferredSize().width);
    669             height = Math.max(height, screen2.getPreferredSize().height);
    670           }
    671           break;
    672         }
    673       }
    674       $('inner-container').style.height = height + 'px';
    675       $('inner-container').style.width = width + 'px';
    676       // This requires |screen| to have 'box-sizing: border-box'.
    677       screen.style.width = width + 'px';
    678       screen.style.height = height + 'px';
    679     },
    680 
    681     /**
    682      * Updates localized content of the screens like headers, buttons and links.
    683      * Should be executed on language change.
    684      */
    685     updateLocalizedContent_: function() {
    686       for (var i = 0, screenId; screenId = this.screens_[i]; ++i) {
    687         var screen = $(screenId);
    688         var buttonStrip = $(screenId + '-controls');
    689         if (buttonStrip)
    690           buttonStrip.innerHTML = '';
    691         // TODO(nkostylev): Update screen headers for new OOBE design.
    692         this.appendButtons_(screen.buttons, screenId);
    693         if (screen.updateLocalizedContent)
    694           screen.updateLocalizedContent();
    695       }
    696 
    697       var currentScreenId = this.screens_[this.currentStep_];
    698       var currentScreen = $(currentScreenId);
    699       this.updateScreenSize(currentScreen);
    700 
    701       // Trigger network drop-down to reload its state
    702       // so that strings are reloaded.
    703       // Will be reloaded if drowdown is actually shown.
    704       cr.ui.DropDown.refresh();
    705     },
    706 
    707     /**
    708      * Initialized first group of OOBE screens.
    709      */
    710     initializeOOBEScreens: function() {
    711       if (this.isOobeUI() && $('inner-container').classList.contains('down')) {
    712         for (var i = 0, screen;
    713              screen = $(SCREEN_GROUPS[SCREEN_GROUP_OOBE][i]); i++) {
    714           screen.hidden = false;
    715         }
    716       }
    717     },
    718 
    719     /**
    720      * Prepares screens to use in login display.
    721      */
    722     prepareForLoginDisplay_: function() {
    723       for (var i = 0, screenId; screenId = this.screens_[i]; ++i) {
    724         var screen = $(screenId);
    725         screen.classList.add('faded');
    726         screen.classList.remove('right');
    727         screen.classList.remove('left');
    728       }
    729     },
    730 
    731     /**
    732      * Shows the device requisition prompt.
    733      */
    734     showDeviceRequisitionPrompt_: function() {
    735       if (!this.deviceRequisitionDialog_) {
    736         this.deviceRequisitionDialog_ =
    737             new cr.ui.dialogs.PromptDialog(document.body);
    738         this.deviceRequisitionDialog_.setOkLabel(
    739             loadTimeData.getString('deviceRequisitionPromptOk'));
    740         this.deviceRequisitionDialog_.setCancelLabel(
    741             loadTimeData.getString('deviceRequisitionPromptCancel'));
    742       }
    743       this.deviceRequisitionDialog_.show(
    744           loadTimeData.getString('deviceRequisitionPromptText'),
    745           this.deviceRequisition_,
    746           this.onConfirmDeviceRequisitionPrompt_.bind(this));
    747     },
    748 
    749     /**
    750      * Confirmation handle for the device requisition prompt.
    751      * @param {string} value The value entered by the user.
    752      * @private
    753      */
    754     onConfirmDeviceRequisitionPrompt_: function(value) {
    755       this.deviceRequisition_ = value;
    756       chrome.send('setDeviceRequisition', [value == '' ? 'none' : value]);
    757     },
    758 
    759     /**
    760      * Called when window size changed. Notifies current screen about change.
    761      * @private
    762      */
    763     onWindowResize_: function() {
    764       var currentScreenId = this.screens_[this.currentStep_];
    765       var currentScreen = $(currentScreenId);
    766       if (currentScreen)
    767         currentScreen.onWindowResize();
    768     },
    769 
    770     /*
    771      * Updates the device requisition string shown in the requisition prompt.
    772      * @param {string} requisition The device requisition.
    773      */
    774     updateDeviceRequisition: function(requisition) {
    775       this.deviceRequisition_ = requisition;
    776     },
    777 
    778     /**
    779      * Shows the special remora device requisition prompt.
    780      * @private
    781      */
    782     showDeviceRequisitionRemoraPrompt_: function() {
    783       if (!this.deviceRequisitionRemoraDialog_) {
    784         this.deviceRequisitionRemoraDialog_ =
    785             new cr.ui.dialogs.ConfirmDialog(document.body);
    786         this.deviceRequisitionRemoraDialog_.setOkLabel(
    787             loadTimeData.getString('deviceRequisitionRemoraPromptOk'));
    788         this.deviceRequisitionRemoraDialog_.setCancelLabel(
    789             loadTimeData.getString('deviceRequisitionRemoraPromptCancel'));
    790       }
    791       this.deviceRequisitionRemoraDialog_.show(
    792           loadTimeData.getString('deviceRequisitionRemoraPromptText'),
    793           function() {  // onShow
    794             chrome.send('setDeviceRequisition', ['remora']);
    795           },
    796           function() {  // onCancel
    797             chrome.send('setDeviceRequisition', ['none']);
    798           });
    799     },
    800 
    801     /**
    802      * Returns true if Oobe UI is shown.
    803      */
    804     isOobeUI: function() {
    805       return document.body.classList.contains('oobe-display');
    806     },
    807 
    808     /**
    809      * Sets or unsets given |className| for top-level container. Useful for
    810      * customizing #inner-container with CSS rules. All classes set with with
    811      * this method will be removed after screen change.
    812      * @param {string} className Class to toggle.
    813      * @param {boolean} enabled Whether class should be enabled or disabled.
    814      */
    815     toggleClass: function(className, enabled) {
    816       $('oobe').classList.toggle(className, enabled);
    817     }
    818   };
    819 
    820   /**
    821    * Initializes display manager.
    822    */
    823   DisplayManager.initialize = function() {
    824     var givenDisplayType = DISPLAY_TYPE.UNKNOWN;
    825     if (document.documentElement.hasAttribute('screen')) {
    826       // Display type set in HTML property.
    827       givenDisplayType = document.documentElement.getAttribute('screen');
    828     } else {
    829       // Extracting display type from URL.
    830       givenDisplayType = window.location.pathname.substr(1);
    831     }
    832     var instance = Oobe.getInstance();
    833     Object.getOwnPropertyNames(DISPLAY_TYPE).forEach(function(type) {
    834       if (DISPLAY_TYPE[type] == givenDisplayType) {
    835         instance.displayType = givenDisplayType;
    836       }
    837     });
    838     if (instance.displayType == DISPLAY_TYPE.UNKNOWN) {
    839       console.error("Unknown display type '" + givenDisplayType +
    840           "'. Setting default.");
    841       instance.displayType = DISPLAY_TYPE.LOGIN;
    842     }
    843 
    844     instance.initializeOOBEScreens();
    845 
    846     window.addEventListener('resize', instance.onWindowResize_.bind(instance));
    847   };
    848 
    849   /**
    850    * Returns offset (top, left) of the element.
    851    * @param {!Element} element HTML element.
    852    * @return {!Object} The offset (top, left).
    853    */
    854   DisplayManager.getOffset = function(element) {
    855     var x = 0;
    856     var y = 0;
    857     while (element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) {
    858       x += element.offsetLeft - element.scrollLeft;
    859       y += element.offsetTop - element.scrollTop;
    860       element = element.offsetParent;
    861     }
    862     return { top: y, left: x };
    863   };
    864 
    865   /**
    866    * Returns position (top, left, right, bottom) of the element.
    867    * @param {!Element} element HTML element.
    868    * @return {!Object} Element position (top, left, right, bottom).
    869    */
    870   DisplayManager.getPosition = function(element) {
    871     var offset = DisplayManager.getOffset(element);
    872     return { top: offset.top,
    873              right: window.innerWidth - element.offsetWidth - offset.left,
    874              bottom: window.innerHeight - element.offsetHeight - offset.top,
    875              left: offset.left };
    876   };
    877 
    878   /**
    879    * Disables signin UI.
    880    */
    881   DisplayManager.disableSigninUI = function() {
    882     $('login-header-bar').disabled = true;
    883     $('pod-row').disabled = true;
    884   };
    885 
    886   /**
    887    * Shows signin UI.
    888    * @param {string} opt_email An optional email for signin UI.
    889    */
    890   DisplayManager.showSigninUI = function(opt_email) {
    891     var currentScreenId = Oobe.getInstance().currentScreen.id;
    892     if (currentScreenId == SCREEN_GAIA_SIGNIN)
    893       $('login-header-bar').signinUIState = SIGNIN_UI_STATE.GAIA_SIGNIN;
    894     else if (currentScreenId == SCREEN_ACCOUNT_PICKER)
    895       $('login-header-bar').signinUIState = SIGNIN_UI_STATE.ACCOUNT_PICKER;
    896     chrome.send('showAddUser', [opt_email]);
    897   };
    898 
    899   /**
    900    * Resets sign-in input fields.
    901    * @param {boolean} forceOnline Whether online sign-in should be forced.
    902    *     If |forceOnline| is false previously used sign-in type will be used.
    903    */
    904   DisplayManager.resetSigninUI = function(forceOnline) {
    905     var currentScreenId = Oobe.getInstance().currentScreen.id;
    906 
    907     $(SCREEN_GAIA_SIGNIN).reset(
    908         currentScreenId == SCREEN_GAIA_SIGNIN, forceOnline);
    909     $('login-header-bar').disabled = false;
    910     $('pod-row').reset(currentScreenId == SCREEN_ACCOUNT_PICKER);
    911   };
    912 
    913   /**
    914    * Shows sign-in error bubble.
    915    * @param {number} loginAttempts Number of login attemps tried.
    916    * @param {string} message Error message to show.
    917    * @param {string} link Text to use for help link.
    918    * @param {number} helpId Help topic Id associated with help link.
    919    */
    920   DisplayManager.showSignInError = function(loginAttempts, message, link,
    921                                             helpId) {
    922     var error = document.createElement('div');
    923 
    924     var messageDiv = document.createElement('div');
    925     messageDiv.className = 'error-message-bubble';
    926     messageDiv.textContent = message;
    927     error.appendChild(messageDiv);
    928 
    929     if (link) {
    930       messageDiv.classList.add('error-message-bubble-padding');
    931 
    932       var helpLink = document.createElement('a');
    933       helpLink.href = '#';
    934       helpLink.textContent = link;
    935       helpLink.addEventListener('click', function(e) {
    936         chrome.send('launchHelpApp', [helpId]);
    937         e.preventDefault();
    938       });
    939       error.appendChild(helpLink);
    940     }
    941 
    942     var currentScreen = Oobe.getInstance().currentScreen;
    943     if (currentScreen && typeof currentScreen.showErrorBubble === 'function') {
    944       currentScreen.showErrorBubble(loginAttempts, error);
    945       this.errorMessageWasShownForTesting_ = true;
    946     }
    947   };
    948 
    949   /**
    950    * Shows password changed screen that offers migration.
    951    * @param {boolean} showError Whether to show the incorrect password error.
    952    */
    953   DisplayManager.showPasswordChangedScreen = function(showError) {
    954     login.PasswordChangedScreen.show(showError);
    955   };
    956 
    957   /**
    958    * Shows dialog to create managed user.
    959    */
    960   DisplayManager.showManagedUserCreationScreen = function() {
    961     login.ManagedUserCreationScreen.show();
    962   };
    963 
    964   /**
    965    * Shows TPM error screen.
    966    */
    967   DisplayManager.showTpmError = function() {
    968     login.TPMErrorMessageScreen.show();
    969   };
    970 
    971   /**
    972    * Clears error bubble.
    973    */
    974   DisplayManager.clearErrors = function() {
    975     $('bubble').hide();
    976     this.errorMessageWasShownForTesting_ = false;
    977 
    978     var bubbles = document.querySelectorAll('.bubble-shown');
    979     for (var i = 0; i < bubbles.length; ++i)
    980       bubbles[i].classList.remove('bubble-shown');
    981   };
    982 
    983   /**
    984    * Sets text content for a div with |labelId|.
    985    * @param {string} labelId Id of the label div.
    986    * @param {string} labelText Text for the label.
    987    */
    988   DisplayManager.setLabelText = function(labelId, labelText) {
    989     $(labelId).textContent = labelText;
    990   };
    991 
    992   /**
    993    * Sets the text content of the enterprise info message.
    994    * @param {string} messageText The message text.
    995    */
    996   DisplayManager.setEnterpriseInfo = function(messageText) {
    997     $('enterprise-info-message').textContent = messageText;
    998     if (messageText) {
    999       $('enterprise-info').hidden = false;
   1000     }
   1001   };
   1002 
   1003   /**
   1004    * Disable Add users button if said.
   1005    * @param {boolean} disable true to disable
   1006    */
   1007   DisplayManager.updateAddUserButtonStatus = function(disable) {
   1008     $('add-user-button').disabled = disable;
   1009     $('add-user-button').classList[
   1010         disable ? 'add' : 'remove']('button-restricted');
   1011     $('add-user-button').title = disable ?
   1012         loadTimeData.getString('disabledAddUserTooltip') : '';
   1013   }
   1014 
   1015   /**
   1016    * Clears password field in user-pod.
   1017    */
   1018   DisplayManager.clearUserPodPassword = function() {
   1019     $('pod-row').clearFocusedPod();
   1020   };
   1021 
   1022   /**
   1023    * Restores input focus to currently selected pod.
   1024    */
   1025   DisplayManager.refocusCurrentPod = function() {
   1026     $('pod-row').refocusCurrentPod();
   1027   };
   1028 
   1029   // Export
   1030   return {
   1031     DisplayManager: DisplayManager
   1032   };
   1033 });
   1034