'use strict'; /* global ActionMenu, BaseModule, applications, ManifestHelper, DefaultActivityHelper, LazyLoader */ (function() { var Activities = function Activities() { this.actionMenu = null; }; Activities.EVENTS = [ 'mozChromeEvent', 'appopened', 'lockscreen-appopened', 'home', '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('lockscreen-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 'home': case 'lockscreen-appopened': if (this._detail) { this.cancel(); } 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.publish('activitychoosen'); 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); this.publish('activitycanceled'); 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; } }); }());