Source: lockscreen/js/lockscreen.js

/* global LockScreenClockWidget, Service, LockScreenSlide, LazyLoader,
          LockScreenConnInfoManager */
'use strict';

/**
 * LockScreen now use strategy pattern to adapt the unlocker, which would
 * report intentions like unlocking and launching camera to finish the job.
 *
 * @see intentionRouter in the component.
 */
(function(exports) {

  var LockScreen = function() {
  };
  LockScreen.prototype = {
    name: 'LockScreen',

    configs: {
      mode: 'default'
    },
    // 'notificationId' for opening app after unlocking.
    _unlockingMessage: {},
    // The unlocking strategy.
    _unlocker: null,
    _unlockerInitialized: false,

    /*
    * Lockscreen connection information manager
    */
    _lockscreenConnInfoManager: null,

    /*
    * Boolean return true when initialized.
    */
    ready: false,

    /*
    * Boolean return the status of the lock screen.
    * Must not multate directly - use unlock()/lockIfEnabled()
    * Listen to 'lockscreen-appclosed/opening/opened' events to properly
    * handle status changes
    */
    locked: true,

    /*
    * Boolean return whether if the lock screen is enabled or not.
    * Must not multate directly - use setEnabled(val)
    * Only Settings Listener should change this value to sync with data
    * in Settings API.
    */
    enabled: true,

    /*
    * Boolean returns wether we want a sound effect when unlocking.
    */
    unlockSoundEnabled: false,

    /*
    * Boolean return whether if the lock screen is enabled or not.
    * Must not multate directly - use setPassCodeEnabled(val)
    * Only Settings Listener should change this value to sync with data
    * in Settings API.
    * Will be ignored if 'enabled' is set to false.
    */
    passCodeEnabled: false,

    /*
    * Boolean returns whether the screen is enabled, as mutated by screenchange
    * event.
    * Note: 'undefined' should be regarded as 'true' as screenchange event
    * doesn't trigger on device boot, and we want to make fail-safe procedures
    * under such circamstances -- as we are never sure if screen is on or off.
    */
    _screenEnabled: undefined,

    /*
    * Boolean should regenerate overlay color for notifications background
    * When this is true, and when we're locking the device, we should
    * regenerate the overlay color as specified in bug 950884
    * Instead of doing the color generation in updateBackground,
    *  by doing this we can reduce critical path of updateBackground,
    * and perceived performance of selecting wallpaper.
    */
    _shouldRegenerateMaskedBackgroundColor: false,

    /*
    * String url of the background image to regenerate overlay color from
    */
    _regenerateMaskedBackgroundColorFrom: undefined,

    /*
    * The time to request for passcode input since device is off.
    */
    passCodeRequestTimeout: 0,

    /**
     * How long the unlocked session is.
     */
    _lastUnlockedInterval: 0,
    _lastUnlockedTimeStamp: 0,
    /**
     * How long the locked session is.
     */
    _lastLockedInterval: 0,
    _lastLockedTimeStamp: 0,

    /*
    * Check the timeout of passcode lock
    */
    _passCodeTimeoutCheck: false,

    /*
    * If user is sliding.
    */
    _sliding: false,

    /*
    * If user had released the finger and the handle already
    * reached one of the ends.
    */
    _slideReachEnd: false,

    /*
    * Current passcode entered by the user
    */
    passCodeEntered: '',

    /**
     * Are we currently switching panels ?
     */
    _switchingPanel: false,

    /*
    * Timeout after incorrect attempt
    */
    kPassCodeErrorTimeout: 500,

    /*
    * Counter after incorrect attempt
    */
    kPassCodeErrorCounter: 0,

    /*
    * Timeout ID for backing from triggered state to normal state
    */
    triggeredTimeoutId: 0,

    /*
    * Max value for handle swiper up
    */
    HANDLE_MAX: 70,
    chargingStatus: new window.LockScreenChargingStatus()
  };  // -- LockScreen.prototype --

  LockScreen.prototype.handleEvent =
  function ls_handleEvent(evt) {
    switch (evt.type) {
      case 'lockscreen-appopened':
        this.lock();
        break;
      case 'lockscreen-notification-request-activate-unlock':
        this._activateUnlock();
        break;
      case 'screenchange':
        // Don't lock if screen is turned off by promixity sensor.
        if (evt.detail.screenOffBy == 'proximity') {
          break;
        }

        this._screenEnabled = evt.detail.screenEnabled;

        // XXX: If the screen is not turned off by ScreenManager
        // we would need to lock the screen again
        // when it's being turned back on
        if (!evt.detail.screenEnabled) {
          // Remove camera once screen turns off
          if (this.camera && this.camera.firstElementChild) {
            this.camera.removeChild(this.camera.firstElementChild);
          }
          this.chargingStatus.stop();
        } else {
          if (!this.lockScreenClockWidget) {
            this.createClockWidget();
          }
          this.chargingStatus.start();
        }
        // No matter turn on or off from screen timeout or poweroff,
        // all secure apps would be hidden.
        this.dispatchEvent('secure-killapps');
        if (this.enabled) {
          this.overlayLocked(true);
        }
        break;

      case 'click':
        if (this.altCameraButton === evt.target) {
          this.handleIconClick(evt.target);
          break;
        }
        break;

      case 'touchstart':
        // Edge case: when the passcode is valid, passpad should fade out.
        // So the touchevent should do nothing.
        var passcodeValid =
          ('success' === this.overlay.dataset.passcodeStatus);
        if (passcodeValid) {
          return;
        }

        window.addEventListener('touchend', this);
        this.overlay.classList.add('touched');
        break;

      case 'touchend':
        window.removeEventListener('touchmove', this);
        window.removeEventListener('touchend', this);
        this.overlay.classList.remove('touched');
        break;
      case 'transitionend':
        if (evt.target !== this.overlay) {
          return;
        }

        if (this.overlay.dataset.panel !== 'camera' &&
            this.camera.firstElementChild) {
          this.camera.removeChild(this.camera.firstElementChild);
        }

        if (!this.locked) {
          this.overlay.hidden = true;
          this.unlockDetail = undefined;
        }
        break;

      case 'home':
        if (this.locked) {
          this.dispatchEvent('secure-closeapps');
          evt.stopImmediatePropagation();
        }
        break;

      case 'holdhome':
        if (!this.locked) {
          return;
        }

        evt.stopImmediatePropagation();
        evt.stopPropagation();
        break;

      case 'lockscreenslide-unlocker-initializer':
        this._unlockerInitialized = true;
        break;
      case 'lockscreenslide-near-left':
        break;
      case 'lockscreenslide-near-right':
        break;
      case 'lockscreenslide-unlocking-start':
        this._notifyUnlockingStart();
        break;
      case 'lockscreenslide-unlocking-stop':
        this._notifyUnlockingStop();
        break;
      case 'lockscreenslide-activate-left':
      case 'holdcamera':
        this._activateCamera();
        break;
      case 'lockscreenslide-activate-right':
        this._activateUnlock();
        break;
      case 'emergency-call-leave':
        this.handleEmergencyCallLeave();
        break;
      case 'lockscreen-mode-on':
        this.modeSwitch(evt.detail, true);
        break;
      case 'lockscreen-mode-off':
        this.modeSwitch(evt.detail, false);
        break;
      /**
       * we need to know whether the media player widget is shown or not,
       * in order to decide notification container's height
       * we listen to the same events (iac-mediacomms & appterminated) as
       * in media player widget's codes (/apps/system/js/media_playback.js)
       */
      case 'iac-mediacomms':
        if (evt.detail.type === 'status') {
          switch (evt.detail.data.playStatus) {
            case 'PLAYING':
            case 'PAUSED':
              window.lockScreenNotifications.collapseNotifications();
              window.lockScreenNotifications.adjustContainerVisualHints();
              break;
            case 'STOPPED':
            case 'mozinterruptbegin':
              window.lockScreenNotifications.expandNotifications();
              window.lockScreenNotifications.adjustContainerVisualHints();
              break;
          }
        }
        break;
      case 'appterminated':
        if (evt.detail.origin === this.mediaPlaybackWidget.origin) {
          window.lockScreenNotifications.expandNotifications();
          window.lockScreenNotifications.adjustContainerVisualHints();
        }
        break;
      case 'scroll':
        if (this.notificationsContainer === evt.target) {
          window.lockScreenNotifications.adjustContainerVisualHints();
          break;
        }
        break;
    }
  };  // -- LockScreen#handleEvent --

  LockScreen.prototype.initEmergencyCallEvents =
  function() {
    window.addEventListener('emergency-call-leave', this);
  };

  /**
   * This function would exist until we refactor the lockscreen.js with
   * new patterns. @see https://bugzil.la/960381
   *
   * @memberof LockScreen
   * @this {LockScreen}
   */
  LockScreen.prototype.init =
  function ls_init() {
    this.ready = true;
    /**
     * "new style" slider: as described in https://bugzil.la/950884
     * setting this parameter to true causes the LockScreenSlide to render
     * the slider specified in that bugzilla issue
     */
    LazyLoader.load(['shared/js/lockscreen_slide.js']).then(() => {
      this._unlocker = new LockScreenSlide({useNewStyle: true});
    }).catch(function(err) {console.error(err);});
    this.getAllElements();
    this.notificationsContainer =
      document.getElementById('notifications-lockscreen-container');

    this.lockIfEnabled(true);
    this.initUnlockerEvents();

    // This component won't know when the it get locked unless
    // it listens to this event.
    window.addEventListener('lockscreen-appopened', this);

    /* Status changes */
    window.addEventListener(
      'lockscreen-notification-request-activate-unlock', this);
    window.addEventListener('screenchange', this);

    /* Incoming and normal mode would be different */
    window.addEventListener('lockscreen-mode-switch', this);

    /* Gesture */
    this.area.addEventListener('touchstart', this);
    this.altCameraButton.addEventListener('click', this);
    this.iconContainer.addEventListener('touchstart', this);

    /* Unlock & camera panel clean up */
    this.overlay.addEventListener('transitionend', this);

    /* switching panels */
    window.addEventListener('home', this);

    /* blocking holdhome and prevent Cards View from show up */
    window.addEventListener('holdhome', this, true);
    window.addEventListener('ftudone', this);
    window.addEventListener('moztimechange', this);
    window.addEventListener('timeformatchange', this);

    /* media playback widget */
    this.mediaPlaybackWidget =
      new window.LockScreenMediaPlaybackWidget(this.mediaContainer);

    // listen to media playback events to adjust notification container height
    window.addEventListener('iac-mediacomms', this);
    window.addEventListener('appterminated', this);

    // Listen to event to start the Camera app
    window.addEventListener('holdcamera', this);

    window.SettingsListener.observe('lockscreen.enabled', true,
      (function(value) {
        this.setEnabled(value);
    }).bind(this));

    // it is possible that lockscreen is initialized after wallpapermanager
    // (e.g. user turns on lockscreen in settings after system is booted);
    // if this is the case, then the wallpaperchange event might not be captured
    //   and the lockscreen would initialize into empty wallpaper
    // so we need to see if there is already a wallpaper blob available
    if (Service.query('getWallpaper')) {
      var wallpaperURL = Service.query('getWallpaper');
      if (wallpaperURL) {
        this.updateBackground(wallpaperURL);
        this.overlay.classList.remove('uninit');
      }
    }
    window.addEventListener('wallpaperchange', (function(evt) {
      this.updateBackground(evt.detail.url);
      this.overlay.classList.remove('uninit');
    }).bind(this));

    window.SettingsListener.observe(
        'lockscreen.passcode-lock.enabled', false, (function(value) {
      this.setPassCodeEnabled(value);
    }).bind(this));

    window.SettingsListener.observe('lockscreen.unlock-sound.enabled',
      true, (function(value) {
      this.setUnlockSoundEnabled(value);
    }).bind(this));

    window.SettingsListener.observe('lockscreen.passcode-lock.timeout',
      0, (function(value) {
      this.setPassCodeLockTimeout(value);
    }).bind(this));

    window.SettingsListener.observe('lockscreen.lock-message',
      '', (function(value) {
      this.setLockMessage(value);
    }).bind(this));


    // FIXME(ggp) this is currently used by Find My Device
    // to force locking. Should be replaced by a proper IAC API in
    // bug 992277. We don't need to use SettingsListener because
    // we're only interested in changes to the setting, and don't
    // keep track of its value.
    navigator.mozSettings.addObserver('lockscreen.lock-immediately',
      (function(event) {
      if (event.settingValue === true) {
        this.lockIfEnabled(true);
      }
    }).bind(this));

    this.notificationsContainer.addEventListener('scroll', this);

    navigator.mozL10n.ready(this.l10nInit.bind(this));

    // when lockscreen is just initialized,
    // it will lock itself (if enabled) before calling updatebackground,
    // so we need to generate overlay if needed here
    if(this._checkGenerateMaskedBackgroundColor()){
      this._generateMaskedBackgroundColor();
    }
    this.chargingStatus.start();
    Service.register('setPassCodeEnabled', this);
    Service.register('setPassCodeLockTimeout', this);
  };

  LockScreen.prototype.initUnlockerEvents =
  function ls_initUnlockerEvents() {
    window.addEventListener('lockscreenslide-unlocker-initializer', this);
    window.addEventListener('lockscreenslide-near-left', this);
    window.addEventListener('lockscreenslide-near-right', this);
    window.addEventListener('lockscreenslide-unlocking-start', this);
    window.addEventListener('lockscreenslide-activate-left', this);
    window.addEventListener('lockscreenslide-activate-right', this);
    window.addEventListener('lockscreenslide-unlocking-stop', this);
  };

  LockScreen.prototype.suspendUnlockerEvents =
  function ls_initUnlockerEvents() {
    window.removeEventListener('lockscreenslide-unlocker-initializer', this);
    window.removeEventListener('lockscreenslide-near-left', this);
    window.removeEventListener('lockscreenslide-near-right', this);
    window.removeEventListener('lockscreenslide-unlocking-start', this);
    window.removeEventListener('lockscreenslide-activate-left', this);
    window.removeEventListener('lockscreenslide-activate-right', this);
    window.removeEventListener('lockscreenslide-unlocking-stop', this);
  };

  /**
   * We need to do some refreshing thing after l10n is ready.
   *
   * @memberof LockScreen
   * @this {LockScreen}
   */
  LockScreen.prototype.l10nInit =
  function ls_l10nInit() {
    this.l10nready = true;
    this.createClockWidget();

    // mobile connection state on lock screen.
    // It needs L10n too. But it's not a re-entrable function,
    // so we need to check if it's already initialized.
    if (this._lockscreenConnInfoManager ||
        !window.navigator.mozMobileConnections) {
      return;
    }
    // XXX: improve the dependency.
    if (window.SIMSlotManager) {
      this.startConnectionInfoManager();
    } else {
      window.addEventListener('simslotmanagerstarted', function s() {
        window.removeEventListener('simslotmanagerstarted', s);
        this.startConnectionInfoManager();
      }.bind(this));
    }
  };

  LockScreen.prototype.startConnectionInfoManager = function() {
    LazyLoader.load(
      ['shared/js/lockscreen_connection_info_manager.js']).then(() => {
        this._lockscreenConnInfoManager =
          new LockScreenConnInfoManager(this.connStates);
      }).catch(function(err) {console.error(err);});
  };

  /*
  * Set enabled state.
  * If enabled state is somehow updated when the lock screen is enabled
  * This function will unlock it.
  */
  LockScreen.prototype.setEnabled =
  function ls_setEnabled(val) {
    var prevEnabled = this.enabled;
    if (typeof val === 'string') {
      this.enabled = val == 'false' ? false : true;
    } else {
      this.enabled = val;
    }

    if (prevEnabled && !this.enabled && this.locked) {
      this.unlock();
    }
  };

  LockScreen.prototype.setPassCodeEnabled =
  function ls_setPassCodeEnabled(val) {
    if (typeof val === 'string') {
      this.passCodeEnabled = val == 'false' ? false : true;
    } else {
      this.passCodeEnabled = val;
    }
  };

  LockScreen.prototype.setUnlockSoundEnabled =
  function ls_setUnlockSoundEnabled(val) {
    if (typeof val === 'string') {
      this.unlockSoundEnabled = val == 'false' ? false : true;
    } else {
      this.unlockSoundEnabled = val;
    }
  };

  LockScreen.prototype.setPassCodeLockTimeout =
  function(val) {
    this.passCodeRequestTimeout = val;
  };

  LockScreen.prototype.setLockMessage =
  function ls_setLockMessage(val) {
    this.message.textContent = val;
    this.message.hidden = (val === '');
  };

  /**
   * Light the camera and unlocking icons when user touch on our LockScreen.
   *
   * @this {LockScreen}
   */
  LockScreen.prototype._lightIcons =
  function() {
    this.rightIcon.classList.remove('dark');
    this.leftIcon.classList.remove('dark');
  };

  /**
   * Dark the camera and unlocking icons when user leave our LockScreen.
   *
   * @this {LockScreen}
   */
  LockScreen.prototype._darkIcons =
  function() {
    this.rightIcon.classList.add('dark');
    this.leftIcon.classList.add('dark');
  };

  LockScreen.prototype._notifyUnlockingStart =
  function ls_notifyUnlockingStart() {
    window.dispatchEvent(new CustomEvent('unlocking-start'));
  };

  LockScreen.prototype._notifyUnlockingStop =
  function ls_notifyUnlockingStop() {
    window.dispatchEvent(new CustomEvent('unlocking-stop'));
  };

  /**
   * Activate the camera.
   *
   * @this {LockScreen}
   */
  LockScreen.prototype._activateCamera =
  function ls_activateCamera() {
    var panelOrFullApp = (function() {
      // If the passcode is enabled and it has a timeout which has passed
      // switch to secure camera
      if (this.passCodeEnabled && this.checkPassCodeTimeout()) {
        this.invokeSecureApp('camera');
        return;
      }
      var activityDetail = {
        name: 'record',
        data: {
          type: 'photos'
        }
      };
      this.unlock(false, { activity: activityDetail } );
    }).bind(this);

    panelOrFullApp();
  };

  LockScreen.prototype._activateUnlock =
  function ls_activateUnlock() {
    if (!(this.passCodeEnabled && this.checkPassCodeTimeout())) {
      this.unlock();
    }
  };

  LockScreen.prototype.handleIconClick =
  function ls_handleIconClick(target) {
    switch (target) {
      case this.areaCamera:
      case this.altCameraButton:
        this._activateCamera();
        break;
      case this.areaUnlock:
        this._activateUnlock();
        break;
    }
  };

  LockScreen.prototype.invokeSecureApp =
  function ls_invokeSecureApp(name) {
    var url =
          window.parent.location.href.replace('system', name),
        manifestUrl =
          url.replace(/(\/)*(index.html#?)*$/, '/manifest.webapp');

    url += '#secure';
    window.dispatchEvent(new window.CustomEvent('secure-launchapp',
      {
        'detail': {
         'appURL': url,
         'appManifestURL': manifestUrl
        }
      }
    ));
  };

  LockScreen.prototype.lockIfEnabled =
  function ls_lockIfEnabled(instant) {
    if (Service.query('isFtuRunning')) {
      this.unlock(instant);
      return;
    }

    if (this.enabled) {
      this.lock(instant);
    } else {
      this.unlock(instant);
    }
  };

  LockScreen.prototype.unlock =
  function ls_unlock(instant, detail) {
    var wasAlreadyUnlocked = !this.locked;
    this.locked = false;

    this.chargingStatus.stop();

    if (wasAlreadyUnlocked) {
      return;
    }
    // It ends the locked session.
    var now = Date.now();
    this._lastLockedInterval = now - this._lastLockedTimeStamp;
    this._lastUnlockedTimeStamp = now;

    this.lockScreenClockWidget.stop().destroy();
    delete this.lockScreenClockWidget;

    if (this.unlockSoundEnabled) {
      var unlockAudio = new Audio('/resources/sounds/unlock.opus');
      unlockAudio.play();
    }

    if (!detail) {
      detail = this._unlockingMessage;
    }
    this.overlay.classList.toggle('no-transition', instant);
    this.dispatchEvent('lockscreen-request-unlock', detail);
    this.dispatchEvent('secure-modeoff');
    this.overlay.classList.add('unlocked');

    // If we don't unlock instantly here,
    // these are run in transitioned callback.
    if (instant) {
      this.overlay.hidden = true;
    } else {
      this.unlockDetail = detail;
    }
    // Clear the state after we send the request.
    this._unlockingMessage = {};
  };

  LockScreen.prototype.overlayLocked = function(instant) {
    this.overlay.focus();
    this.overlay.classList.toggle('no-transition', instant);
    this.overlay.classList.remove('unlocked');
    this.overlay.hidden = false;

  };

  LockScreen.prototype.lock =
  function ls_lock(instant) {
    var wasAlreadyLocked = this.locked;
    this.locked = true;

    if (!wasAlreadyLocked) {
      // It ends the unlocked session.
      var now = Date.now();
      this._lastUnlockedInterval = now - this._lastUnlockedTimeStamp;
      this._lastLockedTimeStamp = now;

      this.overlayLocked();
      // Because 'document.hidden' changes slower than this,
      // so if we depend on that it would create the widget
      // while the screen is off.
      if (!this.mainScreen.classList.contains('screenoff')) {
        this.createClockWidget();
      }
      if (document.mozFullScreen) {
        document.mozCancelFullScreen();
      }
      // Any changes made to this,
      // also need to be reflected in apps/system/js/storage.js
      this.dispatchEvent('secure-modeon');

      if(this._checkGenerateMaskedBackgroundColor()){
        this._generateMaskedBackgroundColor();
      }
    }
  };

  LockScreen.prototype.loadPanel =
  function ls_loadPanel(panel, callback) {
    this._loadingPanel = true;

    switch (panel) {
      case 'passcode':
      case 'main':
        this.overlay.classList.add('no-transition');
        if (callback) {
          setTimeout(callback);
        }
        break;
    }
  };

  LockScreen.prototype.unloadPanel =
  function ls_unloadPanel(panel, toPanel, callback) {
    switch (panel) {
      case 'passcode':
        // Reset passcode panel only if the status is not error
        if (this.overlay.dataset.passcodeStatus == 'error') {
          break;
        }

        delete this.overlay.dataset.passcodeStatus;
        this.passCodeEntered = '';
        break;

      case 'main':
      /* falls through */
      default:
        var unload = (function() {
          this.overlay.classList.remove('triggered');
          clearTimeout(this.triggeredTimeoutId);
        }).bind(this);

        if (toPanel !== 'camera') {
          unload();
          break;
        }

        this.overlay.addEventListener('transitionend',
          (function ls_unloadDefaultPanel(evt) {
            if (evt.target !== this) {
              return;
            }
            this.overlay.removeEventListener('transitionend',
                                             ls_unloadDefaultPanel);
            unload();
          }).bind(this)
        );
        break;
    }

    if (callback) {
      setTimeout(callback);
    }
   };

  /**
   * Switch the panel to the target type.
   * Will actually call the load and unload panel function.
   *
   * @param {PanelType} panel Could be 'camera', 'passcode', 'emergency-call' or
   *                          undefined. Undefined means the main panel.
   */
  LockScreen.prototype.switchPanel =
  function ls_switchPanel(panel) {
    if (this._switchingPanel) {
      return;
    }

    panel = panel || 'main';
    var overlay = this.overlay;
    var currentPanel = overlay.dataset.panel;

    if (currentPanel && currentPanel === panel) {
      return;
    }

    this._switchingPanel = true;
    this.loadPanel(panel, (function() {
      this.unloadPanel(overlay.dataset.panel, panel,
        (function() {
          this.dispatchEvent('lockpanelchange', { 'panel': panel });
          overlay.dataset.panel = panel;
          this._switchingPanel = false;
        }).bind(this));
    }).bind(this));
  };

  /**
   * This function would fire an event to validate the passcode.
   * The validator is a component in System app, and LockScreen should
   * not validate it.
   */
  LockScreen.prototype.checkPassCode =
  function lockscreen_checkPassCode(passcode) {
    var request = {
      passcode: passcode,
      onsuccess: this.onPasscodeValidationSuccess.bind(this),
      onerror: this.onPasscodeValidationFailed.bind(this),
    };
    window.dispatchEvent(new CustomEvent(
      'lockscreen-request-passcode-validate',
      { detail: request }
    ));
  };

  LockScreen.prototype.updateBackground =
  function ls_updateBackground(value) {
    var background = document.getElementById('lockscreen-background'),
        url = 'url(' + value + ')';
    background.style.backgroundImage = url;
    // if screen is locked and display is on, regenerate the color immediately
    // as it's possible that notifications come in without we ever having a
    // chance to generate the color (triggered in lockscreen.locked)
    this._regenerateMaskedBackgroundColorFrom = value;
    // see this._screenEnabled's definition above on
    // why 'undefined' is seen as 'true'
    if ((this._screenEnabled === undefined || this._screenEnabled) &&
        this.locked) {
      this._generateMaskedBackgroundColor();
    }else{
      this._shouldRegenerateMaskedBackgroundColor = true;
    }
  };

  /**
   * Check if we should regenerate masked background color
   */
  LockScreen.prototype._checkGenerateMaskedBackgroundColor =
  function ls_checkGenerateMaskedBackgroundColor() {
    // XXX: request animation frame?
    return (this._shouldRegenerateMaskedBackgroundColor &&
            !!this._regenerateMaskedBackgroundColorFrom);
  };

  /**
   * Generate a single color from wallpaper
   * to be used as the background color of Masked Background
   */
  LockScreen.prototype._generateMaskedBackgroundColor =
  function ls_generateMaskedBackgroundColor() {
    // downsample the image to avoid calculation taking too much time
    var SAMPLE_IMAGE_SIZE_BASE = 100;

    var img = new Image();
    img.onload = (function(){
      var sampleImageWidth;
      var sampleImageHeight;

      if(img.height > img.width){
        sampleImageWidth =
          Math.floor(SAMPLE_IMAGE_SIZE_BASE * window.devicePixelRatio);
        sampleImageHeight =
          Math.floor(sampleImageWidth * (img.height / img.width));
      }else{
        sampleImageHeight =
          Math.floor(SAMPLE_IMAGE_SIZE_BASE * window.devicePixelRatio);
        sampleImageWidth =
          Math.floor(sampleImageHeight * (img.width / img.height));
      }

      var canvas = document.createElement('canvas');
      canvas.width = sampleImageWidth;
      canvas.height = sampleImageHeight;

      var context = canvas.getContext('2d');
      context.drawImage(img, 0, 0, sampleImageWidth, sampleImageHeight);

      var data =
        context.getImageData(0, 0, sampleImageWidth, sampleImageHeight).data;
      var r = 0, g = 0, b = 0;

      for (var row = 0; row < sampleImageHeight; row++) {
        for (var col = 0; col < sampleImageWidth; col++) {
          r += data[((sampleImageWidth * row) + col) * 4];
          g += data[((sampleImageWidth * row) + col) * 4 + 1];
          b += data[((sampleImageWidth * row) + col) * 4 + 2];
        }
      }

      r = r / (sampleImageWidth * sampleImageHeight) / 255;
      g = g / (sampleImageWidth * sampleImageHeight) / 255;
      b = b / (sampleImageWidth * sampleImageHeight) / 255;

      // http://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
      var M = Math.max(r, g, b);
      var m = Math.min(r, g, b);
      var C = M - m;
      var h, s, l;

      l = 0.5 * (M + m);
      if (C === 0) {
        h = s = 0; // no satuaration (monochromatic)
      } else {
        switch (M) {
          case r:
            h = ((g - b) / C) % 6;
            break;
          case g:
            h = ((b - r) / C) + 2;
            break;
          case b:
            h = ((r - g) / C) + 4;
            break;
        }
        h *= 60;
        h = (h + 360) % 360;
        s = C / (1 - Math.abs(2 * l - 1));
      }

      l *= 0.9;

      h = parseInt(h);
      s = parseInt(s * 100) + '%';
      l = parseInt(l * 100) + '%';

      var value = 'hsla(' + h + ', ' + s + ', ' + l + ', 0.7)';
      this.maskedBackground.dataset.wallpaperColor = value;
      if (!this.maskedBackground.classList.contains('blank')) {
        this.maskedBackground.style.backgroundColor = value;
      }
    }).bind(this);

    img.src = this._regenerateMaskedBackgroundColorFrom;
    this._shouldRegenerateMaskedBackgroundColor = false;
    this._regenerateMaskedBackgroundColorFrom = undefined;
  };

  /**
   * To get all elements this component will use.
   * Note we do a name mapping here: DOM variables named like 'passcodePad'
   * are actually corresponding to the lowercases with hyphen one as
   * 'passcode-pad', then be prefixed with 'lookscreen'.
   */
  LockScreen.prototype.getAllElements =
  function ls_getAllElements() {
    // ID of elements to create references
    var elements = ['conn-states', 'clock-time', 'date', 'area',
        'area-unlock', 'area-camera', 'icon-container',
        'area-slide', 'media-container', 'passcode-code',
        'alt-camera', 'alt-camera-button',
        'passcode-pad',
        'panel-emergency-call', 'canvas', 'message',
        'notification-arrow', 'masked-background'];

    var toCamelCase = function toCamelCase(str) {
      return str.replace(/\-(.)/g, function replacer(str, p1) {
        return p1.toUpperCase();
      });
    };

    elements.forEach((function createElementRef(name) {
      var element = document.getElementById('lockscreen-' + name);
      if (!element) {
        console.error('No such element: lockscreen-'+ name);
      }
      name = toCamelCase(name);
      this[name] = element;
    }).bind(this));

    this.overlay = document.getElementById('lockscreen');
    this.mainScreen = document.getElementById('screen');
  };

  LockScreen.prototype.dispatchEvent =
  function ls_dispatchEvent(name, detail) {
    var evt = new CustomEvent(name, {
      'bubbles': true,
      'cancelable': true,
      // Set event detail if needed for the specific event 'name' (relevant for
      // passing which button triggered the event)
      'detail': detail
    });
    window.dispatchEvent(evt);
  };

  /**
   * @param {boolean} switcher - true if mode is on, false if off.
   */
  LockScreen.prototype.modeSwitch =
  function ls_modeSwitch(mode, switcher) {
    if (switcher) {
      if (mode !== this.configs.mode) {
        this.suspend();
      }
    } else {
      if (mode !== this.configs.mode) {
        this.resume();
      }
    }
  };

  LockScreen.prototype.suspend =
  function ls_suspend() {
    this.suspendUnlockerEvents();
  };

  LockScreen.prototype.resume =
  function ls_resume() {
    this.initUnlockerEvents();
  };

  /**
   * Check if the timeout has been expired and we need to check the passcode.
   */
  LockScreen.prototype.checkPassCodeTimeout =
    function ls_checkPassCodeTimeout() {
      var timeout = this.passCodeRequestTimeout * 1000;
      var lockedInterval = this.fetchLockedInterval();
      var unlockedInterval = this.fetchUnlockedInterval();

      // If user set timeout, then
      // - if timeout expired, do check
      // - if timeout is valid, do not check
      if (0 !== this.passCodeRequestTimeout) {
        if (lockedInterval > timeout ||
            unlockedInterval > timeout ) {
          return true;
        } else {
          return false;
        }
      } else {
        return true;
      }
    };

  /**
   * When validation failed, do UI change.
   */
  LockScreen.prototype.onPasscodeValidationFailed =
    function ls_onPasscodeValidationFailed() {
      this.overlay.dataset.passcodeStatus = 'error';
      // To let passcode pad handle it.
      window.dispatchEvent(new CustomEvent(
        'lockscreen-notify-passcode-validationfailed'));

      this.kPassCodeErrorCounter++;
      //double delay if >5 failed attempts
      if (this.kPassCodeErrorCounter > 5) {
        this.kPassCodeErrorTimeout = 2 * this.kPassCodeErrorTimeout;
      }
      if ('vibrate' in navigator) {
        navigator.vibrate([50, 50, 50]);
      }
      setTimeout(() => {
        delete this.overlay.dataset.passcodeStatus;
      }, this.kPassCodeErrorTimeout);
    };

  /**
   * When validation success, do unlock.
   */
  LockScreen.prototype.onPasscodeValidationSuccess =
    function ls_onPasscodeValidationSuccess() {
      window.dispatchEvent(new CustomEvent(
        'lockscreen-notify-passcode-validationsuccess'));
      this.passCodeError = 0;
      this.kPassCodeErrorTimeout = 500;
      this.kPassCodeErrorCounter = 0;
      // delegate the unlocking function call to panel state.
    };

  LockScreen.prototype.createClockWidget = function() {
    // Adapt a state-widget in the curret architecture.
    this.lockScreenClockWidget = new LockScreenClockWidget(
      document.getElementById('lockscreen-clock-widget'));
    this.lockScreenClockWidget.start();
  };

  LockScreen.prototype.fetchLockedInterval = function() {
    // If: the session is still pending, so need to calculate it.
    // Else: the session was already over, so need to get it.
    if (this.locked) {
      this._lastLockedInterval = Date.now() - this._lastLockedTimeStamp;
      return this._lastLockedInterval;
    } else {
      return this._lastLockedInterval;
    }
  };

  LockScreen.prototype.fetchUnlockedInterval = function() {
    // If: the session is still pending, so need to calculate it.
    // Else: the session was already over, so need to get it.
    if (!this.locked) {
      this._lastUnlockedInterval = Date.now() - this._lastUnlockedTimeStamp;
      return this._lastUnlockedInterval;
    } else {
      return this._lastUnlockedInterval;
    }
  };

  /** @exports LockScreen */
  exports.LockScreen = LockScreen;

  // XXX: Before we stop components directly reading this value,
  // we need this to satisfy those components which would be loaded
  // before this file.
  if (!window.lockScreen) {
    /** @global*/
    window.lockScreen = { locked: false };
  }
})(window);