Source: js/activities.js

'use strict';
/* global ActionMenu, BaseModule, applications,
  ManifestHelper, DefaultActivityHelper, LazyLoader */

(function() {


  var Activities = function Activities() {
    this.actionMenu = null;
  };

  Activities.EVENTS = [
    'mozChromeEvent',
    'appopened',
    'applicationinstall'
  ];

  /**
   * Handles relaying of information for web activities.
   * Contains code to display the list of valid activities,
   * and fires an event off when the user selects one.
   * @class Activities
   */

  BaseModule.create(Activities, {
    /** @lends Activities */

    name: 'Activities',

    _start: function() {
      return LazyLoader.load([
        'shared/js/default_activity_helper.js',
        'shared/js/settings_helper.js'
      ]);
    },

    /**
     * Remove all event listeners. This is mainly used in unit tests.
     */
    destroy: function() {
      window.removeEventListener('mozChromeEvent', this);
      window.removeEventListener('appopened', this);
      window.removeEventListener('applicationinstall', this);
    },

    /**
    * General event handler interface.
    * Updates the overlay with as we receive load events.
    * @memberof Activities.prototype
    * @param {DOMEvent} evt The event.
    */
    handleEvent: function(evt) {
      switch (evt.type) {
        case 'mozChromeEvent':
          var detail = evt.detail;
          switch (detail.type) {
            case 'activity-choice':
              this.chooseActivity(detail);
              break;
          }
          break;
        case 'appopened':
          if (this.actionMenu) {
            this.actionMenu.hide();
          }
          break;
        case 'applicationinstall':
          this._onNewAppInstalled(evt.detail.application);
          break;
      }
    },

    _onNewAppInstalled: function(app) {
      var activities = app && app.manifest && app.manifest.activities;
      if (!activities) {
        return;
      }

      Object.keys(activities).forEach(function(activity) {
        var filters = activities[activity].filters;

        // Type can be single value, array, or a definition object (with value)
        var type = filters && filters.type && filters.type.value ||
                   filters && filters.type;

        if (!type) {
          return;
        }

        if (typeof type === 'string') {
          // single value, change to Array
          type = type.split(',');
        }

        // Now that it's an array, we check all the elements
        type.forEach((diff) => {
          DefaultActivityHelper.getDefaultAction(activity, diff)
            .then((defApp) => {
              // If default launch app is set for the type
              if (defApp) {
                // Delete the current default app
                DefaultActivityHelper.setDefaultAction(activity, diff, null);
              }
          });
        });
      });
    },

    /**
     * This gets the index from the defaultChoice in the choices
     * list.
     * @param  {Object} defaultChoice The default choice
     * @return {Integer}              The index where the default
     *                                choice is located. -1 if it
     *                                is not found
     */
    _choiceFromDefaultAction: function(defaultChoiceManifest, detail) {
      var index = -1;
      if (defaultChoiceManifest) {
        index = detail.choices.findIndex(function(choice) {
          return choice.manifest.indexOf(defaultChoiceManifest) !== -1;
        });
      }

      return index;
    },

   /**
    * Displays the activity menu if needed.
    * If there is only one option, the activity is automatically launched.
    * @memberof Activities.prototype
    * @param {Object} detail The activity choose event detail.
    */
    chooseActivity: function(detail) {
      var name = detail && detail.name;
      var type = detail && detail.activityType;
      this._detail = detail;
      this.publish('activityrequesting');

      DefaultActivityHelper.getDefaultAction(name, type).then(
        this._gotDefaultAction.bind(this));
    },

    _gotDefaultAction: function(defaultChoice) {
      var choices = this._detail.choices;
      var index = this._choiceFromDefaultAction(defaultChoice, this._detail);
      if (index > -1) {
        this.choose(index);
      } else if (choices.length === 1) {
        this.choose('0');
      } else {
        //
        // Our OMA Forward Lock DRM implementation relies on a "view"
        // activity to invoke the "fl" app when the user clicks on a
        // link to content with a mime type of
        // "application/vnd.oma.dd+xml" or "application/vnd.oma.drm.message".
        //
        // In order for this to be secure, we need to ensure that the
        // FL app is the only one that can respond to view activity
        // requests for those particular mime types. Here in the System app
        // we don't know what the type associated with an activity request is
        // but we do know the name of the activity. So if this is an activity
        // choice for a "view" activity, and the FL app is one of the choices
        // then we must select the FL app without allowing the user to choose
        // any of the others.
        //
        // If we wanted to be more general here we could perhaps
        // modify this code to allow any certified app to handle the
        // activity, but it is much simpler to restrict to the FL app
        // only.
        //
        if (this._detail.name === 'view') {
          var flAppIndex = choices.findIndex(function(choice) {
            var matchingRegex =
              /^(http|https|app)\:\/\/fl\.gaiamobile\.org\//;
            return matchingRegex.test(choice.manifest);
          });
          if (flAppIndex !== -1) {
            this.choose(flAppIndex.toString(10)); // choose() requires a string
            return;
          }
        }

        // Since the mozChromeEvent could be triggered by a 'click', and gecko
        // event are synchronous make sure to exit the event loop before
        // showing the list.
        setTimeout((function nextTick() {
          // Bug 852785: force the keyboard to close before the activity menu
          // shows
          window.dispatchEvent(new CustomEvent('activitymenuwillopen'));

          var name = this._detail.name;
          var type = this._detail.activityType;
          var config = DefaultActivityHelper.getDefaultConfig(name, type);

          var titleId;
          if (config) {
            titleId = config.l10nId;
          } else {
            titleId = 'activity-' + this._detail.name;
          }

          var askForDefault = config !== undefined;
          var items = this._listItems(choices);

          if (!this.actionMenu) {
            var controller = {
              successCb: this.choose.bind(this),
              cancelCb: this.cancel.bind(this)
            };

            LazyLoader.load('js/action_menu.js', function() {
              this.actionMenu = new ActionMenu(controller);
              this.actionMenu.show(items, titleId, askForDefault);
            }.bind(this));
          } else if (!this.actionMenu.active) {
            this.actionMenu.show(items, titleId, askForDefault);
          }
        }).bind(this));
      }
    },

   /**
    * The user chooses an activity from the activity menu.
    * @memberof Activities.prototype
    * @param {Number} choice The activity choice.
    * @param {Boolean} setAsDefault Should this be set as the default activity.
    */
    choose: function(choice, setAsDefault) {
      if (this.actionMenu) {
        this.actionMenu.hide();
      }
      var returnedChoice = {
        id: this._detail.id,
        type: 'activity-choice',
        value: choice,
        setAsDefault: setAsDefault
      };
      var name = this._detail.name;
      var type = this._detail.activityType;

      if (setAsDefault) {
        DefaultActivityHelper.setDefaultAction(name, type,
          this._detail.choices[choice].manifest);
      }

      this._sendEvent(returnedChoice);
      delete this._detail;
    },

   /**
    * Cancels from the activity menu.
    * @memberof Activities.prototype
    */
    cancel: function() {
      if (this.actionMenu) {
        this.actionMenu.hide();
      }

      var returnedChoice = {
        id: this._detail.id,
        type: 'activity-choice',
        value: -1
      };

      this._sendEvent(returnedChoice);
      delete this._detail;
    },

    /**
     * Sends an event to the platform when a user makes a choice
     * or cancels the activity menu.
     * @memberof Activities.prototype
     * @param {Number} value The index of the selected activity.
     */
    _sendEvent: function(value) {
      var event = document.createEvent('CustomEvent');
      event.initCustomEvent('mozContentEvent', true, true, value);
      window.dispatchEvent(event);
    },

    /**
     * Formats and returns a list of activity choices.
     * @memberof Activities.prototype
     * @param {Array} choices The list of activity choices.
     * @return {Array}
     */
    _listItems: function(choices) {
      var items = [];

      choices.forEach(function(choice, index) {
        var app = applications.getByManifestURL(choice.manifest);
        if (!app) {
          return;
        }

        items.push({
          label: new ManifestHelper(app.manifest).name,
          icon: choice.icon,
          manifest: choice.manifest,
          value: index
        });
      });

      return items;
    }
  });

}());