Source: panels/simcard_manager/simcard_manager.js

/**
 * SimCardManager is responsible for
 *   1. handling simcard UI
 *   2. handling simcard virtual status (please refer SimUIModel class)
 *   3. handling related mozSettings (please refer SimSettingsHelper class)
 *
 * @module SimCardManager
 */
define(function(require) {
  'use strict';

  var _ = window.navigator.mozL10n.get;
  var l10n = window.navigator.mozL10n;
  var Tagged = require('shared/tagged');
  var SimSettingsHelper = require('shared/sim_settings_helper');
  var AirplaneModeHelper = require('shared/airplane_mode_helper');
  var MobileOperator = require('shared/mobile_operator');
  var SimUIModel = require('panels/simcard_manager/sim_ui_model');

  var SimCardManager = function(elements) {
    // we store all SimUIModel instances into this array
    this._elements = elements;
    this._simcards = [];
    this._isAirplaneMode = false;
  };

  SimCardManager.prototype = {
    /**
     * Initiazlization
     *
     * @memberOf SimCardManager
     * @access public
     */
    init: function scm_init() {
      // `handleEvent` is used to handle these sim related changes
      this._elements.outgoingCallSelect.addEventListener('change', this);
      this._elements.outgoingMessagesSelect.addEventListener('change', this);

      // XXX because we handle `onchange` event differently in value selector,
      // in order to show confirm dialog after users changing value, the better
      // way right now is to check values when `onblur` event triggered.
      this._addOutgoingDataSelectEvent();
      this._addVoiceChangeEventOnConns();
      this._addCardStateChangeEventOnIccs();
      this._addLocalizedChangeEventOnIccs();

      // SMS app will directly change this value if users are going to
      // donwload specific sms from differnt simcard, so we have to
      // make sure our UI will reflect the right value at the moment.
      SimSettingsHelper.observe('outgoingData',
        this._outgoingDataChangeEvent.bind(this));

      // because in fugu, airplaneMode will not change cardState
      // but we still have to make UI consistent. In this way,
      // when airplaneMode is on in fugu, we have to mimic the nosim
      // situation in single sim.
      this._addAirplaneModeChangeEvent();

      this._isAirplaneMode =
        AirplaneModeHelper.getStatus() === 'enabled' ? true : false;

      // init UI
      this._initSimCardsInfo();
      this._initSimCardManagerUI();
    },

    simItemView: function({simIndex}) {
      return Tagged.escapeHTML `<li class="sim-card sim-card-${simIndex}">
        <div class="sim-card-icon"></div>
        <div class="information-container">
          <p class="sim-card-name"></p>
          <p class="sim-card-operator"></p>
          <bdi class="sim-card-number"></bdi>
        </div>
      </li>`;
    },

    /**
     * We will initialize SimUIModel and store them into our internal
     * variables.
     *
     * @memberOf SimCardManager
     * @access public
     */
    _initSimCardsInfo: function scm__initSimCardsInfo() {
      var conns = window.navigator.mozMobileConnections;
      for (var cardIndex = 0; cardIndex < conns.length; cardIndex++) {
        var conn = conns[cardIndex];
        var iccId = conn.iccId;
        var simcard = SimUIModel(cardIndex);
        this._simcards.push(simcard);
        this._updateCardState(cardIndex, iccId);
      }
    },

    /**
     * Handle incoming events
     *
     * @memberOf SimCardManager
     * @access private
     * @param {Event} evt
     */
    handleEvent: function scm_handlEvent(evt) {
      var cardIndex = evt.target.value;

      // it means users is seleting '--' options
      // when _simcards are all disabled
      if (cardIndex === SimSettingsHelper.EMPTY_OPTION_VALUE) {
        return;
      }

      switch (evt.target) {
        case this._elements.outgoingCallSelect:
          SimSettingsHelper.setServiceOnCard('outgoingCall', cardIndex);
          break;

        case this._elements.outgoingMessagesSelect:
          SimSettingsHelper.setServiceOnCard('outgoingMessages', cardIndex);
          break;
      }
    },

    /**
     * Handle mozSettings change event for `outgoing data` key
     *
     * @memberOf SimCardManager
     * @access private
     * @param {Number} cardIndex
     */
    _outgoingDataChangeEvent: function scm__outgoingDataChangeEvent(cardIndex) {
      this._elements.outgoingDataSelect.value = cardIndex;
    },

    /**
     * Handle change event for `outgoing data` select
     *
     * @memberOf SimCardManager
     * @access private
     */
    _addOutgoingDataSelectEvent: function scm__addOutgoingDataSelectEvent() {
      var prevCardIndex;
      var newCardIndex;

      // initialize these two variables when focus
      this._elements.outgoingDataSelect.addEventListener('focus', function() {
        prevCardIndex = this.selectedIndex;
        newCardIndex = this.selectedIndex;
      });

      this._elements.outgoingDataSelect.addEventListener('blur', function() {
        newCardIndex = this.selectedIndex;
        if (prevCardIndex !== newCardIndex) {
          // UX needs additional hint for users to make sure
          // they really want to change data connection
          var wantToChange =
            window.confirm(_('change-outgoing-data-confirm'));

          if (wantToChange) {
            SimSettingsHelper.setServiceOnCard('outgoingData',
              newCardIndex);
          } else {
            this.selectedIndex = prevCardIndex;
          }
        }
      });
    },

    /**
     * Get count of current simcards
     *
     * @memberOf SimCardManager
     * @access private
     * @return {Number} count of simcards
     */
    _getSimCardsCount: function scm__getSimCardsCount() {
      return this._simcards.length;
    },

    /**
     * Get information of simcard
     *
     * @memberOf SimCardManager
     * @access private
     * @param {Number} cardIndex
     * @return {Object} information stored in SimUIModel
     */
    _getSimCardInfo: function scm__getSimCardInfo(cardIndex) {
      return this._simcards[cardIndex].getInfo();
    },

    /**
     * Get simcard
     *
     * @memberOf SimCardManager
     * @access private
     * @param {Number} cardIndex
     * @return {SimUIModel}
     */
    _getSimCard: function scm__getSimCard(cardIndex) {
      return this._simcards[cardIndex];
    },

    /**
     * Iterate stored instances of SimUIModel and update each Sim UI
     *
     * @memberOf SimCardManager
     * @access private
     */
    _updateSimCardsUI: function scm__updateSimCardsUI() {
      this._simcards.forEach(function(simcard, cardIndex) {
        this._updateSimCardUI(cardIndex);
      }.bind(this));
    },

    /**
     * We would use specified instance of SimUIModel based on passing cardIndex
     * to render related UI on SimCardManager.
     *
     * @memberOf SimCardManager
     * @access private
     * @param {Number} cardIndex
     */
    _updateSimCardUI: function scm__updateSimCardUI(cardIndex) {
      var simcardInfo = this._getSimCardInfo(cardIndex);
      var selectors = ['name', 'number', 'operator'];

      var cardSelector = '.sim-card-' + cardIndex;

      var cardDom =
        this._elements.simCardContainer.querySelector(cardSelector);

      // reflect cardState on UI
      cardDom.classList.toggle('absent', simcardInfo.absent);
      cardDom.classList.toggle('locked', simcardInfo.locked);
      cardDom.classList.toggle('enabled', simcardInfo.enabled);

      // we are in three rows now, we have to fix styles
      cardDom.classList.toggle('with-number', !!simcardInfo.number);

      // relflect wordings on UI
      selectors.forEach(function(selector) {

        // will generate ".sim-card-0 .sim-card-name" for example
        var targetSelector = cardSelector + ' .sim-card-' + selector;
        var element =
          this._elements.simCardContainer.querySelector(targetSelector);
        if (selector === 'number') {
          element.textContent = simcardInfo[selector];
        } else {
          var l10nObj = simcardInfo[selector];
          if (l10nObj.id) {
            l10n.setAttributes(element, l10nObj.id, l10nObj.args);
          } else {
            element.removeAttribute('data-l10n-id');
            element.textContent = l10nObj.text;
          }
        }
      }.bind(this));
    },

    /**
     * Initialize SimCardManager UIs which includes
     * SimCardsUI, selectOptionsUI, simSecurityUI
     *
     * @memberOf SimCardManager
     * @access private
     */
    _initSimCardManagerUI: function scm__initSimCardManagerUI() {
      this._initSimCardsUI();
      this._updateSelectOptionsUI();

      // we only inject basic DOM from templates before
      // , so we have to map UI to its info
      this._updateSimCardsUI();
      this._updateSimSettingsUI();
    },

    /**
     * Initialize SimCardsUI
     *
     * @memberOf SimCardManager
     * @access private
     */
    _initSimCardsUI: function scm__initSimCardsUI() {
      var simItemHTMLs = [];

      // inject new childs
      this._simcards.forEach(function(simcard, index) {
        simItemHTMLs.push(
          this.simItemView({
            simIndex: index.toString()
          })
        );
      }.bind(this));

      this._elements.simCardContainer.innerHTML = simItemHTMLs.join('');
    },

    /**
     * Update the UI of the sim settings section
     *
     * @memberOf SimCardManager
     * @access private
     */
    _updateSimSettingsUI: function scm__updateSimSettingsUI() {
      var firstCardInfo = this._simcards[0].getInfo();
      var secondCardInfo = this._simcards[1].getInfo();
      var hidden = firstCardInfo.absent && secondCardInfo.absent ||
        this._isAirplaneMode;

      // if we don't have any card available right now
      // or if we are in airplane mode
      this._elements.simSettingsHeader.hidden = hidden;
      this._elements.simSettingsList.hidden = hidden;
    },

    /**
     * Update SelectOptions UI
     *
     * @memberOf SimCardManager
     * @access private
     */
    _updateSelectOptionsUI: function scm__updateSelectOptionsUI() {
      var firstCardInfo = this._simcards[0].getInfo();
      var secondCardInfo = this._simcards[1].getInfo();

      // two cards all are not absent, we have to update separately
      if (!firstCardInfo.absent && !secondCardInfo.absent) {
        SimSettingsHelper.getCardIndexFrom('outgoingCall',
          function(cardIndex) {
            this._updateSelectOptionUI('outgoingCall', cardIndex,
              this._elements.outgoingCallSelect);
        }.bind(this));

        SimSettingsHelper.getCardIndexFrom('outgoingMessages',
          function(cardIndex) {
            this._updateSelectOptionUI('outgoingMessages', cardIndex,
              this._elements.outgoingMessagesSelect);
        }.bind(this));

        SimSettingsHelper.getCardIndexFrom('outgoingData',
          function(cardIndex) {
            this._updateSelectOptionUI('outgoingData', cardIndex,
              this._elements.outgoingDataSelect);
        }.bind(this));
      } else {
        // there is one card absent while the other one is not

        var selectedCardIndex;

        // if two cards all are absent
        if (firstCardInfo.absent && secondCardInfo.absent) {
          // we will just set on the first card even
          // they are all with '--'
          selectedCardIndex = 0;
        } else {
          // if there is one card absent, the other one is not absent

          // we have to set defaultId to available card automatically
          // and disable select/option
          selectedCardIndex = firstCardInfo.absent ? 1 : 0;
        }

        // for these two situations, they all have to be disabled
        // and can not be selected by users
        this._elements.outgoingCallSelect.disabled = true;
        this._elements.outgoingMessagesSelect.disabled = true;
        this._elements.outgoingDataSelect.disabled = true;

        // then change related UI
        this._updateSelectOptionUI('outgoingCall', selectedCardIndex,
          this._elements.outgoingCallSelect);
        this._updateSelectOptionUI('outgoingMessages', selectedCardIndex,
          this._elements.outgoingMessagesSelect);
        this._updateSelectOptionUI('outgoingData', selectedCardIndex,
          this._elements.outgoingDataSelect);
      }
    },

    /**
     * Update SelectOption UI
     *
     * @memberOf SimCardManager
     * @access private
     * @param {String} storageKey
     * @param {Number} selectedCardIndex
     * @param {HTMLElement} selectedDOM
     */
    _updateSelectOptionUI: function scm__updateSelectOptionUI(
      storageKey, selectedCardIndex, selectDOM) {
        // We have to remove old options first
        while (selectDOM.firstChild) {
          selectDOM.removeChild(selectDOM.firstChild);
        }

        // then insert the new ones
        this._simcards.forEach(function(simcard, index) {
          var simcardInfo = simcard.getInfo();
          var option = document.createElement('option');
          option.value = index;

          if (simcardInfo.absent) {
            option.value = SimSettingsHelper.EMPTY_OPTION_VALUE;
            option.text = SimSettingsHelper.EMPTY_OPTION_TEXT;
          } else {
            if (simcardInfo.name.id) {
              l10n.setAttributes(option,
                simcardInfo.name.id, simcardInfo.name.args);
            } else {
              option.text = simcardInfo.name.text;
            }
          }

          if (index == selectedCardIndex) {
            option.selected = true;
          }

          selectDOM.add(option);
        });

        // we will add `always ask` option these two select
        if (storageKey === 'outgoingCall' ||
          storageKey === 'outgoingMessages') {
            var option = document.createElement('option');
            option.value = SimSettingsHelper.ALWAYS_ASK_OPTION_VALUE;
            option.setAttribute('data-l10n-id', 'sim-manager-always-ask');

            if (SimSettingsHelper.ALWAYS_ASK_OPTION_VALUE ===
              selectedCardIndex) {
                option.selected = true;
            }
            selectDOM.add(option);
        }
    },

    /**
     * Check whether current cardState is locked or not.
     *
     * @memberOf SimCardManager
     * @access private
     * @param {String} cardState
     * @return {Boolean}
     */
    _isSimCardLocked: function scm__isSimCardLocked(cardState) {
      var lockedState = [
        'pinRequired',
        'pukRequired',
        'networkLocked',
        'serviceProviderLocked',
        'corporateLocked',
        'network1Locked',
        'network2Locked',
        'hrpdNetworkLocked',
        'ruimCorporateLocked',
        'ruimServiceProviderLocked'
      ];

      // make sure the card is in locked mode or not
      return lockedState.indexOf(cardState) !== -1;
    },

    /**
     * Check whether current cardState is blocked or not.
     *
     * @memberOf SimCardManager
     * @access private
     * @param {String} cardState
     * @return {Boolean}
     */
    _isSimCardBlocked: function scm__isSimCardBlocked(cardState) {
      var uselessState = [
        'permanentBlocked'
      ];
      return uselessState.indexOf(cardState) !== -1;
    },

    /**
     * If voidechange happened on any conn, we would upate its cardState and
     * reflect the change on UI.
     *
     * @memberOf SimCardManager
     * @access private
     */
    _addVoiceChangeEventOnConns: function scm__addVoiceChangeEventOnConns() {
      var conns = window.navigator.mozMobileConnections;
      for (var i = 0; i < conns.length; i++) {
        var iccId = conns[i].iccId;
        conns[i].addEventListener('voicechange',
          this._updateCardStateWithUI.bind(this, i, iccId));
      }
    },

    /**
     * Iterate conns to add changeEvent
     *
     * @memberOf SimCardManager
     * @access private
     */
    _addCardStateChangeEventOnIccs:
      function scm__addCardStateChangeEventOnIccs() {
        var conns = window.navigator.mozMobileConnections;
        var iccManager = window.navigator.mozIccManager;
        for (var i = 0; i < conns.length; i++) {
          var iccId = conns[i].iccId;
          var icc = iccManager.getIccById(iccId);
          if (icc) {
            this._addChangeEventOnIccByIccId(iccId);
          }
        }
    },

    /**
     * When localized event happened, we would update each cardState and its
     * UI.
     *
     * @memberOf SimCardManager
     * @access private
     */
    _addLocalizedChangeEventOnIccs:
      function scm__addLocalizedChangeEventOnIccs() {
        var conns = window.navigator.mozMobileConnections;
        window.addEventListener('localized', function() {
          for (var i = 0; i < conns.length; i++) {
            var iccId = conns[i].iccId;
            this._updateCardStateWithUI(i, iccId);
          }
        }.bind(this));
    },

    /**
     * Add change event on each icc and would update UI if possible.
     *
     * @memberOf SimCardManager
     * @access private
     * @param {String} iccId
     */
    _addChangeEventOnIccByIccId:
      function scm__addChangeEventOnIccByIccId(iccId) {
        var self = this;
        var icc = window.navigator.mozIccManager.getIccById(iccId);
        if (icc) {
          icc.addEventListener('cardstatechange', function() {
            var cardIndex = self._getCardIndexByIccId(iccId);
            self._updateCardStateWithUI(cardIndex, iccId);

            // If we make PUK locked for more than 10 times,
            // we sould get `permanentBlocked` state, in this way
            // we have to update select/options
            if (self._isSimCardBlocked(icc.cardState)) {
              self._updateSelectOptionsUI();
            }
          });
        }
    },

    /**
     * If the state of APM is changed, we will update states and update all
     * related UIs.
     *
     * @memberOf SimCardManager
     * @access private
     */
    _addAirplaneModeChangeEvent: function scm__addAirplaneModeChangeEvent() {
      var self = this;
      AirplaneModeHelper.addEventListener('statechange', function(state) {
        // we only want to handle these two states
        if (state === 'enabled' || state === 'disabled') {
          var enabled = (state === 'enabled') ? true : false;
          self._isAirplaneMode = enabled;
          self._updateCardsState();
          self._updateSimCardsUI();
          self._updateSimSettingsUI();
        }
      });
    },

    /**
     * Iterate conns to call updateCardState on each conn.
     *
     * @memberOf SimCardManager
     * @access private
     */
    _updateCardsState: function scm__updateCardsState() {
      var conns = window.navigator.mozMobileConnections;
      for (var cardIndex = 0; cardIndex < conns.length; cardIndex++) {
        var iccId = conns[cardIndex].iccId;
        this._updateCardState(cardIndex, iccId);
      }
    },

    /**
     * we will use specified conn to update its state on our internal simcards
     *
     * @memberOf SimCardManager
     * @access private
     * @param {Number} cardIndex
     * @param {String} iccId
     */
    _updateCardState: function scm__updateCardState(cardIndex, iccId) {
      var iccManager = window.navigator.mozIccManager;
      var conn = window.navigator.mozMobileConnections[cardIndex];
      var simcard = this._simcards[cardIndex];

      if (!iccId || this._isAirplaneMode) {
        simcard.setState('nosim');
      } else {
        // else if we can get mobileConnection,
        // we have to check locked / enabled state
        var icc = iccManager.getIccById(iccId);
        var iccInfo = icc.iccInfo;
        var cardState = icc.cardState;
        var operatorInfo = MobileOperator.userFacingInfo(conn);

        if (this._isSimCardLocked(cardState)) {
          simcard.setState('locked');
        } else if (this._isSimCardBlocked(cardState)) {
          simcard.setState('blocked');
        } else {
          // TODO:
          // we have to call Gecko API here to make sure the
          // simcard is enabled / disabled
          simcard.setState('normal', {
            number: iccInfo.msisdn || iccInfo.mdn || '',
            operator: operatorInfo.operator
          });
        }
      }
    },

    /**
     * Sometimes, we have to update state and UI at the same time, so this is
     * a handy function to use.
     *
     * @memberOf SimCardManager
     * @access private
     * @param {Number} cardIndex
     * @return {String} iccId
     */
    _updateCardStateWithUI:
      function scm__updateCardStateWithUI(cardIndex, iccId) {
        this._updateCardState(cardIndex, iccId);
        this._updateSimCardUI(cardIndex);
        this._updateSimSettingsUI();
    },

    /**
     * This method would help us find out the index of passed in iccId.
     *
     * @memberOf SimCardManager
     * @access private
     * @param {String} iccId
     * @return {Number} cardIndex
     */
    _getCardIndexByIccId: function scm__getCardIndexByIccId(iccId) {
      var conns = window.navigator.mozMobileConnections;
      var cardIndex;
      for (var i = 0; i < conns.length; i++) {
        if (conns[i].iccId == iccId) {
          cardIndex = i;
        }
      }
      return cardIndex;
    }
  };

  return SimCardManager;
});