Source: js/lock_screen_window_manager.js

'use strict';
/* global Service, LockScreenWindow, LockScreenInputWindow */
/* global LockScreenPasscodeValidator, BaseModule, LazyLoader */

(function(exports) {
  /**
   * Manage LockScreenWindow apps. This is a subset of the AppWindow manager,
   * and will not handle most cases the later one would handle. Only those
   * meaningful cases, like secure app open, close, requesting kill all apps
   * or turn the secure mode on/off, would be handled. However, if we need
   * to handle cases in the future, we would extend this manager.
   *
   * So far the LockScreenWindowManager would only manager 1 secure app at once,
   * but there're already some designations for multiple apps.
   *
   * @constructor LockScreenWindowManager
   */
  var LockScreenWindowManager = function() {};

  LockScreenWindowManager.prototype.setup =
  function lswm_setup() {
    /**
     * @memberof LockScreenWindowManager#
     * @prop {DOMElement} windows - the `#windows` element, which is the same
     *                              element that the would AppWindowManager use.
     * @prop {DOMElement} screen - the `#screen` element.
     */
    this.elements = {
      windows: null,
      screen: null
    };

    /**
     * @memberof LockScreenWindowManager#
     */
    this.states = {
      ready: false,
      FTUOccurs: false,
      enabled: true,
      instance: null,
      windowCreating: false
    };

    /**
     * @memberof LockScreenWindowManager#
     */
    this.configs = {
      inputWindow: {
        // Before we put things inside an iframe, do not resize LW.
        resizeMode: false,
        get height() {
          return 300;
        }
      },
      listens: ['lockscreen-request-unlock',
                'lockscreen-appcreated',
                'lockscreen-appterminated',
                'lockscreen-appclose',
                'screenchange',
                'system-resize',
                'ftuopen',
                'ftudone',
                'overlaystart',
                'showlockscreenwindow',
                'secure-appclosed',
                'lockscreen-request-inputpad-open',
                'lockscreen-request-inputpad-close'
               ]
    };
  };

  LockScreenWindowManager.prototype.name = 'LockScreenWindowManager';
  LockScreenWindowManager.prototype.EVENT_PREFIX = 'lockscreenwindowmanager';
  /**
   * This function will be invoked by hierarchyManager.
   */
  LockScreenWindowManager.prototype.getActiveWindow = function() {
    return this.isActive() ? this.states.instance : null;
  };

  LockScreenWindowManager.prototype.locked = function() {
    return this.isActive();
  };

  /**
   * To initialize the class instance (register events, observe settings, etc.)
   */
  LockScreenWindowManager.prototype.start =
  function lwm_start() {
    this.setup();
    this.startEventListeners();
    this.startObserveSettings();
    this.initElements();
    Service.register('unlock', this);
    Service.register('lock', this);
    Service.request('registerHierarchy', this);
    Service.registerState('locked', this);
    BaseModule.lazyLoad(['LockScreenPasscodeValidator']).then(function() {
      var lockScreenPasscodeValidator = new LockScreenPasscodeValidator();
      lockScreenPasscodeValidator.start();
    });
  };

  LockScreenWindowManager.prototype._handle_home = function() {
    if (this.isActive()) {
      // XXX: I don't want to change the order of event registration
      // at this early-refactoring stage, so do this to minimize the
      // risk and complete the work.
      window.dispatchEvent(
        new CustomEvent('lockscreen-notify-homepressed'));
      return false;
    }
    return true;
  };

  LockScreenWindowManager.prototype._handle_holdhome = function() {
    if (this.isActive()) {
      return false;
    }
    return true;
  };

  LockScreenWindowManager.prototype.respondToHierarchyEvent = function(evt) {
    if (this['_handle_' + evt.type]) {
      return this['_handle_' + evt.type](evt);
    } else {
      return true;
    }
  };

  /**
   * @listens lockscreen-appcreated - when a lockscreen app got created, it
   *                                  would fire this event.
   * @listens lockscreen-appterminated - when a lockscreen app got really
   *                                     closed, it would fire this event.
   * @listens lockscreen-apprequestclose - when a lockscreen app has been
   *                                       called to close itself, the event
   *                                       would be fired
   * @listens screenchange - means to initialize the lockscreen and its window
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.handleEvent =
    function lwm_handleEvent(evt) {
      var app = null;
      switch (evt.type) {
        case 'overlaystart':
          if (this.isActive()) {
            this.states.instance.setVisible(false);
          }
          break;
        case 'showlockscreenwindow':
          if (this.isActive()) {
            this.states.instance.setVisible(true);
          }
          break;
        case 'ftuopen':
          this.states.FTUOccurs = true;
          // Need immediatly unlocking (hide window).
          this.closeApp(true);
          break;
        case 'ftudone':
          this.states.FTUOccurs = false;
          break;
        case 'lockscreen-request-unlock':
          this.responseUnlock(evt.detail);
          break;
        case 'lockscreen-appcreated':
          app = evt.detail;
          this.registerApp(app);
          break;
        case 'lockscreen-appterminated':
          app = evt.detail;
          this.unregisterApp(app);
          break;
        case 'secure-appclosed':
          this.states.instance.lockOrientation();
          break;
        case 'system-resize':
          if (this.states.instance && this.states.instance.isActive()) {
            var p = this.states.instance.resize();
            if (typeof evt.detail.waitUntil === 'function') {
              evt.detail.waitUntil(p);
            }
          }
          break;
        case 'screenchange':
          // The screenchange may be invoked by proximity sensor,
          // or the power button. If it's caused by the proximity sensor,
          // we should not open the LockScreen, because the user may stay
          // in another app, not the LockScreen.
          if ('proximity' !== evt.detail.screenOffBy &&
              !this.states.FTUOccurs &&
              this.states.ready) {
            // The app would be inactive while screen off.
            this.states.enabled && this.openApp();
            if (evt.detail.screenEnabled && this.states.instance &&
                this.isActive() &&
                !Service.query('SecureWindowManager.isActive')) {
              // In theory listen to 'visibilitychange' event can solve this
              // issue, since it would be fired at the correct moment that
              // we can lock the orientation successfully, but this event
              // would not be received when user press the button twice
              // quickly, so we need to keep this workaround.
              this.states.instance.lockOrientation();
            }
          }
          break;
        case 'lockscreen-request-inputpad-open':
          this.onInputpadOpen();
          break;
        case 'lockscreen-request-inputpad-close':
          this.onInputpadClose();
          break;
      }
    };

  /**
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.initElements =
    function lwm_initElements() {
      var selectors = { windows: 'windows', screen: 'screen'};
      for (var name in selectors) {
        var id = selectors[name];
        this.elements[name] = document.getElementById(id);
      }
    };

  /**
   * Hook observers of settings to allow or ban window opening.
   *
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.startObserveSettings =
    function lwm_startObserveSettings() {
      var enabledListener = this.handleEnable.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.
      var lockListener = (event) => {
        if (true === event.settingValue) {
          this.openApp();
        }
      };

      window.SettingsListener.observe('lockscreen.enabled',
        true, enabledListener);

      // We are only interested in changes to the setting, rather
      // than its value, so just observe it instead of using SettingsListener
      navigator.mozSettings.addObserver('lockscreen.lock-immediately',
          lockListener);
    };

  /**
   * Hook listeners of events this manager interested in.
   *
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.startEventListeners =
    function lwm_startEventListeners() {
      this.configs.listens.forEach((function _initEvent(type) {
        self.addEventListener(type, this);
      }).bind(this));
    };

  /**
   * Remove listeners of events this manager interested in.
   *
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.stopEventListeners =
    function lwm_stopEventListeners() {
      this.configs.listens.forEach((function _unbind(ename) {
        self.removeEventListener(ename, this);
      }).bind(this));
    };

  /**
   * Close the lockscreen app.
   * If it's not enabled, would do nothing.
   *
   * @param {boolean} instant - true if instantly close.
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.closeApp =
    function lwm_closeApp(instant) {
      if (!this.states.enabled || !this.isActive()) {
        return;
      }
      this.states.instance.close(instant ? 'immediate': undefined);
      this.elements.screen.classList.remove('locked');
      this.toggleLockedSetting(false);
      this.publish(this.EVENT_PREFIX + '-deactivated', this);
    };

  /**
   * Open the lockscreen app.
   * If it's necessary, would create a new window.
   * If it's not enabled, would do nothing.
   *
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.openApp =
    function lwm_openApp() {
      if (!this.states.instance) {
        var app = this.createWindow();
        app && app.open();
      } else {
        this.states.instance.open();
      }
      this.elements.screen.classList.add('locked');
      this.toggleLockedSetting(true);
      this.publish(this.EVENT_PREFIX + '-activated', this);
    };

  /**
   * Message passing method. Would publish to the whole System app.
   *
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.publish =
    function lwm_publish(ne, detail) {
      var event = new CustomEvent(ne, { detail: detail });
      window.dispatchEvent(event);
    };

  /**
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.registerApp =
    function lwm_registerApp(app) {
      this.states.instance = app;
    };

  /**
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.unregisterApp =
    function lwm_unregisterApp(app) {
      this.states.instance = null;
    };

  /**
   * When screenchange hanneped, create LockScreen and LockScreenWindow
   * if it is needed.
   *
   * @private
   * @return {LockScreenWindow}
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.createWindow =
    function lwm_createWindow() {
      if (this.states.windowCreating) {
        return false;
      }
      this.states.windowCreating = true;
      var app = new LockScreenWindow();
      // XXX: Before we can use real InputWindow and InputWindowManager,
      // we need this to
      LazyLoader.load(['js/lockscreen_input_window.js']).then(() => {
        app.inputWindow = new LockScreenInputWindow();
      }).catch((err) => {
        console.error(err);
      });
      this.states.windowCreating = false;
      return app;
    };

  /**
   * First time we launch, we must check the init value of enabled,
   * to see if we need to open the window.
   *
   * @private
   * @this {LockScreenWindowManager}
   * @memberof LockScreenWindowManager
   */
  LockScreenWindowManager.prototype.handleEnable =
    function(enabled) {
      this.states.ready = true;
      if (typeof(enabled) === String) {
        enabled = (enabled === 'true');
      }
      this.states.enabled = enabled;
    };

  LockScreenWindowManager.prototype.unlock =
    function lwm_unlock(detail) {
      // XXX:
      // There is a self-routing here:
      // Service.request('unlock') ->
      // LockscreenWindowManager#unlock ->
      // ['lockscreen-request-unlock'] ->
      // LockscreenWindowManager#responseUnlock |
      // VisibilityManager#firing showwindow
      //
      // We should just call responseUnlock here,
      // but VisibilityManager needs this event to notify
      // AppWindowManager to showwindow correctly;
      // The reason not using lockscreen-appclosing/lockscreen-appclosed
      // is the race of mozActivity launch coming from lockscreen
      // and homescreen will race to be opened and cause performance issue.
      //
      // We should adjust LockScreenWindow to use lockscreen-appclosing/closed
      // to have the activitiy/notification info hence we could change
      // VisibilityManager later to avoid this workaround.
      this.publish('lockscreen-request-unlock', detail);
    };

  LockScreenWindowManager.prototype.responseUnlock =
    function lwm_responseUnlock(detail) {
      var forcibly = (detail && detail.forcibly) ? true : false;
      this.closeApp(forcibly);
    };

  LockScreenWindowManager.prototype.lock =
    function lwm_lock(detail) {
      this.openApp();
    };

  LockScreenWindowManager.prototype.getInstance =
    function lwm_getInstance() {
      return this.states.instance;
    };

  LockScreenWindowManager.prototype.isActive =
    function lwm_isActive() {
      if (null === this.states.instance) {
        return false;
      } else {
        return this.states.instance.isActive();
      }
    };

  LockScreenWindowManager.prototype.onInputpadOpen =
    function lwm_onInputpadOpen() {
      this.states.instance.inputWindow.open();
      this.states.instance.resize();
    };

  LockScreenWindowManager.prototype.onInputpadClose =
    function lwm_onInputpadClose() {
      this.states.instance.inputWindow.close();
      this.states.instance.resize();
    };

  LockScreenWindowManager.prototype.toggleLockedSetting =
    function lswm_toggleLockedSetting(value) {
      if (!window.navigator.mozSettings) {
        return;
      }
      window.SettingsListener.getSettingsLock().set({
        'lockscreen.locked': value
      });
    };

  /** @exports LockScreenWindowManager */
  exports.LockScreenWindowManager = LockScreenWindowManager;
})(window);