Source: js/app_transition_controller.js

/* global Service */
'use strict';

(function(exports) {
  var TransitionEvents =
    ['open', 'close', 'complete', 'timeout',
      'immediate-open', 'immediate-close'];

  var TransitionStateTable = {
    'closed': ['opening', null, null, null, 'opened', null],
    'opened': [null, 'closing', null, null, null, 'closed'],
    'opening': [null, 'closing', 'opened', 'opened', 'opened', 'closed'],
    'closing': ['opened', null, 'closed', 'closed', 'opened', 'closed']
  };

  /**
   * AppTransitionController controlls the opening and closing animation
   * of the given appWindow.
   *
   * ##### Flow chart #####
   * ![AppTransition Flow chart](http://i.imgur.com/k0hO2AN.png)
   *
   * ##### State machine #####
   * ![AppTransition State machine](http://i.imgur.com/0arU9rl.png)
   *
   * @param {AppWindow} app The app window instance which this controller
   *                        belongs to.
   *
   * @class AppTransitionController
   */
  var AppTransitionController =
    function AppTransitionController(app) {
      if (!app || !app.element) {
        return;
      }

      this.app = app;
      this.app.debug('default animation:',
        this.app.openAnimation, this.app.closeAnimation);
      if (this.app.openAnimation) {
        this.openAnimation = this.app.openAnimation;
      }

      if (this.app.closeAnimation) {
        this.closeAnimation = this.app.closeAnimation;
      }

      if (this.app.CLASS_NAME == 'AppWindow') {
        this.OPENING_TRANSITION_TIMEOUT = 2500;
      }

      this.app.element.addEventListener('_opening', this);
      this.app.element.addEventListener('_closing', this);
      this.app.element.addEventListener('_opened', this);
      this.app.element.addEventListener('_closed', this);
      this.app.element.addEventListener('_opentransitionstart', this);
      this.app.element.addEventListener('_closetransitionstart', this);
      this.app.element.addEventListener('_loaded', this);
      this.app.element.addEventListener('_openingtimeout', this);
      this.app.element.addEventListener('_closingtimeout', this);
      this.app.element.addEventListener('animationend', this);
    };

  AppTransitionController.prototype.destroy = function() {
    if (!this.app || !this.app.element) {
      return;
    }

    this.app.element.removeEventListener('_opening', this);
    this.app.element.removeEventListener('_closing', this);
    this.app.element.removeEventListener('_opened', this);
    this.app.element.removeEventListener('_closed', this);
    this.app.element.removeEventListener('_opentransitionstart', this);
    this.app.element.removeEventListener('_closetransitionstart', this);
    this.app.element.removeEventListener('_loaded', this);
    this.app.element.removeEventListener('_openingtimeout', this);
    this.app.element.removeEventListener('_closingtimeout', this);
    this.app.element.removeEventListener('animationend', this);
    this.app = null;
  };

  AppTransitionController.prototype._transitionState = 'closed';
  AppTransitionController.prototype._waitingForLoad = false;
  AppTransitionController.prototype.openAnimation = 'enlarge';
  AppTransitionController.prototype.closeAnimation = 'reduce';
  AppTransitionController.prototype.OPENING_TRANSITION_TIMEOUT = 200;
  AppTransitionController.prototype.CLOSING_TRANSITION_TIMEOUT = 200;
  AppTransitionController.prototype.SLOW_TRANSITION_TIMEOUT = 3500;
  AppTransitionController.prototype._firstTransition = true;
  AppTransitionController.prototype.changeTransitionState =
    function atc_changeTransitionState(evt) {
      var currentState = this._transitionState;
      var evtIndex = TransitionEvents.indexOf(evt);
      var state = TransitionStateTable[currentState][evtIndex];
      if (!state) {
        return;
      }

      this.app.debug(currentState, state, '::', evt);

      this.switchTransitionState(state);
      this.resetTransition();
      this['_do_' + state]();
      this.app.publish(state);

      //backward compatibility
      if (!this.app) {
        return;
      }
      if (state == 'opening') {
        /**
         * Fired when the app is doing opening animation.
         * @event AppWindow#appopening
         */
        this.app.publish('willopen');
      } else if (state == 'closing') {
        /**
         * Fired when the app is doing closing animation.
         * @event AppWindow#appclosing
         */
        this.app.publish('willclose');
      } else if (state == 'opened') {
        /**
         * Fired when the app's opening animation is ended.
         * @event AppWindow#appopen
         */
        this.app.publish('open');
      } else if (state == 'closed') {
        /**
         * Fired when the app's closing animation is ended.
         * @event AppWindow#appclose
         */
        this.app.publish('close');
      }
    };

  AppTransitionController.prototype._do_closing =
    function atc_do_closing() {
      this.app.debug('timer to ensure closed does occur.');
      this._closingTimeout = window.setTimeout(() => {
        if (!this.app) {
          return;
        }
        this.app.broadcast('closingtimeout');
      },
      Service.query('slowTransition') ? this.SLOW_TRANSITION_TIMEOUT :
                              this.CLOSING_TRANSITION_TIMEOUT);

      if (!this.app || !this.app.element) {
        return;
      }
      this.app.element.classList.add('transition-closing');
      this.app.element.classList.add(this.getAnimationName('close'));
    };

  AppTransitionController.prototype._do_closed =
    function atc_do_closed() {
    };

  AppTransitionController.prototype.getAnimationName = function(type) {
    return this.currentAnimation || this[type + 'Animation'] || type;
  };

  AppTransitionController.prototype._do_opening =
    function atc_do_opening() {
      this.app.debug('timer to ensure opened does occur.');
      this._openingTimeout = window.setTimeout(function() {
        this.app && this.app.broadcast('openingtimeout');
      }.bind(this),
      Service.query('slowTransition') ? this.SLOW_TRANSITION_TIMEOUT :
                              this.OPENING_TRANSITION_TIMEOUT);
      this._waitingForLoad = false;
      this.app.element.classList.add('transition-opening');
      this.app.element.classList.add(this.getAnimationName('open'));
      this.app.debug(this.app.element.classList);
    };

  AppTransitionController.prototype._do_opened =
    function atc_do_opened() {
    };

  AppTransitionController.prototype.switchTransitionState =
    function atc_switchTransitionState(state) {
      this._transitionState = state;
      if (!this.app) {
        return;
      }
      this.app._changeState('transition', this._transitionState);
    };

  // TODO: move general transition handlers into another object.
  AppTransitionController.prototype.handle_closing =
    function atc_handle_closing() {
      if (!this.app || !this.app.element) {
        return;
      }
      this.switchTransitionState('closing');
    };

  AppTransitionController.prototype.handle_closed =
    function atc_handle_closed() {
      if (!this.app || !this.app.element) {
        return;
      }

      this.resetTransition();
      /* The AttentionToaster will take care of that for AttentionWindows */
      /* InputWindow & InputWindowManager will take care of visibility of IM */
      if (!this.app.isAttentionWindow && !this.app.isCallscreenWindow &&
          !this.app.isInputMethod) {
        this.app.setVisible(false);
      }
      this.app.setNFCFocus(false);

      this.app.element.classList.remove('active');
    };

  AppTransitionController.prototype.handle_opening =
    function atc_handle_opening() {
      if (!this.app || !this.app.element) {
        return;
      }
      if (this.app.loaded) {
        var self = this;
        this.app.element.addEventListener('_opened', function onopen() {
          // Perf test needs.
          self.app.element.removeEventListener('_opened', onopen);
          self.app.publish('loadtime', {
            time: parseInt(Date.now() - self.app.launchTime),
            type: 'w',
            src: self.app.config.url
          });
        });
      }
      this.app.reviveBrowser();
      this.app.launchTime = Date.now();
      this.app.fadeIn();
      this.app.requestForeground();

      // TODO:
      // May have orientation manager to deal with lock orientation request.
      if (this.app.isHomescreen ||
          this.app.isCallscreenWindow) {
        this.app.setOrientation();
      }
    };

  AppTransitionController.prototype.handle_opened =
    function atc_handle_opened() {
      if (!this.app || !this.app.element) {
        return;
      }

      this.app.reviveBrowser();
      this.resetTransition();
      this.app.element.removeAttribute('aria-hidden');
      this.app.show();
      this.app.element.classList.add('active');
      this.app.requestForeground();

      // TODO:
      // May have orientation manager to deal with lock orientation request.
      if (!this.app.isCallscreenWindow) {
        this.app.setOrientation();
      }
      this.focusApp();
    };

  AppTransitionController.prototype.focusApp = function() {
    if (!this.app) {
      return;
    }

    if (this._shouldFocusApp()) {
      this.app.debug('focusing this app.');
      this.app.focus();
      this.app.setNFCFocus(true);
    }
  };

  AppTransitionController.prototype._shouldFocusApp = function() {
    // SearchWindow should not focus itself,
    // because the input is inside system app.
    var bottomWindow = this.app.getBottomMostWindow();
    var topmostui = Service.query('getTopMostUI');
    return (this.app.CLASS_NAME !== 'SearchWindow' &&
            this._transitionState == 'opened' &&
            Service.query('getTopMostWindow') === this.app &&
            topmostui &&
            topmostui.name === bottomWindow.HIERARCHY_MANAGER);
  };

  AppTransitionController.prototype.requireOpen = function(animation) {
    this.currentAnimation = animation || this.openAnimation;
    this.app.debug('open with ' + this.currentAnimation);
    if (this.currentAnimation == 'immediate') {
      this.changeTransitionState('immediate-open');
    } else {
      this.changeTransitionState('open');
    }
  };

  AppTransitionController.prototype.requireClose = function(animation) {
    this.currentAnimation = animation || this.closeAnimation;
    this.app.debug('close with ' + this.currentAnimation);
    if (this.currentAnimation == 'immediate') {
      this.changeTransitionState('immediate-close');
    } else {
      this.changeTransitionState('close');
    }
  };

  AppTransitionController.prototype.resetTransition =
    function atc_resetTransition() {
      if (this._openingTimeout) {
        window.clearTimeout(this._openingTimeout);
        this._openingTimeout = null;
      }

      if (this._closingTimeout) {
        window.clearTimeout(this._closingTimeout);
        this._closingTimeout = null;
      }
      this.clearTransitionClasses();
    };

  AppTransitionController.prototype.clearTransitionClasses =
    function atc_removeTransitionClasses() {
      if (!this.app || !this.app.element) {
        return;
      }

      var classes = ['enlarge', 'reduce', 'to-cardview', 'from-cardview',
        'invoking', 'invoked', 'zoom-in', 'zoom-out', 'fade-in', 'fade-out',
        'transition-opening', 'transition-closing', 'immediate', 'fadeout',
        'slideleft', 'slideright', 'in-from-left', 'out-to-right',
        'will-become-active', 'will-become-inactive',
        'slide-to-top', 'slide-from-top',
        'slide-to-bottom', 'slide-from-bottom',
        'home-from-cardview', 'home-to-cardview'];

      classes.forEach(function iterator(cls) {
        this.app.element.classList.remove(cls);
      }, this);
    };

  AppTransitionController.prototype.handleEvent =
    function atc_handleEvent(evt) {
      switch (evt.type) {
        case '_opening':
          this.handle_opening();
          break;
        case '_opened':
          this.handle_opened();
          break;
        case '_closed':
          this.handle_closed();
          break;
        case '_closing':
          this.handle_closing();
          break;
        case '_closingtimeout':
        case '_openingtimeout':
          this.changeTransitionState('timeout', evt.type);
          break;
        case '_loaded':
          if (this._waitingForLoad) {
            this._waitingForLoad = false;
            this.changeTransitionState('complete');
          }
          break;
        case 'animationend':
          evt.stopPropagation();
          // Hide touch-blocker when launching animation is ended.
          this.app.element.classList.remove('transition-opening');

          // We decide to drop this event if system is busy loading
          // the active app or doing some other more important task.
          if (Service.query('isBusyLoading')) {
            this._waitingForLoad = true;
            if (this.app.isHomescreen && this._transitionState == 'opening') {
              /**
               * focusing the app will have some side effect,
               * but we don't care if we are opening the homescreen.
               */
              this.app.focus();
            }
            return;
          }
          this.app.debug(evt.animationName + ' has been ENDED!');
          this.changeTransitionState('complete', evt.type);
          break;
      }
    };
  exports.AppTransitionController = AppTransitionController;
}(window));