Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,121 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global IACHandler */
/* exports AboutServiceWorkersProxy */
/**
* On B2G we cannot directly show the content of about:* pages with access to
* privileged code. We already have an about:serviceworkers chrome page showing
* the list of installed service workers on Desktop. Unfortunately we cannot
* access it from Gaia. To access this information, we need to query the
* platform from the System app. And to show it we chose to add a new panel
* inside the existing "Developer" panel in the Settings app. The communication
* with the Settings app is done via Inter App Communication API.
*
* This module acts as a proxy between the Settings app and the platform.
* The high level picture is the following:
*
* Settings App
* |
* |- IAC
* |
* System App
* |
* |- Chrome/Content events
* |
* Gecko (ServiceWorkersManager accessed from a B2G dedicated component)
*/
'use strict';
(function(exports) {
const ASW_CHROME_EVENT = 'mozAboutServiceWorkersChromeEvent';
const ASW_CONTENT_EVENT = 'mozAboutServiceWorkersContentEvent';
var AboutServiceWorkersProxy = {
start: function() {
window.addEventListener('iac-about-service-workers', this.onPortMessage);
},
sendPortMessage: function(message) {
var port = IACHandler.getPort('about-service-workers', this);
if (port) {
port.postMessage(message);
} else {
console.error('No about-service-workers port');
}
},
onPortMessage: function(event) {
if (!event || !event.detail) {
console.error('Wrong event');
return;
}
var self = AboutServiceWorkersProxy;
var request = event.detail;
switch (request.name) {
case 'init':
case 'update':
case 'unregister':
self.chromeRequest(request).then(result => {
self.sendPortMessage({
id: request.id,
result: result
});
}).catch(error => {
self.sendPortMessage({
id: request.id,
error: error
});
});
break;
default:
console.error('Wrong method name');
break;
}
},
sendContentEvent: function(detail) {
var event = new CustomEvent(ASW_CONTENT_EVENT, {
detail: detail
});
window.dispatchEvent(event);
},
chromeRequest: function(request) {
if (!request) {
return Promise.reject('InternalErrorMissingEventDetail');
}
return new Promise((resolve, reject) => {
var id = request.id;
window.addEventListener(ASW_CHROME_EVENT,
function onChromeEvent(event) {
window.removeEventListener(ASW_CHROME_EVENT, onChromeEvent);
var message = event.detail;
if (!message || !message.id || message.id != id) {
return reject('InternalErrorWrongChromeEvent');
}
if (message.error) {
reject(message.error);
} else if (message.result) {
resolve(message.result);
}
});
AboutServiceWorkersProxy.sendContentEvent(request);
});
}
};
exports.AboutServiceWorkersProxy = AboutServiceWorkersProxy;
}(window));

View file

@ -0,0 +1,838 @@
'use strict';
/* global SettingsListener, LazyLoader, Service, SettingsHelper */
/* global AccessibilityQuicknavMenu */
(function(exports) {
/**
* Accessibility enables and disables the screenreader after the user
* gestures using the hardware buttons of the phone. To toggle the setting.
* the user must press volume up, then volume down three times in a row.
* @class Accessibility
* @requires SettingsListener
*/
function Accessibility() {}
Accessibility.prototype = {
/**
* How fast the autorepeat is.
* @type {Number}
* @memberof Accessibility.prototype
*/
REPEAT_INTERVAL: 600000,
/**
* Maximum interval between initial and final TOGGLE_SCREEN_READER_COUNT
* volume button presses.
* @type {Number}
* @memberof Accessibility.prototype
*/
REPEAT_BUTTON_PRESS: 15000000,
/**
* Number of times the buttons need to be pressed before the screen reader
* setting is toggled.
* @type {Number}
* @memberof Accessibility.prototype
*/
TOGGLE_SCREEN_READER_COUNT: 6,
/**
* Cap the full range of contrast. Actual -1 is completely gray, and 1
* makes things hard to see. This value is the max/min contrast.
*/
CONTRAST_CAP: 0.6,
/**
* Timeout (in milliseconds) between when a vc-change event fires
* and when interaction hints (if any) are spoken
*/
HINTS_TIMEOUT: 2000,
/**
* Default timeout value (in milliseconds) between the launch of the FTU and
* the time when screen reader instructions should be spoken.
* @type {Number}
*/
FTU_STARTED_TIMEOUT: 15000,
/**
* Current counter for button presses in short succession.
* @type {Number}
* @memberof Accessibility.prototype
*/
counter: 0,
/**
* Next expected event.
* @type {Object}
* @memberof Accessibility.prototype
*/
expectedEvent: {
type: 'volumeup',
timeStamp: 0
},
/**
* Expected complete time stamp.
* @type {Number}
* @memberof Accessibility.prototype
*/
expectedCompleteTimeStamp: 0,
/**
* Accessibility settings to be observed.
* @type {Object} name: value pairs.
* @memberof Accessibility.prototype
*/
settings: {
'accessibility.screenreader': false,
'accessibility.screenreader-volume': 1,
'accessibility.screenreader-rate': 0,
'accessibility.screenreader-captions': false,
'accessibility.screenreader-shade': false,
'accessibility.screenreader-ftu-timeout-seconds': 15,
'accessibility.screenreader-fallback-lang': 'en-US',
'accessibility.colors.enable': false,
'accessibility.colors.invert': false,
'accessibility.colors.grayscale': false,
'accessibility.colors.contrast': '0.0'
},
/**
* Audio used by the screen reader.
* Note: Lazy-loaded when first needed
* @type {Object}
* @memberof Accessibility.prototype
*/
sounds: {
clickedAudio: null,
vcKeyAudio: null,
vcMoveAudio: null,
noMoveAudio: null
},
/**
* URLs for screen reader audio files.
* @type {Object}
* @memberof Accessibility.prototype
*/
soundURLs: {
clickedAudio: './resources/sounds/screen_reader_clicked.ogg',
vcKeyAudio: './resources/sounds/screen_reader_virtual_cursor_key.ogg',
vcMoveAudio: './resources/sounds/screen_reader_virtual_cursor_move.ogg',
noMoveAudio: './resources/sounds/screen_reader_no_move.ogg'
},
/**
* Start listening for events.
* @memberof Accessibility.prototype
*/
start: function ar_init() {
this.screen = document.getElementById('screen');
this.speechSynthesizer = speechSynthesizer;
window.addEventListener('volumeup', this);
window.addEventListener('volumedown', this);
window.addEventListener('logohidden', this);
window.addEventListener('screenchange', this);
window.addEventListener('iac-ftucomms', this);
this.FTUStartedTimeout = null;
this.appOpening = false;
// Attach all observers.
Object.keys(this.settings).forEach(function attach(settingKey) {
SettingsListener.observe(settingKey, this.settings[settingKey],
function observe(aValue) {
var oldValue = this.settings[settingKey];
this.settings[settingKey] = aValue;
switch (settingKey) {
case 'accessibility.screenreader':
if (aValue) {
this.setToSupportedLanguage();
window.addEventListener('mozChromeEvent', this);
window.addEventListener('appwillopen', this);
window.addEventListener('appopened', this);
window.addEventListener('homescreenopening', this);
window.addEventListener('homescreenopened', this);
} else {
window.removeEventListener('mozChromeEvent', this);
window.removeEventListener('appwillopen', this);
window.removeEventListener('appopened', this);
window.removeEventListener('homescreenopening', this);
window.removeEventListener('homescreenopened', this);
}
if (this.settings['accessibility.screenreader-shade']) {
this.toggleShade(aValue, !aValue);
}
this.screen.classList.toggle('screenreader', aValue);
break;
case 'accessibility.colors.enable':
SettingsListener.getSettingsLock().set({
'layers.effect.invert': aValue ?
this.settings['accessibility.colors.invert'] : false,
'layers.effect.grayscale': aValue ?
this.settings['accessibility.colors.grayscale'] : false,
'layers.effect.contrast': aValue ?
this.settings['accessibility.colors.contrast'] *
this.CONTRAST_CAP : '0.0'
});
break;
case 'accessibility.screenreader-captions':
this.speechSynthesizer.captions = aValue;
// If captions are displayed hide them.
if (!aValue) {
this.speechSynthesizer.hideSpeech(true);
}
break;
case 'accessibility.screenreader-shade':
if (this.settings['accessibility.screenreader']) {
this.toggleShade(aValue, oldValue === aValue);
}
break;
case 'accessibility.colors.invert':
case 'accessibility.colors.grayscale':
case 'accessibility.colors.contrast':
if (this.settings['accessibility.colors.enable']) {
var effect = settingKey.split('.').pop();
var gfxSetting = {};
if (effect === 'contrast') {
gfxSetting['layers.effect.contrast'] =
aValue * this.CONTRAST_CAP;
} else {
gfxSetting['layers.effect.' + effect] = aValue;
}
SettingsListener.getSettingsLock().set(gfxSetting);
}
break;
case 'accessibility.screenreader-ftu-timeout-seconds':
this.FTU_STARTED_TIMEOUT = 1000 * aValue;
break;
}
}.bind(this));
}, this);
},
/**
* Reset the expected event to defaults.
* @memberof Accessibility.prototype
*/
reset: function ar_resetEvent() {
this.expectedEvent = {
type: 'volumeup',
timeStamp: 0
};
this.counter = 0;
},
/**
* Unset speaking flag and set the expected complete time stamp.
* @param {?Number} aExpectedCompleteTimeStamp Expected complete time
* stamp.
* @memberof Accessibility.prototype
*/
resetSpeaking: function ar_resetSpeaking(aExpectedCompleteTimeStamp) {
this.isSpeaking = false;
this.expectedCompleteTimeStamp = aExpectedCompleteTimeStamp || 0;
},
toggleShade: function ar_toggleShage(aEnable, aSilent) {
Service.request(aEnable ? 'turnShadeOn' : 'turnShadeOff');
if (!aSilent) {
this.speak({ string: aEnable ? 'shadeToggleOn' : 'shadeToggleOff' },
{ enqueue: true });
}
},
/**
* Checks that device language is supported in text to speech, if not
* it is set to a predetermined fallback language.
* @memberof Accessibility.prototype
*/
setToSupportedLanguage: function ar_setToSupportedLanguage() {
var settingsHelper = SettingsHelper('language.current');
var voices = this.speechSynthesizer.speech.getVoices();
var speechLangs = new Set(voices.map(v => v.lang.split('-')[0]));
settingsHelper.get((value) => {
if (!speechLangs.has(value.split('-')[0])) {
settingsHelper.set(
this.settings['accessibility.screenreader-fallback-lang']);
}
});
},
/**
* Handle volumeup and volumedown events generated from HardwareButtons.
* @param {Object} aEvent a high-level key event object generated from
* HardwareButtons.
* @memberof Accessibility.prototype
*/
handleVolumeButtonPress: function ar_handleVolumeButtonPress(aEvent) {
var type = aEvent.type;
var timeStamp = aEvent.timeStamp;
var expectedEvent = this.expectedEvent;
if (type !== expectedEvent.type || timeStamp > expectedEvent.timeStamp) {
this.reset();
if (type !== 'volumeup') {
return;
}
}
this.expectedEvent = {
type: type === 'volumeup' ? 'volumedown' :
'volumeup',
timeStamp: timeStamp + this.REPEAT_INTERVAL
};
if (++this.counter < this.TOGGLE_SCREEN_READER_COUNT) {
return;
}
this.reset();
this.disableFTUStartedTimeout();
if (!this.isSpeaking && timeStamp > this.expectedCompleteTimeStamp) {
this.cancelSpeech();
this.announceScreenReader()
.then(() => this.resetSpeaking(timeStamp + this.REPEAT_BUTTON_PRESS))
.catch(err => console.error(err));
return;
}
this.cancelSpeech();
this.resetSpeaking();
SettingsListener.getSettingsLock().set({
'accessibility.screenreader':
!this.settings['accessibility.screenreader']
});
},
/**
* Announce screen reader FTU_STARTED_TIMEOUT milliseconds after the FTU is
* loaded if the user does not proceed beyond the first step.
* @param {Object} aEvent an event object generated from FTU launcher.
*/
handleFTUStarted: function ar_handleFTUStarted(aEvent) {
if (this.settings['accessibility.screenreader']) {
// Only set the FTU timeout if the screen reader is not enabled.
return;
}
this.FTUStartedTimeout = setTimeout(() => {
this.cancelSpeech();
this.reset();
this.announceScreenReader()
.then(() => this.resetSpeaking(aEvent.timeStamp +
this.REPEAT_BUTTON_PRESS + this.FTU_STARTED_TIMEOUT * 1000))
.catch(err => console.error(err));
}, this.FTU_STARTED_TIMEOUT);
},
/**
* Disable a timeout before the screen reader starts speaking in FTU.
*/
disableFTUStartedTimeout: function ar_disableFTUStartedTimeout() {
clearTimeout(this.FTUStartedTimeout);
this.FTUStartedTimeout = null;
},
/**
* Reset FTU timeout and stop any spoken instructions if the user steps to
* the next FTU screen.
*/
handleFTUStep: function ar_handleFTUStep() {
if (!this.FTUStartedTimeout) {
// we've not got the started event yet
return;
}
this.disableFTUStartedTimeout();
this.cancelSpeech();
this.reset();
window.removeEventListener('iac-ftucomms', this);
},
/**
* Play audio for a screen reader notification.
* @param {String} aSoundKey a key for the screen reader audio.
* XXX: When Bug 848954 lands we should be able to use Web Audio API.
* @memberof Accessibility.prototype
*/
_playSound: function ar__playSound(aSoundKey) {
if (!this.sounds[aSoundKey]) {
this.sounds[aSoundKey] = new Audio(this.soundURLs[aSoundKey]);
this.sounds[aSoundKey].load();
}
var audio = this.sounds[aSoundKey].cloneNode(false);
audio.volume = this.volume;
audio.play();
},
/**
* Get current screen reader volume defined by the setting.
* @return {Number} Screen reader volume wihtin the [0, 1] interval.
* @memberof Accessibility.prototype
*/
get volume() {
return this.settings['accessibility.screenreader-volume'];
},
/**
* Get current screen reader speech rate defined by the setting.
* @return {Number} Screen reader rate within the [0.2, 10] interval.
* @memberof Accessibility.prototype
*/
get rate() {
var rate = this.settings['accessibility.screenreader-rate'];
return rate >= 0 ? rate + 1 : 1 / (Math.abs(rate) + 1);
},
/**
* Start a timeout that waits to display hints
* @memberof Accessibility.prototype
*/
setHintsTimeout: function ar_setHintsTimeout(aHints) {
clearTimeout(this.hintsTimer);
this.hintsTimer = setTimeout(function onHintsTimeout() {
this.isSpeakingHints = true;
this.speak(aHints, { enqueue: true })
.then(() => this.isSpeakingHints = false)
.catch(err => console.error(err));
}.bind(this), this.HINTS_TIMEOUT);
},
/**
* Handle accessfu mozChromeEvent.
* @param {Object} accessfu details object.
* @memberof Accessibility.prototype
*/
handleAccessFuOutput: function ar_handleAccessFuOutput(aDetails) {
this.cancelHints();
var options = aDetails.options || {};
var type = aDetails.eventType;
window.dispatchEvent(new CustomEvent('accessibility-action'));
if (this.appOpening) {
if (type === 'vc-change') {
this.lastVCChangeDetails = aDetails;
}
return;
}
switch (type) {
case 'vc-change':
// Vibrate when the virtual cursor changes.
navigator.vibrate(options.pattern);
this._playSound(options.isKey ? 'vcKeyAudio' : 'vcMoveAudio');
break;
case 'action':
if (aDetails.data[0].string === 'clickAction') {
// If element is clicked, play 'click' sound instead of speech.
this._playSound('clickedAudio');
return;
}
break;
case 'no-move':
this._playSound('noMoveAudio');
return;
}
this.speak(aDetails.data, { enqueue: options.enqueue }).then(() => {
if (options.hints) {
this.setHintsTimeout(options.hints);
}
}).catch(err => console.error(err));
},
handleAccessFuControl: function ar_handleAccessFuControls(aDetails) {
this.cancelHints();
switch (aDetails.eventType) {
case 'quicknav-menu':
if (!this.quicknav) {
LazyLoader.load(['js/accessibility_quicknav_menu.js'])
.then(() => {
this.quicknav = new AccessibilityQuicknavMenu();
this.quicknav.show();
}).catch(err => console.error(err));
} else {
this.quicknav.show();
}
break;
case 'toggle-shade':
SettingsListener.getSettingsLock().set({
'accessibility.screenreader-shade':
!this.settings['accessibility.screenreader-shade']
});
window.dispatchEvent(new CustomEvent('accessibility-action'));
break;
case 'toggle-pause':
this.speechSynthesizer.togglePause();
break;
default:
break;
}
},
/**
* Listen for screen change events and stop speaking if the
* screen is disabled (in 'off' state)
* @memberof Accessibility.prototype
*/
handleScreenChange: function ar_handleScreenChange(aDetail){
if(!aDetail.screenEnabled){
this.cancelHints();
}
},
/**
* Remove aria-hidden from the screen element to make content accessible to
* the screen reader.
* @memberof Accessibility.prototype
*/
activateScreen: function ar_activateScreen() {
// Screen reader will not say anything until the splash animation is
// hidden and the aria-hidden attribute is removed from #screen.
this.screen.removeAttribute('aria-hidden');
window.removeEventListener('logohidden', this);
},
/**
* Set the flag indicating that the app is opening.
*/
prepareForApp: function ar_prepareForApp() {
this.appOpening = true;
},
/**
* Speak the name of the app and announce latest virtual cursor change.
* @param {Object} aDetail app_window object
*/
announceApp: function ar_announceApp(aDetail) {
this.speak(aDetail.name)
.then(() => {
this.appOpening = false;
if (this.lastVCChangeDetails) {
this.handleAccessFuOutput(this.lastVCChangeDetails);
delete this.lastVCChangeDetails;
}
})
.catch(err => console.error(err));
},
/**
* Handle event.
* @param {Object} aEvent mozChromeEvent/logohidden/volumeup/volumedown.
* @memberof Accessibility.prototype
*/
handleEvent: function ar_handleEvent(aEvent) {
switch (aEvent.type) {
case 'screenchange':
this.handleScreenChange(aEvent.detail);
break;
case 'logohidden':
this.activateScreen();
break;
case 'iac-ftucomms':
if (aEvent.detail === 'started') {
this.handleFTUStarted(aEvent);
} else if(aEvent.detail.type == 'step') {
this.handleFTUStep();
}
break;
case 'mozChromeEvent':
switch (aEvent.detail.type) {
case 'accessibility-output':
this.handleAccessFuOutput(JSON.parse(aEvent.detail.details));
break;
case 'accessibility-control':
this.handleAccessFuControl(JSON.parse(aEvent.detail.details));
break;
}
break;
case 'volumeup':
case 'volumedown':
this.handleVolumeButtonPress(aEvent);
break;
case 'appwillopen':
case 'homescreenopening':
this.prepareForApp();
break;
case 'appopened':
case 'homescreenopened':
this.announceApp(aEvent.detail);
break;
}
},
/**
* Check for Hints speech/timer and clear.
* @memberof Accessibility.prototype
*/
cancelHints: function ar_cancelHints() {
clearTimeout(this.hintsTimer);
if(this.isSpeakingHints){
this.cancelSpeech();
this.isSpeakingHints = false;
}
},
/**
* Based on whether the screen reader is currently enabled, announce the
* instructions of how to enable/disable it.
* @memberof Accessibility.prototype
*/
announceScreenReader: function ar_announceScreenReader() {
var enabled = this.settings['accessibility.screenreader'];
this.isSpeaking = true;
return this.speak({
string: enabled ? 'disableScreenReaderSteps' : 'enableScreenReaderSteps'
}, {enqueue: false});
},
/**
* Use speechSynthesis to speak screen reader utterances.
* @param {?Array} aData Speech data before it is localized.
* synthesis is completed.
* @param {?Object} aOptions = {} Speech options such as enqueue etc.
* @memberof Accessibility.prototype
*/
speak: function ar_speak(aData, aOptions = {}) {
return this.speechSynthesizer.speak(
aData, aOptions, this.rate, this.volume);
},
/**
* Cancel any utterances currently being spoken by speechSynthesis.
* @memberof Accessibility.prototype
*/
cancelSpeech: function ar_cancelSpeech() {
this.speechSynthesizer.cancel();
}
};
/**
* A speech synthesizer component that handles speech localization and
* pronunciation.
* @type {Object}
*/
var speechSynthesizer = {
/**
* A delay before hiding screen reader caption.
* @type {Number}
* @memberof speechSynthesizer
*/
CAPTIONS_DELAY: 1500,
/**
* A flag used for speech captions rendering.
* @type {Boolean}
* @memberof speechSynthesizer
*/
captions: false,
/**
* Screen container getter.
* @type {Object}
* @memberof speechSynthesizer
*/
get screen() {
delete this.screen;
this.screen = document.getElementById('screen');
return this.screen;
},
/**
* Speech Synthesis
* @type {Object}
* @memberof speechSynthesizer
*/
get speech() {
delete this.speech;
// If there are no voices bundled, consider speech synthesis unavailable.
if (!window.speechSynthesis ||
window.speechSynthesis.getVoices().length === 0) {
this.speech = null;
}
this.speech = window.speechSynthesis;
return this.speech;
},
/**
* Speech utterance
* @type {Object}
* @memberof speechSynthesizer
*/
get utterance() {
delete this.utterance;
this.utterance = window.SpeechSynthesisUtterance;
return this.utterance;
},
/**
* Cancle speech if the screen reader is speaking.
* @memberof speechSynthesizer
*/
cancel: function ss_cancel() {
if (this.speech) {
this.speech.cancel();
}
},
/**
* Localize speech data.
* @param {Object} aDetails Speech data object.
* @return {String} Localized speech data.
* @memberof speechSynthesizer
*/
localize: function ss_localize(aDetails) {
if (!aDetails || typeof aDetails === 'string') {
return aDetails;
}
var string = aDetails.string;
var data = {
count: aDetails.count
};
if (!string) {
return '';
} else {
string = 'accessibility-' + string;
}
if (aDetails.args) {
data = aDetails.args.reduce(function(aData, val, index) {
aData[index] = val;
return aData;
}, data);
}
return navigator.mozL10n.get(string, data);
},
/**
* Build a complete utterance string by localizing an array of speech data.
* @param {?Array} aData Speech data.
* @return {String} A complete localized string from speech array data.
* @memberof speechSynthesizer
*/
buildUtterance: function ss_buildUtterance(aData) {
if (!Array.isArray(aData)) {
aData = [aData];
}
var words = [], localize = this.localize;
aData.reduce(function(words, details) {
var localized = localize(details);
if (localized) {
var word = localized.trim();
if (word) {
words.push(word);
}
}
return words;
}, words);
return words.join(' ');
},
/**
* Show caption for the screen reader.
* @param {String} aUtterance Current screen reader caption.
* @memberof speechSynthesizer
*/
showSpeech: function ar_showSpeech(aUtterance) {
if (!this.captionsBox) {
this.captionsBox = document.createElement('div');
this.captionsBox.id = 'accessibility-captions-box';
this.captionsBox.setAttribute('data-z-index-level',
'accessibility-captions');
this.captionsBox.setAttribute('aria-hidden', true);
this.screen.appendChild(this.captionsBox);
}
window.clearTimeout(this.captionsHideTimeout);
this.captionsHideTimeout = null;
this.captionsBox.textContent = aUtterance;
this.captionsBox.classList.add('visible');
},
/**
* Hide current screen reader caption.
* @param {Boolean} aImmediately A flag to hide captionsBox immediately.
* @memberof speechSynthesizer
*/
hideSpeech: function ar_hideSpeech(aImmediately) {
if (!this.captionsBox) {
return;
}
// Hide the caption after CAPTIONS_DELAY.
if (aImmediately) {
this.captionsBox.classList.remove('visible');
} else {
this.captionsHideTimeout = window.setTimeout(function() {
this.captionsBox.classList.remove('visible');
}.bind(this), this.CAPTIONS_DELAY);
}
},
togglePause: function ss_togglePause() {
if (this.speech.paused) {
this.speech.resume();
} else if (this.speech.speaking) {
this.speech.pause();
}
},
/**
* Utter a message with a speechSynthesizer.
* @param {?Array} aData A messages array to be localized.
* @param {JSON} aOptions Options to be used when speaking. For example: {
* enqueue: false
* }
* @param {Number} aRate Speech rate.
* @param {Number} aVolume Speech volume.
* @memberof speechSynthesizer
*/
speak: function ss_speak(aData, aOptions, aRate, aVolume) {
if (!this.speech || !this.utterance) {
return Promise.resolve();
}
if (!aOptions.enqueue) {
this.cancel();
}
if (this.speech.paused) {
this.speech.resume();
}
var sentence = this.buildUtterance(aData);
if (!sentence) {
return Promise.resolve();
}
return new Promise(resolve => {
var utterance = new this.utterance(sentence);
utterance.volume = aVolume;
utterance.rate = aRate;
utterance.addEventListener('end', () => {
if (this.captions) {
this.hideSpeech();
}
resolve();
});
if (this.captions) {
this.showSpeech(sentence);
}
this.speech.speak(utterance);
});
}
};
exports.Accessibility = Accessibility;
}(window));

View file

@ -0,0 +1,108 @@
'use strict';
/* global SettingsListener */
(function(exports) {
function AccessibilityQuicknavMenu() {
SettingsListener.observe('accessibility.screenreader_quicknav_modes', '',
function observe(aValue) {
this.updateModes(aValue.split(','));
}.bind(this));
SettingsListener.observe('accessibility.screenreader_quicknav_index', 0,
function observe(aValue) {
this.updateCurrentMode(aValue);
}.bind(this));
this.render();
}
AccessibilityQuicknavMenu.prototype = {
container: null,
currentMode: 0,
modes: [],
render: function render() {
this.element = document.createElement('div');
this.element.id = 'accessibility-quicknav';
this.element.dataset.zIndexLevel = 'accessibility-quicknav-menu';
this.element.dataset.type = 'value-selector';
this.element.setAttribute('role', 'dialog');
this.element.addEventListener('click', this);
this.element.innerHTML =
`<div id="accessibility-quicknav-container">
<section>
<ul role="listbox">
</ul>
</section>
</div>`;
if (this.modes.length) {
this.updateModes(this.modes);
}
var screen = document.getElementById('screen');
screen.appendChild(this.element);
},
updateModes: function updateModes(aModes) {
this.modes = aModes;
if (!this.element) {
return;
}
var ul = this.element.querySelector('ul');
ul.innerHTML = '';
for (var i in aModes) {
var li = document.createElement('li');
li.setAttribute('role', 'option');
if (i == this.currentMode) {
li.setAttribute('aria-selected', true);
}
li.setAttribute('aria-moz-quick-activate', true);
li.dataset.quicknavIndex = i;
var label = document.createElement('label');
label.setAttribute('role', 'presentation');
li.appendChild(label);
var span = document.createElement('span');
span.dataset.l10nId = 'accessibility-quicknav_' + aModes[i];
label.appendChild(span);
ul.appendChild(li);
}
},
updateCurrentMode: function updateCurrentMode(aCurrentMode) {
this.currentMode = aCurrentMode;
if (!this.element) {
return;
}
var item = this.element.querySelector('li[aria-selected=true]');
if (item) {
item.removeAttribute('aria-selected');
}
item = this.element.querySelectorAll('li[role=option]')[aCurrentMode];
if (item) {
item.setAttribute('aria-selected', true);
}
},
show: function show() {
this.element.classList.add('visible');
},
hide: function hide() {
this.element.classList.remove('visible');
},
handleEvent: function quicknavMenu_handleEvent(aEvent) {
var index = Number(aEvent.target.dataset.quicknavIndex);
// This configures the screen reader to use the selected mode for quicknav
SettingsListener.getSettingsLock().set({
'accessibility.screenreader_quicknav_index': index
});
this.hide();
}
};
exports.AccessibilityQuicknavMenu = AccessibilityQuicknavMenu;
}(window));

View file

@ -0,0 +1,162 @@
/* global SystemDialog */
/* global eventSafety */
'use strict';
(function(exports) {
/**
* @class ActionMenu
* @param {options} object for attributes `onShow`, `onHide` callback.
* @extends SystemDialog
*/
var ActionMenu = function(controller) {
this.instanceID = 'action-menu';
this.controller = controller;
this.onselected = controller.successCb || function() {};
this.oncancel = controller.cancelCb || function() {};
this.options = {};
/**
* render the dialog
*/
this.render();
this.publish('created');
};
ActionMenu.prototype = Object.create(SystemDialog.prototype);
ActionMenu.prototype.customID = 'action-menu';
ActionMenu.prototype.DEBUG = false;
ActionMenu.prototype.name = 'ActionMenu';
ActionMenu.prototype.EVENT_PREFIX = 'actionmenu';
ActionMenu.prototype.view = function spd_view() {
return `<div id="action-menu" class="action-menu" hidden>
<form id="action-menu-form" data-z-index-level="action-menu"
role="dialog" data-type="action">
<header id="action-menu-header" data-l10n-id=""></header>
<menu id="action-menu-list">
</menu>
</form>
</div>`;
};
ActionMenu.prototype._fetchElements = function() {
this.menu = document.getElementById('action-menu-list');
this.header = document.getElementById('action-menu-header');
this.form = document.getElementById('action-menu-form');
};
ActionMenu.prototype.show = function(items, titleId, defChoice) {
this.listItems = items;
if (titleId) {
this.header.setAttribute('data-l10n-id', titleId);
}
this.askForDefaultChoice = defChoice;
this._buildMenu(items);
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
this.form.classList.add('visible');
this.active = true;
});
});
SystemDialog.prototype.show.apply(this);
};
ActionMenu.prototype.hide = function() {
if (!this.active) {
return;
}
eventSafety(this.form, 'transitionend', function doHide(e) {
this.form.style.position = '';
SystemDialog.prototype.hide.apply(this);
}.bind(this), 350);
// See bug 1077474
this.form.style.position = 'fixed';
this.form.classList.remove('visible');
this.active = false;
};
ActionMenu.prototype._buildMenu = function(items) {
this.menu.innerHTML = '';
items.forEach(function traveseItems(item) {
var action = document.createElement('button');
action.dataset.value = item.value;
action.dataset.manifest = item.manifest;
action.textContent = item.label;
action.addEventListener('click', function(evt) {
this.onItemSelected(evt);
}.bind(this));
if (item.icon) {
action.classList.add(item.iconClass || 'icon');
action.style.backgroundImage = 'url(' + item.icon + ')';
}
this.menu.appendChild(action);
}, this);
if (this.askForDefaultChoice) {
this._appendDefault();
}
this._appendCancelButton();
};
ActionMenu.prototype.onItemSelected = function(evt) {
evt.preventDefault();
var target = evt.target;
var defaultSelected = false;
if (this.askForDefaultChoice) {
defaultSelected = this.defaultChoiceInput.checked;
}
var value = target.dataset.value;
if (!value) {
return;
}
value = parseInt(value);
this.hide();
this.onselected(value, defaultSelected);
};
ActionMenu.prototype._appendDefault = function() {
var defaultChoice = document.createElement('gaia-checkbox');
defaultChoice.className = 'inline';
defaultChoice.setAttribute('data-action', 'set-default-action');
this.defaultChoiceInput = defaultChoice;
var defaultChoiceLabel = document.createElement('label');
defaultChoiceLabel.setAttribute('data-l10n-id',
'set-default-action');
defaultChoice.appendChild(defaultChoiceLabel);
this.menu.appendChild(defaultChoice);
};
ActionMenu.prototype._appendCancelButton = function() {
var button = document.createElement('button');
button.dataset.action = 'cancel';
button.dataset.l10nId = 'cancel';
button.addEventListener('click', function(evt) {
evt.preventDefault();
this.hide();
this.oncancel();
}.bind(this));
var lastButtonWrapper = document.createElement('span');
lastButtonWrapper.className = 'last-button-container';
this.menu.appendChild(lastButtonWrapper);
lastButtonWrapper.appendChild(button);
};
exports.ActionMenu = ActionMenu;
}(window));

View file

@ -0,0 +1,320 @@
'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;
}
});
}());

View file

@ -0,0 +1,37 @@
/* global LazyLoader, Import */
'use strict';
(function(exports) {
/**
* A global activity handler which dispatches mozSetMessageHandler events.
*/
function ActivityHandler() {}
ActivityHandler.prototype = {
/**
* Starts listening for activities.
*/
start: function() {
window.navigator.mozSetMessageHandler('activity',
this.handleActivity.bind(this));
return LazyLoader.load(['js/import.js']).then(() => {
this.import = new Import();
return Promise.resolve(this.import.start());
});
},
/**
* Handles activity requests.
*/
handleActivity: function(activity) {
var name = activity.source.name;
window.dispatchEvent(new CustomEvent('activity-' + name, {
detail: activity
}));
}
};
exports.ActivityHandler = ActivityHandler;
}(window));

View file

@ -0,0 +1,264 @@
/* global AppWindow, BrowserFrame, Service */
'use strict';
(function(exports) {
var _id = 0;
/**
* ActivityWindow is the wrapper for the inline activity instances.
* For window disposition activity, they are done in AppWindow.
*
* ##### Flow chart
* ![ActivityWindow flow](http://i.imgur.com/4O1Frs3.png)
*
* @example
* var app = new AppWindow({
* url: 'http://uitest.gaiamobile.org:8080/index.html',
* manifestURL: 'http://uitest.gaiamobile.org:8080/manifest.webapp'
* });
* var activity = new ActivityWindow({
* url: 'http://gallery.gaiamobile.org:8080/pick.html',
* manifestURL: 'http://gallery.gaiamobile.org:8080/manifest.webapp'
* }, app);
*
* @class ActivityWindow
* @param {Object} config The configuration object of this activity.
* @param {AppWindow|ActivityWindow} caller The caller of this activity.
*/
/**
* Fired when the activity window is created.
* @event ActivityWindow#activitycreated
*/
/**
* Fired when the activity window is removed.
* @event ActivityWindow#activityterminated
*/
/**
* Fired when the activity window is opening.
* @event ActivityWindow#activityopening
*/
/**
* Fired when the activity window is opened.
* @event ActivityWindow#activityopen
*/
/**
* Fired when the activity window is cloing.
* @event ActivityWindow#activityclosing
*/
/**
* Fired when the activity window is closed.
* @event ActivityWindow#activityclose
*/
/**
* Fired before the activity window is rendered.
* @event ActivityWindow#activitywillrender
*/
/**
* Fired when the activity window is rendered to the DOM tree.
* @event ActivityWindow#activityrendered
*/
/**
* Fired when the page visibility of the activity window is
* changed to foreground.
* @event ActivityWindow#activityforeground
*/
/**
* Fired when the page visibility of the activity window is
* changed to background.
* @event ActivityWindow#activitybackground
*/
var ActivityWindow = function ActivityWindow(config, caller) {
this.config = config;
for (var key in config) {
this[key] = config[key];
}
if (caller) {
caller.setFrontWindow(this);
this.rearWindow = caller;
if (caller.element) {
this.containerElement = caller.element;
}
}
this.publish('creating');
this.render();
this.publish('created');
};
ActivityWindow.prototype = Object.create(AppWindow.prototype);
ActivityWindow.prototype.constructor = ActivityWindow;
ActivityWindow.prototype.eventPrefix = 'activity';
ActivityWindow.prototype.CLASS_NAME = 'ActivityWindow';
/**
* Turn on this flag to dump debugging messages for all activity windows.
* @type {Boolean}
*/
ActivityWindow.prototype._DEBUG = false;
ActivityWindow.prototype.openAnimation = 'fade-in';
ActivityWindow.prototype.closeAnimation = 'fade-out';
/**
* ActivityWindow's fullscreen state is copying from the caller
* instead of reading manifest so we just overwrite the
* isFullScreen method of AppWindow.
*
* @return {Boolean} The activity window is fullscreen or not.
*/
ActivityWindow.prototype.isFullScreen = function acw_isFullScreen() {
if (typeof(this._fullscreen) !== 'undefined') {
return this._fullscreen;
}
this._fullscreen = (this.manifest && !!this.manifest.fullscreen) ?
!!this.manifest.fullscreen :
this.rearWindow ?
this.rearWindow.isFullScreen() :
false;
return this._fullscreen;
};
/**
* Lock or unlock orientation for this activity.
*/
ActivityWindow.prototype.lockOrientation =
function acw_lockOrientation(noCapture) {
if (this.isActive()) {
var orientation1 = (this.manifest) ?
this.manifest.orientation : null;
var orientation2 = (this.config.manifest) ?
this.config.manifest.orientation : null;
var orientation3 = (this.rearWindow.manifest) ?
this.rearWindow.manifest.orientation : null;
var orientation4 = Service.query('globalOrientation');
var orientation = orientation1 ||
orientation2 ||
orientation3 ||
orientation4;
if (orientation) {
var rv = false;
if ('lockOrientation' in screen) {
rv = screen.lockOrientation(orientation);
} else if ('mozLockOrientation' in screen) {
rv = screen.mozLockOrientation(orientation);
}
if (rv === false) {
console.warn('screen.mozLockOrientation() returned false for',
this.origin, 'orientation', orientation);
}
} else { // If no orientation was requested, then let it rotate
if ('unlockOrientation' in screen) {
screen.unlockOrientation();
} else if ('mozUnlockOrientation' in screen) {
screen.mozUnlockOrientation();
}
}
}
};
ActivityWindow.prototype.view = function acw_view() {
this.instanceID = this.CLASS_NAME + '_' + _id;
_id++;
return `<div id="${this.instanceID}"
class="appWindow activityWindow inline-activity">
<div class="fade-overlay"></div>
<div class="browser-container">
<div class="screenshot-overlay"></div>
</div>
</div>`;
};
ActivityWindow.SUB_COMPONENTS = {
'transitionController': 'AppTransitionController',
'modalDialog': 'AppModalDialog',
'valueSelector': 'ValueSelector',
'authDialog': 'AppAuthenticationDialog',
'childWindowFactory': 'ChildWindowFactory',
'statusbar': 'AppStatusbar',
'textSelectionDialog': 'AppTextSelectionDialog'
};
ActivityWindow.SUB_MODULES = {
'contextmenu': 'BrowserContextMenu'
};
ActivityWindow.REGISTERED_EVENTS = AppWindow.REGISTERED_EVENTS;
ActivityWindow.prototype._handle_mozbrowseractivitydone =
function aw__handle_mozbrowseractivitydone() {
this.kill();
};
ActivityWindow.prototype.render = function acw_render() {
this.publish('willrender');
this.containerElement.insertAdjacentHTML('beforeend', this.view());
// TODO: Use BrowserConfigHelper.
this.browser_config = {
parentApp: this.parentApp,
origin: this.origin,
url: this.url,
name: this.name,
manifest: this.manifest,
manifestURL: this.manifestURL,
window_name: 'inline' + this.instanceID,
oop: true
};
this.browser = new BrowserFrame(this.browser_config);
this.element =
document.getElementById(this.instanceID);
this.browserContainer = this.element.querySelector('.browser-container');
this.browserContainer.appendChild(this.browser.element);
this.frame = this.element;
this.iframe = this.browser.element;
this.screenshotOverlay = this.element.querySelector('.screenshot-overlay');
this.fadeOverlay = this.element.querySelector('.fade-overlay');
// Copy fullscreen state from caller.
if (this.isFullScreen()) {
this.element.classList.add('fullscreen-app');
}
this._registerEvents();
this.installSubComponents();
this.publish('rendered');
};
/**
* ActivityWindow's default container is '#windows'.
* However, we could dynamically change this in layout manager
* after it recieves the activitywillrender event.
*/
ActivityWindow.prototype.containerElement =
document.getElementById('windows');
/**
* Bringing ourselves up. To do so we simply do
* requestOpen() the original caller app window instance.
* Note: this overrides AppWindow.prototype.requestOpen().
*/
ActivityWindow.prototype.requestOpen = function acw_requestOpen() {
if (this.rearWindow) {
this.rearWindow.requestOpen();
}
};
/**
* Request to close. We don't need to reset the visibility of
* the bottom window here so we simply close.
* Note: this overrides AppWindow.prototype.requestClose().
*/
ActivityWindow.prototype.requestClose = function acw_requestOpen() {
this.close();
};
exports.ActivityWindow = ActivityWindow;
}(window));

View file

@ -0,0 +1,144 @@
/* global Service */
'use strict';
(function(exports) {
/**
* ActivityWindowManager manages the activity window instances.
*
* Currently it's only responsible to kill an existing
* activity window if the next creating instance has the same
* manifestURL + pageURL.
*
* @class ActivityWindowManager
*/
function ActivityWindowManager() {
}
ActivityWindowManager.prototype = {
/**
* The list of all current running activity window instances.
* @access private
* @type {Array}
* @memberof ActivityWindowManager.prototype
*/
_activities: [],
/**
* Register all event handlers.
* @memberof ActivityWindowManager.prototype
*/
start: function acwf_start() {
if (this._started) {
return;
}
this._started = true;
this.activityPool = new Map();
window.addEventListener('activityopened', this);
window.addEventListener('popupopened', this);
window.addEventListener('appopened', this);
window.addEventListener('activityrequesting', this);
window.addEventListener('activitycreated', this);
window.addEventListener('activityterminated', this);
},
/**
* Unregister all event handlers.
* @memberof ActivityWindowManager.prototype
*/
stop: function acwf_stop() {
if (!this._started) {
return;
}
this._started = false;
this.activityPool = null;
window.removeEventListener('activityopened', this);
window.removeEventListener('popupopened', this);
window.removeEventListener('appopened', this);
window.removeEventListener('activityrequesting', this);
window.removeEventListener('activitycreated', this);
window.removeEventListener('activityterminated', this);
},
/**
* Put all window ID which is involved in an activity here.
* XXX: This is a workaround of bug 931339.
* @type {Map}
*/
activityPool: null,
/**
* Activity Config
* @typedef {Object} ActivityConfig
* @property {String} manifestURL The manifestURL of the activity
* @property {String} url The URL of the activity handling page
* @property {Boolean} isActivity
* @property {Boolean} inline The disposition of the activty is inline
* or not
*/
handleEvent: function acwf_handleEvent(evt) {
switch (evt.type) {
// XXX: Workaround of bug 931339.
// We are maintaining only one chain of activities here
// in this.activityPool.
// If the next coming activity request does not belong
// to the previous pool,
// kill all the background inline activities.
case 'popupopened':
case 'activityopened':
case 'appopened':
var app = evt.detail;
var parent = app.callerWindow ||
app.bottomWindow ||
app.previousWindow;
if (parent && this.activityPool.has(parent.instanceID)) {
// We don't really care about the instance context,
// so we set the value to a boolean.
this.activityPool.set(app.instanceID, true);
}
break;
case 'popupterminated':
case 'appterminated':
this.activityPool.delete(app.instanceID);
break;
case 'activityrequesting':
// The request may come from the top most window
// or the system app, but we don't care here.
var caller = Service.query('getTopMostWindow');
if (!this.activityPool.size) {
this.activityPool.set(caller.instanceID, true);
} else {
if (!this.activityPool.has(caller.instanceID)) {
// A new request from a new chain,
// kill all the background.
this._activities.forEach(function iterator(activity, index) {
if (!activity.getBottomMostWindow().isActive() ||
!activity.isActive()) {
activity.kill();
}
}, this);
this.activityPool.clear();
this.activityPool.set(caller.instanceID, true);
}
}
break;
case 'activityterminated':
this._activities.some(function iterator(activity, index) {
if (activity.instanceID === evt.detail.instanceID) {
this._activities.splice(index, 1);
return true;
}
}, this);
this.activityPool.delete(evt.detail.instanceID);
break;
case 'activitycreated':
this._activities.push(evt.detail);
break;
}
}
};
exports.ActivityWindowManager = ActivityWindowManager;
}(window));

View file

@ -0,0 +1,678 @@
/**
* This module is responsible for managing the AdvancedTelemetry features in
* Gaia. Advanced Telemetry includes:
* 1) Engineering Performance metrics on a per app basis that are collected
* by the HUD engine in gecko.
* 2) Custom App Metrics. Apps can define their custom metrics and have them
* collected in a histogram as well. This API is defined via the
* AdvancedTelemetryHelper.
*
* Metrics are gathered in gecko and requested from AdvancedTelemetry Module
* when it is time to ship the metrics to the server depending on the send
* interval desired.
*/
/* global asyncStorage, SettingsListener, uuid, TelemetryRequest, Gzip,
LazyLoader */
(function(exports) {
'use strict';
// This is the asyncStorage key we use to persist our device ID
const DEVICE_ID_KEY = 'metrics.advanced_telemetry.deviceID.v4';
// Constants for events we register/unreg for
const IDLE = 'idle';
const ACTIVE = 'active';
const HUDMETRICS = 'advanced-telemetry-update';
// Constant for debugging lines
const AT_DEBUG_PREFIX = '[AdvancedTelemetry]';
// This is the list of event types we register handlers for
const EVENT_TYPES = [
IDLE,
ACTIVE,
HUDMETRICS
];
// This AdvancedTelemetry() constructor is the value we export from
// this module. This constructor does no initialization itself: that
// is all done in the start() instance method. See bootstrap.js for
// the code that actually calls the constructor and the start method.
function AdvancedTelemetry() {}
// We use the acronym AT internally to save typing and make
// references to class variables and constants more legible.
const AT = AdvancedTelemetry;
AT.BatchTiming = BatchTiming;
// This the ping type in the v4 telemetry spec. Exposed for unit testing.
AT.REASON = 'advancedtelemetry';
// asyncStorage key that stores the BatchTiming start time.
AT.BATCH_KEY = 'metrics.advanced_telemetry.data.v4';
// This is the asyncStorage key we use to persist our metrics
// in case we need to merge them later.
AT.METRICS_KEY = 'metrics.advanced_telemetry.metrics.v4';
// Set to true to to enable debug output. Not recommended to turn this
// on except while doing deep debugging.
AT.DEBUG = false;
// LOGINFO method allows for debugging of initialization events and
// packet sending events and is at a higher level then DEBUG.
AT.LOGINFO = false;
// Constants for BatchTiming Batch Type
AT.NORMAL_BATCH = true;
AT.RETRY_BATCH = false;
//Constants for BatchTiming reset
AT.RESET_BATCH = true;
AT.EXISTING_BATCH = false;
function debug(...args) {
if (!AT.DEBUG) {
return;
}
var iCount = 0;
// Get the total length of all arguments passed
args.forEach(function (value) {
iCount += value.length;
});
// if the length is greater than what adb console can handle, chunk it up.
if (iCount > 1000) {
var longLine = '';
args.forEach(function (value) {
longLine += value;
});
// Chop it into chunks of what adb console can handle.
var array = longLine.match(/.{1,1000}/g);
// Output each chunk to the console
array.forEach(function (value) {
console.log(value);
});
} else {
// Its < what ADB can handle so just output it normally.
args.unshift(AT_DEBUG_PREFIX);
console.log.apply(console, args);
}
}
// This info level is more of a information level. It's recommended to
// turn this on just to see general events related to initializations,
// and sending of payloads, etc.
function loginfo(...args) {
if (AT.LOGINFO) {
args.unshift('[AdvancedTelemetryInfo]');
console.log.apply(console, args);
}
}
// This value is what turns advanced telemetry on/off
AT.TELEMETRY_ENABLED_KEY = 'metrics.selectedMetrics.level';
// Base URL for sending data reports
// Needs to be formatted as:
// /submit/telemetry/id/reason/appName/appUpdateChannel/appVersion/appBuildID
AT.REPORT_URL = 'https://incoming.telemetry.mozilla.org/submit/telemetry';
// How often do we try to send the reports
// Can be overridden with metrics.advancedtelemetry.reportInterval setting.
AT.REPORT_INTERVAL = 14 * 24 * 60 * 60 * 1000; // Two Weeks
// If the telemetry server does not respond within this amount of time
// just give up and try again later.
// Can be overridden with metrics.advancedtelemetry.reportTimeout setting.
AT.REPORT_TIMEOUT = 20 * 1000; // 20 seconds
// How long do we wait before trying it again?
AT.RETRY_INTERVAL = 60 * 60 * 1000; // 1 hour
// How much user idle time (in seconds, not ms) do we wait for before
// persisting our data to asyncStorage or trying to transmit it.
AT.IDLE_TIME = 5; // seconds
// Telemetry payload version
AT.TELEMETRY_VERSION = 4;
// Interval to merge the metrics to guard against reboot
AT.MERGE_INTERVAL = 60 * 60 * 1000; // 1 hour
// App name (static for Telemetry)
AT.TELEMETRY_APP_NAME = 'FirefoxOS';
AT.prototype.start = function start() {
loginfo('Starting AdvancedTelemetry');
this.advancedTelemetryEnabledListener =
function advancedTelemetryEnabledListener(enabled) {
if (enabled === 'Enhanced') {
loginfo('Start Collecting');
this.startCollecting();
}
else {
loginfo('Stop Collecting');
this.stopCollecting();
}
}.bind(this);
SettingsListener.observe(AT.TELEMETRY_ENABLED_KEY,
false, this.advancedTelemetryEnabledListener);
};
AT.prototype.stopCollecting = function stopCollecting() {
this.collecting = false;
EVENT_TYPES.forEach(function (type) {
window.removeEventListener(type, self);
});
};
AT.prototype.startCollecting = function startCollecting(done) {
var self = this;
// If we're already running there is nothing to start
if (this.collecting) {
return;
}
this.collecting = true;
this.metrics = new BatchTiming(AT.RESET_BATCH);
this.mergeTimeStart = Date.now();
this.merge = false;
getDeviceID();
function getDeviceID() {
asyncStorage.getItem(DEVICE_ID_KEY, function(value) {
if (value) {
self.deviceID = value;
}
else {
// Our device id does not need to be unique, just probably unique.
self.deviceID = uuid();
asyncStorage.setItem(DEVICE_ID_KEY, self.deviceID);
}
// Move on to the next step in the startup process
registerHandlers();
});
}
function registerHandlers() {
// Basic event handlers
EVENT_TYPES.forEach(function (type) {
window.addEventListener(type, self);
});
self.startBatch();
self.idleObserver = {
time: AT.IDLE_TIME,
onidle: function() { self.handleEvent(new CustomEvent(IDLE)); },
onactive: function() { self.handleEvent(new CustomEvent(ACTIVE)); }
};
// Register for idle events
navigator.addIdleObserver(self.idleObserver);
if (done) {
loginfo('Initialization Complete');
done();
}
}
};
// This method shuts everything down and is only exposed for unit testing.
// Note that this is not the same as the stopCollecting() method which is
// used to stop data collection but keep the module running.
AT.prototype.stop = function stop() {
this.stopCollecting();
SettingsListener.unobserve(AT.TELEMETRY_ENABLED_KEY,
this.advancedTelemetryEnabledListener);
};
// Reset (or initialize) the AdvancedTelemetry instance variables
AT.prototype.reset = function() {
this.collecting = false;
this.metrics = null;
this.deviceID = null;
// Is the user idle? Updated in handleEvent() based on an idle observer
this.idle = false;
this.merge = false;
};
AT.prototype.handleEvent = function handleEvent(e) {
switch (e.type) {
case HUDMETRICS:
this.handleGeckoMessage(e.detail);
break;
case IDLE:
this.idle = true;
// If we are idle and past the merge time, merge the metrics.
if ((Date.now() - this.mergeTimeStart) > AT.MERGE_INTERVAL) {
this.merge = true;
this.mergeTimeStart = Date.now();
this.getPayload();
}
break;
case ACTIVE:
this.idle = false;
break;
default:
break;
}
};
AT.prototype.handleGeckoMessage = function handleGeckoMessage(payload,
callback) {
var self = this;
// We always need to merge the Metrics when we get this.
var promise = self.mergeMetrics(payload);
promise.then(function(result) {
// If it's a merge request mark it as completed.
if (self.merge) {
self.merge = false;
self.clearPayload(false);
}
// if it's a request to transmit, transmit the merged metrics.
else if (self.collecting) {
self.transmit(result);
}
if (callback) {
callback();
}
});
};
// Signal gecko to send the histograms with a telemetry message.
AT.prototype.getPayload = function getPayload() {
// TODO: Add this line below to AdvancedTelemetryHelper as an API.
console.info('telemetry|MGMT|TIMETOSHIP');
};
// Signal gecko to clear out the histograms and start fresh.
AT.prototype.clearPayload = function clearPayload(resetStorage) {
// TODO: Add this line below to AdvancedTelemetryHelper as an API.
console.info('telemetry|MGMT|CLEARMETRICS');
if (resetStorage) {
asyncStorage.setItem(AT.METRICS_KEY, null);
}
};
// Start a timer to notify when to send the payload.
AT.prototype.startBatch = function startBatch() {
var self = this;
this.timer = setTimeout(function() {
self.getPayload();
}, self.metrics.getInterval());
};
AT.prototype.stopBatch = function stopBatch() {
clearTimeout(this.timer);
};
// Once we have the histogram payload, we can send it to the telemetry
// server. Once we receive 200 OK back from telemetry server, we can
// start a fresh batch of metrics.
AT.prototype.transmit = function transmit(payload) {
var self = this;
if (!this.collecting) {
return;
}
var deviceInfoQuery = {
'app.update.channel': 'unknown',
'deviceinfo.platform_version': 'unknown',
'deviceinfo.platform_build_id': 'unknown'
};
// Query the settings db for parameters for the URL
TelemetryRequest.getSettings(deviceInfoQuery, (deviceResponse) => {
// Note that this wrapper is using the new v4 Unified Telemetry format
var wrapper = {
type: AT.REASON,
id: uuid(),
creationDate: new Date().toISOString(),
version: AT.TELEMETRY_VERSION,
application: {
architecture: 'arm',
buildId: deviceResponse['deviceinfo.platform_build_id'],
name: AT.TELEMETRY_APP_NAME,
version: deviceResponse['deviceinfo.platform_version'],
vendor: 'Mozilla',
platformVersion: deviceResponse['deviceinfo.platform_version'],
xpcomAbi: 'arm-gcc3',
channel: deviceResponse['app.update.channel']
},
clientId: this.deviceID,
payload: payload
};
debug(JSON.stringify(wrapper));
// Build the wrapper for the telemetry version
send(wrapper, deviceResponse);
});
function send(payload, deviceInfoQuery) {
if (!navigator.onLine) {
self.startRetryBatch();
return;
}
self.request = new AdvancedTelemetryPing(payload, deviceInfoQuery,
self.deviceID);
// We don't actually have to do anything if the data is transmitted
// successfully. We are already set up to collect the next batch of data.
function onload() {
loginfo('Transmitted Successfully.');
AdvancedTelemetry.prototype.clearPayload(true);
// Start a new batch.
self.startNewBatch();
}
function retry(e) {
loginfo('Advanced Telemetry metrics transmission failure:', e.type);
// Start a retry batch. Don't clear the payload yet. It will continue
// to accumulate until the retry interval expires.
self.startRetryBatch();
}
self.request.packHistograms();
self.request.send({
timeout: AT.REPORT_TIMEOUT,
onload: onload,
onerror: retry,
onabort: retry,
ontimeout: retry
});
}
};
AT.prototype.startNewBatch = function startNewBatch() {
this.metrics = new BatchTiming(AT.EXISTING_BATCH, AT.RETRY_BATCH);
this.startBatch();
};
AT.prototype.startRetryBatch = function startRetryBatch() {
this.metrics = new BatchTiming(AT.EXISTING_BATCH, AT.NORMAL_BATCH);
this.startBatch();
};
// Check if there are existing metrics that need to be merged, if so,
// merge them and return them for either storing or sending.
AT.prototype.mergeMetrics = function mergeMetrics(payloadNew) {
return new Promise(function(resolve) {
// Get the old Payload for merging.
asyncStorage.getItem(AT.METRICS_KEY, function(value) {
var merg;
if (value) {
merg = mergePayloads(value, payloadNew);
} else {
merg = payloadNew;
}
saveMergedPayload(merg, function() {
resolve(merg);
});
});
function saveMergedPayload(payload, callback) {
asyncStorage.setItem(AT.METRICS_KEY, payload, function() {
callback();
});
}
// Merge the new metrics into the old metrics and then transmit the
// result to the server.
function mergePayloads(payloadOld, payloadNew) {
mergeHistogramType(payloadOld.keyedHistograms, payloadOld, payloadNew,
'keyedHistograms');
mergeHistogramType(payloadOld.addonHistograms, payloadOld, payloadNew,
'addonHistograms');
return payloadOld;
}
});
};
function mergeHistogram(oldHist, newHist) {
oldHist.sum += newHist.sum;
oldHist.log_sum += newHist.log_sum;
oldHist.log_sum_squares += newHist.log_sum_squares;
for (var i = 0; i < oldHist.counts.length; i++) {
oldHist.counts[i] += newHist.counts[i];
}
}
function mergeLists(oldList, newList) {
var histMap = new Map();
for (var keyOld in oldList) {
histMap.set(keyOld, oldList[keyOld]);
}
for (var keyNew in newList) {
if (histMap.has(keyNew)) {
mergeHistogram(oldList[keyNew], newList[keyNew]);
} else {
oldList[keyNew] = newList[keyNew];
}
}
}
function mergeHistogramType(oldHist, payloadOld, payloadNew, histType) {
var keyedOldMapKeys = new Map();
for (var keyOld in oldHist) {
keyedOldMapKeys.set(keyOld, oldHist[keyOld]);
}
var keyedNew = payloadNew[histType];
var keyedOld = payloadOld[histType];
for (var keyNew in keyedNew) {
if (keyedOldMapKeys.has(keyNew)) {
mergeLists(keyedOld[keyNew], keyedNew[keyNew]);
} else {
keyedOld[keyNew] = keyedNew[keyNew];
}
}
}
function AdvancedTelemetryPing(payload, deviceQuery) {
if (!payload) {
throw new Error('No arguments');
}
// clone so we don't put data into the object that was given to us
this.packet = JSON.parse(JSON.stringify(payload));
// URL format: /id/reason/appName/appVersion/appUpdateChannel/appBuildID?v=4
var uriParts = [
AT.REPORT_URL,
encodeURIComponent(payload.id),
encodeURIComponent(AT.REASON),
encodeURIComponent(AT.TELEMETRY_APP_NAME),
encodeURIComponent(deviceQuery['deviceinfo.platform_version']),
encodeURIComponent(deviceQuery['app.update.channel']),
encodeURIComponent(deviceQuery['deviceinfo.platform_build_id']),
];
this.url = uriParts.join('/');
this.url += ('?v=4');
debug('Telemetry URL is: ' + this.url);
}
/*
* Expose `getData` for unit testing purposes
*/
AdvancedTelemetryPing.prototype.getData = function(packet) {
return JSON.stringify(packet);
};
// This method converts a raw histogram with the .counts and .ranges arrays
// in it to one with just .values using the dict format.
AdvancedTelemetryPing.prototype.packHistogram =
function packHistogram(hgram) {
//Constant for Histogram types
const HISTOGRAM_EXP = 0;
if (typeof hgram.ranges === 'undefined' ||
typeof hgram.counts === 'undefined') {
return hgram;
}
let r = hgram.ranges;
let c = hgram.counts;
let retgram = {
range: [r[1], r[r.length - 1]],
bucket_count: r.length,
histogram_type: hgram.histogram_type,
values: {},
sum: hgram.sum
};
if (hgram.histogram_type === HISTOGRAM_EXP) {
retgram.log_sum = hgram.log_sum;
retgram.log_sum_squares = hgram.log_sum_squares;
} else {
retgram.sum_squares_lo = hgram.sum_squares_lo;
retgram.sum_squares_hi = hgram.sum_squares_hi;
}
let first = true;
let last = 0;
for (let i = 0; i < c.length; i++) {
let value = c[i];
if (!value) {
continue;
}
// add a lower bound
if (i && first) {
retgram.values[r[i - 1]] = 0;
}
first = false;
last = i + 1;
retgram.values[r[i]] = value;
}
// add an upper bound
if (last && last < c.length) {
retgram.values[r[last]] = 0;
}
return retgram;
},
// This method loops through the payload of histograms and converts each
// histogram payload to the 'dict' format required by the server for parsing.
AdvancedTelemetryPing.prototype.packHistograms = function() {
var nameAddonHist = this.packet.payload.addonHistograms;
var nameKeyHist = this.packet.payload.keyedHistograms;
// Pack the Addon Histograms.
for (var addon in nameAddonHist) {
for (var hist in nameAddonHist[addon]) {
nameAddonHist[addon][hist] =
this.packHistogram(nameAddonHist[addon][hist]);
}
}
// Pack the Keyed Histograms.
for (var key in nameKeyHist) {
for (var hist2 in nameKeyHist[key]) {
nameKeyHist[key][hist2] = this.packHistogram(nameKeyHist[key][hist2]);
}
}
};
AdvancedTelemetryPing.prototype.send = function(xhrAttrs) {
return new Promise((resolve, reject) => {
this.data = this.getData(this.packet);
LazyLoader.load(['/shared/js/gzip/gzip.js']).then(() => {
Gzip.compress(this.data).then((gzipData) => {
debug('Length of compressed data:', gzipData.length);
return this.post(xhrAttrs, gzipData);
}, (msg) => {
debug('Error compressing telemetry payload:', msg);
return this.post(xhrAttrs, this.data);
}).then(function () {
resolve();
});
});
});
};
AdvancedTelemetryPing.prototype.post = function post(xhrAttrs, gzipData) {
var xhr = new XMLHttpRequest({ mozSystem: true, mozAnon: true });
return new Promise((resolve, reject) => {
xhr.open('POST', this.url);
if (xhrAttrs) {
xhr.onload = xhrAttrs.onload;
xhr.onerror = xhrAttrs.onerror;
xhr.onabort = xhrAttrs.onabort;
xhr.ontimeout = xhrAttrs.ontimeout;
if (xhrAttrs.timeout) {
xhr.timeout = xhrAttrs.timeout;
}
}
xhr.setRequestHeader('Content-type', 'application/json');
xhr.setRequestHeader('Content-Encoding', 'gzip');
xhr.responseType = 'text';
xhr.send(gzipData);
resolve();
});
};
/*
* A helper class that tracks the start time of the current batch.
*/
function BatchTiming(startup, retry) {
var self = this;
this.start = Date.now();
if (startup) {
asyncStorage.getItem(AT.BATCH_KEY, function(value) {
if (value) {
var oldDate = new Date(value);
self.interval = self.start - oldDate.getTime();
if (self.interval > AT.REPORT_INTERVAL) {
self.interval = AT.REPORT_INTERVAL;
asyncStorage.setItem(AT.BATCH_KEY, self.start);
}
} else {
// here if it's the first time it's being run and there is nothing
// saved in async.
asyncStorage.setItem(AT.BATCH_KEY, self.start);
self.interval = AT.REPORT_INTERVAL;
}
});
} else {
asyncStorage.setItem(AT.BATCH_KEY, this.start);
if (retry) {
self.interval = AT.RETRY_INTERVAL;
} else {
self.interval = AT.REPORT_INTERVAL;
}
}
}
BatchTiming.prototype.startTime = function() {
return this.start;
};
BatchTiming.prototype.getInterval = function(){
return this.interval;
};
exports.AdvancedTelemetry = AdvancedTelemetry;
// Exported for unit testing.
exports.AdvancedTelemetryPing = AdvancedTelemetryPing;
}(window));

View file

@ -0,0 +1,291 @@
/* global AirplaneMode, BaseModule, LazyLoader, AirplaneModeIcon */
/* exported AirplaneMode */
'use strict';
(function() {
// main
var AirplaneMode = function() {
};
AirplaneMode.SETTINGS = [
'airplaneMode.enabled'
];
AirplaneMode.EVENTS = [
'radiostatechange',
'request-airplane-mode-enable',
'request-airplane-mode-disable'
];
AirplaneMode.SUB_MODULES = [
'AirplaneModeServiceHelper'
];
AirplaneMode.SERVICES = [
'registerNetwork',
'unregisterNetwork'
];
AirplaneMode.STATES = [
'isActive'
];
BaseModule.create(AirplaneMode, {
name: 'AirplaneMode',
isActive: function() {
return this._enabled;
},
/*
* This is an internal key to store current state of AirplaneMode
*/
_enabled: undefined,
/*
* This is an event mapping table that will help us wait for
* specific event from its manager to make sure we are now
* in airplane mode or not.
*/
_checkedActionsMap: {
wifi: {
enabled: 'wifi-enabled',
disabled: 'wifi-disabled'
},
bluetooth: {
enabled: 'bluetooth-enabled',
disabled: 'bluetooth-disabled'
},
radio: {
enabled: 'radio-enabled',
disabled: 'radio-disabled'
}
},
_airplaneModeServiceHelper_loaded: function() {
return this.airplaneModeServiceHelper.start().then(() => {
if (Object.prototype.hasOwnProperty.call(this, '_pendingEnabled')) {
this.enabled = this._pendingEnabled;
delete this._pendingEnabled;
}
});
},
'_observe_airplaneMode.enabled': function(value) {
if (!this.airplaneModeServiceHelper) {
this._pendingEnabled = value;
return;
}
this.enabled = value;
},
/*
* If we are in airplane mode and the user just dial out an
* emergency call, we have to exit airplane mode.
*/
_handle_radiostatechange: function(evt) {
if (evt.detail.state === 'enabled' && this._enabled === true) {
this.enabled = false;
}
},
'_handle_request-airplane-mode-enable': function() {
if (this.enabled === false) {
this.enabled = true;
}
},
'_handle_request-airplane-mode-disable': function() {
if (this.enabled === true) {
this.enabled = false;
}
},
_start: function() {
this._watchList = {};
LazyLoader.load(['js/airplane_mode_icon.js']).then(function() {
this.icon = new AirplaneModeIcon(this);
this.icon.start();
}.bind(this))['catch'](function(err) { // XXX: workaround gjslint
console.error(err);
});
},
_stop: function() {
this._watchList = {};
},
/*
* When turning on / off airplane mode, we will start watching
* needed events to make sure we are in airplane mode or not.
*
* @param {boolean} value
* @param {Object} checkedActions
*/
watchEvents: function(value, checkedActions) {
var self = this;
// We don't want to wait until the first event reacts in order to
// update the status, because we can set the status to 'enabling' or
// 'disabling' already through `_updateAirplaneModeStatus`.
this._updateAirplaneModeStatus(checkedActions);
function updateAirplaneModeHandler(eventName, serviceName) {
return function toUpdateAirplaneMode() {
self.debug('handling ' + eventName);
window.removeEventListener(eventName, toUpdateAirplaneMode);
checkedActions[serviceName] = true;
self._updateAirplaneModeStatus(checkedActions);
};
}
for (var serviceName in this._checkedActionsMap) {
// if we are waiting for specific service
if (serviceName in checkedActions) {
var action = value ? 'disabled' : 'enabled';
var eventName = this._checkedActionsMap[serviceName][action];
// then we will start watch events coming from its manager
window.addEventListener(eventName,
updateAirplaneModeHandler(eventName, serviceName));
}
}
},
/*
* In order to make sure all needed managers work successfully. We have to
* use this method to update airplaneMode related keys to tell
* AirplaneModeHelper our current states and is finised or not.
*/
_updateAirplaneModeStatus: function(checkActions) {
var areAllActionsDone;
areAllActionsDone = this._areCheckedActionsAllDone(checkActions);
if (areAllActionsDone) {
this.debug('write settings...', this._enabled);
this.writeSetting({
'airplaneMode.enabled': this._enabled,
'airplaneMode.status': this._enabled ? 'enabled' : 'disabled',
// NOTE
// this is for backward compatibility,
// because we will update this value only when airplane mode
// is on / off, it will not affect apps using this value
'ril.radio.disabled': this._enabled
});
this.icon && this.icon.update();
} else {
// keep updating the status to reflect current status
this.writeSetting({
'airplaneMode.status': this._enabled ? 'enabling' : 'disabling'
});
}
},
registerNetwork: function(network, handler) {
if (this._watchList[network]) {
return;
}
this._watchList[network] = handler;
this.debug(network + ' is registered.');
},
unregisterNetwork: function(network) {
if (!this._watchList[network]) {
return;
}
delete this._watchList[network];
this.debug(network + ' is unregistered.');
},
/*
* By default, these three API takes longer time and with success / error
* callback. we just have to wait for these three items.
*
* @param {boolean} value
* @return {Object} checkedActions
*/
_getCheckedActions: function(value) {
// we have to re-init all need-to-check managers
var checkedActions = {};
if (value === true) {
// check connection
if (this._watchList.radio && this._watchList.radio.enabled) {
checkedActions.radio = false;
}
// check bluetooth
if (this.airplaneModeServiceHelper.isEnabled('bluetooth')) {
checkedActions.bluetooth = false;
}
// check wifi
if (this.airplaneModeServiceHelper.isEnabled('wifi')) {
checkedActions.wifi = false;
}
} else {
// check connection
if (this._watchList.radio && !this._watchList.radio.enabled) {
checkedActions.radio = false;
}
// check bluetooth
if (this.airplaneModeServiceHelper.isSuspended('bluetooth')) {
checkedActions.bluetooth = false;
}
// check wifi
if (this.airplaneModeServiceHelper.isSuspended('wifi')) {
checkedActions.wifi = false;
}
}
return checkedActions;
},
/*
* We have to use this method to check whether all actions
* are done or not.
*
* @return {boolean}
*/
_areCheckedActionsAllDone: function(checkedActions) {
this.debug('checking action is all done ?');
for (var key in checkedActions) {
if (checkedActions[key] === false) {
this.debug(key + '...not yet.');
return false;
}
}
this.debug('...all done');
return true;
}
}, {
enabled: {
configurable: false,
/*
* This is a ES5 feature that can help the others easily get/set
* AirplaneMode.
*
* @param {boolean} value
*/
set: function(value) {
this.debug('current: ' + this._enabled);
if (value !== this._enabled) {
this.debug('turned to ' + value);
this._enabled = value;
// start watching events
this.watchEvents(value, this._getCheckedActions(value));
// tell services to do their own operations
this.airplaneModeServiceHelper.updateStatus(value);
}
},
/*
* This is a ES5 feature that can help the others easily get AirplaneMode
* states.
*
* @return {boolean}
*/
get: function() {
return this._enabled;
}
}
});
})();

View file

@ -0,0 +1,23 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var AirplaneModeIcon = function(manager) {
BaseIcon.call(this, manager);
};
AirplaneModeIcon.prototype = Object.create(BaseIcon.prototype);
AirplaneModeIcon.prototype.name = 'AirplaneModeIcon';
AirplaneModeIcon.prototype.shouldDisplay = function() {
return this.manager.enabled;
};
AirplaneModeIcon.prototype.view = function view() {
return `<div id="statusbar-airplane-mode"
class="sb-icon sb-icon-airplane-mode" hidden
role="listitem"
data-l10n-id="statusbarAirplaneMode">
</div>`;
};
exports.AirplaneModeIcon = AirplaneModeIcon;
}(window));

View file

@ -0,0 +1,185 @@
/* global BaseModule */
'use strict';
(function() {
var AirplaneModeServiceHelper = function() {};
/**
* AirplaneModeServiceHelper should be deprecated in the future.
* We should let each API handler to observe airplane mode
* status change to turn on/off each API on their own.
*/
AirplaneModeServiceHelper.SETTINGS = [
'ril.data.enabled',
'ril.data.suspended',
'bluetooth.enabled',
'bluetooth.suspended',
'wifi.enabled',
'wifi.suspended',
'geolocation.enabled',
'geolocation.suspended',
'nfc.enabled',
'nfc.suspended'
];
BaseModule.create(AirplaneModeServiceHelper, {
name: 'AirplaneModeServiceHelper',
'_observe_ril.data.enabled': function(value) {
if (value) {
this._unsuspend('ril.data.suspended');
}
},
'_observe_bluetooth.enabled': function(value) {
if (value) {
this._unsuspend('bluetooth.suspended');
}
},
'_observe_geolocation.enabled': function(value) {
if (value) {
this._unsuspend('geolocation.suspended');
}
},
'_observe_wifi.enabled': function(value) {
if (value) {
this._unsuspend('wifi.suspended');
}
},
'_observe_nfc.enabled': function(value) {
if (value) {
this._unsuspend('nfc.suspended');
}
},
// turn off the mozSetting corresponding to `key'
// and remember its initial state by storing it in another setting
_suspend: function(key) {
this.debug('suspending: ' + key);
var enabled = this._settings[key + '.enabled'];
var suspended = this._settings[key + '.suspended'];
if (suspended) {
this.debug('already suspended.');
return;
}
// remember the state before switching it to false
var sset = {};
sset[key + '.suspended'] = enabled;
this.writeSetting(sset);
// switch the state to false if necessary
if (enabled) {
// make sure both BT API and settings key are handled
if ('bluetooth' === key) {
window.dispatchEvent(new CustomEvent('request-disable-bluetooth'));
} else {
var eset = {};
eset[key + '.enabled'] = false;
this.writeSetting(eset);
}
}
},
// turn on the mozSetting corresponding to `key'
// if it has been suspended by the airplane mode
_restore: function(key) {
this.debug('restoring: ' + key);
var suspended = this._settings[key + '.suspended'];
// clear the 'suspended' state
var sset = {};
sset[key + '.suspended'] = false;
this.writeSetting(sset);
// switch the state to true if it was suspended
if (suspended) {
// make sure both BT API and settings key are handled
if ('bluetooth' === key) {
window.dispatchEvent(new CustomEvent('request-enable-bluetooth'));
} else {
var rset = {};
rset[key + '.enabled'] = true;
this.writeSetting(rset);
}
}
},
_unsuspend: function(settingSuspendedID) {
this.debug('unsuspending: ' + settingSuspendedID);
// clear the 'suspended' state
var sset = {};
sset[settingSuspendedID] = false;
this.writeSetting(sset);
},
isEnabled: function(key) {
return this._settings[key + '.enabled'];
},
isSuspended: function(key) {
return this._settings[key + '.suspended'];
},
updateStatus: function(value) {
this.debug('updating status.');
// FM Radio will be turned off in Gecko, more detailed about why we do
// this in Gecko instead, please check bug 997064.
var bluetooth = window.navigator.mozBluetooth;
var wifiManager = window.navigator.mozWifiManager;
var nfc = window.navigator.mozNfc;
this.publish(value ? 'airplanemode-enabled' : 'airplanemode-disabled');
if (value) {
// Turn off mobile data:
// we toggle the mozSettings value here just for the sake of UI,
// platform RIL disconnects mobile data when
// 'ril.radio.disabled' is true.
this._suspend('ril.data');
// Turn off Bluetooth.
if (bluetooth) {
this._suspend('bluetooth');
}
// Turn off Wifi and Wifi tethering.
if (wifiManager) {
this._suspend('wifi');
this.writeSetting({
'tethering.wifi.enabled': false
});
}
// Turn off Geolocation.
this._suspend('geolocation');
// Turn off NFC
if (nfc) {
this._suspend('nfc');
}
} else {
// Note that we don't restore Wifi tethering when leaving airplane mode
// because Wifi tethering can't be switched on before data connection is
// established.
// Don't attempt to turn on mobile data if it's already on
if (!this._settings['ril.data.enabled']) {
this._restore('ril.data');
}
// Don't attempt to turn on Bluetooth if it's already on
if (bluetooth && !this._settings['bluetooth.enabled']) {
this._restore('bluetooth');
}
// Don't attempt to turn on Wifi if it's already on
if (wifiManager && !this._settings['wifi.enabled']) {
this._restore('wifi');
}
// Don't attempt to turn on Geolocation if it's already on
if (!this._settings['geolocation.enabled']) {
this._restore('geolocation');
}
// Don't attempt to turn on NFC if it's already on
if (nfc && !this._settings['nfc.enabled']) {
this._restore('nfc');
}
}
}
});
}());

View file

@ -0,0 +1,23 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var AlarmIcon = function(manager) {
BaseIcon.call(this, manager);
};
AlarmIcon.prototype = Object.create(BaseIcon.prototype);
AlarmIcon.prototype.name = 'AlarmIcon';
AlarmIcon.prototype.shouldDisplay = function() {
return this.manager.enabled;
};
AlarmIcon.prototype.view = function view() {
return `<div id="statusbar-alarm"
class="sb-icon sb-icon-alarm"
hidden role="listitem"
data-l10n-id="statusbarAlarm">
</div>`;
};
exports.AlarmIcon = AlarmIcon;
}(window));

View file

@ -0,0 +1,24 @@
/* global BaseModule, AlarmIcon, LazyLoader */
'use strict';
(function() {
var AlarmMonitor = function() {};
AlarmMonitor.SETTINGS = [
'alarm.enabled'
];
BaseModule.create(AlarmMonitor, {
name: 'AlarmMonitor',
_start: function() {
LazyLoader.load(['js/alarm_icon.js']).then(function() {
this.icon = new AlarmIcon(this);
this.icon.start();
}.bind(this)).catch(function(err) {
console.error(err);
});
},
'_observe_alarm.enabled': function(value) {
this.enabled = value;
this.icon && this.icon.update();
}
});
}());

View file

@ -0,0 +1,84 @@
/* global BaseModule, LazyLoader, applications */
'use strict';
(function(exports) {
/**
* The entry point of the whole system app.
* It's duty is to prepare everything ready to launch
* the application core (core.js).
*
* Core could run without App, but it is expected to be slower
* because App will prepare the launch config for it with
* Launcher's help (launcher.js).
*
* In Launcher it will read the launch configurations and put
* some requests according to the config in the Service. When
* the relative modules which are started by Core is started,
* they could just fetch the launch config sychronously to
* fasten to launch progress.
*
* If Launcher does not prepare the value for them,
* the modules who are started by the Core will still get
* the values asynchronously.
*/
var App = function() {};
App.prototype = {
// TODO: decrease the files here and move them into specific loader
FILES: [
'js/wake_lock_manager.js',
'js/base_ui.js',
'js/base_icon.js',
'js/browser_config_helper.js',
'js/browser_frame.js',
'js/app_window.js',
'js/attention_window.js',
'js/system_dialog.js',
'js/core.js',
'js/launcher.js',
'js/settings_core.js',
'js/modal_dialog.js', // XXX: Wrap with ModalDialogService later
'shared/js/event_safety.js',
'shared/js/mobile_operator.js',
'shared/js/screen_layout.js',
'shared/js/settings_listener.js',
'shared/js/async_storage.js',
'shared/js/manifest_helper.js',
'shared/js/notification_helper.js',
'shared/js/sanitizer.js',
'shared/js/idletimer.js' // XXX: Move into ScreenManager
],
start: function() {
window.performance.mark('loadEnd');
return LazyLoader.load(this.FILES).then(() => {
return this.bootstrap();
});
},
bootstrap: function() {
if (this._booted) {
throw new Error('App: bootstrap should not be called twice.');
}
this._booted = true;
window.settingsCore = BaseModule.instantiate('SettingsCore');
window.settingsCore.start();
window.launcher = BaseModule.instantiate('Launcher');
return Promise.all([
// XXX:
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1161489
// Consider to move applications.start() into Core or AppCore
// but it may miss the mozChromeEvent to tell us application
// is ready to query.
applications.waitForReady(),
window.launcher.start()
]).then(() => {
window.core = BaseModule.instantiate('Core');
return window.core.start();
}).then(() => {
// To let integration test know we are ready to test.
document.body.setAttribute('ready-state', 'fullyLoaded');
window.performance.mark('fullyLoaded');
});
}
};
exports.App = App;
}(window));

View file

@ -0,0 +1,215 @@
'use strict';
(function(exports) {
var _id = 0;
/**
* The https authentication dialog of the AppWindow.
*
* @class AppAuthenticationDialog
* @param {AppWindow} app The app window instance
* where this dialog should popup.
* @extends BaseUI
*/
function AppAuthenticationDialog(app) {
this.app = app;
this._event = null;
this.containerElement = app.element;
this.instanceID = _id++;
this._injected = false;
this.app.element.addEventListener('mozbrowserusernameandpasswordrequired',
this);
}
AppAuthenticationDialog.prototype = Object.create(window.BaseUI.prototype);
AppAuthenticationDialog.prototype.CLASS_NAME = 'AuthenticationDialog';
AppAuthenticationDialog.prototype.EVENT_PREFIX = 'authdialog';
/**
* Used for element id access.
* e.g., 'authentication-dialog-alert-ok'
* @type {String}
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype.ELEMENT_PREFIX = 'authentication-dialog-';
/**
* Maps to DOM elements.
* @type {Object}
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype.elements = null;
/**
* The current authentication event.
* Note: Only one event one time.
* @type {Event}
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype._event = null;
/**
* Get all elements when inited.
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype._fetchElements =
function aad__fetchElements() {
this.element = document.getElementById(this.CLASS_NAME + this.instanceID);
this.elements = {};
var toCamelCase = function toCamelCase(str) {
return str.replace(/\-(.)/g, function replacer(str, p1) {
return p1.toUpperCase();
});
};
this.elementClasses = [
'http-username-input', 'http-password-input',
'http-authentication-message', 'http-authentication-ok',
'http-authentication-cancel', 'http-authentication-header'
];
this.elementClasses.forEach(function createElementRef(name) {
this.elements[toCamelCase(name)] =
this.element.querySelector('.' + this.ELEMENT_PREFIX + name);
}, this);
};
/**
* Generates markup for the dialog.
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype.view = function aad_view() {
var id = this.CLASS_NAME + this.instanceID;
return `<section class="authentication-dialog skin-organic" id="${id}"
role="region">
<gaia-header action="close"
class="authentication-dialog-http-authentication-header">
<button class="authentication-dialog-http-authentication-cancel">
</button>
<h1 data-l10n-id="sign-in-to-website"></h1>
<button class="authentication-dialog-http-authentication-ok"
data-l10n-id="login"></button>
</gaia-header>
<span class="authentication-dialog-http-authentication-message">
</span>
<label>
<span data-l10n-id="username"></span>
<input type="text" class="authentication-dialog-http-username-input" />
</label>
<label>
<span data-l10n-id="password"></span>
<input type="password"
class="authentication-dialog-http-password-input" />
</label>
</section>`;
};
/**
* General event handler interface.
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype.handleEvent = function(evt) {
this.debug(' AAD>> got event: ' + evt.type);
evt.preventDefault();
evt.stopPropagation();
// We don't want to show the auth dialog when we are trying to fetch
// the favicon. Check bug 1180330.
if (evt.detail.path && evt.detail.path.indexOf('favicon.ico') > -1) {
evt.detail.cancel();
return;
}
this._event = evt;
if (!this._injected) {
this.render();
}
this.show();
this._injected = true;
};
/**
* Registers click events.
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype._registerEvents =
function aad__registerEvents() {
this.elements.httpAuthenticationOk.
addEventListener('click', this.confirmHandler.bind(this));
this.elements.httpAuthenticationHeader.
addEventListener('action', this.cancelHandler.bind(this));
};
/**
* Unregisters authentication events from the app element.
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype._unregisterEvents =
function aad__unregisterEvents() {
this.app.element.removeEventListener(
'mozbrowserusernameandpasswordrequired',
this);
};
/**
* Shows the authentication dialog.
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype.show = function aad_show() {
var evt = this._event;
var elements = this.elements;
this.element.classList.add('visible');
this.debug(' AAD>> showing');
navigator.mozL10n.setAttributes(
elements.httpAuthenticationMessage,
'http-authentication-message2',
{host: evt.detail.host}
);
elements.httpUsernameInput.value = '';
elements.httpPasswordInput.value = '';
};
/**
* Hides the authentication dialog.
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype.hide = function aad_hide() {
this.elements.httpUsernameInput.blur();
this.elements.httpPasswordInput.blur();
this.element.classList.remove('visible');
this.debug(' AAD>> hided');
};
/**
* Called when the user confirms the authentication dialog.
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype.confirmHandler =
function aad_confirmHandler() {
var elements = this.elements;
var evt = this._event;
this.hide();
evt.detail.authenticate(elements.httpUsernameInput.value,
elements.httpPasswordInput.value);
this._event = null;
};
/**
* Called when the user cancels the authentication dialog.
* @memberof AppAuthenticationDialog.prototype
*/
AppAuthenticationDialog.prototype.cancelHandler =
function aad_cancelHandler() {
var evt = this._event;
this.hide();
evt.detail.cancel();
this._event = null;
};
exports.AppAuthenticationDialog = AppAuthenticationDialog;
}(window));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,66 @@
/* global BaseModule, LazyLoader */
'use strict';
(function() {
// Responsible to load and init the sub system for mozApps.
var AppCore = function(core) {
this.core = core;
};
AppCore.IMPORTS = [
'js/value_selector/value_picker.js',
'js/value_selector/spin_date_picker.js',
'js/value_selector/value_selector.js',
'js/search_window.js',
'js/browser_context_menu.js',
'js/child_window_factory.js',
'js/audio_channel_controller.js',
'js/app_modal_dialog.js',
'js/app_chrome.js',
'js/attention_toaster.js',
'js/app_statusbar.js',
'js/app_transition_controller.js',
'js/app_authentication_dialog.js',
'js/popup_window.js',
'js/browser_mixin.js',
'js/wrapper_factory.js',
'js/homescreen_window.js',
'js/global_overlay_window.js',
'js/trusted_window.js',
'js/touch_forwarder.js',
'js/callscreen_window.js',
'js/secure_window.js',
'js/lockscreen_window.js',
'js/input_window.js',
'js/activity_window.js'
];
AppCore.SUB_MODULES = [
'SystemWindow',
'AudioChannelService',
'VisibilityManager',
'AppWindowManager',
'KeyboardManager',
'Browser', // Blocked by integration tests.
'Activities' // Blocked by integration tests.
];
BaseModule.create(AppCore, {
name: 'AppCore',
DEBUG: false,
_start: function() {
return Promise.all([
LazyLoader.load(['shared/js/iac_handler.js']),
this.loadWhenIdle([
'GlobalOverlayWindowManager',
'AttentionWindowManager',
'TrustedWindowManager',
'SecureWindowFactory',
'SecureWindowManager',
'ActivityWindowManager',
'PermissionManager',
'Rocketbar',
'ActivityHandler'
])
]);
}
});
}());

View file

@ -0,0 +1,872 @@
/* jshint moz:true */
/* global ConfirmDialogHelper */
/* global KeyboardHelper */
/* global LazyLoader */
/* global ManifestHelper */
/* global MatchPattern */
/* global ModalDialog */
/* global NotificationScreen */
/* global Service */
/* global SystemBanner */
/* global Sanitizer */
/* global UtilityTray */
/* global applications */
'use strict';
(function(exports) {
var AppInstallManager = {
DEBUG: false,
name: 'AppInstallManager',
mapDownloadErrorsToMessage: {
'NETWORK_ERROR': 'download-failed',
'DOWNLOAD_ERROR': 'download-failed',
'MISSING_MANIFEST': 'install-failed',
'INVALID_MANIFEST': 'install-failed',
'INSTALL_FROM_DENIED': 'install-failed',
'INVALID_SECURITY_LEVEL': 'install-failed',
'INVALID_PACKAGE': 'install-failed',
'APP_CACHE_DOWNLOAD_ERROR': 'download-failed',
'DOWNLOAD_CANCELED':'download-canceled'
},
start: function() {
var lazyLoadScripts = [
'js/system_banner.js',
'shared/js/addons/match_pattern.js'
];
LazyLoader.load(lazyLoadScripts).then(() => {
this.systemBanner = new SystemBanner();
}).catch((err) => {
console.error(err);
});
this.dialog = document.getElementById('app-install-dialog');
this.msg = document.getElementById('app-install-message');
this.size = document.getElementById('app-install-size');
this.authorName = document.getElementById('app-install-author-name');
this.authorUrl = document.getElementById('app-install-author-url');
this.installButton =
document.getElementById('app-install-install-button');
this.cancelButton = document.getElementById('app-install-cancel-button');
this.imeLayoutDialog = document.getElementById('ime-layout-dialog');
this.imeList = document.getElementById('ime-list');
this.imeCancelButton = document.getElementById('ime-cancel-button');
this.imeConfirmButton = document.getElementById('ime-confirm-button');
this.setupCancelButton =
document.getElementById('setup-cancel-button');
this.setupConfirmButton =
document.getElementById('setup-confirm-button');
this.installCancelDialog =
document.getElementById('app-install-cancel-dialog');
this.downloadCancelDialog =
document.getElementById('app-download-cancel-dialog');
this.setupInstalledAppDialog =
document.getElementById('setup-installed-app-dialog');
this.confirmCancelButton =
document.getElementById('app-install-confirm-cancel-button');
this.setupAppName = document.getElementById('setup-app-name');
this.setupAppDescription =
document.getElementById('setup-app-description');
this.resumeButton = document.getElementById('app-install-resume-button');
this.notifContainer =
document.getElementById('install-manager-notification-container');
this.appInfos = {};
this.setupQueue = [];
this.isSetupInProgress = false;
window.addEventListener('mozChromeEvent', this);
window.addEventListener('applicationinstall', this);
window.addEventListener('applicationuninstall', this);
this.installButton.onclick = this.handleInstall.bind(this);
this.cancelButton.onclick = this.showInstallCancelDialog.bind(this);
this.confirmCancelButton.onclick = this.handleInstallCancel.bind(this);
this.resumeButton.onclick = this.hideInstallCancelDialog.bind(this);
this.notifContainer.onclick = this.showDownloadCancelDialog.bind(this);
this.downloadCancelDialog.querySelector('.confirm').onclick =
this.handleConfirmDownloadCancel.bind(this);
this.downloadCancelDialog.querySelector('.cancel').onclick =
this.handleCancelDownloadCancel.bind(this);
this.setupCancelButton.onclick = this.handleSetupCancelAction.bind(this);
this.setupConfirmButton.onclick =
this.handleSetupConfirmAction.bind(this);
this.imeCancelButton.onclick = this.hideIMEList.bind(this);
this.imeConfirmButton.onclick = this.handleImeConfirmAction.bind(this);
LazyLoader.load(['shared/js/homescreens/confirm_dialog_helper.js']);
window.addEventListener('applicationready', this);
window.addEventListener('home', this);
window.addEventListener('holdhome', this);
// bind these handlers so that we can have only one instance and check
// them later on
['handleDownloadSuccess',
'handleDownloadError',
'handleProgress',
'handleApplicationReady'
].forEach(function(name) {
this[name] = this[name].bind(this);
}, this);
},
stop: function ai_stop() {
window.removeEventListener('mozChromeEvent', this);
window.removeEventListener('applicationinstall', this);
window.removeEventListener('applicationuninstall', this);
window.removeEventListener('applicationready', this);
window.removeEventListener('home', this);
window.removeEventListener('holdhome', this);
},
handleEvent: function ai_handleEvent(evt) {
switch (evt.type) {
case 'mozChromeEvent':
return this.handleMozChromeEvent(evt);
case 'applicationready':
return this.handleApplicationReady(evt);
case 'home':
case 'holdhome':
return this.cancelInstallation(evt);
case 'applicationinstall':
return this.handleApplicationInstall(evt);
case 'applicationuninstall':
return this.handleApplicationUninstall(evt);
}
},
imeListView: function({displayName, imeName}) {
return Sanitizer.createSafeHTML `
<li>
<gaia-checkbox class="ime inline" name="keyboards" value="${imeName}">
<label>${displayName}</label>
</gaia-checkbox>
</li>`;
},
cancelInstallation: function ai_cancelInstallation() {
this.dialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
this.handleInstallCancel();
// hide IME setup dialog if presented
if (this.setupInstalledAppDialog.classList.contains('visible') ) {
this.handleSetupCancelAction();
}
// hide IME layout list if presented
if (this.imeLayoutDialog.classList.contains('visible')) {
this.hideIMEList();
}
},
handleMozChromeEvent: function ai_handleMozChromeEvent(e) {
switch (e.detail.type) {
case 'webapps-ask-install':
var manifestURL = e.detail.app && e.detail.app.manifestURL;
var isCustomizing = Service.query('ftuCustomizationContains',
manifestURL);
this.debug('handling webapps-ask-install, ' +
'manifestURL: ' + manifestURL + ', ' +
'isCustomizing: ' + isCustomizing);
if (manifestURL && isCustomizing) {
var grantedEvent = new CustomEvent('mozContentEvent', {
bubbles: true,
cancelable: true,
detail: {
id: e.detail.id,
type: 'webapps-install-granted'
}
});
window.dispatchEvent(grantedEvent);
} else {
this.handleAppInstallPrompt(e.detail);
}
break;
case 'webapps-ask-uninstall':
this.handleAppUninstallPrompt(e.detail);
break;
}
},
handleApplicationReady: function ai_handleApplicationReady(e) {
window.removeEventListener('applicationready',
this.handleApplicationReady);
var apps = e.detail.applications;
Object.keys(apps)
.filter(function(key) { return apps[key].installState === 'pending'; })
.map(function(key) { return apps[key]; })
.forEach(this.prepareForDownload, this);
},
handleApplicationInstall: function ai_handleApplicationInstallEvent(e) {
var app = e.detail.application;
if (app.installState === 'installed') {
this.handleInstallSuccess(app);
return;
}
this.prepareForDownload(app);
},
handleApplicationUninstall: function ai_handleApplicationUninstall(e) {
var app = e.detail.application;
this.onDownloadStop(app);
this.onDownloadFinish(app);
},
handleAppInstallPrompt: function ai_handleInstallPrompt(detail) {
var app = detail.app;
// updateManifest is used by packaged apps until they are installed
var manifest = app.manifest ? app.manifest : app.updateManifest;
if (!manifest) {
return;
}
this.dialog.classList.add('visible');
this.dispatchPromptEvent('shown');
var id = detail.id;
if (manifest.size) {
this.size.removeAttribute('data-l10n-id');
this.size.textContent = this.humanizeSize(manifest.size);
} else {
this.size.setAttribute('data-l10n-id', 'size-unknown');
}
// Wrap manifest to get localized properties
manifest = new ManifestHelper(manifest);
navigator.mozL10n.setAttributes(this.msg, 'install-app', {
'name': manifest.displayName
});
if (manifest.developer) {
if (manifest.developer.name) {
this.authorName.removeAttribute('data-l10n-id');
this.authorName.textContent = manifest.developer.name;
} else {
this.authorName.setAttribute('data-l10n-id', 'author-unknown');
}
this.authorUrl.textContent = manifest.developer.url || '';
} else {
this.authorName.setAttribute('data-l10n-id', 'author-unknown');
this.authorUrl.textContent = '';
}
// Warn about system level addons?
var warningEl = document.getElementById('system-addon-warning');
warningEl.hidden = !this.requiresRestart(manifest.content_scripts);
this.installCallback = (function ai_installCallback() {
this.dispatchResponse(id, 'webapps-install-granted');
}).bind(this);
this.installCancelCallback = (function ai_cancelCallback() {
this.dispatchResponse(id, 'webapps-install-denied');
}).bind(this);
},
requiresRestart: function ai_requiresRestart(contentScripts) {
var restartRequired = false;
if (contentScripts) {
var patterns = contentScripts.map(contentScript => {
return {
matches: new MatchPattern(contentScript.matches),
excludeMatches: new MatchPattern(contentScript.exclude_matches)
};
});
appLoop:
for (var appKey in applications.installedApps) {
if (!appKey) {
continue;
}
var installedApp = applications.installedApps[appKey];
var launchPath = installedApp.manifest.launch_path || '';
if (!installedApp.manifest.origin) {
continue;
}
if (installedApp.manifest.role !== 'system' &&
installedApp.manifest.role !== 'homescreen') {
continue;
}
var launchURL = new URL(launchPath, installedApp.manifest.origin);
for(var i = 0; i < patterns.length; i++) {
var pattern = patterns[i];
if (!pattern.excludeMatches.matches(launchURL) &&
pattern.matches.matches(launchURL)) {
restartRequired = true;
break appLoop;
}
}
}
}
return restartRequired;
},
handleInstall: function ai_handleInstall(evt) {
if (evt) {
evt.preventDefault();
}
if (this.installCallback) {
this.installCallback();
}
this.installCallback = null;
this.installCancelCallback = null;
this.dialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
},
handleAppUninstallPrompt: function ai_handleUninstallPrompt(detail) {
var app = detail.app;
var id = detail.id;
// updateManifest is used by packaged apps until they are installed
var manifest = app.manifest ? app.manifest : app.updateManifest;
if (!manifest) {
return;
}
// Wrap manifest to get localized properties
manifest = new ManifestHelper(manifest);
if (manifest.role === 'theme') {
this.dispatchResponse(id, 'webapps-uninstall-granted');
return;
}
var unrecoverable = app.installState === 'pending' &&
!app.downloadAvailable &&
!app.readyToApplyDownload;
var dialogConfig;
if (unrecoverable) {
dialogConfig = {
type: 'unrecoverable',
title: 'unrecoverable-error-title',
body: 'unrecoverable-error-body',
confirm: {
title: 'unrecoverable-error-action',
cb: () => {
this.dispatchResponse(id, 'webapps-uninstall-granted');
}
}
};
} else {
var nameObj = { name: manifest.displayName };
dialogConfig = {
type: 'remove',
title: {id: 'delete-title', args: nameObj},
body: {id: 'delete-body', args: nameObj},
cancel: {
title: 'cancel',
cb: () => { this.dispatchResponse(id, 'webapps-uninstall-denied'); }
},
confirm: {
title: 'delete',
type: 'danger',
cb: () => {
this.dispatchResponse(id, 'webapps-uninstall-granted');
}
}
};
}
var dialog = new ConfirmDialogHelper(dialogConfig);
dialog.show(document.getElementById('app-uninstall-dialog'));
},
prepareForDownload: function ai_prepareForDownload(app) {
var manifestURL = app.manifestURL;
this.appInfos[manifestURL] = {};
// these methods are already bound to |this|
app.ondownloadsuccess = this.handleDownloadSuccess;
app.ondownloaderror = this.handleDownloadError;
app.onprogress = this.handleProgress;
},
configurations: {
'input': {
fnName: 'showIMEList'
}
},
handleInstallSuccess: function ai_handleInstallSuccess(app) {
var manifest = app.manifest || app.updateManifest;
var role = manifest.role;
// We must stop 3rd-party keyboard app from being installed
// if the feature is not enabled.
if (role === 'input' &&
!Service.query('isOutOfProcessEnabled')) {
navigator.mozApps.mgmt.uninstall(app);
return;
}
if (this.configurations[role]) {
this.setupQueue.push(app);
this.checkSetupQueue();
} else {
this.showInstallSuccess(app);
}
// send event
var evt = new CustomEvent('applicationinstallsuccess',
{ detail: { application: app } });
window.dispatchEvent(evt);
},
showInstallSuccess: function ai_showInstallSuccess(app) {
if (Service.query('isFtuRunning')) {
return;
}
var manifest = app.manifest || app.updateManifest;
var appManifest = new ManifestHelper(manifest);
var name = appManifest.displayName;
var l10nId = appManifest.role === 'langpack' ?
'langpack-install-success2' : 'app-install-success';
this.systemBanner.show({id: l10nId, args: { appName: name }});
},
checkSetupQueue: function ai_checkSetupQueue() {
if (this.setupQueue.length && !(this.isSetupInProgress)) {
this.isSetupInProgress = true;
this.showSetupDialog();
}
},
completedSetupTask: function ai_completedSetupTask() {
// clean completed app
this.setupQueue.shift();
this.isSetupInProgress = false;
this.checkSetupQueue();
},
hideSetupDialog: function ai_hideSetupDialog() {
this.setupAppName.textContent = '';
this.setupAppDescription.textContent = '';
this.setupInstalledAppDialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
},
showSetupDialog: function ai_showSetupDialog() {
var app = this.setupQueue[0];
var manifest = app.manifest;
var appManifest = new ManifestHelper(manifest);
var appName = appManifest.displayName;
var appDescription = appManifest.description;
this.setupAppDescription.textContent = appDescription;
navigator.mozL10n.setAttributes(this.setupAppName,
'app-install-success',
{ appName: appName });
this.setupInstalledAppDialog.classList.add('visible');
this.dispatchPromptEvent('shown');
window.dispatchEvent(new CustomEvent('applicationsetupdialogshow'));
},
handleSetupCancelAction: function ai_handleSetupCancelAction() {
this.hideSetupDialog();
this.completedSetupTask();
},
handleSetupConfirmAction: function ai_handleSetupConfirmAction() {
var fnName = this.configurations[this.setupQueue[0].manifest.role].fnName;
this[fnName].call(this);
this.hideSetupDialog();
},
showIMEList: function ai_showIMEList() {
var app = this.setupQueue[0];
var inputs = app.manifest.inputs;
if (typeof inputs !== 'object') {
console.error('inputs must be an object for ' +
'third-party keyboard layouts');
this.completedSetupTask();
return;
}
// Check permission level is correct
var hasInputPermission = (app.manifest.type === 'certified' ||
app.manifest.type === 'privileged') &&
(app.manifest.permissions &&
'input' in app.manifest.permissions);
if (!hasInputPermission) {
console.error('third-party IME does not have correct input permission');
this.completedSetupTask();
return;
}
// build the list of keyboard layouts
var listHtml = [];
for (var name in inputs) {
var displayIMEName = new ManifestHelper(inputs[name]).displayName;
listHtml.push(this.imeListView({
imeName: name,
displayName: displayIMEName
}));
}
// keeping li template
this.imeList.innerHTML = Sanitizer.unwrapSafeHTML(...listHtml);
this.imeLayoutDialog.classList.add('visible');
this.dispatchPromptEvent('shown');
},
hideIMEList: function ai_hideIMEList() {
this.imeLayoutDialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
this.imeList.innerHTML = '';
this.completedSetupTask();
},
handleImeConfirmAction: function ai_handleImeConfirmAction() {
var manifestURL = this.setupQueue[0].manifestURL;
var keyboards = this.imeList.getElementsByTagName('gaia-checkbox');
for (var i = 0, l = keyboards.length; i < l; i++) {
var keyboardIME = keyboards[i];
if (keyboardIME.checked) {
KeyboardHelper.setLayoutEnabled(manifestURL,
keyboardIME.getAttribute('value'), true);
KeyboardHelper.saveToSettings();
}
}
this.hideIMEList();
},
handleDownloadSuccess: function ai_handleDownloadSuccess(evt) {
var app = evt.application;
this.handleInstallSuccess(app);
this.onDownloadStop(app);
this.onDownloadFinish(app);
},
handleDownloadError: function ai_handleDownloadError(evt) {
var app = evt.application;
var manifest = app.manifest || app.updateManifest;
var name = new ManifestHelper(manifest).displayName;
var errorName = app.downloadError.name;
switch (errorName) {
case 'INSUFFICIENT_STORAGE':
var title = 'not-enough-space',
buttonText = 'ok',
message = 'not-enough-space-message';
ModalDialog.alert(title, message, {title: buttonText});
break;
default:
// showing the real error to a potential developer
console.info('downloadError event, error code is', errorName);
var key =
this.mapDownloadErrorsToMessage[errorName] || 'generic-error';
this.systemBanner.show({
id: 'app-install-' + key,
args: { appName: name }
});
}
this.onDownloadStop(app);
},
onDownloadStart: function ai_onDownloadStart(app) {
if (! this.hasNotification(app)) {
Service.request('incDownloads');
this.addNotification(app);
this.requestWifiLock(app);
}
},
onDownloadStop: function ai_onDownloadStop(app) {
if (this.hasNotification(app)) {
Service.request('decDownloads');
this.removeNotification(app);
this.releaseWifiLock(app);
}
},
hasNotification: function ai_hasNotification(app) {
var appInfo = this.appInfos[app.manifestURL];
return appInfo && !!appInfo.installNotification;
},
onDownloadFinish: function ai_onDownloadFinish(app) {
delete this.appInfos[app.manifestURL];
// check if these are our handlers before removing them
if (app.ondownloadsuccess === this.handleDownloadSuccess) {
app.ondownloadsuccess = null;
}
if (app.ondownloaderror === this.handleDownloadError) {
app.ondownloaderror = null;
}
if (app.onprogress === this.handleProgress) {
app.onprogress = null;
}
},
addNotification: function ai_addNotification(app) {
// should be unique (this is used already in applications.js)
var manifestURL = app.manifestURL,
appInfo = this.appInfos[manifestURL];
if (appInfo.installNotification) {
return;
}
var newNotif = Sanitizer.createSafeHTML `
<div class="fake-notification" role="link">
<div data-icon="rocket" class="alert" aria-hidden="true"></div>
<div class="title-container"></div>
<progress></progress>
</div>`;
this.notifContainer.insertAdjacentHTML('afterbegin',
Sanitizer.unwrapSafeHTML(newNotif));
var newNode = this.notifContainer.firstElementChild;
newNode.dataset.manifest = manifestURL;
var manifest = app.manifest || app.updateManifest;
navigator.mozL10n.setAttributes(
newNode.querySelector('.title-container'),
'downloadingAppMessage',
{ appName: new ManifestHelper(manifest).displayName }
);
var progressNode = newNode.querySelector('progress');
if (app.updateManifest && app.updateManifest.size) {
progressNode.max = app.updateManifest.size;
appInfo.hasMax = true;
}
appInfo.installNotification = newNode;
NotificationScreen.addUnreadNotification(manifestURL);
},
getNotificationProgressNode: function ai_getNotificationProgressNode(app) {
var appInfo = this.appInfos[app.manifestURL];
var progressNode = appInfo &&
appInfo.installNotification &&
appInfo.installNotification.querySelector('progress');
return progressNode || null;
},
handleProgress: function ai_handleProgress(evt) {
var app = evt.application,
appInfo = this.appInfos[app.manifestURL];
this.onDownloadStart(app);
var progressNode = this.getNotificationProgressNode(app);
var message;
if (isNaN(app.progress) || app.progress == null) {
// now we get NaN if there is no progress information but let's
// handle the null and undefined cases as well
message = {
id: 'downloadingAppProgressIndeterminate',
args: null
};
progressNode.removeAttribute('value'); // switch to indeterminate state
} else if (appInfo.hasMax) {
message = {
id: 'downloadingAppProgress',
args: {
progress: this.humanizeSize(app.progress),
max: this.humanizeSize(progressNode.max)
}
};
progressNode.value = app.progress;
} else {
message = {
id: 'downloadingAppProgressNoMax',
args: { progress: this.humanizeSize(app.progress) }
};
}
navigator.mozL10n.setAttributes(
progressNode,
message.id,
message.args);
},
removeNotification: function ai_removeNotification(app) {
var manifestURL = app.manifestURL,
appInfo = this.appInfos[manifestURL],
node = appInfo.installNotification;
if (!node) {
return;
}
node.parentNode.removeChild(node);
delete appInfo.installNotification;
NotificationScreen.removeUnreadNotification(manifestURL);
},
requestWifiLock: function ai_requestWifiLock(app) {
var appInfo = this.appInfos[app.manifestURL];
if (! appInfo.wifiLock) {
// we don't want 2 locks for the same app
appInfo.wifiLock = navigator.requestWakeLock('wifi');
}
},
releaseWifiLock: function ai_releaseWifiLock(app) {
var appInfo = this.appInfos[app.manifestURL];
if (appInfo.wifiLock) {
try {
appInfo.wifiLock.unlock();
} catch (e) {
// this can happen if the lock is already unlocked
console.error('error during unlock', e);
}
delete appInfo.wifiLock;
}
},
dispatchResponse: function ai_dispatchResponse(id, type) {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentEvent', true, true, {
id: id,
type: type
});
window.dispatchEvent(event);
},
humanizeSize: function ai_humanizeSize(bytes) {
var _ = navigator.mozL10n.get;
var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'];
if (!bytes) {
return '0.00 ' + _(units[0]);
}
var e = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, Math.floor(e))).toFixed(2) + ' ' +
_(units[e]);
},
showInstallCancelDialog: function ai_showInstallCancelDialog(evt) {
if (evt) {
evt.preventDefault();
}
this.installCancelDialog.classList.add('visible');
this.dialog.classList.remove('visible');
},
hideInstallCancelDialog: function ai_hideInstallCancelDialog(evt) {
if (evt) {
evt.preventDefault();
}
this.dialog.classList.add('visible');
this.installCancelDialog.classList.remove('visible');
},
showDownloadCancelDialog: function ai_showDownloadCancelDialog(e) {
var currentNode = e.target;
if (!currentNode.classList.contains('fake-notification')) {
// tapped outside of a notification
return;
}
var manifestURL = currentNode.dataset.manifest,
app = applications.getByManifestURL(manifestURL),
manifest = app.manifest || app.updateManifest,
dialog = this.downloadCancelDialog;
var title = dialog.querySelector('h1');
navigator.mozL10n.setAttributes(title, 'stopDownloading', {
app: new ManifestHelper(manifest).displayName
});
dialog.classList.add('visible');
this.dispatchPromptEvent('shown');
dialog.dataset.manifest = manifestURL;
UtilityTray.hide();
},
handleInstallCancel: function ai_handleInstallCancel() {
if (this.installCancelCallback) {
this.installCancelCallback();
}
this.installCallback = null;
this.installCancelCallback = null;
this.installCancelDialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
},
handleConfirmDownloadCancel: function ai_handleConfirmDownloadCancel(e) {
e && e.preventDefault();
var dialog = this.downloadCancelDialog,
manifestURL = dialog.dataset.manifest;
if (manifestURL) {
var app = applications.getByManifestURL(manifestURL);
app && app.cancelDownload();
}
this.hideDownloadCancelDialog();
},
handleCancelDownloadCancel: function ai_handleCancelDownloadCancel(e) {
e && e.preventDefault();
this.hideDownloadCancelDialog();
},
hideDownloadCancelDialog: function() {
var dialog = this.downloadCancelDialog;
dialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
delete dialog.dataset.manifest;
},
dispatchPromptEvent: function(state) {
window.dispatchEvent(new CustomEvent('installprompt' + state));
},
debug: function() {
if (this.DEBUG) {
console.log('[' + this.name + ']' +
'[' + Service.currentTime() + '] ' +
Array.slice(arguments).concat());
if (this.TRACE) {
console.trace();
}
}
}
};
exports.AppInstallManager = AppInstallManager;
}(window));

View file

@ -0,0 +1,75 @@
'use strict';
/* global Service */
/**
The app migrator is in charge of migrating app data between releases
*/
(function(exports) {
const HOMESCREEN_MANIFEST =
'app://homescreen.gaiamobile.org/manifest.webapp';
const VERTICALHOME_PREFS_STORE = 'vertical_preferences_store';
const MIGRATION_PREF = 'migrated';
var AppMigrator = function AppMigrator() {
};
AppMigrator.prototype = {
start: function() {
// Probably should not happen, but just to be safe we early exit in case
// we get a second migrate message.
if (this.migrating) {
return;
}
var self = this;
if (Service.query('justUpgraded')) {
self.migrating = true;
// Migrate to new home screen if migration hasn't already been
// performed.
navigator.getDataStores(VERTICALHOME_PREFS_STORE).then(
stores => {
if (stores.length < 1) {
console.error('Error opening verticalhome prefs datastore');
return;
}
var migrate = () => {
navigator.mozApps.getSelf().onsuccess = e => {
var app = e.target.result;
if (!app) {
console.error('Error retrieving app object');
return;
}
app.connect('verticalhome-migrate').then(ports => {
ports.forEach(port => {
port.postMessage({ action: 'migrate',
manifestURL: HOMESCREEN_MANIFEST });
});
}, e => {
console.error('Error communicating with verticalhome', e);
});
};
};
stores[0].get(MIGRATION_PREF).then(
migrated => {
if (!migrated) {
migrate();
}
}, e => {
console.error('Error getting verticalhome migration pref', e);
});
});
}
}
};
exports.AppMigrator = AppMigrator;
})(window);

View file

@ -0,0 +1,488 @@
/* global BaseUI */
'use strict';
(function(exports) {
var _id = 0;
/**
* The ModalDialog of the AppWindow.
*
* Including **alert**, **prompt**, **confirm**, and
* **single select** dialogs.
*
* @class AppModalDialog
* @param {AppWindow} app The app window instance
* where this dialog should popup.
* @extends BaseUI
*/
var AppModalDialog = function AppModalDialog(app) {
this.app = app;
this.containerElement = app.element;
this.events = [];
// One to one mapping.
this.instanceID = _id++;
this._injected = false;
this._visible = false;
app.element.addEventListener('mozbrowsershowmodalprompt', this);
return this;
};
AppModalDialog.prototype = Object.create(BaseUI.prototype);
AppModalDialog.prototype.CLASS_NAME = 'AppModalDialog';
AppModalDialog.prototype.ELEMENT_PREFIX = 'modal-dialog-';
AppModalDialog.prototype.customID = function amd_customID() {
if (this.app) {
return '[' + this.app.origin + ']';
} else {
return '';
}
};
AppModalDialog.prototype.handleEvent = function amd_handleEvent(evt) {
this.app.debug('handling ' + evt.type);
evt.preventDefault();
evt.stopPropagation();
this.events.push(evt);
this.menuHeight = 0;
if (!this._injected) {
this.render();
}
this.show();
this._injected = true;
};
AppModalDialog.prototype.isVisible = function amd_isVisible() {
return this._visible;
};
AppModalDialog.prototype._fetchElements = function amd__fetchElements() {
this.element = document.getElementById(this.CLASS_NAME + this.instanceID);
this.elements = {};
var toCamelCase = function toCamelCase(str) {
return str.replace(/\-(.)/g, function replacer(str, p1) {
return p1.toUpperCase();
});
};
this.elementClasses = ['alert', 'alert-ok', 'alert-message',
'prompt', 'prompt-ok', 'prompt-cancel', 'prompt-input', 'prompt-message',
'confirm', 'confirm-ok', 'confirm-cancel', 'confirm-message',
'select-one', 'select-one-cancel', 'select-one-menu', 'select-one-title',
'alert-title', 'confirm-title', 'prompt-title',
'custom-prompt', 'custom-prompt-message', 'custom-prompt-buttons',
'custom-prompt-checkbox'];
// Loop and add element with camel style name to Modal Dialog attribute.
this.elementClasses.forEach(function createElementRef(name) {
this.elements[toCamelCase(name)] =
this.element.querySelector('.' + this.ELEMENT_PREFIX + name);
}, this);
this.elements.menu = this.element.querySelector('menu');
};
AppModalDialog.prototype._registerEvents = function amd__registerEvents() {
var elements = this.elements;
for (var id in elements) {
var tagName = elements[id].tagName.toLowerCase();
if (tagName == 'button' || tagName == 'ul') {
if (elements[id].classList.contains('confirm')) {
elements[id].addEventListener('click',
this.confirmHandler.bind(this));
} else if (elements[id].classList.contains('cancel')) {
elements[id].addEventListener('click', this.cancelHandler.bind(this));
}
}
}
};
AppModalDialog.prototype.view = function amd_view() {
var id = this.CLASS_NAME + this.instanceID;
return `<div class="modal-dialog" id="${id}">
<form class="modal-dialog-alert generic-dialog"
role="dialog" tabindex="-1">
<div class="modal-dialog-message-container inner">
<h3 class="modal-dialog-alert-title"></h3>
<p>
<span class="modal-dialog-alert-message"></span>
</p>
</div>
<menu>
<button type="submit"
class="modal-dialog-alert-ok confirm affirmative"
data-l10n-id="ok">Value for Marionette (see bug 1224063)</button>
</menu>
</form>
<form class="modal-dialog-confirm generic-dialog"
role="dialog" tabindex="-1">
<div class="modal-dialog-message-container inner">
<h3 class="modal-dialog-confirm-title"></h3>
<p>
<span class="modal-dialog-confirm-message"></span>
</p>
</div>
<menu data-items="2">
<button type="button" class="modal-dialog-confirm-cancel cancel"
data-l10n-id="cancel"></button>
<button type="submit"
class="modal-dialog-confirm-ok confirm affirmative"
data-l10n-id="ok"></button>
</menu>
</form>
<form class="modal-dialog-prompt generic-dialog"
role="dialog" tabindex="-1">
<div class="modal-dialog-message-container inner">
<h3 class="modal-dialog-prompt-title"></h3>
<p>
<span class="modal-dialog-prompt-message"></span>
<input class="modal-dialog-prompt-input" />
</p>
</div>
<menu data-items="2">
<button type="button" class="modal-dialog-prompt-cancel cancel"
data-l10n-id="cancel"></button>
<button type="submit"
class="modal-dialog-prompt-ok confirm affirmative"
data-l10n-id="ok"></button>
</menu>
</form>
<form class="modal-dialog-select-one generic-dialog"
role="dialog" tabindex="-1">
<div class="modal-dialog-message-container inner">
<h3 class="modal-dialog-select-one-title"></h3>
<ul class="modal-dialog-select-one-menu"></ul>
</div>
<menu>
<button type="submit"
class="modal-dialog-select-one-cancel cancel"
data-l10n-id="cancel"></button>
</menu>
</form>
<form class="modal-dialog-custom-prompt generic-dialog"
role="dialog" tabindex="-1">
<div class="modal-dialog-message-container inner">
<h3 class="modal-dialog-custom-prompt-title"></h3>
<p class="modal-dialog-custom-prompt-message"></p>
<gaia-checkbox class="modal-dialog-custom-prompt-checkbox">
<label></label>
</gaia-checkbox>
</div>
<menu class="modal-dialog-custom-prompt-buttons"></menu>
</form>
</div>`;
};
AppModalDialog.prototype.processNextEvent = function amd_processNextEvent() {
this.events.splice(0, 1);
if (this.events.length) {
this.show();
} else {
this.hide();
}
};
AppModalDialog.prototype.kill = function amd_kill() {
this._visible = false;
this.containerElement.removeChild(this.element);
};
// Show relative dialog and set message/input value well
AppModalDialog.prototype.show = function amd_show() {
if (!this.events.length) {
return;
}
this._visible = true;
var evt = this.events[0];
var message = evt.detail.message || '';
var title = this._getTitle(evt.detail.title);
var elements = this.elements;
var type = evt.detail.promptType || evt.detail.type;
switch (type) {
case 'alert':
elements.alertTitle.textContent = title;
elements.alertMessage.textContent = message;
elements.alert.classList.add('visible');
if (evt.yesText) {
elements.alertOk.removeAttribute('data-l10n-id');
elements.alertOk.textContent = evt.yesText;
} else {
elements.alertOk.setAttribute('data-l10n-id', 'ok');
}
elements.alert.focus();
this.updateMaxHeight();
break;
case 'prompt':
elements.prompt.classList.add('visible');
elements.promptInput.value = evt.detail.initialValue;
elements.promptTitle.textContent = title;
elements.promptMessage.textContent = message;
if (evt.yesText) {
elements.promptOk.removeAttribute('data-l10n-id');
elements.promptOk.textContent = evt.yesText;
} else {
elements.promptOk.setAttribute('data-l10n-id', 'ok');
}
if (evt.noText) {
elements.promptCancel.removeAttribute('data-l10n-id');
elements.promptCancel.textContent = evt.noText;
} else {
elements.promptCancel.setAttribute('data-l10n-id', 'cancel');
}
elements.prompt.focus();
break;
case 'confirm':
elements.confirm.classList.add('visible');
elements.confirmTitle.textContent = title;
elements.confirmMessage.textContent = message;
if (evt.yesText) {
elements.confirmOk.removeAttribute('data-l10n-id');
elements.confirmOk.textContent = evt.yesText;
} else {
elements.confirmOk.setAttribute('data-l10n-id', 'ok');
}
if (evt.noText) {
elements.confirmCancel.removeAttribute('data-l10n-id');
elements.confirmCancel.textContent = evt.noText;
} else {
elements.confirmCancel.setAttribute('data-l10n-id', 'cancel');
}
elements.confirm.focus();
break;
case 'selectone':
this.buildSelectOneDialog(message);
elements.selectOne.classList.add('visible');
elements.selectOne.focus();
break;
case 'custom-prompt':
var customPrompt = evt.detail;
elements.customPrompt.classList.add('visible');
elements.customPromptMessage.textContent = customPrompt.message;
// Display custom list of buttons
elements.customPromptButtons.innerHTML = '';
elements.customPromptButtons.setAttribute('data-items',
customPrompt.buttons.length);
var domElement = null;
for (var i = customPrompt.buttons.length - 1; i >= 0; i--) {
var button = customPrompt.buttons[i];
domElement = document.createElement('button');
domElement.dataset.buttonIndex = i;
if (button.messageType === 'builtin') {
domElement.setAttribute('data-l10n-id', button.message);
} else if (button.messageType === 'custom') {
// For custom button, we assume that the text is already translated
domElement.textContent = button.message;
} else {
console.error('Unexpected button type : ' + button.messageType);
continue;
}
domElement.addEventListener('click', this.confirmHandler.bind(this));
elements.customPromptButtons.appendChild(domElement);
}
domElement.classList.add('affirmative');
// Eventualy display a checkbox:
var checkbox = elements.customPromptCheckbox;
if (customPrompt.showCheckbox) {
if (customPrompt.checkboxCheckedByDefault) {
checkbox.checked = true;
} else {
checkbox.checked = false;
}
// We assume that checkbox custom message is already translated
checkbox.querySelector('label').textContent =
customPrompt.checkboxMessage;
} else {
checkbox.style.display = 'none';
}
elements.customPrompt.focus();
break;
}
this.app.browser.element.setAttribute('aria-hidden', true);
this.element.classList.add('visible');
};
AppModalDialog.prototype.hide = function amd_hide() {
this._visible = false;
this.element.blur();
this.app.browser.element.removeAttribute('aria-hidden');
this.element.classList.remove('visible');
if (this.app) {
this.app.focus();
}
if (!this.events.length) {
return;
}
var evt = this.events[0];
var type = evt.detail.promptType || evt.detail.type;
if (type === 'prompt') {
this.elements.promptInput.blur();
}
this.elements[type].classList.remove('visible');
};
// When user clicks OK button on alert/confirm/prompt
AppModalDialog.prototype.confirmHandler =
function amd_confirmHandler(clickEvt) {
if (!this.events.length) {
return;
}
clickEvt.preventDefault();
var elements = this.elements;
var evt = this.events[0];
var type = evt.detail.promptType || evt.detail.type;
switch (type) {
case 'alert':
elements.alert.classList.remove('visible');
break;
case 'prompt':
evt.detail.returnValue = elements.promptInput.value;
elements.prompt.classList.remove('visible');
break;
case 'confirm':
evt.detail.returnValue = true;
elements.confirm.classList.remove('visible');
break;
case 'custom-prompt':
var returnValue = {
selectedButton: clickEvt.target.dataset.buttonIndex
};
if (evt.detail.showCheckbox) {
returnValue.checked = elements.customPromptCheckbox.checked;
}
evt.detail.returnValue = returnValue;
elements.customPrompt.classList.remove('visible');
break;
}
if (evt.detail.unblock) {
evt.detail.unblock();
}
this.processNextEvent();
};
// When user clicks cancel button on confirm/prompt or
// when the user try to escape the dialog with the escape key
AppModalDialog.prototype.cancelHandler =
function amd_cancelHandler(clickEvt) {
if (!this.events.length) {
return;
}
clickEvt.preventDefault();
var evt = this.events[0];
var elements = this.elements;
var type = evt.detail.promptType || evt.detail.type;
switch (type) {
case 'alert':
elements.alert.classList.remove('visible');
break;
case 'prompt':
/* return null when click cancel */
evt.detail.returnValue = null;
elements.prompt.classList.remove('visible');
break;
case 'confirm':
/* return false when click cancel */
evt.detail.returnValue = false;
elements.confirm.classList.remove('visible');
break;
case 'selectone':
/* return null when click cancel */
evt.detail.returnValue = null;
elements.selectOne.classList.remove('visible');
break;
}
if (evt.detail.unblock) {
evt.detail.unblock();
}
this.processNextEvent();
};
// When user selects an option on selectone dialog
AppModalDialog.prototype.selectOneHandler =
function amd_selectOneHandler(target) {
if (!this.events.length) {
return;
}
var elements = this.elements;
var evt = this.events[0];
evt.detail.returnValue = target.id;
elements.selectOne.classList.remove('visible');
if (evt.detail.unblock) {
evt.detail.unblock();
}
this.processNextEvent();
};
AppModalDialog.prototype.updateMaxHeight = function() {
// Setting maxHeight for being able to scroll long
// texts: formHeight - menuHeight - titleHeight - margin
// We should fix this in a common way for all the dialogs
// in the building blocks layer: Bug 1096902
this.menuHeight = this.menuHeight || this.elements.menu.offsetHeight;
var messageHeight = this.element.offsetHeight - this.menuHeight;
messageHeight -= this.elements.alertTitle.offsetHeight;
var margin = window.getComputedStyle(this.elements.alertTitle).marginBottom;
var messageContainer = this.elements.alert.querySelector('.inner p');
var calc = 'calc(' + messageHeight + 'px - ' + margin + ')';
messageContainer.style.maxHeight = calc;
};
AppModalDialog.prototype._getTitle =
function amd__getTitle(title) {
//
// XXX Bug 982006, subsystems like uriloader still report errors with
// titles which are important to the user for context in diagnosing
// issues.
//
// However, we will ignore all titles containing a URL using the app
// protocol. These types of titles simply indicate that the active
// application is prompting and are more confusing to the user than
// useful. Instead we will return the application name if there is one
// or an empty string.
//
if (!title ||
title.includes('app://')) {
return this.app.name || '';
}
return title;
};
exports.AppModalDialog = AppModalDialog;
}(window));

View file

@ -0,0 +1,200 @@
/* global BaseUI, TouchForwarder */
'use strict';
(function(exports) {
var AppStatusbar = function(app) {
this.app = app;
this.containerElement = app.element;
this.render();
this._fetchAllElements();
this._touchForwarder = new TouchForwarder();
this._touchForwarder.destination = this.app.browser.element;
this.app.element.addEventListener('_suspended', this);
this.app.element.addEventListener('_resumed', this);
};
AppStatusbar.prototype = Object.create(BaseUI.prototype);
AppStatusbar.prototype.constructor = AppStatusbar;
AppStatusbar.prototype.EVENT_PREFIX = 'appstatusbar';
AppStatusbar.prototype.DEBUG = false;
AppStatusbar.prototype.view = function() {
var content = `<div class="titlebar">
<div class="notifications-shadow"></div>
<div class="statusbar-shadow titlebar-icons"></div>
</div>`;
return content;
};
AppStatusbar.prototype.RELEASE_TIMEOUT = 5000;
AppStatusbar.prototype.screen = document.getElementById('screen');
AppStatusbar.prototype._fetchAllElements = function(first_argument) {
this.titleBar = this.containerElement.querySelector('.titlebar');
};
AppStatusbar.prototype.handleEvent = function(evt) {
switch (evt.type) {
case '_resumed':
this._touchForwarder.destination = this.app.browser.element;
break;
case '_suspended':
this._touchForwarder.destination = null;
break;
}
};
AppStatusbar.prototype.handleStatusbarTouch = function(evt, barHeight) {
this.app.debug('preprocessing touch event...', evt.type);
var touch;
var cacheHeight = barHeight;
if (!this.chromeBar) {
// Because app chrome is loaded after us.
this.chromeBar = this.app.element.querySelector('.chrome');
}
if (this._dontStopEvent) {
return;
}
if (!this.app || !(this.app.isFullScreen() || document.mozFullScreen)) {
return;
}
this.app.debug('processing touch event...', evt.type);
evt.stopImmediatePropagation();
evt.preventDefault();
switch (evt.type) {
case 'touchstart':
clearTimeout(this._releaseTimeout);
this._touchStart = evt;
this._shouldForwardTap = true;
touch = evt.changedTouches[0];
this._startX = touch.clientX;
this._startY = touch.clientY;
this.chromeBar.style.transition = 'transform';
this.titleBar.style.transition = 'transform';
this.chromeBar.classList.add('dragging');
// The status bar might show, so ensure it knows we're fullscreen.
this.publish('-fullscreen-statusbar-set-appearance');
break;
case 'touchmove':
touch = evt.touches[0];
var height = cacheHeight;
var deltaX = touch.clientX - this._startX;
var deltaY = touch.clientY - this._startY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
this._shouldForwardTap = false;
}
var translate = Math.min(deltaY, height);
var heightThreshold = height;
if (this.app && this.app.isFullScreen() && this.app.config.chrome &&
this.app.config.chrome.navigation) {
translate = Math.min(deltaY, this.app.appChrome.height);
heightThreshold = this.app.appChrome.height;
this.titleBar.style.transform = 'translateY(calc(' +
(translate - this.app.appChrome.height) + 'px)';
} else {
this.titleBar.style.transform =
'translateY(calc(' + translate + 'px - 100%)';
}
this.chromeBar.style.transform =
'translateY(calc(' + translate + 'px - 100%)';
this.app.debug(translate, heightThreshold);
if (translate >= heightThreshold) {
if (this._touchStart) {
this._touchForwarder.forward(this._touchStart);
this._touchStart = null;
}
this._touchForwarder.forward(evt);
}
break;
case 'touchend':
clearTimeout(this._releaseTimeout);
if (this._touchStart) {
if (this._shouldForwardTap) {
this._touchForwarder.forward(this._touchStart);
this._touchForwarder.forward(evt);
this._touchStart = null;
}
this._releaseBar();
} else {
this._dontStopEvent = true;
this.publish('-fullscreen-statusbar-show');
this._touchForwarder.forward(evt);
this._releaseAfterTimeout();
}
break;
}
};
AppStatusbar.prototype._releaseBar = function() {
this.app.debug('releasing statusbar');
this._dontStopEvent = false;
this.publish('-fullscreen-statusbar-hide');
var titleBar = this.titleBar;
var chromeBar = this.chromeBar;
chromeBar.classList.remove('dragged');
chromeBar.classList.remove('dragging');
chromeBar.style.transform = '';
chromeBar.style.transition = '';
titleBar.classList.remove('dragged');
titleBar.style.transform = '';
titleBar.style.transition = '';
this.screen.classList.remove('minimized-tray');
clearTimeout(this._releaseTimeout);
this._releaseTimeout = null;
};
AppStatusbar.prototype._releaseAfterTimeout = function() {
this.app.debug('stay until 5s');
this.screen.classList.add('minimized-tray');
var chromeBar = this.chromeBar;
var titleBar = this.titleBar;
var self = this;
titleBar.style.transform = '';
titleBar.style.transition = '';
titleBar.classList.add('dragged');
chromeBar.style.transform = '';
chromeBar.style.transition = '';
chromeBar.classList.add('dragged');
chromeBar.classList.remove('dragging');
function releaseFn() {
window.removeEventListener('touchstart', closeOnTap);
window.removeEventListener('utilitytraywillshow', releaseFn);
clearTimeout(self._releaseTimeout);
self._releaseBar();
}
window.addEventListener('touchstart', closeOnTap);
window.addEventListener('utilitytraywillshow', releaseFn);
self._releaseTimeout = setTimeout(releaseFn, this.RELEASE_TIMEOUT);
function closeOnTap(evt) {
if (evt.target != self._touchForwarder.destination) {
return;
}
self.app.debug('user interaction, will release statusbar.');
releaseFn();
}
};
exports.AppStatusbar = AppStatusbar;
}(window));

View file

@ -0,0 +1,503 @@
/* global SettingsListener, Service */
'use strict';
(function(exports) {
var DEBUG = false;
var _id = 0;
var _globalStates = null;
// Manipulate global states for app text selection dialog across all apps. All
// states which are shared by apps should be put in this class.
var AppTextSelectionDialogGlobalStates = function() {
this._hasCutOrCopiedTimeoutId = null;
this._isPrefOn = true;
this.appTSDs = new Set();
this.bindOnObservePrefChanged = this.onObservePrefChanged.bind(this);
SettingsListener.observe('copypaste.enabled', true,
this.bindOnObservePrefChanged);
};
// If a user cuts or copies something, a paste shortcut will pop up every time
// when single tapping on an editable field within this timeout.
AppTextSelectionDialogGlobalStates.prototype.CUT_OR_COPIED_TIMEOUT_MS = 15000;
AppTextSelectionDialogGlobalStates.prototype.hasCutOrCopied =
function() {
return !!this._hasCutOrCopiedTimeoutId;
};
AppTextSelectionDialogGlobalStates.prototype.launchHasCutOrCopiedTimer =
function() {
this._hasCutOrCopiedTimeoutId = window.setTimeout(() => {
this._hasCutOrCopiedTimeoutId = null;
}, this.CUT_OR_COPIED_TIMEOUT_MS);
};
AppTextSelectionDialogGlobalStates.prototype.cancelHasCutOrCopiedTimer =
function() {
window.clearTimeout(this._hasCutOrCopiedTimeoutId);
this._hasCutOrCopiedTimeoutId = null;
};
AppTextSelectionDialogGlobalStates.prototype.resetAllStates =
function() {
this._hasCutOrCopiedTimeoutId = null;
this._isPrefOn = true;
this.appTSDs.clear();
};
AppTextSelectionDialogGlobalStates.prototype.onObservePrefChanged =
function (value) {
this._isPrefOn = value;
this.appTSDs.forEach(appTSD => {
this._isPrefOn ? appTSD.start() : appTSD.stop();
});
};
// Text selection dialog per AppWindow.
var AppTextSelectionDialog = function (app) {
this.app = app;
this.containerElement = (app && app.element) ? app.element :
document.getElementById('text-selection-dialog-root');
this.instanceID = _id++;
this.event = null;
this._enabled = false;
this._injected = false;
this._isCommandSendable = false;
this._transitionState = 'closed';
this._transitionStateOnPressCaret = null;
this.textualmenuDetail = null;
this.globalStates = _globalStates =
_globalStates || new AppTextSelectionDialogGlobalStates();
this.globalStates.appTSDs.add(this);
};
AppTextSelectionDialog.prototype = Object.create(window.BaseUI.prototype);
AppTextSelectionDialog.prototype.TEXTDIALOG_HEIGHT = 46;
AppTextSelectionDialog.prototype.TEXTDIALOG_WIDTH = 48;
// Distance between selected area and the bottom of menu when menu show on
// the top of selected area.
// By UI spec, 12px from the top of selected area to utility menu.
AppTextSelectionDialog.prototype.DISTANCE_FROM_MENUBOTTOM_TO_SELECTEDAREA =
12;
// Distance between selected area and the top of menu when menu show on
// the bottom of selected area.
// Caret height is 36px which is controlled by gecko (libpref/init/all.js).
// Note that 36px includes 28px for caret and 8px for space underneath caret.
// So 28px plus 12px which defined in UI spec, we get 40px from the bottom
// of selected area to utility menu.
AppTextSelectionDialog.prototype.DISTANCE_FROM_SELECTEDAREA_TO_MENUTOP = 40;
// Minimum distance between bubble and boundary.
AppTextSelectionDialog.prototype.DISTANCE_FROM_BOUNDARY = 5;
AppTextSelectionDialog.prototype.ID_NAME = 'AppTextSelectionDialog';
AppTextSelectionDialog.prototype.ELEMENT_PREFIX = 'textselection-dialog-';
// Overwrite _unregisterEvents to remove this AppTextSelectionDialog
// from global states when destroying this app.
AppTextSelectionDialog.prototype._unregisterEvents =
function tsd__unregisterEvents() {
this.globalStates.appTSDs.delete(this);
};
AppTextSelectionDialog.prototype.start = function tsd_start() {
if (this._enabled) {
return;
}
this._enabled = true;
if (this.app && this.app.element) {
this.app.element.addEventListener('mozbrowsercaretstatechanged', this);
} else {
window.addEventListener('mozChromeEvent', this);
}
};
AppTextSelectionDialog.prototype.stop = function tsd_stop() {
if (!this._enabled) {
return;
}
this._enabled = false;
if (this.app && this.app.element) {
this.app.element.removeEventListener('mozbrowsercaretstatechanged', this);
} else {
window.removeEventListener('mozChromeEvent', this);
}
};
AppTextSelectionDialog.prototype.debug = function tsd_debug(msg) {
if (DEBUG || this._DEBUG) {
console.log(this.ID_NAME + '(' + this.CLASS_NAME + this.instanceID +
'): ' + msg);
}
};
AppTextSelectionDialog.prototype.handleEvent = function tsd_handleEvent(evt) {
switch (evt.type) {
case 'mozbrowsercaretstatechanged':
evt.preventDefault();
evt.stopPropagation();
this._handleCaretStateChanged(evt.detail);
break;
case 'mozChromeEvent':
switch (evt.detail.type) {
case 'caretstatechanged':
evt.preventDefault();
evt.stopPropagation();
this._handleCaretStateChanged(evt.detail.detail);
break;
}
}
};
AppTextSelectionDialog.prototype._handleCaretStateChanged =
function tsd__handleCaretStateChanged(detail) {
if (!detail) {
return;
}
this.debug('on receive caret state change event');
this.debug(JSON.stringify(detail));
if (detail.reason === 'visibilitychange' && !detail.caretVisible) {
this.hide();
return;
}
if (detail.reason === 'presscaret') {
this._transitionStateOnPressCaret = this._transitionState;
this.hide();
return;
}
if (!detail.selectionVisible) {
this.hide();
return;
}
if (detail.collapsed) {
this._onCollapsedMode(detail);
} else {
this._onSelectionMode(detail);
}
};
AppTextSelectionDialog.prototype._onCollapsedMode =
function tsd__onCollapsedMode(detail) {
var showDialog = false;
switch (detail.reason) {
case 'taponcaret':
case 'longpressonemptycontent':
// Always allow, do nothing here.
showDialog = true;
break;
case 'updateposition':
// Only allow when something had been cut or copied.
if (this.globalStates.hasCutOrCopied()) {
showDialog = true;
}
break;
case 'releasecaret':
// Show the dialog if it was shown when pressing the caret.
if (this._transitionStateOnPressCaret === 'opened') {
showDialog = true;
}
this._transitionStateOnPressCaret = null;
break;
default:
// Not allow
break;
}
if (showDialog) {
detail.commands.canCut = false;
detail.commands.canCopy = false;
detail.commands.canSelectAll = false;
this.show(detail);
} else {
this.hide();
}
};
AppTextSelectionDialog.prototype._onSelectionMode =
function tsd__onSelectionMode(detail) {
// make sure cut command option is only shown on editable element
detail.commands.canCut = detail.commands.canCut &&
detail.selectionEditable;
this.show(detail);
};
AppTextSelectionDialog.prototype._fetchElements =
function tsd__fetchElements() {
this.element = document.getElementById(this.CLASS_NAME + this.instanceID);
this.elements = {};
var toCamelCase = function toCamelCase(str) {
return str.replace(/\-(.)/g, function replacer(str, p1) {
return p1.toUpperCase();
});
};
this.elementClasses = ['copy', 'cut', 'paste', 'selectall'];
// Loop and add element with camel style name to Modal Dialog attribute.
this.elementClasses.forEach(function createElementRef(name) {
this.elements[toCamelCase(name)] =
this.element.querySelector('.' + this.ELEMENT_PREFIX + name);
}, this);
};
AppTextSelectionDialog.prototype._registerEvents =
function tsd__registerEvents() {
var elements = this.elements;
for (var ele in elements) {
elements[ele].addEventListener('mousedown',
this._elementEventHandler.bind(this));
// We should not send command to gecko if user move their finger out of
// the original button.
elements[ele].addEventListener('mouseout',
this._elementEventHandler.bind(this));
elements[ele].addEventListener('click',
this._elementEventHandler.bind(this));
}
this.element.addEventListener('transitionend',
this._elementEventHandler.bind(this));
};
AppTextSelectionDialog.prototype._elementEventHandler =
function tsd__elementEventHandler(evt) {
switch (evt.type) {
case 'mousedown':
this._isCommandSendable = true;
evt.preventDefault();
break;
case 'transitionend':
if (this._transitionState === 'closing') {
this._changeTransitionState('closed');
}
break;
case 'click':
this[evt.target.dataset.action + 'Handler'] &&
this[evt.target.dataset.action + 'Handler'](evt);
break;
case 'mouseout':
this._isCommandSendable = false;
break;
}
};
AppTextSelectionDialog.prototype._changeTransitionState =
function tsd__changeTransitionState(state) {
if (!this.element) {
return;
}
switch (state) {
case 'opened':
this.element.classList.add('active');
this.element.classList.add('visible');
break;
case 'closing':
this.element.classList.remove('visible');
break;
case 'closed':
this.element.classList.remove('active');
break;
}
this._transitionState = state;
};
AppTextSelectionDialog.prototype._doCommand =
function tsd_doCommand(evt, cmd, closeDialog) {
if (!this._isCommandSendable) {
return;
}
if (this.app && this.app.element) {
if (this.textualmenuDetail) {
this.textualmenuDetail.sendDoCommandMsg(cmd);
}
} else {
var props = {
detail: {
type: 'copypaste-do-command',
cmd: cmd
}
};
this.debug(JSON.stringify(props));
window.dispatchEvent(
new CustomEvent('mozContentEvent', props));
}
if (closeDialog) {
this.close();
}
evt.preventDefault();
evt.stopPropagation();
};
AppTextSelectionDialog.prototype.copyHandler =
function tsd_copyHandler(evt) {
this._doCommand(evt, 'copy', true);
this.globalStates.launchHasCutOrCopiedTimer();
};
AppTextSelectionDialog.prototype.cutHandler =
function tsd_cutHandler(evt) {
this._doCommand(evt, 'cut', true);
this.globalStates.launchHasCutOrCopiedTimer();
};
AppTextSelectionDialog.prototype.pasteHandler =
function tsd_pasteHandler(evt) {
this._doCommand(evt, 'paste', true);
this.globalStates.cancelHasCutOrCopiedTimer();
};
AppTextSelectionDialog.prototype.selectallHandler =
function tsd_selectallHandler(evt) {
this._doCommand(evt, 'selectall', false);
};
AppTextSelectionDialog.prototype.view = function tsd_view() {
var id = this.CLASS_NAME + this.instanceID;
var temp = `<div class="textselection-dialog" id="${id}">
<div data-action="selectall"
class="textselection-dialog-selectall"></div>
<div data-action="cut" class="textselection-dialog-cut"></div>
<div data-action="copy" class="textselection-dialog-copy">
</div>
<div data-action="paste" class="textselection-dialog-paste">
</div>
</div>`;
return temp;
};
AppTextSelectionDialog.prototype.show = function tsd_show(detail) {
var numOfSelectOptions = 0;
var options = [ 'Paste', 'Copy', 'Cut', 'SelectAll' ];
// Check this._injected here to make sure this.elements is initialized.
if (!this._injected) {
this.render();
this._injected = true;
}
// Based on UI spec, we should have dividers ONLY between each select option
// So, we use css to put divider in pseudo element and set the last visible
// option without it.
var lastVisibleOption;
options.forEach(function(option) {
if (detail.commands['can' + option]) {
numOfSelectOptions++;
lastVisibleOption = this.elements[option.toLowerCase()];
lastVisibleOption.classList.remove('hidden', 'last-option');
} else {
this.elements[option.toLowerCase()].classList.add('hidden');
}
}, this);
this.numOfSelectOptions = numOfSelectOptions;
this.textualmenuDetail = detail;
// Add last-option class to the last item of options array;
if (lastVisibleOption) {
lastVisibleOption.classList.add('last-option');
}
this.updateDialogPosition();
};
AppTextSelectionDialog.prototype.updateDialogPosition =
function tsd_updateDialogPosition() {
var pos = this.calculateDialogPostion();
this.debug(JSON.stringify(pos));
this.element.style.top = pos.top + 'px';
this.element.style.left = pos.left + 'px';
this.element.style.height = this.TEXTDIALOG_HEIGHT + 'px';
this._changeTransitionState('opened');
};
AppTextSelectionDialog.prototype.calculateDialogPostion =
function tsd_calculateDialogPostion() {
var numOfSelectOptions = this.numOfSelectOptions;
var detail = this.textualmenuDetail;
var frameHeight = Service.query('LayoutManager.height') ||
window.innerHeight;
var frameWidth = Service.query('LayoutManager.width') ||
window.innerWidth;
var selectOptionWidth = this.TEXTDIALOG_WIDTH;
var selectOptionHeight = this.TEXTDIALOG_HEIGHT;
var selectDialogTop = (detail.rect.top) *
detail.zoomFactor;
var selectDialogBottom =
(detail.rect.bottom) * detail.zoomFactor;
var selectDialogLeft = (detail.rect.left) *
detail.zoomFactor;
var selectDialogRight =
(detail.rect.right) * detail.zoomFactor;
var distanceFromBottom = this.DISTANCE_FROM_SELECTEDAREA_TO_MENUTOP;
var distanceFromTop = this.DISTANCE_FROM_MENUBOTTOM_TO_SELECTEDAREA;
var posTop = selectDialogTop - selectOptionHeight - distanceFromTop;
// Dialog position align to the center of selected area.
var posLeft = (selectDialogLeft + selectDialogRight -
numOfSelectOptions * selectOptionWidth)/ 2;
// Put dialog under selected area if it overlap statusbar.
if (posTop < 0) {
posTop = selectDialogBottom + distanceFromBottom;
}
var offset = 0;
if (this.app && this.app.appChrome) {
offset = this.app.appChrome.isMaximized() ?
this.app.appChrome.height -
this.app.appChrome.scrollable.scrollTop :
Service.query('Statusbar.height');
}
posTop += offset;
// Put dialog in the center of selected area if it overlap keyboard.
if (posTop >= (frameHeight - selectOptionHeight)) {
posTop = (((selectDialogTop >= 0) ? selectDialogTop : 0) +
((selectDialogBottom >= frameHeight) ? frameHeight :
selectDialogBottom) - selectOptionHeight) / 2;
}
if (posLeft < this.DISTANCE_FROM_BOUNDARY) {
posLeft = this.DISTANCE_FROM_BOUNDARY;
}
if ((posLeft + numOfSelectOptions * selectOptionWidth +
this.DISTANCE_FROM_BOUNDARY) > frameWidth) {
posLeft = frameWidth - numOfSelectOptions * selectOptionWidth -
this.DISTANCE_FROM_BOUNDARY;
}
return {
top: posTop,
left: posLeft
};
};
AppTextSelectionDialog.prototype.hide = function tsd_hide() {
if (!this.element) {
return;
}
this._changeTransitionState('closing');
};
AppTextSelectionDialog.prototype.close = function tsd_close() {
if (this._transitionState !== 'opened') {
return;
}
this.hide();
this.element.blur();
this.textualmenuDetail = null;
};
exports.AppTextSelectionDialog = AppTextSelectionDialog;
}(window));

View file

@ -0,0 +1,418 @@
/* 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 = 1000;
AppTransitionController.prototype.CLOSING_TRANSITION_TIMEOUT = 1000;
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() {
var slow = Service.query('slowTransition');
var timeout = slow ? this.SLOW_TRANSITION_TIMEOUT :
this.CLOSING_TRANSITION_TIMEOUT;
this.app.debug('timer to ensure closed does occur.');
this._closingTimeout = window.setTimeout(() => {
if (!this.app) {
return;
}
this.app.broadcast('closingtimeout');
}, 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() {
var slow = Service.query('slowTransition');
var timeout = slow ? this.SLOW_TRANSITION_TIMEOUT :
this.OPENING_TRANSITION_TIMEOUT;
this.app.debug('timer to ensure opened does occur.');
this._openingTimeout = window.setTimeout(() => {
this.app && this.app.broadcast('openingtimeout');
}, 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;
}
/* 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.switchTransitionState('closing');
};
AppTransitionController.prototype.handle_closed =
function atc_handle_closed() {
if (!this.app || !this.app.element) {
return;
}
this.resetTransition();
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();
// 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');
// If we set the orientation in handle_opening we want to let the resize
// happen before requesting foreground.
if (this.currentAnimation == 'immediate') {
setTimeout(this.app.requestForeground.bind(this.app));
} else {
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');
var stayBackground = this.app.config &&
this.app.config.stayBackground;
return (this.app.CLASS_NAME !== 'SearchWindow' &&
this._transitionState == 'opened' &&
Service.query('getTopMostWindow') === this.app &&
!stayBackground &&
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.app.publish('opening');
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.app.publish('closing');
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', 'from-new-card'];
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();
if (this._transitionState == 'closing') {
this.changeTransitionState('complete', evt.type);
break;
}
// 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.
// In case of the browser, we don't want to wait for the page
// being fully loaded to trigger the 'opened' event
if (!this.app.isBrowser() && 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();
}
break;
}
this.app.debug(evt.animationName + ' has been ENDED!');
this.changeTransitionState('complete', evt.type);
break;
}
};
exports.AppTransitionController = AppTransitionController;
}(window));

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,298 @@
'use strict';
/* global applications, BrowserConfigHelper, AppWindow, Service */
/* jshint nonew: false */
(function(exports) {
/**
* AppWindowFactory handle the launch request from gecko and
* wrap the config with properer parameters.
*
* If gecko is asking us to open a webapp,
* AppWindowFactory would do the instantiation and let
* AppWindowManager to do the following app opening control via
* event <code>launchapp</code>.
*
* If gecko is asking us to open an inline activity page,
* AppWindowFactory would wrap the configuration and sent it to
* AppWindowFactory for it to do instantiation via event
* <code>launchactivity</code>.
*
* ![app and activity launch flow](http://i.imgur.com/ZyMcgft.png)
*
* @class AppWindowFactory
*/
function AppWindowFactory() {
this.preHandleEvent = this.preHandleEvent.bind(this);
}
AppWindowFactory.prototype = {
name: 'AppWindowFactory',
/**
* Indicate whether this class is started or not.
* @access private
* @type {Boolean}
* @memberof AppWindowFactory.prototype
*/
_started: false,
/**
* Register all event handlers.
* @memberof AppWindowFactory.prototype
*/
start: function awf_start() {
if (this._started) {
return;
}
this._started = true;
window.addEventListener('webapps-launch', this.preHandleEvent);
window.addEventListener('webapps-close', this.preHandleEvent);
window.addEventListener('open-app', this.preHandleEvent);
window.addEventListener('openwindow', this.preHandleEvent);
window.addEventListener('appopenwindow', this.preHandleEvent);
window.addEventListener('applicationready', (function appReady(e) {
window.removeEventListener('applicationready', appReady);
this._handlePendingEvents();
}).bind(this));
Service.registerState('isLaunchingWindow', this);
},
/**
* Unregister all event handlers.
* @memberof AppWindowFactory.prototype
*/
stop: function awf_stop() {
if (!this._started) {
return;
}
this._started = false;
window.removeEventListener('webapps-launch', this.preHandleEvent);
window.removeEventListener('webapps-close', this.preHandleEvent);
window.removeEventListener('open-app', this.preHandleEvent);
window.removeEventListener('openwindow', this.preHandleEvent);
window.removeEventListener('appopenwindow', this.preHandleEvent);
},
/**
* Queue events until AppWindowFactory is ready to handle them.
*/
_queueEvents: [],
_queuePendingEvent: function(evt) {
this._queueEvents.push(evt);
},
_handlePendingEvents: function() {
this._queueEvents.forEach((function(evt) {
this.handleEvent(evt);
}).bind(this));
this._queueEvents = [];
},
preHandleEvent: function(evt) {
if (applications.ready) {
this.handleEvent(evt);
} else {
this._queuePendingEvent(evt);
}
},
handleEvent: function awf_handleEvent(evt) {
var detail = evt.detail;
if (evt.type === '_opened' || evt.type === '_terminated') {
if (this._launchingApp === detail) {
this.forgetLastLaunchingWindow();
}
return;
}
if (!detail.url && !detail.manifestURL) {
return;
}
var config = new BrowserConfigHelper(detail);
config.evtType = evt.type;
switch (evt.type) {
case 'openwindow':
case 'appopenwindow':
case 'webapps-launch':
config.timestamp = detail.timestamp;
// TODO: Look up current opened window list,
// and then create a new instance here.
this.launch(config);
break;
case 'open-app':
// System Message Handler API is asking us to open the specific URL
// that handles the pending system message.
// We will launch it in background if it's not handling an activity.
config.isSystemMessage = true;
if (detail.isActivity) {
config.isActivity = true;
if (detail.target.disposition &&
detail.target.disposition == 'inline') {
config.inline = true;
}
}
config.changeURL = !detail.onlyShowApp;
config.stayBackground = !detail.showApp;
if (detail.extra && detail.extra.manifestURL) {
config.parentApp = detail.extra.manifestURL;
}
// TODO: Create activity window instance
// or background app window instance for system message here.
this.launch(config);
break;
case 'webapps-close':
this.publish('killapp', config);
break;
}
},
/**
* Browser Configuration
* @typedef {Object} BrowserConfig
* @property {String} origin the same as appURL.
* @property {String} manifestURL the same as manifestURL.
* @property {Object} manifest the parsed manifest object.
* If the app is not an entry point app,
* the manifest would be the reference of application
* manifest stored in Applications module. But if the app
* is an entry point app, we will do deep clone to
* generate a new object and replace the properties of
* entry point to proper position.
* @property {String} name the name of the app, retrieved from manifest.
* @property {Boolean} oop Indicate it's running out of process or in
* process.
*/
/**
* Launch an app window.
* @param {BrowserConfig} config Generated by BrowserConfigHelper.
* @memberof AppWindowFactory.prototype
*/
launch: function awf_launch(config) {
if (config.url === window.location.href) {
return;
}
if (config.isActivity && config.inline) {
this.publish('launchactivity', config);
return;
}
// The rocketbar currently handles the management of normal search app
// launches. Requests for the 'newtab' page will continue to filter
// through and publish the launchapp event.
if (this._isSearch(config)) {
if (config.url.indexOf('newtab.html') === -1) {
return;
}
// In case of the Browser, oppen an already open
// instance instead of creating a new one
var openInstance = this._findBrowserInstance(config);
if (openInstance) {
openInstance.requestOpen();
return;
}
}
var app = Service.query('getApp', config.origin, config.manifestURL);
if (app) {
if (config.evtType == 'appopenwindow') {
app.browser.element.src = config.url;
}
app.reviveBrowser();
// Always relaunch background app locally
this.publish('launchapp', config);
} else {
var launchApp = () => {
// homescreenWindowManager already listens webapps-launch and
// open-app. We don't need to check if the launched app is homescreen.
this.forgetLastLaunchingWindow();
this.trackLauchingWindow(config);
this.publish('launchapp', config);
};
if (Service.query('MultiScreenController.enabled')) {
Service.request('chooseDisplay', config).catch(launchApp);
} else {
launchApp();
}
}
},
trackLauchingWindow: function(config) {
var app = new AppWindow(config);
if (config.stayBackground) {
return;
}
this._launchingApp = app;
this._launchingApp.element.addEventListener('_opened', this);
this._launchingApp.element.addEventListener('_terminated', this);
},
forgetLastLaunchingWindow: function() {
if (this._launchingApp && this._launchingApp.element) {
this._launchingApp.element.removeEventListener('_opened', this);
this._launchingApp.element.removeEventListener('_terminated', this);
}
this._launchingApp = null;
},
isLaunchingWindow: function() {
return !!this._launchingApp;
},
/**
* Returns an appWindow instace that matches
* the criteria given by the config
* @param {Object} detail The data passed when initializing the event.
* @memberof AppWindowFactory.prototype
*/
_findBrowserInstance: function(config) {
// Special case for the browser. Openning the last instance
// of any unpinned window, if it exists.
var activeApp = Service.query('AppWindowManager.getActiveApp');
if (!this._isSearch(config) || activeApp.isBrowser()) {
return;
}
var unpinned = Service.query('AppWindowManager.getUnpinnedWindows');
if (unpinned.length) {
return unpinned.sort(function(app1, app2) {
return app2.launchTime - app1.launchTime;
})[0];
}
},
/**
* Determines if the current app to be opened
* is the Browser app
* @param {Object} detail The data passed when initializing the event.
* @memberof AppWindowFactory.prototype
*/
_isSearch: function(config) {
return (config.manifest && config.manifest.role === 'search');
},
/**
* Publish a CustomEvent.
* @param {String} event The name of the event.
* @param {Object} detail The data passed when initializing the event.
* @memberof AppWindowFactory.prototype
*/
publish: function awf_publish(event, detail, scope) {
scope = scope || window;
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, true, false, detail);
scope.dispatchEvent(evt);
}
};
exports.AppWindowFactory = AppWindowFactory;
}(window));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
;'use strict';(function(exports){function Applications(){}
Applications.prototype={installedApps:{},ready:false,stop:function a_stop(){this.ready=false;this.installedApps={};navigator.mozApps.mgmt.getAll().onsuccess=null;navigator.mozApps.mgmt.oninstall=null;navigator.mozApps.mgmt.onuninstall=null;},waitForReady:function(){return new Promise((resolve)=>{if(this.ready){resolve();}else{this.waitingResolve=resolve;}});},start:function a_start(){var self=this;var apps=navigator.mozApps;var getAllApps=function getAllApps(){navigator.mozApps.mgmt.getAll().onsuccess=function mozAppGotAll(evt){var apps=evt.target.result;apps.forEach(function(app){self.installedApps[app.manifestURL]=app;});self.ready=true;self.fireApplicationReadyEvent();self.waitingResolve&&self.waitingResolve();};};if(window.sessionStorage.getItem('webapps-registry-ready')){getAllApps();}else{window.addEventListener('mozChromeEvent',function mozAppReady(event){if(event.detail.type!='webapps-registry-ready'){return;}
window.sessionStorage.setItem('webapps-registry-ready','yes');window.removeEventListener('mozChromeEvent',mozAppReady);getAllApps();});}
apps.mgmt.oninstall=function a_install(evt){var newapp=evt.application;self.installedApps[newapp.manifestURL]=newapp;self.fireApplicationInstallEvent(newapp);};apps.mgmt.onuninstall=function a_uninstall(evt){var deletedapp=evt.application;delete self.installedApps[deletedapp.manifestURL];self.fireApplicationUninstallEvent(deletedapp);};apps.mgmt.onenabledstatechange=function a_enabledstatechange(evt){var app=evt.application;if(app.enabled){self.fireApplicationEnabledEvent(app);}else{self.fireApplicationDisabledEvent(app);}};},getByManifestURL:function a_getByManifestURL(manifestURL){if(manifestURL in this.installedApps){return this.installedApps[manifestURL];}
return null;},fireApplicationReadyEvent:function a_fireAppReadyEvent(){var evt=new CustomEvent('applicationready',{bubbles:true,cancelable:false,detail:{applications:this.installedApps}});window.dispatchEvent(evt);},fireApplicationInstallEvent:function a_fireApplicationInstallEvent(app){var evt=new CustomEvent('applicationinstall',{bubbles:true,cancelable:false,detail:{application:app}});window.dispatchEvent(evt);},fireApplicationUninstallEvent:function a_fireApplicationUninstallEvent(app){var evt=new CustomEvent('applicationuninstall',{bubbles:true,cancelable:false,detail:{application:app}});window.dispatchEvent(evt);},fireApplicationEnabledEvent:function a_fireApplicationEnabledEvent(app){var evt=new CustomEvent('applicationenabled',{bubbles:true,cancelable:false,detail:{application:app}});window.dispatchEvent(evt);},fireApplicationDisabledEvent:function a_fireApplicationDisabledEvent(app){var evt=new CustomEvent('applicationdisabled',{bubbles:true,cancelable:false,detail:{application:app}});window.dispatchEvent(evt);}};exports.Applications=Applications;}(window));window.applications=new Applications();applications.start();

View file

@ -0,0 +1,207 @@
/* global Service */
'use strict';
(function(exports) {
var toasterEvents = ['init', 'open', 'complete', 'terminate', 'close'];
var toasterStateTable = {
'uninit': ['closed', null, null, null, null],
'closed': [null, 'opening', null, 'uninit', null],
'opening': [null, null, 'opened', 'uninit', null],
'opened': [null, null, null, 'uninit', 'closing'],
'closing': [null, 'opening', 'closed', 'uninit', null]
};
/**
* AttentionToaster is a sub component of AttentionWindow
* to help the attention window instance to play like a notification toaster
* and control the toaster's transition state.
* @param {AppWindow} app The app window installing this sub component.
*/
var AttentionToaster = function(app) {
this.app = app;
};
AttentionToaster.prototype = {
_currentToasterState: 'uninit',
TOASTER_TIMEOUT: 5000,
RESPAWN_TIMEOUT: 45000,
start: function() {
this.app.element.addEventListener('_closed', this);
this.app.element.addEventListener('_requestopen', this);
this.app.element.addEventListener('transitionend', this);
this.app.element.addEventListener('_secure-appclosed', this);
this.app.element.addEventListener('_secure-appopened', this);
this.app.element.addEventListener('_lockscreen-appclosed', this);
this.app.element.addEventListener('_lockscreen-appopened', this);
},
stop: function() {
this.app.element.removeEventListener('_closed', this);
this.app.element.removeEventListener('_requestopen', this);
this.app.element.removeEventListener('transitionend', this);
this.app.element.removeEventListener('_secure-appclosed', this);
this.app.element.removeEventListener('_secure-appopened', this);
this.app.element.removeEventListener('_lockscreen-appclosed', this);
this.app.element.removeEventListener('_lockscreen-appopened', this);
},
shouldProcess: function() {
return this.app && this.app.element && !this.app.isHidden() &&
!this.app.isActive() && !this.app.isDead();
},
handleEvent: function(evt) {
if (!this.shouldProcess()) {
this.app.debug('should not process');
return;
}
switch (evt.type) {
case '_lockscreen-appclosed':
case '_secure-appopened':
this.processStateChange('close', evt.type);
break;
case '_requestopen':
this.processStateChange('terminate', evt.type);
break;
case 'transitionend':
if (evt.target !== this.app.element) {
return;
}
this.processStateChange('complete', evt.type);
break;
case '_closed':
window.requestAnimationFrame(() => {
this.processStateChange('init', evt.type);
});
break;
case '_lockscreen-appopened':
case '_secure-appclosed':
this.processStateChange('open', evt.type);
break;
}
},
processStateChange: function(event, reason) {
if (!this.shouldProcess()) {
this.app.debug('should not process ' + event);
return;
}
var eventIndex = toasterEvents.indexOf(event);
var nextState =
toasterStateTable[this._currentToasterState][eventIndex];
this.app.debug(this._currentToasterState + ' -> ' + nextState);
if (!nextState) {
return;
}
this.app.debug('[toaster] ' +
this._currentToasterState + '->' + nextState +
':' + event + ',' + reason);
this._currentToasterState = nextState;
this.app.element.setAttribute('toaster-transition-state',
this._currentToasterState);
if (typeof(this['_enter_' + nextState]) == 'function') {
this['_enter_' + nextState](event);
}
},
_enter_closing: function() {
if (!this.app || !this.app.element ||
(Service && Service.query('locked') &&
!Service.query('getTopMostWindow').isFullScreen())) {
return;
}
this.app.element.classList.remove('displayed');
},
_enter_closed: function(evt) {
if (!this.app || !this.app.element) {
return;
}
if (evt == 'init') {
this.becomeToaster();
this.app.tryWaitForFullRepaint(function() {
this.processStateChange('open', 'repainted');
}.bind(this));
} else if (evt == 'complete') {
this.app && this.app.setVisible(false);
this.respawnTimer = window.setTimeout(function() {
this.processStateChange('open', 'respawn');
}.bind(this), this.RESPAWN_TIMEOUT);
}
},
_enter_opened: function() {
if (!this.app || !this.app.element) {
return;
}
this.app && this.app.setVisible(true);
this._toasterTimer = window.setTimeout(function() {
if (Service && Service.query('locked') &&
!Service.query('getTopMostWindow').isFullScreen()) {
return;
}
this.processStateChange('close', 'timeout');
}.bind(this), this.TOASTER_TIMEOUT);
},
_enter_opening: function() {
if (!this.app || !this.app.element) {
return;
}
window.clearTimeout(this.respawnTimer);
this.respawnTimer = null;
this.app.setVisible(true);
this.app.element.classList.add('displayed');
},
_enter_uninit: function() {
if (!this.app || !this.app.element) {
return;
}
if (this._toasterTimer) {
window.clearTimeout(this._toasterTimer);
this._toasterTimer = null;
}
this.recoverLayout();
},
recoverLayout: function() {
if (this.app && this.app.resized) {
this.app.element.classList.remove('displayed');
this.app.element.classList.remove('toaster-mode');
this.app.resized = false;
this.app.publish('recovering');
this.app._resize();
}
},
becomeToaster: function() {
if (!this.shouldProcess()) {
return;
}
// Override the resized height anyway
this.app.element.style.height = this.app.closedHeight + 'px';
this.app.element.classList.add('toaster-mode');
this.app.resized = true;
}
};
exports.AttentionToaster = AttentionToaster;
}(window));

View file

@ -0,0 +1,299 @@
/* global AppWindow, applications, ManifestHelper */
'use strict';
(function(exports) {
/**
* AttentionWindow is a special opened window with specific
* permission: 'attention'. It would show in front of any
* existing app windows to get users' attention.
*
* ##### Flow chart
* <a href="http://i.imgur.com/4O1Frs3.png" target="_blank">
* <img src="http://i.imgur.com/4O1Frs3.png"></img>
* </a>
*
* @example
* var attention = new AttentionWindow({
* url: 'app://clock.gaiamobile.org:8080/alarm.html',
* manifestURL: 'http://gallery.gaiamobile.org:8080/manifest.webapp',
* iframe: iframe
* });
*
* @class AttentionWindow
* @param {Object} config The configuration object of this attention.
* @extends AppWindow
*/
/**
* Fired when the attention window is created.
* @event AttentionWindow#attentioncreated
*/
/**
* Fired when the attention window is removed.
* @event AttentionWindow#attentionterminated
*/
/**
* Fired when the attention window is opening.
* @event AttentionWindow#attentionopening
*/
/**
* Fired when the attention window is opened.
* @event AttentionWindow#attentionopened
*/
/**
* Fired when the attention window is closing.
* @event AttentionWindow#attentionclosing
*/
/**
* Fired when the attention window is closed.
* @event AttentionWindow#attentionclosed
*/
/**
* Fired before the attention window will be rendered.
* @event AttentionWindow#attentionwillrender
*/
/**
* Fired when the attention window is rendered to the DOM tree.
* @event AttentionWindow#attentionrendered
*/
var AttentionWindow = function AttentionWindow(config) {
this.reConfig(config);
this.render();
if (this._DEBUG) {
AttentionWindow[this.instanceID] = this;
}
this.makeNotification();
this.publish('created');
};
AttentionWindow.prototype = Object.create(AppWindow.prototype);
AttentionWindow.prototype.constructor = AttentionWindow;
AttentionWindow.prototype.eventPrefix = 'attention';
AttentionWindow.prototype.CLASS_NAME = 'AttentionWindow';
AttentionWindow.prototype.CLASS_LIST = 'appWindow attentionWindow';
AttentionWindow.prototype.HIERARCHY_MANAGER = 'AttentionWindowManager';
/**
* Turn on this flag to dump debugging messages for all attention windows.
* @type {Boolean}
*/
AttentionWindow.prototype._DEBUG = false;
AttentionWindow.prototype.closedHeight = 40;
AttentionWindow.prototype.openAnimation = 'immediate';
AttentionWindow.prototype.closeAnimation = 'immediate';
AttentionWindow.prototype.isAttentionWindow = true;
AttentionWindow.prototype.view = function attw_view() {
this.debug('intance id: ' + this.instanceID);
return `<div class="${this.CLASS_LIST}" id="${this.instanceID}">
<div class="browser-container"></div>
</div>`;
};
AttentionWindow.SUB_COMPONENTS = {
'transitionController': 'AppTransitionController',
'modalDialog': 'AppModalDialog',
'authDialog': 'AppAuthenticationDialog',
'attentionToaster': 'AttentionToaster',
'stautsbar': 'AppStatusbar'
};
AttentionWindow.REGISTERED_EVENTS =
['mozbrowserclose', 'mozbrowsererror', 'mozbrowservisibilitychange',
'mozbrowserloadend', 'mozbrowserloadstart',
'_localized', 'click', '_willdestroy', '_languagechange'];
AttentionWindow.prototype.render = function attw_render() {
this.publish('willrender');
this.containerElement.insertAdjacentHTML('beforeend', this.view());
// the iframe is provided already.
this.browser = {
element: this.config.iframe
};
this.element = document.getElementById(this.instanceID);
this.browserContainer = this.element.querySelector('.browser-container');
this.browserContainer.insertBefore(this.browser.element, null);
this.frame = this.element;
this.iframe = this.browser.element;
this._registerEvents();
this.installSubComponents();
this.publish('rendered');
};
AttentionWindow.prototype._handle_click =
function attw__handle_click(evt) {
this.publish('will-become-active');
this.requestOpen();
};
AttentionWindow.prototype.ready = function attw_ready(callback) {
if (!this.element) {
return;
}
var self = this;
if (!this.loaded) {
this.element.addEventListener('_loaded', function onLoaded() {
self.element.removeEventListener('_loaded', onLoaded);
setTimeout(callback);
});
} else {
this.tryWaitForFullRepaint(function() {
setTimeout(callback);
});
}
};
// XXX: We may need to wait the underlying window,
// which may be attention window or app window to be painted,
// so keeping track of the request in order to handle
// |mozbrowserclose| events properly.
AttentionWindow.prototype.requestOpen = function() {
this.element.classList.remove('fake-notification');
this.element.classList.remove('notification-disappearing');
this._requestedOpen = true;
// XXX: A hack to reset height.
AppWindow.prototype.requestOpen.apply(this);
};
AttentionWindow.prototype.open = function(animation) {
this._requestedOpen = false;
AppWindow.prototype.open.apply(this);
};
/**
* Make a fake notification node inside Utility tray.
* XXX: We should make app to create this notification on their own.
* XXX: The problem is app is not able to 'launch attention window'
* in the click callback of the notification. The workaround
* might be using the same name to open the attention window again.
* And in child window factory we need to check if current attention
* window has the same name as the event detail to reopen it or kill it.
*/
AttentionWindow.prototype.makeNotification = function() {
var manifestURL = this.manifestURL;
if (this.notification || !manifestURL) {
return;
}
var manifest = applications.getByManifestURL(manifestURL).manifest;
this.manifest = this.config.manifest = manifest;
var iconSrc = manifestURL.replace(
'/manifest.webapp',
manifest.icons[Object.keys(manifest.icons)[0]]
);
// Let's create the fake notification.
var notification = document.createElement('div');
notification.id = 'notification-' + this.instanceID;
notification.classList.add('notification');
notification.classList.add('attention-notification');
var icon = document.createElement('img');
icon.src = iconSrc;
icon.classList.add('icon');
notification.appendChild(icon);
var message = document.createElement('div');
this.notificationTitle = document.createTextNode(manifest.name);
message.appendChild(this.notificationTitle);
message.classList.add('title-container');
notification.appendChild(message);
var tip = document.createElement('div');
tip.setAttribute('data-l10n-id', 'attentionScreen-tapToShow');
tip.classList.add('detail');
notification.appendChild(tip);
var container =
document.getElementById('attention-window-notifications-container');
container.insertBefore(notification, null);
// Attach an event listener to the fake notification so the
// attention screen is shown when the user tap on it.
notification.addEventListener('click',
function(evt) {
this._handle_click(evt);
}.bind(this));
this.notification = notification;
// Hide on creating.
this.notification.style.display = 'none';
this.translateNotification();
};
AttentionWindow.prototype.translateNotification = function() {
if (!this.notification) {
return;
}
var manifest = this.manifest;
var title = this.notificationTitle;
navigator.mozL10n.once(function() {
var helper = new ManifestHelper(manifest);
title.textContent = helper.name;
});
};
AttentionWindow.prototype.show = function() {
if (this.notification) {
this.translateNotification();
this.notification.style.display = 'block';
}
// Resize the window to accommodate the presence or absence of the software
// home button.
this._resize();
// Unset width because we don't need
this.element.style.width = '';
AppWindow.prototype.show.call(this);
};
AttentionWindow.prototype.hide = function() {
if (this.notification) {
this.notification.style.display = 'none';
}
AppWindow.prototype.hide.call(this);
};
AttentionWindow.prototype._handle__willdestroy = function() {
if (this.notification) {
this.notification.parentNode.removeChild(this.notification);
this.notification = null;
}
};
AttentionWindow.prototype._handle__languagechange = function() {
this.translateNotification();
};
AttentionWindow.prototype.promote = function() {
this.element && this.element.classList.add('top-most');
};
AttentionWindow.prototype.demote = function() {
this.element && this.element.classList.remove('top-most');
};
/**
* AttentionWindow's default container is '#windows'.
* However, we could dynamically change this in layout manager
* after it recieves the attentionwillrender event.
*/
AttentionWindow.prototype.containerElement =
document.getElementById('windows');
exports.AttentionWindow = AttentionWindow;
}(window));

View file

@ -0,0 +1,312 @@
/* globals Service, BaseModule,
DialerAgent */
'use strict';
(function(exports) {
var AttentionWindowManager = function() {};
AttentionWindowManager.prototype = {
DEBUG: false,
TRACE: false,
name: 'AttentionWindowManager',
_openedInstances: null,
EVENT_PREFIX: 'attentionwindowmanager',
publish: function vm_publish(eventName, detail) {
this.debug('publishing: ', eventName);
var evt = new CustomEvent(this.EVENT_PREFIX + eventName,
{ detail: detail });
window.dispatchEvent(evt);
},
debug: function aw_debug() {
if (this.DEBUG) {
console.log('[' + this.name + ']' +
'[' + Service.currentTime() + '] ' +
Array.slice(arguments).concat());
if (this.TRACE) {
console.trace();
}
}
},
isActive: function() {
return this.hasActiveWindow();
},
hasActiveWindow: function attwm_hasActiveWindow() {
return (this._openedInstances.size !== 0);
},
getActiveWindow: function() {
return this.getTopMostWindow();
},
getTopMostWindow: function attwm_hasActiveWindow() {
return this._topMostWindow;
},
respondToHierarchyEvent: function(evt) {
if (this['_handle_' + evt.type]) {
return this['_handle_' + evt.type](evt);
} else {
return true;
}
},
_handle_home: function(evt) {
if (!this.hasActiveWindow()) {
return true;
}
this._topMostWindow = null;
var nextApp = Service.query('getHomescreen');
if (Service.query('locked')) {
this.closeAllAttentionWindows();
} else if (nextApp && !nextApp.isDead()) {
nextApp.ready(this.closeAllAttentionWindows.bind(this));
} else {
this.closeAllAttentionWindows();
}
return true;
},
'_handle_system-resize': function(evt) {
if (this._topMostWindow) {
var p = this._topMostWindow.resize();
if (typeof evt.detail.waitUntil === 'function') {
evt.detail.waitUntil(p);
}
return false;
}
return true;
},
_handle_holdhome: function() {
if (this.isActive()) {
this._topMostWindow = null;
this.closeAllAttentionWindows();
}
return true;
},
/**
* Return current alive attention window instances in a map.
* @return {Map} The attention window map
*/
getInstances: function() {
return this._instances;
},
screen: document.getElementById('screen'),
start: function attwm_start() {
this._instances = [];
this._openedInstances = new Map();
window.addEventListener('attentioncreated', this);
window.addEventListener('attentionterminated', this);
window.addEventListener('attentionshown', this);
window.addEventListener('attentionopening', this);
window.addEventListener('attentionopened', this);
window.addEventListener('attentionclosed', this);
window.addEventListener('attentionclosing', this);
window.addEventListener('attentionrequestopen', this);
window.addEventListener('attentionrequestclose', this);
window.addEventListener('emergencyalert', this);
window.addEventListener('launchapp', this);
window.addEventListener('lockscreen-appclosed', this);
window.addEventListener('lockscreen-appopened', this);
window.addEventListener('secure-appclosed', this);
window.addEventListener('secure-appopened', this);
window.addEventListener('rocketbar-overlayopened', this);
window.addEventListener('languagechange', this);
window.addEventListener('updatepromptshown', this);
Service.request('registerHierarchy', this);
if (navigator.mozTelephony) {
BaseModule.lazyLoad(['DialerAgent']).then(() => {
// DialerAgent will create callscreen window
// so it is 'CallscreenWindowLauncher' exactly.
this.dialerAgent = new DialerAgent();
this.dialerAgent.start();
});
}
},
stop: function attwm_stop() {
this._instances = null;
this._openedInstances = null;
window.removeEventListener('attentioncreated', this);
window.removeEventListener('attentionterminated', this);
window.removeEventListener('attentionshow', this);
window.removeEventListener('attentionopening', this);
window.removeEventListener('attentionopened', this);
window.removeEventListener('attentionclosed', this);
window.removeEventListener('attentionclosing', this);
window.removeEventListener('attentionrequestopen', this);
window.removeEventListener('attentionrequestclose', this);
window.removeEventListener('emergencyalert', this);
window.removeEventListener('launchapp', this);
window.removeEventListener('lockscreen-appclosed', this);
window.removeEventListener('lockscreen-appopened', this);
window.removeEventListener('secure-appclosed', this);
window.removeEventListener('secure-appopened', this);
window.removeEventListener('rocketbar-overlayopened', this);
window.removeEventListener('languagechange', this);
window.removeEventListener('updatepromptshown', this);
Service.request('unregisterHierarchy', this);
},
handleEvent: function attwm_handleEvent(evt) {
this.debug('handling ' + evt.type);
var attention = evt.detail;
switch (evt.type) {
case 'attentioncreated':
this._instances.push(attention);
break;
case 'attentionopening':
case 'attentionopened':
this._openedInstances.set(attention, attention);
this.updateAttentionIndicator();
this.publish('-activated');
this.updateClassState();
break;
case 'attentionrequestclose':
this._openedInstances.delete(attention);
if (this._topMostWindow !== attention) {
attention.close();
break;
}
var candidate = null;
if (this._openedInstances.size === 0) {
this._topMostWindow = null;
candidate = Service.query('AppWindowManager.getActiveWindow');
} else {
this._openedInstances.forEach(function(instance) {
candidate = instance;
});
this._topMostWindow = candidate;
}
if (!candidate) {
attention.close();
break;
}
candidate.ready(function() {
attention.close();
});
break;
case 'attentionclosed':
this._openedInstances.delete(attention);
if (this._topMostWindow) {
this._topMostWindow.promote();
this._topMostWindow.setVisible(true);
}
attention.demote();
if (this._openedInstances.size === 0) {
this.publish('-deactivated');
}
this.updateAttentionIndicator();
this.updateClassState();
break;
case 'attentionrequestopen':
this._topMostWindow = attention;
attention.ready(function() {
if (document.mozFullScreen) {
document.mozCancelFullScreen();
}
this._openedInstances.forEach(function(opened) {
if (opened !== attention) {
opened.demote();
opened.setVisible(false);
}
});
attention.promote();
attention.setVisible(true);
attention.open();
}.bind(this));
break;
// For callscreen window.
case 'attentionshown':
if (this._instances.indexOf(attention) < 0) {
this._instances.push(attention);
}
break;
case 'attentionterminated':
var index = this._instances.indexOf(attention);
if (index >= 0) {
this._instances.splice(index, 1);
}
this._openedInstances.delete(attention);
this.updateAttentionIndicator();
// In case of a kill/crash we might not get the attentionclosed
// event, so updating the attention class if that's the case.
if (attention.isCrashed) {
this.updateClassState();
}
break;
case 'launchapp':
if (evt.detail && evt.detail.stayBackground) {
break;
} // jshint ignore:line
case 'emergencyalert':
case 'rocketbar-overlayopened':
case 'updatepromptshown':
this._topMostWindow = null;
this.closeAllAttentionWindows();
break;
case 'lockscreen-appclosed':
case 'lockscreen-appopened':
case 'secure-appopened':
case 'secure-appclosed':
case 'languagechange':
this._instances.forEach(function(instance) {
instance.broadcast(evt.type);
});
break;
}
},
/**
* Traverse the instances list to get the count of displayed window.
* @return {Number} The count of displayed window
*/
getShownWindowCount: function() {
var count = this._instances.length;
this._instances.forEach(function(attention) {
if (attention.isHidden()) {
count--;
}
});
return count;
},
updateAttentionIndicator: function() {
if (this._openedInstances.size == this.getShownWindowCount()) {
Service.request('makeAmbientIndicatorInactive');
} else {
Service.request('makeAmbientIndicatorActive');
}
},
updateClassState: function() {
// XXX We set a class to screen to allow overriding the screen.lock class.
// When we get rid of screen.lock in the future, this can go away safely.
if (this._openedInstances.size !== 0) {
this.screen.classList.add('attention');
} else {
this.screen.classList.remove('attention');
}
},
closeAllAttentionWindows: function() {
this._openedInstances.forEach(function(value) {
value.close();
});
}
};
exports.AttentionWindowManager = AttentionWindowManager;
}(window));

View file

@ -0,0 +1,255 @@
/* global BaseUI */
'use strict';
(function(exports) {
var FADE_IN_VOLUME = 1;
var FADE_OUT_VOLUME = 0.2;
var VIBRATION_DURATION = 1000;
/**
* AudioChannelController controls the audio channel to
* play or pause.
*
* There are two ways to create an AudioChannelController instance.
* One is for System app, and the other one is for other apps.
*
* Doing `new AudioChannelController('notification')`
* to create a controller for System app's notification audio channel.
*
* Doing `new AudioChannelController(musicApp, contentChannel)`
* to create a controller for Music app's content audio channel.
*
* @param {AppWindow|String} app
* If you want to create a controller for System app's audio channel,
* use a String.
* If you want to create a controller for other app's audio channel,
* use a AppWindow.
* @param {BrowserElementAudioChannel} channel
* An audio channel belongs to the app.
*/
var AudioChannelController = function(app, channel) {
this.app = app;
this.name = channel.name;
this._channel = channel;
channel.addEventListener('activestatechanged', this);
this._states = {
active: false,
playing: false,
fadingOut: false,
vibrating: false
};
this._policy = {};
this._generateID();
};
AudioChannelController.prototype = Object.create(BaseUI.prototype);
AudioChannelController.prototype.EVENT_PREFIX = 'audiochannel';
/**
* General event handler interface.
*
* @param {Event} evt The event to handle.
*/
AudioChannelController.prototype.handleEvent = function(evt) {
switch (evt.type) {
case 'activestatechanged':
var channel = evt.target;
// TODO: We should get the `isActive` state from evt.isActive.
// Then we don't need to do `channel.isActive()` here.
channel.isActive().onsuccess = (evt) => {
this._states.active = evt.target.result;
this.publish('statechanged');
};
break;
}
};
/**
* Set the policy for handling this audio channel.
*
* @param {Object} policy Policy to handle this audio channel.
* @param {Boolean} [policy.isAllowedToPlay] Play or pause it.
* @param {Boolean} [policy.isNeededToFadeOut] Fade out or not.
* @param {Boolean} [policy.isNeededToVibrate] Vibrate or not.
* @param {Boolean} [policy.isNeededToResumeWhenOtherEnds]
* Resume when other audio channel ends if as true.
* @return {AudioChannelController}
*/
AudioChannelController.prototype.setPolicy = function(policy) {
this._policy = policy || this._policy;
return this;
};
AudioChannelController.prototype.getPolicy = function() {
return this._policy;
};
/**
* Handle the audio channel with the policy.
*
* @return {AudioChannelController}
*/
AudioChannelController.prototype.proceedPolicy = function() {
var policy = this._policy;
if (policy.isAllowedToPlay) {
this._play();
} else if (policy.isAllowedToPlay != null) {
this._pause();
}
if (policy.isNeededToFadeOut) {
this._fadeOut();
} else if (policy.isNeededToFadeOut != null) {
this._fadeIn();
}
if (policy.isNeededToVibrate) {
this._vibrate();
}
return this;
};
/**
* Get active state.
*
* @return {Boolean}
*/
AudioChannelController.prototype.isActive = function() {
return this._states.active;
};
/**
* Get state of playing.
*
* @return {Boolean}
*/
AudioChannelController.prototype.isPlaying = function() {
return this._states.playing;
};
/**
* Get state of fading out.
*
* @return {Boolean}
*/
AudioChannelController.prototype.isFadingOut = function() {
return this._states.fadingOut;
};
/**
* Get state of vibrating.
*
* @return {Boolean}
*/
AudioChannelController.prototype.isVibrating = function() {
return this._states.vibrating;
};
AudioChannelController.prototype.destroy = function() {
this.publish('destroyed');
};
/**
* Play the audio channel.
*/
AudioChannelController.prototype._play = function() {
this._states.playing = true;
var promise = new Promise((resolve) => {
var request = this._channel.setMuted(false);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
throw 'Cannot play the audio channel.';
};
});
promise.then(() => {
this.app.debug('Play the audio channel.');
}).catch(e => {
this.app.debug(e);
});
};
/**
* Fade in the audio channel.
*/
AudioChannelController.prototype._fadeIn = function() {
this._setVolume(FADE_IN_VOLUME);
this._states.fadingOut = false;
},
/**
* Fade out the audio channel.
*/
AudioChannelController.prototype._fadeOut = function() {
this._setVolume(FADE_OUT_VOLUME);
this._states.fadingOut = true;
!this._states.playing && this._play();
},
/**
* Pause the audio channel.
*/
AudioChannelController.prototype._pause = function() {
this._states.playing = false;
var promise = new Promise((resolve) => {
var request = this._channel.setMuted(true);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
throw 'Cannot pause the audio channel.';
};
});
promise.then(() => {
this.app.debug('Pause the audio channel');
}).catch(e => {
this.app.debug(e);
});
};
/**
* Vibrate for one second
* and the vibration pattern is [200, 100, 200, 100, ...].
*/
AudioChannelController.prototype._vibrate = function() {
var intervalId = setInterval(() => {
navigator.vibrate(200);
}, 300);
this._states.vibrating = true;
setTimeout(() => {
clearInterval(intervalId);
this._states.vibrating = false;
}, VIBRATION_DURATION);
};
/**
* Change volume of audio channels.
*
* @param {Number} volume 0 to 1.
*/
AudioChannelController.prototype._setVolume = function(volume) {
var promise = new Promise((resolve) => {
var request = this._channel.setVolume(volume);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
throw 'Cannot set volume of the audio channel.';
};
});
promise.then(() => {
this.app.debug('Set volume: ' + volume);
}).catch(e => {
this.app.debug(e);
});
};
/**
* Generate instance ID.
*/
AudioChannelController.prototype._generateID = function() {
this.instanceID = this.app.instanceID + '_' + this.name;
};
exports.AudioChannelController = AudioChannelController;
}(window));

View file

@ -0,0 +1,288 @@
/* global BaseModule */
'use strict';
(function() {
// The results of audo channel competition.
var PLAY = true;
var PAUSE = false;
/**
* It is the table to get the result of
* the competition of audio channels.
*
* Get the result of the competition
* of normal and content audio channels with
* `AUDIO_CHANNEL_COMPETITION_RESULTS.normal.content`.
*/
var AUDIO_CHANNEL_COMPETITION_RESULTS = {
normal: {
normal: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
content: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
alarm: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
system: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
ringer: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
telephony: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
notification: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
publicnotification: { activeAudioChannel: PLAY, newAudioChannel: PLAY }
},
content: {
normal: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
content: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
alarm: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
system: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
ringer: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
telephony: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
notification: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
publicnotification: { activeAudioChannel: PLAY, newAudioChannel: PLAY }
},
alarm: {
normal: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
content: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
alarm: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
system: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
ringer: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
telephony: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
notification: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
publicnotification: { activeAudioChannel: PLAY, newAudioChannel: PLAY }
},
system: {
normal: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
content: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
alarm: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
system: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
ringer: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
telephony: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
notification: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
publicnotification: { activeAudioChannel: PLAY, newAudioChannel: PLAY }
},
ringer: {
normal: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
content: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
alarm: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
system: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
ringer: { activeAudioChannel: PAUSE, newAudioChannel: PLAY },
telephony: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
notification: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
publicnotification: { activeAudioChannel: PLAY, newAudioChannel: PLAY }
},
telephony: {
normal: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
content: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
alarm: { activeAudioChannel: PLAY, newAudioChannel: PAUSE },
system: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
ringer: { activeAudioChannel: PLAY, newAudioChannel: PAUSE },
notification: { activeAudioChannel: PLAY, newAudioChannel: PAUSE },
publicnotification: { activeAudioChannel: PLAY, newAudioChannel: PLAY }
},
notification: {
normal: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
content: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
alarm: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
system: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
ringer: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
telephony: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
notification: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
publicnotification: { activeAudioChannel: PLAY, newAudioChannel: PLAY }
},
publicnotification: {
normal: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
content: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
alarm: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
system: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
ringer: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
telephony: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
notification: { activeAudioChannel: PLAY, newAudioChannel: PLAY },
publicnotification: { activeAudioChannel: PLAY, newAudioChannel: PLAY }
}
};
/**
* AudioChannelPolicy provides policies to handle audio channels.
*/
var AudioChannelPolicy = function() {};
AudioChannelPolicy.SETTINGS = [
'vibration.enabled'
];
BaseModule.create(AudioChannelPolicy, {
name: 'AudioChannelPolicy',
DEBUG: false,
// The value of the vibration.enabled settings.
_isVibrateEnabled: true,
/**
* Get the policies of
* handling the new audio channel and active audio channels.
*
* @param {AudioChannelController} newAudioChannel
* A new audio channel you want to handle.
* @param {Map} activeAudioChannels
* Active audio channels playing noew.
* @param {Object} [options] Options.
* Is the new audio channel in background.
*/
applyPolicy: function(newAudioChannel, activeAudioChannels, options) {
var newAudioChannelName = newAudioChannel.name;
// Deconflict the conflicted policies for the new audio channel.
// For example, we have a new audio channel `alarm` and
// two active audio channels `system` and `telephony`.
// `AUDIO_CHANNEL_COMPETITION_RESULTS.system.alarm` is `true`,
// but `AUDIO_CHANNEL_COMPETITION_RESULTS.telephony.alarm`
// is `false`.
// And we get conflict policies for the `alarm` audio channel.
// Once we get conflcts, we will take higher priority policy.
// `PAUSE` is higher than `PLAY`,
// and doing fade out is higher than not doing fade out.
var isAllowedToPlayForNewAudioChannel = [true];
var isNeededToFadeOutForNewAudioChannel = [false];
// The new audio channel will be allowed to play,
// if any other audio channel belonged to its app is already playing.
var isNewAudioChannelsAppPlaying = false;
activeAudioChannels.forEach((audioChannel) => {
if (audioChannel.app.instanceID === newAudioChannel.app.instanceID) {
isNewAudioChannelsAppPlaying = true;
}
});
!isNewAudioChannelsAppPlaying &&
activeAudioChannels.forEach((audioChannel) => {
var activeAudioChannelName = audioChannel.name;
var results = AUDIO_CHANNEL_COMPETITION_RESULTS
[activeAudioChannelName][newAudioChannelName];
var policy = {
isAllowedToPlay: results.activeAudioChannel
};
if (results.activeAudioChannel) {
policy.isNeededToFadeOut =
this._isNeededToFadeOutForActiveAudioChannel(
activeAudioChannelName, newAudioChannelName
);
}
if (!results.activeAudioChannel) {
policy.isNeededToVibrate = this._isVibrateEnabled &&
this._isNeededToVibrateForActiveAudioChannel(
activeAudioChannelName, newAudioChannelName
);
policy.isNeededToResumeWhenOtherEnds =
this._isNeededToResumeWhenOtherEndsForActiveAudioChannel(
activeAudioChannelName, newAudioChannelName
);
}
audioChannel.setPolicy(policy);
this.debug('Policy for ' + audioChannel.instanceID +
': ' + JSON.stringify(policy));
isAllowedToPlayForNewAudioChannel.push(results.newAudioChannel);
results.newAudioChannel && isNeededToFadeOutForNewAudioChannel.push(
this._isNeededToFadeOutForNewAudioChannel
(activeAudioChannelName, newAudioChannelName)
);
});
// Deconflict the policies.
isAllowedToPlayForNewAudioChannel =
isAllowedToPlayForNewAudioChannel.every(isAllowed => isAllowed);
isNeededToFadeOutForNewAudioChannel =
isNeededToFadeOutForNewAudioChannel.some(isNeeded => isNeeded);
var policy = {
isAllowedToPlay: isAllowedToPlayForNewAudioChannel
};
if (isAllowedToPlayForNewAudioChannel) {
policy.isNeededToFadeOut = isNeededToFadeOutForNewAudioChannel;
}
if (!isAllowedToPlayForNewAudioChannel) {
policy.isNeededToVibrate = this._isVibrateEnabled &&
!isAllowedToPlayForNewAudioChannel &&
// Don't vibrate for background normal audio channel.
newAudioChannelName !== 'normal';
}
newAudioChannel.setPolicy(policy);
this.debug('Policy for ' + newAudioChannel.instanceID +
': ' + JSON.stringify(policy));
},
/**
* Observer the value of vibration.enabled settings.
*
* @param {Boolean} value The value of the settings.
*/
'_observe_vibration.enabled': function(value) {
this._isVibrateEnabled = value;
},
/**
* Get the policy of fading out the new audio channel.
*
* @param {String} activeChannelName The active audio channel name.
* @param {String} newChannelName The new audio channel name.
* @return {Boolean}
*/
_isNeededToFadeOutForNewAudioChannel:
function(activeChannelName, newChannelName) {
var isNeeded = false;
if ((activeChannelName === 'notification' ||
activeChannelName === 'publicnotification') &&
(newChannelName === 'normal' ||
newChannelName === 'content')
)
{
isNeeded = true;
}
return isNeeded;
},
/**
* Get the policy of fading out the active audio channel.
*
* @param {String} activeChannelName The active audio channel name.
* @param {String} newChannelName The new audio channel name.
* @return {Boolean}
*/
_isNeededToFadeOutForActiveAudioChannel:
function(activeChannelName, newChannelName) {
var isNeeded = false;
if (((activeChannelName === 'normal' ||
activeChannelName === 'content') &&
(newChannelName === 'notification' ||
newChannelName === 'publicnotification')
) ||
(activeChannelName === 'alarm' &&
(newChannelName === 'ringer' ||
newChannelName === 'telephony')
)
)
{
isNeeded = true;
}
return isNeeded;
},
/**
* Get the policy of vibrating for the active audio channel.
*
* @param {String} activeChannelName The active audio channel name.
* @param {String} newChannelName The new audio channel name.
* @return {Boolean}
*/
_isNeededToVibrateForActiveAudioChannel:
function(activeChannelName, newChannelName) {
return activeChannelName === 'ringer' && newChannelName === 'ringer';
},
/**
* Get the policy of resuming the active audio channel
* when any other audio channel ends.
*
* @param {String} activeChannelName The active audio channel name.
* @param {String} newChannelName The new audio channel name.
* @return {Boolean}
*/
_isNeededToResumeWhenOtherEndsForActiveAudioChannel:
function(activeChannelName, newChannelName) {
var isNeeded = true;
if (activeChannelName === 'normal' ||
(activeChannelName === 'content' && newChannelName === 'normal') ||
(activeChannelName === 'alarm' && newChannelName === 'alarm')) {
isNeeded = false;
}
return isNeeded;
}
});
}());

View file

@ -0,0 +1,266 @@
/* global BaseModule */
/* global Service */
'use strict';
(function() {
/**
* AudioChannelService manages audio channels in apps.
* It could allow or deny a audio channel to play in some specific cases.
* For example, Music app would like to play when FM app is already playing.
* It will allow Music app to play, and pause FM app.
*
* @class AudioChannelService
*/
var AudioChannelService = function() {};
AudioChannelService.EVENTS = [
'audiochannelstatechanged',
'audiochanneldestroyed',
'hierarchytopmostwindowchanged',
'systemwindowaudiochannelsregistered'
];
AudioChannelService.SUB_MODULES = [
'AudioChannelPolicy'
];
BaseModule.create(AudioChannelService, {
name: 'AudioChannelService',
DEBUG: false,
// If true, all audio channels of System app are muted.
_isSystemMuted: false,
// A map contains playing audio channels.
_activeAudioChannels: null,
// A map contains audio channels' weights.
_audioChannelWeights: null,
// An array contains audio channels could be resumed
// when any other audio channel ends.
_interruptedAudioChannels: null,
// The most top app window.
_topMostWindow: null,
/**
* Initial the module.
*/
_start: function() {
this._activeAudioChannels = new Map();
this._audioChannelWeights = new Map([
['none', 0], ['system', 1], ['normal', 2], ['content', 3],
['alarm', 4], ['ringer', 5], ['telephony', 6],
['notification', 7], ['publicnotification', 8]
]);
this._interruptedAudioChannels = [];
this._muteSystemAudioChannels();
this.debug('Start Audio Channel Manager');
},
/**
* Handle the audio chanel when it is active or in inactive.
*
* @param {Event} evt The event to handle.
*/
_handle_audiochannelstatechanged: function(evt) {
var audioChannel = evt.detail;
this.debug('Audio channel state is ' + audioChannel.isActive());
this._manageAudioChannels(audioChannel);
},
/**
* Remove the window's audio channels from
* `_activeAudioChannels` and `_interruptedAudioChannels`
* when the window is terminated.
*
* @param {Event} evt The event to handle.
*/
_handle_audiochanneldestroyed: function(evt) {
var audioChannel = evt.detail;
this._activeAudioChannels.delete(audioChannel.instanceID);
this._deleteAudioChannelFromInterruptedAudioChannels(audioChannel);
this._resumeAudioChannels();
},
/**
* Handle the audio chanel when the app is in foreground.
*/
_handle_hierarchytopmostwindowchanged: function() {
this._topMostWindow = Service.query('getTopMostWindow');
if (this._topMostWindow) {
this.debug(this._topMostWindow.name + ' is opened');
this._resumeAudioChannels(this._topMostWindow);
}
},
/**
* Mute the System app's audio channels after they are registered.
*/
_handle_systemwindowaudiochannelsregistered: function() {
this._muteSystemAudioChannels();
},
/**
* Play or pause the new audio channel and the active audio channels.
*
* @param {AudioChannelController} audioChannel The new audio channel.
*/
_manageAudioChannels: function(audioChannel) {
if (audioChannel.isActive()) {
this.audioChannelPolicy.applyPolicy(
audioChannel,
this._activeAudioChannels
);
this._activeAudioChannels.forEach((audioChannel) => {
this._handleAudioChannel(audioChannel);
});
this._handleAudioChannel(audioChannel);
if (!this._isAudioChannelInBackground(audioChannel)) {
this.publish('visibleaudiochannelchanged', {
channel: audioChannel.name
});
}
var channel = this._getTopPriorityAudioChannel();
this.publish('audiochannelchanged', { channel: channel });
} else {
this._resetAudioChannel(audioChannel);
this._resumeAudioChannels();
}
},
/**
* Set the audio channel as default state as muted,
* and fade in the faded out audio channels.
*
* @param {AudioChannelController} audioChannel The audio channel.
*/
_resetAudioChannel: function(audioChannel) {
audioChannel.setPolicy({ isAllowedToPlay: false });
this._handleAudioChannel(audioChannel);
if (audioChannel.name === 'notification' ||
audioChannel.name === 'publicnotification') {
this._fadeInFadedOutAudioChannels();
}
},
/**
* Handle the audio channel
* and update `_activeAudioChannels` and `_interruptedAudioChannels`.
*
* @param {AudioChannelController} audioChannel The audio channel.
*/
_handleAudioChannel: function(audioChannel) {
var policy = audioChannel.proceedPolicy().getPolicy();
if (policy.isAllowedToPlay) {
this._activeAudioChannels.set(
audioChannel.instanceID,
audioChannel
);
this.debug('Playing ' + audioChannel.instanceID);
} else {
this._activeAudioChannels.delete(audioChannel.instanceID);
if (policy.isNeededToResumeWhenOtherEnds) {
this._interruptedAudioChannels.push(audioChannel);
this.debug('Interrupted ' + audioChannel.instanceID);
}
}
},
/**
* Fade in all faded out audio channels.
*/
_fadeInFadedOutAudioChannels: function() {
this._activeAudioChannels.forEach((audioChannel) => {
audioChannel.isFadingOut() && audioChannel
.setPolicy({ isNeededToFadeOut: false })
.proceedPolicy();
});
},
/**
* Resume interrupted audio channels.
*
* @param {AppWindow} [app] The app window in foreground.
*/
_resumeAudioChannels: function(app) {
// FIXME: The `app` param should always have `audioChannels`,
// then we don't need to check it.
// Resume the app's audio channels.
if (app) {
app.audioChannels && app.audioChannels.forEach((audioChannel) => {
audioChannel.isActive() && this._manageAudioChannels(audioChannel);
audioChannel.isPlaying() &&
this._deleteAudioChannelFromInterruptedAudioChannels(audioChannel);
});
}
// Resume the latest interrupted audio channel.
var audioChannel;
var length = this._interruptedAudioChannels.length;
if (this._activeAudioChannels.size === 0 && length) {
audioChannel = this._interruptedAudioChannels[length - 1];
audioChannel.setPolicy({ isAllowedToPlay: true });
this._handleAudioChannel(audioChannel);
audioChannel.isPlaying() && this._interruptedAudioChannels.pop();
}
// Send audiochannelchanged evnet to SoundManager.
var channel = this._getTopPriorityAudioChannel();
this.publish('audiochannelchanged', { channel: channel });
},
/**
* Get the top priority audio channel from activeAudioChannels.
*
* @return {String}
*/
_getTopPriorityAudioChannel: function() {
var channel = 'none';
this._activeAudioChannels.forEach((audioChannel) => {
if (this._audioChannelWeights.get(audioChannel.name) >
this._audioChannelWeights.get(channel)) {
channel = audioChannel.name;
}
});
return channel;
},
/**
* Delete the audio channel from `_interruptedAudioChannels` array.
*
* @param {AudioChannelController} audioChannel
* The audio channel want to delete.
*/
_deleteAudioChannelFromInterruptedAudioChannels: function(audioChannel) {
var index = this._interruptedAudioChannels
.findIndex(function(interruptedAudioChannel) {
return interruptedAudioChannel.instanceID ===
audioChannel.instanceID;
});
index !== -1 && this._interruptedAudioChannels.splice(index, 1);
},
/**
* Check the audio channel is in background or not.
*
* @param {AudioChannelController} audioChannel The audio channel.
* @return {Boolean}
*/
_isAudioChannelInBackground: function(audioChannel) {
var isAudioChannelInBackground = true;
if ((this._topMostWindow &&
this._topMostWindow.instanceID === audioChannel.app.instanceID) ||
(audioChannel.isActive() && audioChannel.app.isInputMethod)) {
isAudioChannelInBackground = false;
}
return isAudioChannelInBackground;
},
_muteSystemAudioChannels: function() {
var audioChannels = this.service.query('SystemWindow.getAudioChannels');
if (!this._isSystemMuted && audioChannels && audioChannels.size) {
audioChannels.forEach((audioChannel) => {
audioChannel
.setPolicy({ isAllowedToPlay: false })
.proceedPolicy();
});
this._isSystemMuted = true;
}
}
});
}());

View file

@ -0,0 +1,208 @@
/* global Service, BaseUI */
'use strict';
(function(exports) {
/**
* BaseIcon is a base class for all icons on statusbar
* @param {Object} manager The manager object
* who has the information for the icon
* @param {Number} [index] Specify the index if it is not a singleton
*/
var BaseIcon = function(manager, index) {
if (index !== undefined) {
this.index = index;
this.debug('index=',index);
} else {
this.index = '';
}
this.amendProperty();
this.manager = manager;
this.publish('created');
};
BaseIcon.prototype = Object.create(BaseUI.prototype);
BaseIcon.prototype.constructor = BaseIcon;
BaseIcon.prototype.DEBUG = false;
BaseUI.prototype.view = function bu_view() {
var view = '<div id="' + this.instanceID;
view += '" class="' + this.CLASS_NAME + '"></div>';
return view;
};
BaseIcon.prototype.setOrder = function(priority) {
this.element.style.order = priority;
};
BaseIcon.prototype.camelToDash = function(strings) {
var i = 0;
var ch = '';
while (i <= strings.length) {
var character = strings.charAt(i);
if (character !== character.toLowerCase()) {
if (ch === '') {
ch += character.toLowerCase();
} else {
ch += '-' + character.toLowerCase();
}
} else {
ch += character;
}
i++;
}
return ch;
};
BaseIcon.prototype.amendProperty = function() {
if (!this.name) {
throw new Error('please specify a name when constructing a new icon');
}
var pureName = this.name.replace(/Icon$/, '');
this.dashPureName = this.camelToDash(pureName);
this.instanceID = 'statusbar-' + this.dashPureName;
if (!this.CLASS_LIST) {
this.CLASS_LIST = 'sb-icon sb-icon-' + this.dashPureName;
}
if (!this.l10nId) {
this.l10nId = 'statusbar' + pureName;
}
};
BaseIcon.prototype.EVENT_PREFIX = 'icon';
BaseIcon.prototype.name = 'BaseIcon';
// Overload me
BaseIcon.prototype.instanceID = 'statusbar-base';
BaseIcon.prototype.additionalProperties = '';
BaseIcon.prototype.role = 'listitem';
/**
* This flag is normally true because most icons want to update
* the status once it's rendered. But for certain special icon
* they don't want to be updated on started, set this flag to false.
* @type {Boolean}
*/
BaseIcon.prototype.UPDATE_ON_START = true;
BaseIcon.prototype.show = function(force) {
if (!this.element) {
return;
}
var hidden = this.element.hidden;
if (!hidden) {
return;
}
this.element.hidden = false;
if (!force) {
this.publish('shown');
}
};
BaseIcon.prototype.hide = function() {
if (!this.element) {
return;
}
var hidden = this.element.hidden;
if (hidden) {
return;
}
this.element.hidden = true;
this.publish('hidden');
};
BaseIcon.prototype.render = function() {
this.publish('willrender');
Service.request('Statusbar:iconContainer', this)
.then(function(container) {
var selector = '#' + this.instanceID;
if (container.querySelector(selector)) {
//Do nothing if the element already exists
return;
}
this.containerElement = container;
this.containerElement.insertAdjacentHTML('afterbegin', this.view());
this._fetchElements();
this._registerEvents();
this.element = this.containerElement.querySelector(selector);
this.hide();
this.element.classList.add('active');
this.UPDATE_ON_START && this.update();
this.onrender && this.onrender();
this.publish('rendered');
}.bind(this)
);
return true;
};
/**
* An icon would be enabled once it is started.
* We will try to render then update if necessary.
*/
BaseIcon.prototype.start = function() {
if (this._started) {
return;
}
this.debug('starting..');
this._started = true;
this._start();
// If we are already rendered, update right away.
this.render();
this.UPDATE_ON_START && this.update();
// Integration test needs to toggle some icons actively.
Service.register('show', this);
Service.register('hide', this);
Service.registerState('isVisible', this);
Service.register('update', this);
// Integration test needs to stop some icons actively.
Service.register('stop', this);
};
BaseIcon.prototype.onrender = function() {};
BaseIcon.prototype._start = function() {};
BaseIcon.prototype.stop = function() {
if (!this._started) {
return;
}
this._started = false;
this._stop();
this.hide();
this.element && this.element.classList.remove('active');
Service.register('start', this);
};
BaseIcon.prototype._stop = function() {};
BaseIcon.prototype.isVisible = function() {
return !!(this.element && !this.element.hidden && this.enabled());
};
BaseIcon.prototype.handleEvent = function(evt) {
this.update();
};
/**
* this.update() will be controlled by the icon's manager actively.
* Override this function if your icon has more behavior than
* just showing/hiding.
*
* If update() is not overrided, you should at least override shouldDisplay()
* to let update know how to show/hide the icon.
*/
BaseIcon.prototype.update = function() {
if (!this.enabled() || !this.element) {
this.debug('not started or no element');
this.debug(this._started);
this.debug(this.element);
return;
}
this.shouldDisplay() ? this.show() : this.hide();
};
BaseIcon.prototype.enabled = function() {
return this._started;
};
BaseIcon.prototype.updateLabel = function(type, active) {
if (!this.element && !this.isVisible()) {
return;
}
navigator.mozL10n.setAttributes(this.element, (active ?
'statusbarIconOnActive-' : 'statusbarIconOn-') + type);
};
/**
* The result of this function will be used in this.update()
* to decide to show or hide the icon.
* @return {Boolean} Should show the icon or not
*/
BaseIcon.prototype.shouldDisplay = function() {
throw 'Should be implemented by the Icon constructor.';
};
exports.BaseIcon = BaseIcon;
}(window));

View file

@ -0,0 +1,787 @@
/* global LazyLoader, DUMP */
'use strict';
(function(exports) {
/**
* Turn of this flag to debug all BaseModule based modules.
* @type {Boolean}
*/
var GLOBAL_DEBUG = false;
/**
* `_GAIA_DEVICE_TYPE_` is a placeholder and will be replaced by real
* device type in build time, `system/build/build.js` does the trick.
*/
var DEVICE_TYPE = '_GAIA_DEVICE_TYPE_';
/**
* This is used to store the constructors which are created
* via BaseModule.create().
* constructor.name => constructor
* @type {Object}
*/
var AVAILABLE_MODULES = {};
/**
* BaseModule is a class skeleton which helps you to build a module with
* * Centralized event handler
* * Centralized settings observation
* * Sub modules management including loading and starting
* * Import preload files
* * DOM rendering
* * Consistent logging function with System boot time and module name
* * Common publishing interface
* @class BaseModule
*/
var BaseModule = function() {};
/**
* The sub modules belong to this module.
* BaseModule will load and then start these sub modules automatically.
* The expressions can include path (e.g. 'path/to/ModuleName'). BaseModule
* will load them from specified subdirectory. However, the module names
* (without path) should be unique even they're under different folders.
*
* You can provide an object:
* Define a list of default type modules are able to be loaded in anytime.
* Define a list of device type modules can be loaded when running on a given
* device-type.
* {
* default: ['base_module1', 'base_module2', ...],
* device-type: ['module1', 'module2', ...]
* }
*
* Or provide an array:
* ['module1', 'module2', ...]
*
* @type {Object|Array}
*/
BaseModule.SUB_MODULES = {};
/**
* All events of need to be listened.
* BaseModule will add/remove the event listener in start/stop functions.
* The function of '_handle_' form in this module will be invoked
* when the event is caught.
* @type {Array}
*/
BaseModule.EVENTS = [];
/**
* All mozSettings need to be observed.
* BaseModule will observe and invoke the responsive
* this['_observe_' + key] function.
* @type {Array}
*/
BaseModule.SETTINGS = [];
/**
* This defines a list of file path needs to be imported
* before the real start of this module.
*
* You can provide an object:
* Define a list of default type modules are able to be loaded in anytime.
* Define a list of device type modules can be loaded when running on a given
* device-type.
* {
* default: ['base_module1', 'base_module2', ...],
* device-type: ['module1', 'module2', ...]
* }
*
* Or provide an array:
* ['module1', 'module2', ...]
*
* @type {Object|Array}
*/
BaseModule.IMPORTS = {};
/**
* This tells System the sandbox what methods you are going to
* register and let the other to request.
*
* @example
* var MyModule = function() {};
* MyModule.SERVICES = ['unlock'];
* MyModule.prototype = Object.create(BaseModule.prototype);
* MyModule.prototype.constructor = MyModule;
* MyModule.prototype.name = 'MyModule';
* var m = new MyModule();
* m.start();
* // other module
* Service.request('MyModule:unlock').then(function(result) {
* });
* Service.request('unlock').then(function(result) {
* // if the request is registered by only one module.
* });
*/
BaseModule.SERVICES = [];
/**
* The function or property exported here will be
* synchronously queried by other module in system app.
* If we are not started yet, they will get undefined.
*
* @example
* var MyModule = function() {};
* MyModule.STATES = ['isActive'];
* MyModule.prototype = Object.create(BaseModule.prototype);
* MyModule.prototype.constructor = MyModule;
* MyModule.prototype.name = 'MyModule';
* var m = new MyModule();
* m.start();
* // other module
* console.log(Service.query('MyModule.isActive'));
* // if the method name is unique.
* console.log(Service.query('isActive'));
* @type {Array}
*/
BaseModule.STATES = [];
var SubmoduleMixin = {
loadWhenIdle: function(modules) {
return this.service.request('schedule', () => {
var submodules = this._arraylizeModules(this.constructor.SUB_MODULES);
var newModules = this._arraylizeModules(modules);
this.constructor.SUB_MODULES = submodules.concat(newModules);
return this._startSubModules();
});
},
_arraylizeModules: function(submodules) {
var modules;
var defaulModules;
var deviceModules;
if (submodules && Array.isArray(submodules)) {
modules = submodules;
} else if (submodules && typeof submodules === 'object') {
defaulModules = submodules.default || [];
deviceModules = submodules[DEVICE_TYPE] || [];
modules = defaulModules.concat(deviceModules);
} else {
modules = [];
}
return modules;
},
/**
* Helper function to load and start the submodules defined in
* |this.constructor.SUB_MODULES|.
*/
_startSubModules: function() {
var submodules = this._arraylizeModules(this.constructor.SUB_MODULES);
this.constructor.SUB_MODULES = submodules;
if (submodules.length === 0) {
return Promise.resolve();
}
var unloaded = [];
submodules.forEach(function(submodule) {
if (BaseModule.defined(submodule) || window[submodule]) {
var name = BaseModule.lowerCapital(submodule);
if (!this[name]) {
this._initializeSubModule(name, submodule);
}
} else {
unloaded.push(submodule);
}
}, this);
if (unloaded.length === 0) {
this.baseSubModuleLoaded && this.baseSubModuleLoaded();
return;
}
this.debug('lazy loading submodules: ' +
unloaded.concat());
return BaseModule.lazyLoad(unloaded).then(() => {
this.debug('lazy loaded submodules: ' +
unloaded.concat());
return Promise.all(
unloaded
.map(BaseModule.parsePath)
.map(function(module) {
var moduleName = BaseModule.lowerCapital(module.name);
if (!this[moduleName]) {
return this._initializeSubModule(moduleName, module.name);
} else {
return Promise.resolve();
}
}, this));
});
},
_initializeSubModule: function(moduleName, module) {
var constructor = AVAILABLE_MODULES[module] || window[module];
if (typeof(constructor) == 'function') {
this.debug('instantiating submodule: ' + moduleName);
this[moduleName] = new constructor(this);
// If there is a custom submodule loaded handler, call it.
// Otherwise we will start the submodule right away.
if (typeof(this['_' + moduleName + '_loaded']) == 'function') {
return this['_' + moduleName + '_loaded']();
} else if (this.lifeCycleState !== 'stopped') {
return this[moduleName].start && this[moduleName].start();
}
} else {
this[moduleName] = constructor;
// For the module which does not become class yet
if (this[moduleName] && this[moduleName].start) {
return this[moduleName].start();
} else if (this[moduleName] && this[moduleName].init) {
// backward compatibility with init.
return this[moduleName].init();
} else {
return undefined;
}
}
},
_stopSubModules: function() {
if (!this.constructor.SUB_MODULES) {
return;
}
this.constructor.SUB_MODULES.map(BaseModule.parsePath)
.forEach(function(module) {
var moduleName = BaseModule.lowerCapital(module.name);
if (this[moduleName]) {
this.debug('Stopping submodule: ' + moduleName);
this[moduleName].stop && this[moduleName].stop();
}
}, this);
}
};
/**
* SettingsMixin will provide you the ability to watch
* and store the settings in this._settings
* @type {Object}
*/
var SettingMixin = {
observe: function(name, value) {
this.debug('observing ' + name + ' : ' + value);
this._settings[name] = value;
if (typeof(this['_observe_' + name]) == 'function') {
this.debug('observer for ' + name + ' found, invoking.');
this['_observe_' + name](value);
}
},
_observeSettings: function() {
if (!this.constructor.SETTINGS) {
this.debug('No settings needed, skipping.');
return;
}
this._settings = {};
this.debug('~observing following settings: ' +
this.constructor.SETTINGS.concat());
this.constructor.SETTINGS.forEach(function(setting) {
this.service.request('SettingsCore:addObserver', setting, this);
}, this);
},
_unobserveSettings: function() {
if (!this.constructor.SETTINGS) {
return;
}
this.constructor.SETTINGS.forEach(function(setting) {
this.service.request('SettingsCore:removeObserver', setting, this);
}, this);
}
};
var EventMixin = {
/**
* Custom global event handler before the event is handled
* by a specific handler.
* Override it if necessary.
*/
_pre_handleEvent: function() {
},
/**
* Custom global event handler after the event is handled.
* Override it if necessary.
*/
_post_handleEvent: function() {
},
_subscribeEvents: function() {
if (!this.constructor.EVENTS) {
this.debug('No events wanted, skipping.');
return;
}
this.debug('event subcribing stage..');
this.constructor.EVENTS.forEach(function(event) {
this.debug('subscribing ' + event);
window.addEventListener(event, this);
}, this);
},
_unsubscribeEvents: function() {
if (!this.constructor.EVENTS) {
return;
}
this.constructor.EVENTS.forEach(function(event) {
window.removeEventListener(event, this);
}, this);
},
handleEvent: function(evt) {
if (typeof(this._pre_handleEvent) == 'function') {
var shouldContinue = this._pre_handleEvent(evt);
if (shouldContinue === false) {
return;
}
} else {
this.debug('no handle event pre found. skip');
}
if (typeof(this['_handle_' + evt.type]) == 'function') {
this.debug('handling ' + evt.type);
this['_handle_' + evt.type](evt);
}
if (typeof(this._post_handleEvent) == 'function') {
this._post_handleEvent(evt);
}
}
};
var ServiceMixin = {
_registerServices: function() {
if (!this.constructor.SERVICES) {
return;
}
this.constructor.SERVICES.forEach(function(service) {
this.service.register(service, this);
}, this);
},
_unregisterServices: function() {
if (!this.constructor.SERVICES) {
return;
}
this.constructor.SERVICES.forEach(function(service) {
this.service.unregister(service, this);
}, this);
}
};
var StateMixin = {
_registerStates: function() {
if (!this.constructor.STATES) {
return;
}
this.constructor.STATES.forEach(function(state) {
this.service.registerState(state, this);
}, this);
},
_unregisterStates: function() {
if (!this.constructor.STATES) {
return;
}
this.constructor.STATES.forEach(function(state) {
this.service.unregisterState(state, this);
}, this);
}
};
BaseModule.defined = function(name) {
return !!AVAILABLE_MODULES[name];
};
BaseModule.__clearDefined = function() {
AVAILABLE_MODULES = [];
};
/**
* Mixin the prototype with give mixin object.
* @param {Object} prototype The prototype of a class
* @param {Object} mixin An object will be mixed into the prototype
*/
BaseModule.mixin = function (prototype, mixin) {
for (var prop in mixin) {
if (mixin.hasOwnProperty(prop)) {
prototype[prop] = mixin[prop];
}
}
};
/**
* Create a module based on base module and give properties.
* The constructor will be placed in AVAILABLE_MODULES if you
* specify an unique name in the prototype.
* @example
* var MyModule = function() {};
* BaseModule.create(MyModule, {
* name: 'MyModule'
* });
* var myModule = BaseModule.instantiate('MyModule');
*
* @param {Function} constructor The constructor function.
* @param {Object} prototype
* The prototype which will be injected into the class.
* @param {Object} properties
* The property object which includes getter/setter.
*/
BaseModule.create = function(constructor, prototype, properties) {
constructor.prototype = Object.create(BaseModule.prototype, properties);
constructor.prototype.constructor = constructor;
if (constructor.SETTINGS) {
BaseModule.mixin(constructor.prototype, SettingMixin);
}
if (constructor.EVENTS) {
BaseModule.mixin(constructor.prototype, EventMixin);
}
if (constructor.SERVICES) {
BaseModule.mixin(constructor.prototype, ServiceMixin);
}
if (constructor.STATES) {
BaseModule.mixin(constructor.prototype, StateMixin);
}
// Inject this anyway.
BaseModule.mixin(constructor.prototype, SubmoduleMixin);
if (prototype) {
BaseModule.mixin(constructor.prototype, prototype);
if (prototype.name) {
AVAILABLE_MODULES[prototype.name] = constructor;
} else {
console.warn('No name give, impossible to instantiate without name.');
}
}
return constructor;
};
/**
* Create a new instance based on the module name given.
* It will look up |AVAILABLE_MODULES|.
* Note: this will instante multiple instances if called more than once.
* Also it's impossible to pass arguments now.
* @param {String} moduleName The module name
* which comes from the prototype of the module.
* @return {Object} Created instance.
*/
BaseModule.instantiate = function(moduleName) {
if (moduleName in AVAILABLE_MODULES) {
var args = Array.prototype.slice.call(arguments, 1);
var constructor = function() {
AVAILABLE_MODULES[moduleName].apply(this, args);
};
constructor.prototype = AVAILABLE_MODULES[moduleName].prototype;
return new constructor();
}
return undefined;
};
/**
* Lazy load an list of modules
* @param {Array} array A list of module names
* @return {Promise} The promise of lazy loading;
* it will be invoked once lazy loading is done.
*/
BaseModule.lazyLoad = function(array) {
var self = this;
return new Promise(function(resolve) {
var fileList = [];
array.forEach(function(module) {
fileList.push(BaseModule.object2fileName(module));
}, self);
LazyLoader.load(fileList, function() {
resolve();
});
});
};
/**
* A helper function to lowercase only the capital character.
* @example
* Service.lowerCapital('AppWindowManager');
* // appWindowManager
* @param {String} str String to be lowercased on capital
* @return {String} Captital lowerred string
*/
BaseModule.lowerCapital = function(str) {
return str.charAt(0).toLowerCase() + str.slice(1);
};
/**
* A helper function to split module expressions into "path" and "name".
* @example
* Service.parsePath('path/to/ModuleName');
* // {path: 'path/to/', name: 'ModuleName'}
* @param {String} str String to be splitted
* @return {Object} The result object with members: "path" and "name".
*/
BaseModule.parsePath = function(str) {
var [, path, name] = /^(.*\/|)(.+)$/.exec(str);
return {
path: path,
name: name
};
};
/**
* A helper function to transform object name to file name
* @example
* Service.object2fileName('AppWindowManager');
* // 'js/app_window_manager.js'
* Service.object2fileName('path/to/ModuleName');
* // 'js/path/to/module_name.js'
*
* @param {String} string Module name
* @return {String} File name
*/
BaseModule.object2fileName = function(string) {
var i = 0;
var ch = '';
var module = BaseModule.parsePath(string);
var moduleName = module.name;
while (i <= moduleName.length) {
var character = moduleName.charAt(i);
if (character !== character.toLowerCase()) {
if (ch === '') {
ch += character.toLowerCase();
} else {
ch += '_' + character.toLowerCase();
}
} else {
ch += character;
}
i++;
}
return '/js/' + module.path + ch + '.js';
};
BaseModule.prototype = {
service: window.Service,
DEBUG: false,
TRACE: false,
/**
* The name of this module which is usually the constructor function name
* and could be converted to the file name.
* For example, AppWindowManager will be mapped to app_window_manager.js
* This should be unique.
* @type {String}
*/
name: '(Anonymous)',
EVENT_PREFIX: '',
/**
* We are having three states:
* * starting
* * started
* * stopped
* @type {String}
*/
lifeCycleState: 'stopped',
publish: function(event, detail, noPrefix) {
var prefix = noPrefix ? '' : this.EVENT_PREFIX;
var evt = new CustomEvent(prefix + event,
{
bubbles: true,
detail: detail || this
});
this.debug('publishing: ' + prefix + event);
window.dispatchEvent(evt);
},
/**
* Basic log.
*/
debug: function() {
if (this.DEBUG || GLOBAL_DEBUG) {
console.log('[' + this.name + ']' +
'[' + this.service.currentTime() + '] ' +
Array.slice(arguments).concat());
if (this.TRACE) {
console.trace();
}
} else if (window.DUMP) {
DUMP('[' + this.name + ']' +
'[' + this.service.currentTime() + '] ' +
Array.slice(arguments).concat());
}
},
/**
* Log some infomation.
*/
info: function() {
if (this.DEBUG || GLOBAL_DEBUG) {
console.info('[' + this.name + ']' +
'[' + this.service.currentTime() + '] ' +
Array.slice(arguments).concat());
}
},
/**
* Log some warning message.
*/
warn: function() {
if (this.DEBUG || GLOBAL_DEBUG) {
console.warn('[' + this.name + ']' +
'[' + this.service.currentTime() + '] ' +
Array.slice(arguments).concat());
}
},
/**
* Log some error message.
*/
error: function() {
if (this.DEBUG || GLOBAL_DEBUG) {
console.error('[' + this.name + ']' +
'[' + this.service.currentTime() + '] ' +
Array.slice(arguments).concat());
}
},
writeSetting: function(settingObject) {
this.debug('writing ' + JSON.stringify(settingObject) +
' to settings db');
return this.service.request('SettingsCore:set', settingObject);
},
readSetting: function(name) {
if (this._settings && this._settings[name]) {
return Promise.resolve(this._settings[name]);
} else {
this.debug('reading ' + name + ' from settings db');
return this.service.request('SettingsCore:get', name);
}
},
/**
* Custom start function, override it if necessary.
* If you want to block the start process of this module,
* return a promise here.
*/
_start: function() {},
/**
* Custom stop function. Override it if necessary.
*/
_stop: function() {},
/**
* The starting progress of a module has these steps:
* * import javascript files
* * lazy load submodules and instantiate once loaded.
* * custom start function
* * attach event listeners
* * observe settings
* * register services to System
* * DOM elements rendering (not implemented)
* The import is guranteed to happen before anything else.
* The service registration is expected to happen after everything is done.
* The ordering of the remaining parts should not depends each other.
*
* The start function will return a promise to let you know
* when the module is started.
* @example
* var a = BaseModule.instantiate('A');
* a.start().then(() => {
* console.log('started');
* });
*
* Note: a module start promise will only be resolved
* after all the steps are resolved, including
* the custom start promise and the promises from all the submodules.
* So when you see a module is started, that means all of its
* submodules are started as well.
*
* @memberOf BaseModule.prototype
*/
start: function() {
if (this.lifeCycleState !== 'stopped') {
this.warn('already started');
return Promise.reject('already started');
}
this.switchLifeCycle('starting');
return this.imports();
},
__imported: function() {
// Do nothing if we are stopped.
if (this.lifeCycleState === 'stopped') {
this.warn('already stopped');
return Promise.resolve();
}
this.debug('in imported');
return Promise.all([
// Parent module needs to know the events from the submodule.
this._subscribeEvents && this._subscribeEvents(),
this._startSubModules && this._startSubModules(),
this._start(),
this._observeSettings && this._observeSettings(),
this._registerServices && this._registerServices(),
this._registerStates && this._registerStates()]).then(() => {
this.switchLifeCycle('started');
});
},
/**
* The stopping of a module has these steps:
* * unregister services to System sandbox
* * lazy load submodules and instantiate once loaded.
* * attach event listeners
* * observe settings
* * custom stop function
*/
stop: function() {
if (this.lifeCycleState === 'stopped') {
this.warn('already stopped');
return;
}
this._unregisterServices && this._unregisterServices();
this._unregisterStates && this._unregisterStates();
this._stopSubModules && this._stopSubModules();
this._unsubscribeEvents && this._unsubscribeEvents();
this._unobserveSettings && this._unobserveSettings();
this._stop();
this.switchLifeCycle('stopped');
},
switchLifeCycle: function(state) {
if (this.lifeCycleState === state) {
return;
}
this.debug('life cycle state change: ' +
this.lifeCycleState + ' -> ' + state);
this.lifeCycleState = state;
this.publish(state);
},
imports: function() {
var imports = this._arraylizeModules(this.constructor.IMPORTS);
this.constructor.IMPORTS = imports;
if (imports.length === 0) {
return this.__imported();
}
this.debug(imports);
this.debug('import loading.');
return LazyLoader.load(imports)
.then(() => {
this.debug('imported..');
return this.__imported();
});
}
};
exports.BaseModule = BaseModule;
}(window));

View file

@ -0,0 +1,152 @@
/* global Service */
'use strict';
(function(exports) {
var DEBUG = false;
/**
* The virtual class inherited by all UI in system which has
* the similar behavior.
*
* @class BaseUI
*/
var BaseUI = function BaseUI() {
};
BaseUI.prototype.EVENT_PREFIX = 'base-';
/**
* Operations to render UI
* Overwrite `view` to provide HTML interface.
* Overwrite `_fetchElements` to provide elements reference.
* Overwrite `_registerEvents` to register event handler.
*/
BaseUI.prototype.render = function bu_render() {
if (this.element) {
return;
}
this.publish('willrender');
this.containerElement.insertAdjacentHTML('afterbegin', this.view());
this._fetchElements();
this._registerEvents();
if (this.element) {
// Force a style flush so that if the UI is immediately shown, any
// transition associated with the visible class will play.
this.element.clientTop;
}
this.publish('rendered');
};
/**
* Get nessesary elements reference when inited
*/
BaseUI.prototype._fetchElements = function bu__fetchElements() {
};
/**
* Register event handler
*/
BaseUI.prototype._registerEvents = function bu__registerEvents() {
};
/**
* Modal Dialog html view
*/
BaseUI.prototype.view = function bu_view() {
return '<div class="' + this.CLASS_NAME + '"></div>';
};
BaseUI.prototype.show = function bu_show(ele) {
ele = ele || this.element;
ele.classList.add('visible');
};
BaseUI.prototype.isShown = function bu_isShown(ele) {
ele = ele || this.element;
return ele && ele.classList.contains('visible');
};
BaseUI.prototype.hide = function bu_hide(ele) {
ele = ele || this.element;
ele.classList.remove('visible');
};
BaseUI.prototype.broadcast = function bu_broadcast(event, detail) {
if (this.element) {
var internalEvent = new CustomEvent('_' + event, {
bubbles: false,
detail: detail || this
});
this.debug(' publishing internal event: ' + event);
this.element.dispatchEvent(internalEvent);
}
};
BaseUI.prototype.publish = function bu_publish(event, detail) {
// Dispatch internal event before external events.
this.broadcast(event, detail);
var evt = new CustomEvent(this.EVENT_PREFIX + event, {
bubbles: true,
cancelable: false,
detail: detail || this
});
this.debug(' publishing external event: ' + event);
// Publish external event.
window.dispatchEvent(evt);
};
// Override me.
BaseUI.prototype.CLASS_NAME = 'BaseUI';
// Override me. Human readable ID.
BaseUI.prototype.customID = function bu_customID() {
return '';
};
/**
* Overwrite me if you need to unregister event handlers.
*/
BaseUI.prototype._unregisterEvents = function bu__unregisterEvents() {
};
BaseUI.prototype.start = function bu_start() {
this._start();
this.publish('started');
};
BaseUI.prototype._start = function bu__start() {
};
BaseUI.prototype.stop = function bu_start() {
this._stop();
this.publish('stopped');
};
BaseUI.prototype._stop = function bu__stop() {
};
BaseUI.prototype.destroy = function bu_destroy() {
this.publish('willdestroy');
this._unregisterEvents();
if (this.element) {
this.element.parentNode.removeChild(this.element);
this.element = null;
}
this.publish('destroyed');
};
BaseUI.prototype.debug = function bu_debug(msg) {
if (DEBUG || this.DEBUG) {
console.log('[' + (this.name || this.CLASS_NAME) + ']' +
'[' + (this.customID() || (this.index + 1) || this.instanceID) + ']' +
'[' + Service.currentTime() + ']' +
Array.slice(arguments).concat());
}
};
exports.BaseUI = BaseUI;
}(window));

View file

@ -0,0 +1,49 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var BatteryIcon = function(manager) {
BaseIcon.call(this, manager);
};
BatteryIcon.prototype = Object.create(BaseIcon.prototype);
BatteryIcon.prototype.name = 'BatteryIcon';
BatteryIcon.prototype.update = function() {
if (!this.element) {
return;
}
this.show();
var battery = this.manager._battery;
var icon = this.element;
var previousLevel = parseInt(icon.dataset.level, 10);
var previousCharging = (icon.dataset.charging === 'true');
icon.dataset.charging = battery.charging;
var level = Math.floor(battery.level * 10) * 10;
this.debug('previous level: ', previousLevel);
this.debug('current level:', level);
if (previousLevel === level &&
previousCharging === battery.charging) {
return;
}
icon.dataset.level = level;
navigator.mozL10n.setAttributes(
icon,
battery.charging ? 'statusbarBatteryCharging' : 'statusbarBattery',
{level: level}
);
this.previousCharging = battery.charging;
this.publish('changed');
};
BatteryIcon.prototype.view = function view() {
return `<div id="statusbar-battery"
class="sb-icon sb-icon-battery"
role="listitem">
</div>`;
};
exports.BatteryIcon = BatteryIcon;
}(window));

View file

@ -0,0 +1,147 @@
'use strict';
/* global BaseModule */
/* global Service */
/* global BatteryIcon */
/* global LazyLoader */
(function() {
function BatteryOverlay() {
LazyLoader.load(['js/battery_icon.js']).then(function() {
this.icon = new BatteryIcon(this);
this.icon.start();
}.bind(this)).catch(function(err) {
console.error(err);
});
}
BatteryOverlay.SUB_MODULES = [
'PowerSave'
];
BaseModule.create(BatteryOverlay, {
name: 'BatteryOverlay',
TOASTER_TIMEOUT: 5000,
TRANSITION_SPEED: 1.8,
TRANSITION_FRACTION: 0.30,
AUTO_SHUTDOWN_LEVEL: 0.00,
EMPTY_BATTERY_LEVEL: 0.1,
_battery: window.navigator.battery,
_notification: null,
getAllElements: function bm_getAllElements() {
this.screen = document.getElementById('screen');
this.overlay = document.getElementById('system-overlay');
this.notification = document.getElementById('battery');
},
checkBatteryDrainage: function bm_checkBatteryDrainage() {
var battery = this._battery;
if (!battery) {
return;
}
if (battery.level <= this.AUTO_SHUTDOWN_LEVEL && !battery.charging) {
// Fire a event to inform sleepMenu perform shutdown.
window.dispatchEvent(new CustomEvent('batteryshutdown'));
}
},
_start: function bm_init() {
this.getAllElements();
var battery = this._battery;
if (battery) {
// When the device is booted, check if the battery is drained. If so,
// batteryshutdown would be triggered to inform sleepMenu shutdown.
this.checkBatteryDrainage.bind(this);
battery.addEventListener('levelchange', this);
battery.addEventListener('chargingchange', this);
}
window.addEventListener('screenchange', this);
this._screenOn = true;
this._wasEmptyBatteryNotificationDisplayed = false;
this.displayIfNecessary();
},
handleEvent: function bm_handleEvent(evt) {
var battery = this._battery;
switch (evt.type) {
case 'screenchange':
this._screenOn = evt.detail.screenEnabled;
this.displayIfNecessary();
break;
case 'levelchange':
if (!battery) {
return;
}
this.checkBatteryDrainage();
this.displayIfNecessary();
this.powerSave && this.powerSave.onBatteryChange();
this.icon && this.icon.update();
break;
case 'chargingchange':
this.powerSave && this.powerSave.onBatteryChange();
this.icon && this.icon.update();
// We turn the screen on if needed in order to let
// the user knows the device is charging
if (battery && battery.charging) {
this.hide();
this._wasEmptyBatteryNotificationDisplayed = false;
if (!this._screenOn) {
Service.request('turnScreenOn');
}
} else {
this.displayIfNecessary();
}
break;
}
},
_shouldWeDisplay: function bm_shouldWeDisplay() {
var battery = this._battery;
if (!battery) {
return false;
}
return (!this._wasEmptyBatteryNotificationDisplayed &&
!battery.charging &&
battery.level <= this.EMPTY_BATTERY_LEVEL &&
this._screenOn);
},
displayIfNecessary: function bm_display() {
if (!this._shouldWeDisplay()) {
return;
}
this.overlay.classList.add('battery');
this._wasEmptyBatteryNotificationDisplayed = true;
if (this._toasterTimeout) {
clearTimeout(this._toasterTimeout);
}
this._toasterTimeout = setTimeout(this.hide.bind(this),
this.TOASTER_TIMEOUT);
},
hide: function bm_hide() {
var overlayCss = this.overlay.classList;
if (overlayCss.contains('battery')) {
this.overlay.classList.remove('battery');
this._toasterTimeout = null;
}
}
});
}());

View file

@ -0,0 +1,26 @@
/* exported BluetoothCore */
/* global BaseModule, LazyLoader, Bluetooth2 */
'use strict';
(function() {
var BluetoothCore = function(bluetooth) {
this.bluetooth = bluetooth;
};
/**
* BluetoothCore handle bluetooth related function and bootstrap
* modules for v1/v2 API.
*
* @class BluetoothCore
*/
BaseModule.create(BluetoothCore, {
name: 'BluetoothCore',
_start: function() {
return LazyLoader.load(['js/bluetooth_v2.js']).then(function() {
window.Bluetooth = new Bluetooth2();
return window.Bluetooth.start();
});
}
});
}());

View file

@ -0,0 +1,24 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var BluetoothHeadphoneIcon = function(manager) {
BaseIcon.call(this, manager);
};
BluetoothHeadphoneIcon.prototype = Object.create(BaseIcon.prototype);
BluetoothHeadphoneIcon.prototype.name = 'BluetoothHeadphoneIcon';
BluetoothHeadphoneIcon.prototype.shouldDisplay = function() {
return this.manager.isA2DPProfileConnected;
};
BluetoothHeadphoneIcon.prototype.view = function view() {
return `<div id="statusbar-bluetooth-headphone"
class="sb-icon sb-icon-bluetooth-headphone"
role="listitem" hidden
aria-live="polite"
data-l10n-id="statusbarBluetoothHeadphone">
</div>`;
};
exports.BluetoothHeadphoneIcon = BluetoothHeadphoneIcon;
}(window));

View file

@ -0,0 +1,28 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var BluetoothIcon = function(manager) {
BaseIcon.call(this, manager);
};
BluetoothIcon.prototype = Object.create(BaseIcon.prototype);
BluetoothIcon.prototype.name = 'BluetoothIcon';
BluetoothIcon.prototype.update = function() {
var icon = this.element;
if (!icon) {
return;
}
this.manager.isEnabled ? this.show() : this.hide();
icon.dataset.active = this.manager.connected;
this.updateLabel('bluetooth', this.manager.connected);
};
BluetoothIcon.prototype.view = function view() {
return `<div id="statusbar-bluetooth"
class="sb-icon sb-icon-bluetooth" hidden
role="listitem">
</div>`;
};
exports.BluetoothIcon = BluetoothIcon;
}(window));

View file

@ -0,0 +1,717 @@
/* API Summary:
stopSendingFile(in DOMString aDeviceAddress);
confirmReceivingFile(in DOMString aDeviceAddress, in bool aConfirmation); */
'use strict';
/* global MimeMapper, Service, LazyLoader,
MozActivity, NotificationHelper, UtilityTray */
/* exported BluetoothTransfer */
(function(exports) {
var BluetoothTransfer = {
name: 'BluetoothTransfer',
// The first-in-first-out queue maintain each scheduled sending task.
// Each element is a object for scheduled sending tasks.
_sendingFilesQueue: [],
_bindOnCancelTransferTask: null,
/**
* Debug message.
*
* @private
* @type {Boolean} turn on/off the console log
*/
onDebug: false,
bluetoothIcon: 'style/bluetooth_transfer/images/icon_bluetooth.png',
get _deviceStorage() {
return navigator.getDeviceStorage('sdcard');
},
get transferStatusList() {
var transferStatusList =
document.getElementById('bluetooth-transfer-status-list');
delete this.transferStatusList;
this.transferStatusList = transferStatusList;
return transferStatusList;
},
/**
* Initialize BluetoothTransfer module.
*
* Service request handleSystemMessageNotification is used to filter
* and relay BluetoothTransfer related notifications handle event after
* reboot. Now only `_onTransferComplete` method generate notifications
* that need to be handled after reboot.
*
* @public
*/
start: function bt_start() {
// Bind message handler for sending files from Bluetooth app
window.addEventListener('iac-bluetoothTransfercomms',
this._onFilesSending.bind(this)
);
// Bind message handler for transferring file callback
navigator.mozSetMessageHandler(
'bluetooth-opp-receiving-file-confirmation',
this.onReceivingFileConfirmation.bind(this)
);
// Listen to 'bluetooth-opp-transfer-start' from bluetooth.js
window.addEventListener('bluetooth-opp-transfer-start',
this._onUpdateProgress.bind(this, 'start')
);
navigator.mozSetMessageHandler('bluetooth-opp-update-progress',
this._onUpdateProgress.bind(this, 'progress')
);
// Listen to 'bluetooth-opp-transfer-complete' from bluetooth.js
window.addEventListener('bluetooth-opp-transfer-complete',
this._onTransferComplete.bind(this));
window.addEventListener('bluetooth-sendfile-via-handover',
this.sendFileViaHandover.bind(this));
Service.registerState('isSendFileQueueEmpty', this);
Service.registerState('isFileTransferInProgress', this);
Service.request('handleSystemMessageNotification',
'BluetoothTransfer', this);
this._bindOnCancelTransferTask = this.onCancelTransferTask.bind(this);
},
getDeviceName: function bt_getDeviceName(address) {
return new Promise((resolve) => {
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter === null) {
var msg = 'Since cannot get Bluetooth adapter, ' +
'resolve with an unknown device.';
this.debug(msg);
navigator.mozL10n.formatValue('unknown-device').then(resolve);
return;
}
// Service Class Name: OBEXObjectPush, UUID: 0x1105
// Specification: Object Push Profile (OPP)
// NOTE: Used as both Service Class Identifier and Profile.
// Allowed Usage: Service Class/Profile
// https://www.bluetooth.org/en-us/specification/assigned-numbers/
// service-discovery
var serviceUuid = '0x1105';
var req = adapter.getConnectedDevices(serviceUuid);
req.onsuccess = () => {
if (req.result) {
this.debug('got connectedList');
var connectedList = req.result;
var length = connectedList.length;
for (var i = 0; i < length; i++) {
if (connectedList[i].address == address) {
resolve(connectedList[i].name);
}
}
} else {
navigator.mozL10n.formatValue('unknown-device').then(resolve);
}
};
req.onerror = () => {
var msg = 'Can not check is device connected from adapter.';
this.debug(msg);
navigator.mozL10n.formatValue('unknown-device').then(resolve);
};
});
},
debug: function bt_debug(msg) {
if (!this.onDebug) {
return;
}
console.log('[System Bluetooth Transfer]: ' + msg);
},
humanizeSize: function bt_humanizeSize(bytes) {
var _ = navigator.mozL10n.get;
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var size, e;
if (bytes) {
e = Math.floor(Math.log(bytes) / Math.log(1024));
size = (bytes / Math.pow(1024, e)).toFixed(2);
} else {
e = 0;
size = '0.00';
}
return _('fileSize', {
size: size,
unit: _('byteUnit-' + units[e])
});
},
_onFilesSending: function bt__onFilesSending(evt) {
// Notify user that we are sending files
var icon = 'style/bluetooth_transfer/images/transfer.png';
NotificationHelper.send('transferHasStartedTitle', {
'bodyL10n': 'transferHasStartedDescription',
'icon': icon
}).then(function(notification) {
notification.addEventListener('click',
UtilityTray.show.bind(UtilityTray));
});
// Push sending files request in queue
var sendingFilesSchedule = evt.detail;
this._sendingFilesQueue.push(sendingFilesSchedule);
var msg = 'push sending files request in queue, queued length = ' +
this._sendingFilesQueue.length;
this.debug(msg);
},
onReceivingFileConfirmation: function bt_onReceivingFileConfirmation(evt) {
if (Service.query('NfcHandoverManager.isHandoverInProgress')) {
// Bypassing confirm dialog while incoming file transfer via
// NFC Handover
this.debug('Incoming file via NFC Handover. Bypassing confirm dialog');
window.dispatchEvent(new CustomEvent('nfc-transfer-started'));
this.acceptReceive(evt);
return;
}
// Prompt appears when a transfer request from a paired device is
// received.
this.debug('show receive confirm dialog');
var address = evt.address;
this.getDeviceName(address).then((deviceName) => {
var title = {
id: 'transfer-confirmation-title',
args: { deviceName: deviceName }
};
NotificationHelper.send(title, {
'bodyL10n': 'transfer-confirmation-desc',
'icon': this.bluetoothIcon,
'mozbehavior': {noclear: true}
}).then((notification) => {
notification.addEventListener('click', () => {
UtilityTray.hide();
this.showReceivePrompt(evt);
});
});
});
},
showReceivePrompt: function bt_showReceivePrompt(evt) {
var address = evt.address;
var fileName = evt.fileName;
var fileSize = this.humanizeSize(evt.fileLength);
var cancel = {
title: 'deny',
callback: this.declineReceive.bind(this, address)
};
var confirm = {
title: 'transfer',
callback: this.acceptReceive.bind(this, evt),
recommend: true
};
this.getDeviceName(address).then(function(deviceName) {
Service.request('showCustomDialog', 'acceptFileTransfer',
{
id: 'want-to-receive-file',
args: {
deviceName: deviceName,
fileName: fileName,
fileSize: fileSize
}
},
cancel,
confirm
);
});
},
declineReceive: function bt_declineReceive(address) {
Service.request('hideCustomDialog');
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter != null) {
adapter.confirmReceivingFile(address, false);
} else {
var msg = 'Cannot get adapter from system Bluetooth monitor.';
this.debug(msg);
}
},
acceptReceive: function bt_acceptReceive(evt) {
this.debug('accepted the file transfer');
Service.request('hideCustomDialog');
// Check storage is available or not before confirm receiving file
var address = evt.address;
var fileSize = evt.fileLength;
this.checkStorageSpace(fileSize,
(isStorageAvailable, errorMessage) => {
var option = (isStorageAvailable) ? true : false;
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter) {
adapter.confirmReceivingFile(address, option);
} else {
var msg = 'Cannot get adapter from system Bluetooth monitor.';
this.debug(msg);
}
// Storage is not available, then pop out a prompt with the reason
if (!isStorageAvailable) {
this.showStorageUnavaliablePrompt(errorMessage);
}
});
},
showStorageUnavaliablePrompt:
function bt_showStorageUnavaliablePrompt(msg) {
var confirm = {
title: 'confirm',
callback: function() {
Service.request('hideCustomDialog');
}
};
Service.request('showCustomDialog',
'cannotReceiveFile', msg, confirm, null);
},
checkStorageSpace: function bt_checkStorageSpace(fileSize, callback) {
if (!callback) {
return;
}
var storage = this._deviceStorage;
var availreq = storage.available();
availreq.onsuccess = function(e) {
switch (availreq.result) {
case 'available':
// skip down to the code below
break;
case 'unavailable':
callback(false, 'sdcard-not-exist2');
return;
case 'shared':
callback(false, 'sdcard-in-use');
return;
default:
callback(false, 'unknown-error');
return;
}
// If we get here, then the sdcard is available, so we need to find out
// if there is enough free space on it
var freereq = storage.freeSpace();
freereq.onsuccess = function() {
if (freereq.result >= fileSize) {
callback(true, '');
} else {
callback(false, 'sdcard-no-space2');
}
};
freereq.onerror = function() {
callback(false, 'cannotGetStorageState');
};
};
availreq.onerror = function(e) {
callback(false, 'cannotGetStorageState');
};
},
isSendFileQueueEmpty: function() {
return this._sendingFilesQueue.length === 0;
},
isFileTransferInProgress: function() {
var jobs = this.transferStatusList.querySelector('div');
return jobs != null;
},
sendFileViaHandover: function bt_sendFileViaHandover(evt) {
var mac = evt.detail.mac;
var blob = evt.detail.blob;
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter !== null) {
var sendingFilesSchedule = {
viaHandover: true,
numberOfFiles: 1,
numSuccessful: 0,
numUnsuccessful: 0
};
this._onFilesSending({detail: sendingFilesSchedule});
// XXX: Bug 915602 - [Bluetooth] Call sendFile api will crash
// the system while device is just paired.
// The paired device is ready to send file.
// Since above issue is existed, we use a setTimeout with 3 secs delay
var waitConnectionReadyTimeoutTime = 3000;
setTimeout(() => {
adapter.sendFile(mac, blob);
}, waitConnectionReadyTimeoutTime);
} else {
var msg = 'Cannot get adapter from system Bluetooth monitor.';
this.debug(msg);
}
},
_onUpdateProgress: function bt__onUpdateProgress(mode, evt) {
switch (mode) {
case 'start':
var transferInfo = evt.detail.transferInfo;
this.initProgress(transferInfo);
break;
case 'progress':
var processedLength = evt.processedLength;
var fileLength = evt.fileLength;
var progress = 0;
if (fileLength === 0) {
//XXX: May need to handle unknow progress
} else if (processedLength > fileLength) {
// According Bluetooth spec.,
// the processed length is a referenced value only.
// XXX: If processed length is bigger than file length,
// show an unknown progress
} else {
progress = processedLength / fileLength;
}
this.updateProgress(progress, evt);
break;
}
},
initProgress: function bt_initProgress(evt) {
// Create progress dynamically in notification center
var address = evt.address;
var transferMode =
(evt.received === true) ?
'bluetooth-receiving-progress2' : 'bluetooth-sending-progress2';
var content =
`<div data-icon="bluetooth-transfer-circle" aria-hidden="true"></div>
<div class="title-container" data-l10n-id="${transferMode}"></div>
<progress value="0" max="1"></progress>`;
var transferTask = document.createElement('div');
transferTask.id = 'bluetooth-transfer-status';
transferTask.className = 'fake-notification';
transferTask.setAttribute('data-id', address);
transferTask.setAttribute('role', 'link');
transferTask.innerHTML = content;
transferTask.addEventListener('click', this._bindOnCancelTransferTask);
this.transferStatusList.appendChild(transferTask);
},
updateProgress: function bt_updateProgress(value, evt) {
var address = evt.address;
var id = 'div[data-id="' + address + '"] progress';
var progressEl = this.transferStatusList.querySelector(id);
progressEl.value = value;
},
removeProgress: function bt_removeProgress(evt) {
var address = evt.address;
var id = 'div[data-id="' + address + '"]';
var finishedTask = this.transferStatusList.querySelector(id);
// If we decline receiving file, Bluetooth won't callback
// 'bluetooth-opp-transfer-start', 'bluetooth-opp-update-progress' event.
// So that there is no progress element which was created on notification.
// There is only 'bluetooth-opp-transfer-complete' event to notify Gaia
// the transferring request in failed case.
if (finishedTask == null) {
return;
}
finishedTask.removeEventListener('click', this._bindOnCancelTransferTask);
this.transferStatusList.removeChild(finishedTask);
},
onCancelTransferTask: function bt_onCancelTransferTask(evt) {
var address = evt.currentTarget.dataset.id;
// Show confirm dialog for user to cancel transferring task
UtilityTray.hide();
this.showCancelTransferPrompt(address);
},
showCancelTransferPrompt: function bt_showCancelTransferPrompt(address) {
var cancel = {
title: 'no',
callback: this.continueTransfer.bind(this)
};
var confirm = {
title: 'yes',
callback: this.cancelTransfer.bind(this, address)
};
Service.request('showCustomDialog',
'cancelFileTransfer',
'cancelFileTransfer',
cancel,
confirm
);
},
continueTransfer: function bt_continueTransfer() {
Service.request('hideCustomDialog');
},
cancelTransfer: function bt_cancelTransfer(address) {
Service.request('hideCustomDialog');
var adapter = Service.query('Bluetooth.getAdapter');
if (adapter !== null) {
adapter.stopSendingFile(address);
} else {
var msg = 'Cannot get adapter from system Bluetooth monitor.';
this.debug(msg);
}
},
/**
* Create notification when transfer complete.
*
* @param {Object} evt bluetooth-opp-transfer-complete event
*/
_onTransferComplete: function bt__onTransferComplete(evt) {
var transferInfo = evt.detail.transferInfo;
// Remove transferring progress
this.removeProgress(transferInfo);
// Show notification
var nData = {
titleL10n: null,
icon: this.bluetoothIcon,
transferInfo: null,
onclick: null
};
if (transferInfo.success === true) {
if (transferInfo.received) {
// Received file can be opened only
nData.titleL10n = 'transferFinished-receivedSuccessful-title';
nData.transferInfo = transferInfo;
nData.onclick = this.openReceivedFile.bind(this, transferInfo);
} else {
nData.titleL10n = 'transferFinished-sentSuccessful-title';
}
} else {
if (transferInfo.received) {
nData.titleL10n = 'transferFinished-receivedFailed-title';
} else {
nData.titleL10n = 'transferFinished-canceled';
}
}
var l10nArgs = {
icon: nData.icon,
data: {
systemMessageTarget: 'BluetoothTransfer'
}
};
if (transferInfo.fileName) {
l10nArgs.body = transferInfo.fileName;
// put original message in data for reference
l10nArgs.data.transferInfo = nData.transferInfo;
} else {
l10nArgs.bodyL10n = 'unknown-file';
}
var promise = NotificationHelper.send(nData.titleL10n, l10nArgs);
if (nData.onclick) {
promise.then(function(notification) {
notification.addEventListener('click', nData.onclick);
});
}
var viaHandover = false;
if (this._sendingFilesQueue.length > 0) {
viaHandover = this._sendingFilesQueue[0].viaHandover || false;
}
// Have a report notification for sending multiple files.
this.summarizeSentFilesReport(transferInfo);
// Inform NfcHandoverManager that the transfer completed
var details = {received: transferInfo.received,
success: transferInfo.success,
viaHandover: viaHandover};
window.dispatchEvent(new CustomEvent('nfc-transfer-completed', {
detail: details}));
},
/**
* Handle related system notification message operation after reboot.
* Need close notification message by ourself.
*
* @param {Object} message system notification message
*/
handleSystemMessageNotification: function(message) {
this.debug('Received system message: ' + JSON.stringify(message));
if (message.data && message.data.transferInfo) {
var evt = {
fileName: message.data.transferInfo.fileName,
contentType: message.data.transferInfo.contentType
};
this.openReceivedFile(evt);
} else {
console.error('The notification does not carry correct data');
}
this.closeSystemMessageNotification(message);
},
/**
* Close the system notification message.
*
* @param {Object} message system notification message
*/
closeSystemMessageNotification: function(message) {
Notification.get({ tag: message.tag }).then(notifs => {
notifs.forEach(notif => {
// check on the body
if (notif.body === message.body) {
notif.close && notif.close();
}
});
});
},
summarizeSentFilesReport:
function bt_summarizeSentFilesReport(transferInfo) {
// Ignore received files
if (transferInfo.received) {
return;
}
// Consumer: System app consume each sending file request from Bluetooth
// app
var msg = 'remove the finished sending task from queue, ' +
'queue length = ';
var successful = transferInfo.success;
var sendingFilesSchedule = this._sendingFilesQueue[0];
var numberOfFiles = sendingFilesSchedule.numberOfFiles;
if (numberOfFiles == 1) { // for sent one file only.
// We don't need to summarize a report for sent one file only.
// Remove the finished sending task from the queue
this._sendingFilesQueue.shift();
msg += this._sendingFilesQueue.length;
this.debug(msg);
} else { // The scheduled task is for sent multiple files.
// Create a report in notification.
// Record each transferring report.
if (successful) {
this._sendingFilesQueue[0].numSuccessful++;
} else {
this._sendingFilesQueue[0].numUnsuccessful++;
}
var numSuccessful = this._sendingFilesQueue[0].numSuccessful;
var numUnsuccessful = this._sendingFilesQueue[0].numUnsuccessful;
if ((numSuccessful + numUnsuccessful) == numberOfFiles) {
// In this item of queue, all files were sent completely.
NotificationHelper.send('transferReportTitle', {
'bodyL10n': {
id: 'transferReport-description',
args: {
numSuccessful: numSuccessful,
numUnsuccessful: numUnsuccessful
}
},
'icon': this.bluetoothIcon
});
// Remove the finished sending task from the queue
this._sendingFilesQueue.shift();
msg += this._sendingFilesQueue.length;
this.debug(msg);
}
}
},
openReceivedFile: function bt_openReceivedFile(evt) {
// Launch the gallery with an open activity to view this specific photo
// XXX: Bug 897434 - Save received/downloaded files in one specific folder
// with meaningful path and filename
var filePath = 'Download/Bluetooth/' + evt.fileName;
var contentType = evt.contentType;
var storageType = 'sdcard';
var storage = navigator.getDeviceStorage(storageType);
var getreq = storage.get(filePath);
var loader = LazyLoader.load(['shared/js/mime_mapper.js']);
Promise.all([getreq, loader]).then(() => {
var file = getreq.result;
// When we got the file by storage type of "sdcard"
// use the file.type to replace the empty fileType which is given by API
var fileName = file.name;
var extension = fileName.split('.').pop();
var originalType = file.type || contentType;
var mappedType = (MimeMapper.isSupportedType(originalType)) ?
originalType : MimeMapper.guessTypeFromExtension(extension);
var a = new MozActivity({
name: mappedType == 'text/vcard' ? 'import' : 'open',
data: {
type: mappedType,
blob: file,
// XXX: https://bugzilla.mozilla.org/show_bug.cgi?id=812098
// Pass the file name for Music APP since it can not open blob
filename: fileName
}
});
a.onerror = (e) => {
var msg = 'open activity error:' + a.error.name;
this.debug(msg);
switch (a.error.name) {
case 'NO_PROVIDER':
UtilityTray.hide();
// Cannot identify MIMETYPE
// So, show cannot open file dialog with unknow media type
this.showUnknownMediaPrompt(fileName);
return;
case 'ActivityCanceled':
return;
case 'USER_ABORT':
return;
default:
return;
}
};
a.onsuccess = (e) => {
var msg = 'open activity onsuccess';
this.debug(msg);
};
}, (err) => {
if (getreq.readyState === 'done' && getreq.error) {
// DeviceStorage Error
var msg = 'failed to get file:' + filePath + ' ' + err.name;
this.debug(msg);
} else { // LazyLoader Error
throw err;
}
}).catch((err) => {
console.error(err);
});
},
showUnknownMediaPrompt: function bt_showUnknownMediaPrompt(fileName) {
var confirm = {
title: 'confirm',
callback: function() {
Service.request('hideCustomDialog');
}
};
var body = {id: 'unknownMediaTypeToOpenFile', args: {fileName: fileName}};
Service.request('showCustomDialog',
'cannotOpenFile', body, confirm, null);
}
};
exports.BluetoothTransfer = BluetoothTransfer;
})(window);

View file

@ -0,0 +1,23 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var BluetoothTransferIcon = function(manager) {
BaseIcon.call(this, manager);
};
BluetoothTransferIcon.prototype = Object.create(BaseIcon.prototype);
BluetoothTransferIcon.prototype.name = 'BluetoothTransferIcon';
BluetoothTransferIcon.prototype.shouldDisplay = function() {
return this.manager.isOPPProfileConnected;
};
BluetoothTransferIcon.prototype.view = function view() {
return `<div id="statusbar-bluetooth-transfer"
class="sb-icon sb-icon-bluetooth-transfer"
role="listitem" hidden
data-l10n-id="statusbarBluetoothTransfer">
</div>`;
};
exports.BluetoothTransferIcon = BluetoothTransferIcon;
}(window));

View file

@ -0,0 +1,575 @@
'use strict';
/**
* Bluetooth2 is compatible with Bluetooth APIv2 and used to enable/
* disable bluetooth hardware, get bluetooth adapter, and check target
* bluetooth profile is connected.
*
* Bluetooth api v2 is the two level structure.
* We use _btManagerHandler to handle BluetoothManager defaultAdapter
* change event and _btAdapterHandler to handle status change event.
*
* Current supported Bluetooth profiles are:
* - hfp: Hands-Free Profile
* - opp: Object Push Profile
* - a2dp: Advenced Audio Distribution Profile
* - sco: Synchronous Connection-Oriented Profile
*/
/* global Service, LazyLoader, BluetoothIcon, BluetoothTransferIcon,
BluetoothHeadphoneIcon, BluetoothTransfer */
/* exported Bluetooth2 */
(function(exports) {
var Bluetooth = function() {};
Bluetooth.prototype = {
name: 'Bluetooth',
/**
* Debug message.
*
* @public
* @type {Boolean} turn on/off the console log
*/
onDebug: false,
/**
* Store a reference of the default adapter.
* If the adapter is not availble, no bluetooth operation could be
* executed.
*
* @private
*/
_adapter: null,
/**
* Hold instance of bluetooth adapter state change handler.
*
* @private
*/
_bindBtAdapterHandler: null,
/**
* Hold instance of HFP status changed handler.
*
* @private
*/
_bindHfpStatusChangedHandler: null,
/**
* Hold instance of A2DP status changed handler.
*
* @private
*/
_bindA2dpStatusChangedHandler: null,
/**
* Hold instance of SCO status changed handler.
*
* @private
*/
_bindScoStatusChangedHandler: null,
/**
* Store a reference for mozBluetooth.
*
* @private
*/
_bluetooth: null,
/**
* State of Bluetooth default adapter.
*
* @private
*/
_isEnabled: false,
/**
* Set profile connect state.
*
* @private
* @param {String} profile profile id
* @param {Boolean} connected connect status
*/
_setProfileConnected: function bt_setProfileConnected(profile, connected) {
var value = this['_' + profile + 'Connected'];
if (value !== connected) {
this['_' + profile + 'Connected'] = connected;
// Raise an event for the profile connection changes.
window.dispatchEvent(new CustomEvent('bluetoothprofileconnectionchange',
{
detail: {
name: profile,
connected: connected
}
}
));
// Update Bluetooth and profile icons
this.icon && this.icon.update();
if (profile === 'opp' && this.transferIcon) {
this.transferIcon.update();
}
if (profile === 'a2dp' && this.headphoneIcon) {
this.headphoneIcon.update();
}
}
},
/**
* Check if bluetooth profile is connected.
*
* @private
* @param {String} profile profile name
* @return {Boolean} connected state
*/
_isProfileConnected: function bt__isProfileConnected(profile) {
var isConnected = this['_' + profile + 'Connected'];
if (isConnected === undefined) {
return false;
} else {
return isConnected;
}
},
/**
* Initialize bluetooth module.
*
* @public
*/
start: function bt_start() {
if (!window.navigator.mozSettings || !window.navigator.mozBluetooth) {
return;
}
this._bluetooth = window.navigator.mozBluetooth;
// hold the instances so we can remove it when needed
this._bindBtAdapterHandler =
this._btAdapterHandler.bind(this);
this._bindHfpStatusChangedHandler = function(evt) {
this._setProfileConnected('hfp', evt.status);
}.bind(this);
this._bindA2dpStatusChangedHandler = function(evt) {
this._setProfileConnected('a2dp', evt.status);
}.bind(this);
this._bindScoStatusChangedHandler = function(evt) {
this._setProfileConnected('sco', evt.status);
}.bind(this);
this._initDefaultAdapter();
this._bluetooth.addEventListener('attributechanged',
this._btManagerHandler.bind(this));
// decouple enable/disable bluetooth function from other system part
window.addEventListener('request-enable-bluetooth',
this._requestEnableHandler.bind(this));
window.addEventListener('request-disable-bluetooth',
this._requestDisableHandler.bind(this));
// relay bluetooth transfer system messages
navigator.mozSetMessageHandler('bluetooth-opp-transfer-start',
this._oppTransferStartHandler.bind(this));
navigator.mozSetMessageHandler('bluetooth-opp-transfer-complete',
this._oppTransferCompleteHandler.bind(this));
// expose functions to Service.request
Service.register('adapter', this);
Service.register('pair', this);
Service.register('getPairedDevices', this);
// expose functions to Service.query
Service.registerState('isEnabled', this);
Service.registerState('getAdapter', this);
Service.registerState('isOPPProfileConnected', this);
Service.registerState('isA2DPProfileConnected', this);
Service.registerState('isSCOProfileConnected', this);
// handle statusbar icons
LazyLoader.load(['js/bluetooth_transfer.js',
'js/bluetooth_icon.js',
'js/bluetooth_transfer_icon.js',
'js/bluetooth_headphone_icon.js']).then(() => {
BluetoothTransfer.start();
this.icon = new BluetoothIcon(this);
this.icon.start();
this.transferIcon = new BluetoothTransferIcon(this);
this.transferIcon.start();
this.headphoneIcon = new BluetoothHeadphoneIcon(this);
this.headphoneIcon.start();
}).catch(function(err) {
console.error(err);
});
},
stop: function() {
Service.unregister('adapter', this);
Service.unregister('pair', this);
Service.unregister('getPairedDevices', this);
Service.unregisterState('isEnabled', this);
Service.unregisterState('getAdapter', this);
Service.unregisterState('isOPPProfileConnected', this);
Service.unregisterState('isA2DPProfileConnected', this);
Service.unregisterState('isSCOProfileConnected', this);
},
/**
* Generally bluetooth adapter is already available when start
* this module and no related event will be received.
* so we try to get the adapter then listen to mozBluetooth change
* to prevent race condition
*
* @private
*/
_initDefaultAdapter: function() {
this.debug('init bluetooth adapter');
this._adapter = this._bluetooth.defaultAdapter;
if (this._adapter) {
this.debug('adapter is available');
// listen states when the adapter is available
this._adapterAvailableHandler();
// dispatch default bluetooth enable state
this._isEnabled = (this._adapter.state === 'enabled');
this._dispatchEnableState();
}
},
/**
* Once adapter is available, listen to bluetooth state and
* the connect stat of supported profiles.
*
* we only support two use cases for bluetooth connection:
* 1. connecting with a headset
* 2. transfering a file to/from another device
* So we need to listen to corresponding events to know we are (aren't)
* connected, then summarize to an event and dispatch to Statusbar
*
* @private
*/
_adapterAvailableHandler: function bt__adapterAvailableHandler() {
// listen to adapter state
this._adapter.addEventListener('attributechanged',
this._bindBtAdapterHandler);
// headset connected cases
this._adapter.addEventListener('hfpstatuschanged',
this._bindHfpStatusChangedHandler);
this._adapter.addEventListener('a2dpstatuschanged',
this._bindA2dpStatusChangedHandler);
this._adapter.addEventListener('scostatuschanged',
this._bindScoStatusChangedHandler);
},
/**
* Remove all EventListeners when default adapter is changed.
*/
_adapterUnavailableHandler: function bt__adapterUnavailableHandler() {
this.debug('default adapter is removed');
// unbind headset connected cases
this._adapter.removeEventListener('hfpstatuschanged',
this._bindHfpStatusChangedHandler);
this._adapter.removeEventListener('a2dpstatuschanged',
this._bindA2dpStatusChangedHandler);
this._adapter.removeEventListener('scostatuschanged',
this._bindScoStatusChangedHandler);
// unbind adapter
this._adapter.removeEventListener('attributechanged',
this._bindBtAdapterHandler);
},
/**
* Update settings value and enable bluetooth hardware.
*
* @private
*/
_requestEnableHandler: function bt__requestEnableHandler() {
this.debug('enabling bluetooth');
if (this._adapter) {
// the state will be dispatched in _btAdapterHandler
this._adapter.enable().then(() => { //resolve
this.debug('bluetooth enabled');
}, () => { //reject
this.debug('can not get bluetooth adapter');
});
} else { // return current state since we can't process the request
this.debug('adapter is not available');
this._dispatchEnableState();
}
},
/**
* Update settings value and disable bluetooth hardware.
*
* @private
*/
_requestDisableHandler: function bt__requestDisableHandler() {
this.debug('disabling bluetooth');
if (this._adapter) {
// the state will be dispatched in _btAdapterHandler
this._adapter.disable().then(() => { //resolve
this.debug('bluetooth disabled');
}, () => { //reject
this.debug('can not get bluetooth adapter');
});
} else { // return current state since we can't process the request
this.debug('adapter is not available');
this._dispatchEnableState();
}
},
/*
* In file transfering case:
* since System Message can't be listened in two js files within a app,
* so we listen here but dispatch events to bluetooth_transfer.js
* while getting bluetooth file transfer start system messages
*
* @private
*/
_oppTransferStartHandler: function(transferInfo) {
this._setProfileConnected('opp', true);
window.dispatchEvent(new CustomEvent('bluetooth-opp-transfer-start',
{
detail: { transferInfo: transferInfo }
}
));
},
/*
* In file transfering case:
* since System Message can't be listened in two js files within a app,
* so we listen here but dispatch events to bluetooth_transfer.js
* while getting bluetooth file transfer complete system messages
*
* @private
*/
_oppTransferCompleteHandler: function(transferInfo) {
this._setProfileConnected('opp', false);
window.dispatchEvent(new CustomEvent('bluetooth-opp-transfer-complete',
{
detail: { transferInfo: transferInfo }
}
));
},
/**
* BT APIv2: Watch 'onattributechanged' event from
* mozBluetooth.defaultAdapter for updating state information.
*
* @private
* @param {Object} evt event object
*/
_btAdapterHandler:
function bt__btAdapterHandler(evt) {
for (var i in evt.attrs) {
switch (evt.attrs[i]) {
case 'state':
if (this._adapter.state === 'enabled') {
this.debug('state true');
this._isEnabled = true;
navigator.mozSettings.createLock()
.set({'bluetooth.enabled': true});
window.dispatchEvent(new CustomEvent('bluetooth-enabled'));
this.icon && this.icon.update();
} else if (this._adapter.state === 'disabled') {
this.debug('state false');
this._isEnabled = false;
navigator.mozSettings.createLock()
.set({'bluetooth.enabled': false});
window.dispatchEvent(new CustomEvent('bluetooth-disabled'));
this.icon && this.icon.update();
}
break;
default:
break;
}
}
},
/**
* Watch 'onattributechanged' event from mozBluetooth for updating default
* adapter information.
*
* 'onattributechanged' event description:
* A handler to trigger when bluetooth manager's only property
* defaultAdapter has changed.
*
* Here are three adapter change cases:
*
* null -> adapter: set new adapter and dispatch adapter state.
* adapter -> null: clear defaultAdapter and listener.
* adapterA -> adapterB: clean defaultAdapter and listener,
* then set new adapter and dispatch adapter state.
*
* @private
* @param {Object} evt event object
*/
_btManagerHandler:
function bt__btManagerHandler(evt) {
for (var i in evt.attrs) {
switch (evt.attrs[i]) {
// Default adapter attribute change.
// Usually, it means that we reach new default adapter.
case 'defaultAdapter':
this.debug('defaultAdapter changed.');
if (this._bluetooth.defaultAdapter) {
if (this._adapter !== this._bluetooth.defaultAdapter) {
if (this._adapter !== null) { // adapter A -> adapter B
this.debug('origin adapter is removed');
this._adapterUnavailableHandler();
this._adapter = null;
}
// set new adapter and dispatch adapter state
this.debug('new adapter is added');
this._adapter = this._bluetooth.defaultAdapter;
this._isEnabled = (this._adapter.state === 'enabled');
// listen states when the adapter is available
this._adapterAvailableHandler();
}
} else { // adapter -> null
this.debug('default adapter is removed');
this._adapterUnavailableHandler();
this._adapter = null;
}
break;
default:
break;
}
}
},
/**
* Dispatch bluetooth enable/disable state to system.
*
* @private
*/
_dispatchEnableState: function bt__dispatchEnableState() {
if (this._isEnabled) {
window.dispatchEvent(new CustomEvent('bluetooth-enabled'));
} else {
window.dispatchEvent(new CustomEvent('bluetooth-disabled'));
}
},
/**
* Get adapter from bluetooth through promise interface.
* XXX: the function abstract the Bluetooth API difference.
* We can remove it and use service query once BTv1 is deprecated.
*
* @public
* @return {Promise} A promise that resolve the Bluetooth Adapter
*/
adapter: function bt__adapter() {
return new Promise((resolve, reject) => {
if (this._adapter !== null) {
resolve(this._adapter);
} else {
this.debug('No BT adapter retrieved');
reject();
}
});
},
/**
* Return device pairing result.
* XXX: the function abstract the Bluetooth API difference.
*
* @public
* @param {string} mac target device address
* @return {Promise} A promise that resolve when pair successfully,
* reject when pair failed
*/
pair: function bt__pair(mac) {
return this._adapter.pair(mac);
},
/**
* Return paired devices list.
* XXX: the function abstract the Bluetooth API difference.
* We can use service query to return list once BTv1 is deprecated.
*
* @public
* @returns {Object[]} sequence of BluetoothDevice
*/
getPairedDevices: function bt__getPairedDevices() {
return this._adapter.getPairedDevices();
},
/**
* Return bluetooth adapter.
*
* @public
*/
get getAdapter() {
return this._adapter;
},
/**
* Maintain bluetooth enable/disable state.
*
* @public
*/
get isEnabled() {
return this._isEnabled;
},
/**
* Check if bluetooth OPP profile is connected.
*
* @public
* @return {Boolean} connected state
*/
get isOPPProfileConnected() {
return this._isProfileConnected('opp');
},
/**
* Check if bluetooth A2DP profile is connected.
*
* @public
* @return {Boolean} connected state
*/
get isA2DPProfileConnected() {
return this._isProfileConnected('a2dp');
},
/**
* Check if bluetooth SCO profile is connected.
*
* @public
* @return {Boolean} connected state
*/
get isSCOProfileConnected() {
return this._isProfileConnected('sco');
},
/**
* Check if any of bluetooth profiles is connected.
* Referenced by Bluetooth icon update
*
* @public
* @return {Boolean} connected state
*/
get connected() {
return this._isProfileConnected('hfp') ||
this._isProfileConnected('a2dp') ||
this._isProfileConnected('opp');
},
/**
* Console log.
*
* @private
* @param {Object} msg debug message
*/
debug: function bt_debug(msg) {
if (!this.onDebug) {
return;
}
console.log('[System Bluetooth]: ' + msg);
}
};
exports.Bluetooth2 = Bluetooth;
})(window);

View file

@ -0,0 +1,35 @@
/* global App */
'use strict';
(function() {
/**
* This file should never be changed or add new functionalities.
* The responsibility is only start the app instance and catch
* the error from the start function.
*
* The main reason is this file is not test-able so
* it should not grow anyway.
*
* If you have a new module, please find a proper parent
* in the dependency tree and put it there.
*/
window.app = new App();
var startApp = function() {
window.app.start().catch(function(err) {
console.error(err);
console.trace();
});
};
if (document.readyState !== 'loading') {
startApp();
} else {
document.addEventListener('readystatechange',
function readyStateChange() {
if (document.readyState == 'interactive') {
document.removeEventListener('readystatechange',
readyStateChange);
startApp();
}
});
}
}());

View file

@ -0,0 +1,84 @@
/* global UrlHelper, AppWindow, BrowserConfigHelper, LazyLoader,
SettingsListener */
(function(exports) {
'use strict';
function handleSettingChange (value) {
Browser.privateByDefault = value;
}
SettingsListener.observe('browser.private.default', false,
handleSettingChange);
function handleOpenUrl(url, isPrivate) {
var config = new BrowserConfigHelper({url: url});
config.oop = true;
config.isPrivate = isPrivate;
var newApp = new AppWindow(config);
newApp.requestOpen();
}
function Browser() {}
Browser.privateByDefault = false;
Browser.prototype = {
start: function() {
window.addEventListener('new-private-window',
this.newWindow.bind(this));
window.addEventListener('new-non-private-window',
this.newWindow.bind(this));
window.addEventListener('activity-view',
this.handleActivity.bind(this));
},
handleActivity: function(e) {
// Activities can send multiple names, right now we only handle
// one so we only filter on types
var data = e.detail.source.data;
switch (data.type) {
case 'url':
LazyLoader.load(['shared/js/url_helper.js']).then(function() {
var url = UrlHelper.getUrlFromInput(data.url);
var isPrivate = data.hasOwnProperty('isPrivate') ?
data.isPrivate : Browser.privateByDefault;
handleOpenUrl(url, isPrivate);
}).catch(function(err) {
console.error(err);
});
break;
}
},
/**
* Opens a new browser window.
*/
newWindow: function(e) {
var basePath = 'app://search.gaiamobile.org';
var isPrivate = e && e.type === 'new-private-window';
var privateFlag = isPrivate ? '1' : '0';
var config = new BrowserConfigHelper({
url: basePath + '/newtab.html?private=' + privateFlag,
manifestURL: basePath + '/manifest.webapp'
});
if (isPrivate) {
config.isMockPrivate = true;
config.isPrivate = true;
}
config.oop = true;
var newApp = new AppWindow(config);
newApp.requestOpen();
}
};
exports.Browser = Browser;
}(window));

View file

@ -0,0 +1,105 @@
/* global applications, ManifestHelper */
'use strict';
(function(exports) {
var isOutOfProcessDisabled = false;
/**
* This class generates browser configuration object
* from manifestURL or src provided.
*
* The configuration object is used for BrowserFrame generation.
*
* * If manifestURL is provided, we would treat it as a web app,
* and then use Applications to fetch the remaining info we need
* to construct the options for mozbrowser iframe.
*
* * If only URL is provided, we would treat it as a web page.
*
* The returned configuration object contains:
*
* * Origin: the same as appURL.
* * manifestURL: the same as manifestURL.
*
* * manifest: the parsed manifest object.
* If the app is not an entry point app,
* the manifest would be the reference of application manifest
* stored in Applications module.
* But if the app is an entry point app,
* we will do deep clone to generate a new object and
* replace the properties of entry point to proper position.
*
* * name: the name of the app, retrieved from manifest.
*
* * oop: indicate it's running out of process or in process.
*
* @param {Object} [config] Config for creating appWindow.
* @param {String} [config.appURL] The URL of the app or the page to be
* opened.
* @param {String} [config.manifestURL] The manifest URL of the app.
* @param {String} [config.name] - optional The name of the app.
* @param {DOMFRAMEElement} [config.iframe] - optionalThe exisiting frame to
* inject.
*/
var BrowserConfigHelper = function(config) {
var app = config.manifestURL &&
applications.getByManifestURL(config.manifestURL);
this.url = config.url;
if (app) {
var manifest = app.manifest;
var name = new ManifestHelper(manifest).name;
var origin = app.origin;
// Check if it's a virtual app from a entry point.
// If so, change the app name and origin to the
// entry point.
var entryPoints = manifest.entry_points;
if (entryPoints && manifest.type == 'certified') {
// Do deep copy to avoid reference to be overwritten
// only when we're entry points.
manifest = JSON.parse(JSON.stringify(manifest));
var givenPath = this.url.substr(origin.length);
// Workaround here until the bug (to be filed) is fixed
// Basicly, gecko is sending the URL without launch_path sometimes
for (var ep in entryPoints) {
var currentEp = entryPoints[ep];
var path = givenPath;
if (path.indexOf('?') != -1) {
path = path.substr(0, path.indexOf('?'));
}
//Remove the origin and / to find if if the url is the entry point
if (path.indexOf('/' + ep) === 0 &&
(currentEp.launch_path === path)) {
origin = origin + currentEp.launch_path;
name = new ManifestHelper(currentEp).name;
for (var key in currentEp) {
if (key !== 'locale' && key !== 'name') {
manifest[key] = currentEp[key];
}
}
}
}
}
if (!isOutOfProcessDisabled) {
this.oop = true;
}
this.name = name;
this.manifestURL = config.manifestURL;
this.origin = origin;
this.manifest = manifest;
} else {
this.iframe = config.iframe;
this.isPrivate = config.isPrivate;
this.name = config.name || '';
this.origin = config.url;
this.manifestURL = '';
this.manifest = null;
}
};
exports.BrowserConfigHelper = BrowserConfigHelper;
})(window);

View file

@ -0,0 +1,379 @@
/* global MozActivity, IconsHelper, LazyLoader */
/* global BaseModule, Service */
(function(window) {
'use strict';
var _ = navigator.mozL10n.get;
const PINNING_PREF = 'dev.gaia.pinning_the_web';
const SITE_ICON_SIZE = 72;
/**
* The ContextMenu of the AppWindow.
*
* @class BrowserContextMenu
* @param {AppWindow} app The app window instance
* where this dialog should popup.
*/
function BrowserContextMenu(app) {
this.app = app;
this.containerElement = app.element;
this.app.element.addEventListener('mozbrowsercontextmenu', this);
return this;
}
BrowserContextMenu.SUB_MODULES = [
'ContextMenuView'
];
BaseModule.create(BrowserContextMenu, {
name: 'BrowserContextMenu',
handleEvent: function(evt) {
switch (evt.type) {
case 'mozbrowsercontextmenu':
if (!Service.query('isFtuRunning')) {
this.show(evt);
}
break;
}
},
show: function(evt) {
var detail = evt.detail;
var hasContextMenu = detail.contextmenu &&
detail.contextmenu.items.length > 0 &&
detail.contextmenu.customized;
var hasSystemTargets = detail.systemTargets &&
detail.systemTargets.length > 0;
// Nothing to show
if (!hasSystemTargets && !hasContextMenu) {
return;
}
// context menus in certified apps that only have system targets are
// currently disabled. https://bugzil.la/1010160 is tracking reenabling
if (!hasContextMenu && hasSystemTargets && this.app.isCertified()) {
return;
}
var items = this._listItems(detail);
if (!items.length) {
return;
}
evt.preventDefault();
evt.stopPropagation();
// This temporary check will be removed as soon as
// Pinning the Web lands without a Setting. We won't need
// the second call to _listItems anymore then.
this._getPinningEnabled(function(value) {
this.pinningEnabled = value;
items = this._listItems(detail);
// Notify the embedder we are handling the context menu
this.contextMenuView.show(items);
}.bind(this));
},
_listItems: function(detail) {
var items = [];
// contextmenu.items are specified by the web content via html5
// context menu api and a specific system command, copy-image.
if (detail.contextmenu && detail.contextmenu.items.length) {
detail.contextmenu.items.forEach(function(choice, index) {
var itemObj = null;
switch (choice.id) {
case 'copy-image':
case 'copy-link':
itemObj = {
id: choice.id,
label: _(choice.id),
};
break;
default:
// Customized menu items
itemObj = {
label: choice.label,
icon: choice.icon,
};
}
itemObj.callback = () => {
detail.contextMenuItemSelected(choice.id);
};
items.push(itemObj);
}, this);
}
if (detail.systemTargets) {
detail.systemTargets.forEach(function(item) {
this.generateSystemMenuItem(item).forEach(function(menuItem) {
items.push(menuItem);
});
}, this);
}
return items;
},
_getPinningEnabled: function(callback) {
var req = navigator.mozSettings.createLock().get(PINNING_PREF);
req.onsuccess = () => {
callback(req.result[PINNING_PREF]);
};
req.onerror = () => {
callback(false);
};
},
isShown: function() {
return this.contextMenuView.isShown();
},
hide: function() {
if (!this.contextMenuView.isShown()) {
return;
}
this.contextMenuView.hide();
},
openUrl: function(url, isPrivate) {
/* jshint nonew: false */
new MozActivity({
name: 'view',
data: {
type: 'url',
url: url,
isPrivate: isPrivate
}
});
},
shareUrl: function(url) {
/* jshint nonew: false */
new MozActivity({
name: 'share',
data: {
type: 'url',
url: url
}
});
},
_getSaveUrlItem: function(url, name) {
if (this.pinningEnabled) {
return {
id: 'pin-to-home-screen',
label: _('pin-to-home-screen'),
callback: this.pinUrl.bind(this, url, name)
};
}
return {
id: 'add-to-homescreen',
label: _('add-link-to-home-screen'),
callback: this.bookmarkUrl.bind(this, url, name)
};
},
bookmarkUrl: function(url, name) {
var favicons = this.app.favicons;
/* jshint nonew: false */
var data = {
type: 'url',
url: url,
name: name,
iconable: false
};
if (this.app.webManifestURL) {
data.manifestURL = this.app.webManifestURL;
}
LazyLoader.load('shared/js/icons_helper.js', (() => {
IconsHelper.getIcon(url, null, {icons: favicons}).then(icon => {
if (icon) {
data.icon = icon;
}
new MozActivity({
name: 'save-bookmark',
data: data
});
});
}));
},
pinUrl: function(url, name) {
var data = {
name: name,
type: 'url',
url: url,
iconable: false,
title: this.app.title,
meta: this.app.meta,
app: this.app
};
this.app.getSiteIconUrl(SITE_ICON_SIZE)
.then(iconObject => {
if (iconObject) {
data.icon = iconObject.blob;
}
this.app.getScreenshot(() => {
data.meta.screenshot = this.app._screenshotBlob;
this.publish('pin-page-dialog-requestopen', data);
});
})
.catch((err) => {
this.app.debug('bookmarkUrl, error from getSiteIcon: %s', err);
this.app.getScreenshot(() => {
data.meta.screenshot = this.app._screenshotBlob;
this.publish('pin-page-dialog-requestopen', data);
});
});
},
newWindow: function(manifest, isPrivate) {
// For private windows we create an empty private app window.
if (isPrivate) {
window.dispatchEvent(new CustomEvent('new-private-window'));
} else {
window.dispatchEvent(new CustomEvent('new-non-private-window'));
}
},
showWindows: function(manifest) {
window.dispatchEvent(
new CustomEvent('taskmanagershow',
{ detail: { filter: 'browser-only' }})
);
},
generateSystemMenuItem: function(item) {
var nodeName = item.nodeName.toUpperCase();
var documentURI = item.data.documentURI;
var uri = item.data.uri;
switch (nodeName) {
case 'A':
return [{
id: 'open-in-new-window',
label: _('open-in-new-window'),
callback: this.openUrl.bind(this, uri)
}, {
id: 'open-in-new-private-window',
label: _('open-in-new-private-window'),
callback: this.openUrl.bind(this, uri, true)
}, {
id: 'save-link',
label: _('save-link'),
callback: this.app.browser.element.download.bind(
this.app.browser.element, uri, { referrer: documentURI })
}, {
id: 'share-link',
label: _('share-link'),
callback: this.shareUrl.bind(this, uri)
}];
case 'IMG':
case 'VIDEO':
case 'AUDIO':
var typeMap = {
'IMG': 'image',
'VIDEO': 'video',
'AUDIO': 'audio'
};
var type = typeMap[nodeName];
if (nodeName === 'VIDEO' && !item.data.hasVideo) {
type = 'audio';
}
return [{
id: 'save-' + type,
label: _('save-' + type),
callback: this.app.browser.element.download.bind(
this.app.browser.element, uri, { referrer: documentURI })
}, {
id: 'share-' + type,
label: _('share-' + type),
callback: this.shareUrl.bind(this, uri)
}];
default:
return [];
}
},
showDefaultMenu: function(manifest, name) {
return new Promise((resolve) => {
this._getPinningEnabled(function(value) {
this.pinningEnabled = value;
var config = this.app.config;
var menuData = [];
var finish = () => {
this.contextMenuView.show(menuData);
resolve();
};
menuData.push({
id: 'new-window',
label: _('new-window'),
callback: this.newWindow.bind(this, manifest)
});
menuData.push({
id: 'new-private-window',
label: _('new-private-window'),
callback: this.newWindow.bind(this, manifest, true)
});
menuData.push({
id: 'show-windows',
label: _('show-windows'),
callback: this.showWindows.bind(this)
});
// Do not show the bookmark/share buttons if the url starts with app.
// This is because in some cases we use the app chrome to view system
// pages. E.g., private browsing.
if (config.url.startsWith('app')) {
finish();
return;
}
menuData.push(this._getSaveUrlItem(config.url, name));
menuData.push({
id: 'share',
label: _('share'),
callback: this.shareUrl.bind(this, config.url)
});
finish();
}.bind(this));
});
},
_stop: function() {
this.contextMenuView.destroy();
this.app.element.removeEventListener('mozbrowsercontextmenu', this);
this.containerElement = null;
this.app = null;
}
});
BrowserContextMenu.prototype.focus = function() {
this.contextMenuView.focus();
};
}(this));

View file

@ -0,0 +1,9 @@
;'use strict';(function(exports){var nextId=0;var BrowserFrame=function BrowserFrame(){this.element=null;this._id=nextId++;createFrame.apply(this,arguments);return this;};BrowserFrame.prototype.CLASS_NAME='browser';function createFrame(config,frame){var browser=frame||document.createElement('iframe');browser.setAttribute('mozbrowser','true');browser.name=config.window_name||'main';if(config.oop){browser.setAttribute('remote','true');}
if(config.isPrivate&&!config.isMockPrivate){browser.setAttribute('mozprivatebrowsing','true');}
if(config.manifestURL){browser.setAttribute('mozapp',config.manifestURL);if(config.isSystemMessage){browser.setAttribute('expecting-system-message','expecting-system-message');}}
if(config.parentApp){browser.setAttribute('parentapp',config.parentApp);}
if(config.isInputMethod){browser.setAttribute('mozpasspointerevents','true');browser.setAttribute('ignoreuserfocus','true');}else{browser.setAttribute('mozallowfullscreen','true');}
setMozAppType(browser,config);if(config.url){browser.src=config.url;browser.dataset.url=config.url;}
browser.id=this.CLASS_NAME+this._id;browser.classList.add(this.CLASS_NAME);this.config=config;this.element=browser;}
function setMozAppType(iframe,config){if(config.url.startsWith(window.location.protocol+'//callscreen.gaiamobile.org')||config.url.startsWith(window.location.protocol+'//clock.gaiamobile.org')){iframe.setAttribute('mozapptype','critical');}else if(config.isHomescreen){iframe.setAttribute('mozapptype','homescreen');}else if(config.isSearch){iframe.setAttribute('mozapptype','search');}else if(config.isInputMethod){iframe.setAttribute('mozapptype','inputmethod');}}
exports.BrowserFrame=BrowserFrame;}(window));

View file

@ -0,0 +1,139 @@
'use strict';
(function(exports) {
var BrowserKeyEventManager = function BrowserKeyEventManager() {
};
BrowserKeyEventManager.prototype = {
KEY_EVENTS: Object.freeze([
'mozbrowserbeforekeydown',
'mozbrowserbeforekeyup',
'mozbrowserafterkeydown',
'mozbrowserafterkeyup',
'keydown',
'keyup'
]),
SYSTEM_ONLY_KEYS: Object.freeze([
'power',
'home',
'mozhomescreen',
'exit'
]),
APP_CANCELLED_KEYS: Object.freeze([
'volumeup',
'volumedown',
'camera'
]),
// Home key has different .key values on different devices.
HOME_KEY_ALIAS: Object.freeze([
'home',
'mozhomescreen',
'exit'
]),
TRANSLATION_TABLE: Object.freeze({
'power': 'sleep-button',
'exit': 'home-button',
'home': 'home-button',
'mozhomescreen': 'home-button',
'volumeup': 'volume-up-button',
'volumedown': 'volume-down-button',
'camera': 'camera-button'
}),
_getLowerCaseKeyName: function bkem_getLowerCaseKeyName(event) {
return event.key && event.key.toLowerCase();
},
_isSystemOnlyKey: function bkem_isSystemOnlyKey(event) {
var key = this._getLowerCaseKeyName(event);
return (this.SYSTEM_ONLY_KEYS.indexOf(key) > -1);
},
_isAppCancelledKey: function bkem_isAppCancelledKey(event) {
var key = this._getLowerCaseKeyName(event);
return (this.APP_CANCELLED_KEYS.indexOf(key) > -1);
},
_isBeforeEvent: function bkem_isBeforeEvent(event) {
return event.type === 'mozbrowserbeforekeyup' ||
event.type === 'mozbrowserbeforekeydown';
},
_isAfterEvent: function bkem_isAfterEvent(event) {
return event.type === 'mozbrowserafterkeyup' ||
event.type === 'mozbrowserafterkeydown';
},
_isKeyEvent: function bkem_isKeyEvent(event) {
return event.type === 'keyup' ||
event.type === 'keydown';
},
_targetToIframe: function bkem_targetToIframe(event) {
return (event.target instanceof HTMLIFrameElement);
},
// This function applies the appropriate event dispatch policy for this
// key event, and returns true if the system app should handle the event
// or false if the system app should not handle it. For system-only keys,
// it prevents them from being dispatched to the app. For "app-cancelled"
// keys, it allows them to be dispatched to the app first, and then
// only allows them to be handled by the system app if the app did not
// call preventDefault(). If more than one button is pressed, then
// app-cancelled buttons are treated as system-only and not passed to the
// app. Finally, unknown keys are never processed by the system app.
_applyPolicy: function bkem_applyPolicy(event, isButtonPressed=false) {
if (this._isSystemOnlyKey(event) &&
(this._isBeforeEvent(event) || this._isKeyEvent(event))) {
event.preventDefault();
return true;
} else if (isButtonPressed &&
this._isAppCancelledKey(event) &&
event.type === 'mozbrowserbeforekeydown') {
// If there is already a button held down and this is another key down
// event then we want to treat it as a system only key and ensure that
// it is not dispatched to the app. Note that we won't handle it here
// because that might change whether we're in the base state or
// not. We'll handle it below when the afterkeydown arrives
event.preventDefault();
return false;
} else if (isButtonPressed &&
this._isAppCancelledKey(event) &&
event.type === 'mozbrowserafterkeydown') {
return true;
} else if (this._isAppCancelledKey(event) && this._isAfterEvent(event)) {
// If the app handled and cancelled the event, we don't want to
// process it again here at the system app level.
return !event.embeddedCancelled;
} else if (this._isKeyEvent(event) &&
!this._targetToIframe(event)) {
// When focus is on embedded iframe and user press hardware key, system
// app will receive an extra keydown keyup event targeted to the iframe.
// We should ignore this event otherwise we will have strange state
// transition in HardwareButton module.
// Please see https://bugzilla.mozilla.org/show_bug.cgi?id=989198#c194
// and https://bugzilla.mozilla.org/show_bug.cgi?id=1014418#c20
return true;
}
// all unknown keys default to APP-ONLY, so the system app
// is not interested in them, by definition
return false;
},
isHomeKey: function bkem_isHomeKey(event) {
var key = this._getLowerCaseKeyName(event);
return (this.HOME_KEY_ALIAS.indexOf(key) > -1);
},
isHardwareKeyEvent: function bkem_isHardwareKeyEvent(type) {
return (this.KEY_EVENTS.indexOf(type) > -1);
},
getButtonEventType: function bkem_getButtonEventType(event,
isButtonPressed) {
var key;
var suffix;
if (this.isHardwareKeyEvent(event.type) &&
this._applyPolicy(event, isButtonPressed)) {
key = this._getLowerCaseKeyName(event);
if (this.TRANSLATION_TABLE[key]) {
suffix = (event.type.indexOf('keyup') > -1) ? '-release' : '-press';
return this.TRANSLATION_TABLE[key] + suffix;
}
}
}
};
exports.BrowserKeyEventManager = BrowserKeyEventManager;
}(window));

View file

@ -0,0 +1,356 @@
/* global Service */
'use strict';
(function(exports) {
/**
* @mixin BrowserMixin
*/
var BrowserMixin = {
reload: function bm_reload() {
if (this.browser.element) {
this.browser.element.reload();
}
},
/**
* A static timeout to make sure
* the next event don't happen too late.
*/
NEXTPAINT_TIMEOUT: 500,
/**
* Wait for a next paint event from mozbrowser iframe,
* The callback would be called in this.NEXTPAINT_TIMEOUT ms
* if the next paint event doesn't happen.
* The use case is for the moment just before we turn on
* the iframe visibility, so the TIMEOUT isn't too long.
*
* Note: for some reason we intend to use waitForNextPaint now.
*
* @param {Function} callback The callback function to be invoked
* after we get next paint event.
*
* @return {Object} Promise that resolves when the next paint triggers.
* The promise never rejects.
*/
waitForNextPaint: function bm_waitForNextPaint(callback) {
if (!this.browser || !this.browser.element) {
if (callback) {
callback();
}
return;
}
var iframe = this.browser.element;
var nextPaintTimer;
var self = this;
var resolver;
var p = new Promise(function(resolve) {
resolver = resolve;
});
var onNextPaint = function aw_onNextPaint() {
self.debug(' nextpainted.');
iframe.removeNextPaintListener(onNextPaint);
clearTimeout(nextPaintTimer);
resolver();
if (callback) {
callback();
}
};
nextPaintTimer = setTimeout(function ifNextPaintIsTooLate() {
self.debug(' nextpaint is timeouted.');
iframe.removeNextPaintListener(onNextPaint);
resolver();
if (callback) {
callback();
}
}, this.NEXTPAINT_TIMEOUT);
iframe.addNextPaintListener(onNextPaint);
return p;
},
/**
* This function will be called by window managers while top most app
* window is changed to notify nfc module in gecko.
*/
setNFCFocus: function(enable) {
var topWindow = this.getTopMostWindow();
if (!topWindow.browser || !topWindow.browser.element ||
topWindow._nfcActive === enable ||
(topWindow.CLASS_NAME !== 'AppWindow' &&
topWindow.CLASS_NAME !== 'ActivityWindow' &&
topWindow.CLASS_NAME !== 'PopupWindow') &&
topWindow.CLASS_NAME !== 'HomescreenWindow') {
// XXX: Implement this.belongToAppWindow()
return;
}
this.debug(topWindow.name + ':' + topWindow.instanceID +
' is setting nfc active to: ' + enable);
try {
topWindow._nfcActive = enable;
topWindow.browser.element.setNFCFocus(enable);
} catch (err) {
this.debug('set nfc active is not implemented');
}
},
/**
* get the screenshot of mozbrowser iframe.
* If it succeed, the blob would be stored in this._screenshotBlob.
* @param {Function} callback The callback function to be invoked
* after we get the screenshot.
*/
getScreenshot: function bm_getScreenshot(callback, width, height, timeout,
ignoreFront) {
if (!this.browser || !this.browser.element) {
if (callback) {
callback();
}
return;
}
this.debug('getting screenshot..');
var self = this;
var invoked = false;
var timer;
// First, let's check if we have a frontWindow, if so this is the one
// we will want a screenshot of, passing ignoreFront lets us skip this
// if we want a screenshot of the browser element
ignoreFront = (typeof ignoreFront === 'undefined') ? false : ignoreFront;
if (!ignoreFront && this.frontWindow) {
this.frontWindow.getScreenshot(callback, width, height, timeout);
return;
}
if (timeout) {
timer = window.setTimeout(function() {
if (invoked) {
return;
}
self.debug('getScreenshot timeout!');
invoked = true;
callback();
}, timeout);
}
//
// Since homescreen is the only app contains transparent background,
// we only store png screenshot for homescreen to save more memory.
//
var type = this.isHomescreen ?
'image/png' : 'image/jpeg';
var _width = width || this.width ||
Service.query('LayoutManager.width');
var _height = height || this.height ||
Service.query('LayoutManager.height');
this.debug('w=' + _width + ';h=' + _height);
var req = this.iframe.getScreenshot(_width, _height, type);
var success = function(result) {
if (!width) {
// Refresh _screenshotBlob when no width/height is specified.
self.renewCachedScreenshotBlob(result);
}
self.debug('getScreenshot succeed!');
if (invoked) {
return;
}
self.debug('get screenshot success!!!!');
invoked = true;
if (timer) {
window.clearTimeout(timer);
}
if (callback) {
callback(result);
}
};
var error = function() {
self.debug('getScreenshot failed!');
if (invoked) {
return;
}
invoked = true;
if (timer) {
window.clearTimeout(timer);
}
if (callback) {
callback();
}
};
if (req.then) {
req.then(success, error);
} else {
req.onsuccess = function(evt) {
success(evt.target.result);
};
req.onerror = error;
}
},
/**
* For test purpose, we create this method for changing active element. The
* activeElement is readonly property. We may use defineProperty to override
* it. But we get undefined with getOwnPropertyDescriptor. As to
* window.document, it is also a readonly and non-configurable property. We
* cannot override it directly.
*
* @return {HTMLDOMElement} the active element of current document.
*/
getActiveElement: function bm_getActiveElement() {
return document.activeElement;
},
focus: function bm_focus() {
var topWindow = this.getTopMostWindow();
if (topWindow.contextmenu && topWindow.contextmenu.isShown()) {
topWindow.contextmenu.focus();
} else if (topWindow.browser && topWindow.browser.element &&
topWindow.getActiveElement() !== topWindow.browser.element) {
topWindow.browser.element.focus();
}
},
blur: function bm_blur() {
var topWindow = this.getTopMostWindow();
if (topWindow.browser && topWindow.browser.element &&
topWindow.getActiveElement() === topWindow.browser.element) {
topWindow.browser.element.blur();
}
},
back: function bm_back() {
if (this.browser.element) {
this.browser.element.goBack();
}
},
forward: function bm_forward() {
if (this.browser.element) {
this.browser.element.goForward();
}
},
stop: function bm_stop() {
if (this.browser.element) {
this.browser.element.stop();
}
},
_setVisible: function bm__setVisible(visible) {
if (this.browser && this.browser.element &&
'setVisible' in this.browser.element) {
this.debug('setVisible on browser element:' + visible);
this.browser.element.setVisible(visible);
}
},
_setActive: function bm__setActive(active) {
if (this.browser && this.browser.element &&
'setActive' in this.browser.element) {
this.debug('setActive on browser element:' + active);
this.browser.element.setActive(active);
}
},
/**
* Set aria-hidden attribute on browser's element to handle its screen
* reader visibility.
* @type {Boolean} visible A flag indicating if the element should be screen
* reader visible.
*/
_setVisibleForScreenReader:
function bm__setVisibleForScreenReader(visible) {
if (this.browser && this.browser.element) {
this.debug('aria-hidden on browser element:' + !visible);
this.browser.element.setAttribute('aria-hidden', !visible);
}
},
/**
* Fire a DOM Request to detect the history has previous page or not.
* @param {Function} callback Called when DOM Request is done.
*/
canGoBack: function bm_cangoback(callback) {
var self = this;
if (this.browser.element) {
var r = this.browser.element.getCanGoBack();
var success = function(result) {
self._backable = result;
if (callback) {
callback(result);
}
};
var error = function() {
if (callback) {
callback();
}
};
if (r.then) {
r.then(success, error);
} else {
r.onsuccess = function(evt) {
success(evt.target.result);
};
r.onerror = error;
}
} else {
if (callback) {
callback();
}
}
},
/**
* Fire a DOM Request to detect the history has next page or not.
* @param {Function} callback Called when DOM Request is done.
*/
canGoForward: function bm_cangoforward(callback) {
var self = this;
if (this.browser.element) {
var r = this.browser.element.getCanGoForward();
var success = function(result) {
self._forwardable = result;
if (callback) {
callback(result);
}
};
var error = function() {
if (callback) {
callback();
}
};
if (r.then) {
r.then(success, error);
} else {
r.onsuccess = function(evt) {
success(evt.target.result);
};
r.onerror = error;
}
} else {
if (callback) {
callback();
}
}
},
/**
* Decide we are in out of process or not.
*/
isOOP: function bm_isOOP() {
return (this.browser_config && this.browser_config.oop);
}
};
if (exports.AppWindow) {
exports.AppWindow.addMixin(BrowserMixin);
}
}(window));

View file

@ -0,0 +1,59 @@
/* global Service, LazyLoader, IconsHelper */
(function(exports) {
'use strict';
function BrowserSettings() {
}
// Use a setting in order to be "called" by settings app
BrowserSettings.prototype.addRemoteRequestSetting =
function(key, requestFunction) {
function observer(setting) {
var isRequested = setting.settingValue;
if (!isRequested) {
return;
}
requestFunction();
// Reset the setting value to false
var lock = navigator.mozSettings.createLock();
var falseSetting = {};
falseSetting[key] = false;
lock.set(falseSetting);
}
navigator.mozSettings.addObserver(key, observer);
var getRequest = navigator.mozSettings.createLock().get(key);
getRequest.onsuccess = function() {
observer({settingName: key, settingValue: getRequest.result[key]});
};
};
BrowserSettings.prototype.start = function() {
this.addRemoteRequestSetting(
'clear.browser.history',
function clearHistory() {
Service.request('Places:clearHistory');
}
);
this.addRemoteRequestSetting(
'clear.browser.private-data',
function clearPrivateData() {
var request = navigator.mozApps.getSelf();
request.onsuccess = function() {
request.result.clearBrowserData();
};
// Clear the icon dataStore.
LazyLoader.load('shared/js/icons_helper.js').then(() => {
IconsHelper.clear();
});
}
);
};
exports.BrowserSettings = BrowserSettings;
})(window);

View file

@ -0,0 +1,240 @@
/* global BaseModule, asyncStorage, SIMSlotManager, SettingsHelper,
CallForwardingsIcon, LazyLoader */
'use strict';
(function() {
// Must be in sync with nsIDOMMozMobileCFInfo interface.
var _cfReason = {
CALL_FORWARD_REASON_UNCONDITIONAL: 0,
CALL_FORWARD_REASON_MOBILE_BUSY: 1,
CALL_FORWARD_REASON_NO_REPLY: 2,
CALL_FORWARD_REASON_NOT_REACHABLE: 3,
CALL_FORWARD_REASON_ALL_CALL_FORWARDING: 4,
CALL_FORWARD_REASON_ALL_CONDITIONAL_CALL_FORWARDING: 5
};
var _cfAction = {
CALL_FORWARD_ACTION_DISABLE: 0,
CALL_FORWARD_ACTION_ENABLE: 1,
CALL_FORWARD_ACTION_QUERY_STATUS: 2,
CALL_FORWARD_ACTION_REGISTRATION: 3,
CALL_FORWARD_ACTION_ERASURE: 4
};
/**
* The module initializes the call forwarding icon states and update the icons
* when call forwarding states changed.
* As we are not able to query the current call forwarding states from the
* carrier on startup, we initialized the icon state based on the cached
* information. When call forwarding states are changed, the module caches
* the state along with the corresponding iccId.
* @class CallForwarding
* @requires SIMSlotManager
* @requires SettingsHelper
*/
function CallForwarding() {
this._slots = null;
this._callForwardingHelper = null;
this._defaultCallForwardingIconStates = null;
this._callForwardingIconInitializedStates = null;
}
CallForwarding.STATES = [
'enabled'
];
CallForwarding.SETTINGS = [
'ril.cf.enabled'
];
BaseModule.create(CallForwarding, {
name: 'CallForwarding',
/**
* Add related event handlers. The sim cards may not be ready when starting.
* We register to "simslot-cardstatechange" and "simslot-iccinfochange" for
* updating the icons when ready. When users query the current call
* forwarding states in settings app, 'ril.cf.carrier.enabled' is used to
* notify the state changes. When users change the call forwarding states in
* settings app, we will receive the 'cfstatechange' event. All the events
* must be watched to have correct icon states.
* @memberof CallForwarding.prototype
*/
_addEventHandlers: function() {
window.addEventListener('simslot-cardstatechange', (function(event) {
this._initCallForwardingState(event.detail);
}).bind(this));
window.addEventListener('simslot-iccinfochange', (function(event) {
this._initCallForwardingState(event.detail);
}).bind(this));
// Get notified when users query the latest call forwarding states in
// settings app
navigator.mozSettings.addObserver('ril.cf.carrier.enabled',
(function(event) {
var detail = event.settingValue;
if (detail) {
this._onCallForwardingStateChanged(detail.index, detail.enabled);
}
}).bind(this));
this._slots.forEach(function(slot) {
var conn = slot.conn;
conn.addEventListener('cfstatechange',
this._updateCallForwardingIconState.bind(this, slot));
}, this);
},
/**
* Initialize the icon states based on iccId and cached information.
* @param {SIMSlot} slot The target sim slot.
* @memberof CallForwarding.prototype
*/
_initCallForwardingState: function(slot) {
var index = slot.index;
var simCard = slot.simCard;
if (this._callForwardingIconInitializedStates[index]) {
return;
}
var that = this;
var cardState = simCard && simCard.cardState;
var iccid = simCard && simCard.iccInfo && simCard.iccInfo.iccid;
// Disable call forwarding icon and early return when:
// 1. sim card not present, or
// 2. sim card state is not ready, or
// 3. sim card iccId is not available.
if (!simCard || cardState !== 'ready' || !iccid) {
that._callForwardingHelper.get(function(states) {
states[index] = false;
that._callForwardingHelper.set(states);
});
return;
}
asyncStorage.getItem('ril.cf.enabled.' + iccid, function(value) {
if (value === null) {
value = false;
}
that._callForwardingHelper.get(function(states) {
states[index] = value;
that._callForwardingHelper.set(states, function() {
that._callForwardingIconInitializedStates[index] = true;
});
});
});
},
/**
* Gets called when receiving "cfstatechange". It updates the cached
* information and icon states.
* @param {SIMSlot} slot The target sim slot.
* @param {Event} event The event.
* @memberof CallForwarding.prototype
*/
_updateCallForwardingIconState: function(slot, event) {
if (!event ||
(event.reason != _cfReason.CALL_FORWARD_REASON_UNCONDITIONAL &&
event.reason != _cfReason.CALL_FORWARD_REASON_ALL_CALL_FORWARDING)) {
return;
}
var index = slot.index;
var simCard = slot.simCard;
var enabled = false;
switch (event.action) {
case _cfAction.CALL_FORWARD_ACTION_REGISTRATION:
case _cfAction.CALL_FORWARD_ACTION_ENABLE:
enabled = true;
break;
case _cfAction.CALL_FORWARD_ACTION_ERASURE:
enabled = false;
break;
default:
enabled = false;
break;
}
this._callForwardingHelper.get((function(states) {
states[index] = enabled;
this._callForwardingHelper.set(states);
}).bind(this));
if (!simCard) {
return;
}
var iccid = simCard.iccInfo && simCard.iccInfo.iccid;
asyncStorage.setItem('ril.cf.enabled.' + iccid, enabled);
},
/**
* Gets called when 'ril.cf.carrier.enabled' changes. It updates the cached
* information and icon states.
* @param {Number} index The index of the sim card being changed.
* @param {Boolean} enabled The call forwarding state.
* @memberof CallForwarding.prototype
*/
_onCallForwardingStateChanged: function(index, enabled) {
this._callForwardingHelper.get((function(states) {
states[index] = enabled;
this._callForwardingHelper.set(states);
}).bind(this));
var simCard = this._slots[index].simCard;
if (!simCard) {
return;
}
var iccid = simCard.iccInfo && simCard.iccInfo.iccid;
asyncStorage.setItem('ril.cf.enabled.' + iccid, enabled);
},
enabled: function(index) {
return this._enabled[index];
},
/**
* Start the module.
* @memberof CallForwarding.prototype
*/
_start: function() {
this._slots = SIMSlotManager.getSlots();
this._enabled = [];
this._defaultCallForwardingIconStates =
Array.prototype.map.call(this._slots, function() { return false; });
this._callForwardingIconInitializedStates =
Array.prototype.map.call(this._slots, function() { return false; });
// XXX:
// We should use observe/read/write functionality in baseModule
// and remove the usage here.
LazyLoader.load(['shared/js/settings_helper.js']).then(() => {
this._callForwardingHelper =
SettingsHelper('ril.cf.enabled',
this._defaultCallForwardingIconStates);
this._addEventHandlers();
// Initialize the icon states
this._slots.forEach(function(slot) {
this._initCallForwardingState(slot);
}, this);
}).catch((err) => {
console.error(err);
});
},
'_observe_ril.cf.enabled': function(value) {
this._enabled = value || [];
var isAnyEnabled =
this._enabled.some(function(enabled) { return enabled; });
if (!this.icon && isAnyEnabled) {
LazyLoader.load(['js/call_forwarding_icon.js']).then(function() {
this.icon = new CallForwardingsIcon(this);
this.icon.start();
}.bind(this)).catch(function(err) {
console.error(err);
});
}
this.icon && this.icon.update();
}
});
})();

View file

@ -0,0 +1,83 @@
/* global BaseIcon, SIMSlotManager */
'use strict';
(function(exports) {
var CallForwardingIcon = function(manager, index, container) {
BaseIcon.call(this, manager, index);
this.instanceID = this.instanceID + index;
this.containerElement = container;
};
CallForwardingIcon.prototype = Object.create(BaseIcon.prototype);
CallForwardingIcon.prototype.name = 'CallForwardingIcon';
CallForwardingIcon.prototype.EVENT_PREFIX = 'subicon';
CallForwardingIcon.prototype.shouldDisplay = function() {
return this.manager.enabled(this.index);
};
CallForwardingIcon.prototype.name = 'CallForwardingIcon';
CallForwardingIcon.prototype.view = function() {
var index = this.index;
var content = `
<div class="sb-icon sb-icon-call-forwarding"
role="listitem" data-index="${index}"
aria-label="statusbarForwarding">
</div>`;
return content;
};
CallForwardingIcon.prototype.render = function() {
this.debug('renderring...');
this.containerElement.insertAdjacentHTML('beforeend', this.view());
this.element =
this.containerElement.querySelector(
'.sb-icon-call-forwarding[data-index="' +
this.index + '"]');
};
var CallForwardingsIcon = function(manager) {
this.icons = [];
BaseIcon.call(this, manager);
};
CallForwardingsIcon.prototype = Object.create(BaseIcon.prototype);
CallForwardingsIcon.prototype.name = 'CallForwardingsIcon';
CallForwardingsIcon.prototype.onrender = function() {
if (!this.element) {
return;
}
var multipleSims = SIMSlotManager.isMultiSIM();
// Create signal elements based on the number of SIM slots.
this.element.dataset.multiple = multipleSims;
this.updateVisibility = this.updateVisibility.bind(this);
SIMSlotManager.getSlots().forEach(function(slot, i) {
var icon = new CallForwardingIcon(this.manager, i, this.containerElement);
this.icons.push(icon);
icon.start();
icon.element.addEventListener('_shown', this.updateVisibility);
icon.element.addEventListener('_hidden', this.updateVisibility);
}, this);
this.updateVisibility();
};
CallForwardingsIcon.prototype.updateVisibility = function() {
var visible = this.icons.some(function(icon) {
return icon.isVisible();
});
visible ? this.show() : this.hide();
};
CallForwardingsIcon.prototype.update = function() {
if (!this.icons.length) {
return;
}
this.icons.forEach(function(icon) {
icon.update();
});
};
CallForwardingsIcon.prototype.view = function view() {
return `<div id="statusbar-call-forwardings"
class="sb-icon-call-forwarding"
hidden role="presentation">
</div>`;
};
exports.CallForwardingsIcon = CallForwardingsIcon;
}(window));

View file

@ -0,0 +1,168 @@
/* global AttentionWindow, Service */
'use strict';
(function(exports) {
/**
* CallscreenWindow is a special case of attention window.
* It's the call screen UI but lives in system app.
*
* @example
* var attention = new CallscreenWindow();
*
* @class CallscreenWindow
* @requires System
* @extends AttentionWindow
*/
/**
* Fired when the attention window is created.
* @event AttentionWindow#attentioncreated
*/
/**
* Fired when the attention window is removed.
* @event AttentionWindow#attentionterminated
*/
/**
* Fired when the attention window is opening.
* @event AttentionWindow#attentionopening
*/
/**
* Fired when the attention window is opened.
* @event AttentionWindow#attentionopened
*/
/**
* Fired when the attention window is cloing.
* @event AttentionWindow#attentionclosing
*/
/**
* Fired when the attention window is closed.
* @event AttentionWindow#attentionclosed
*/
/**
* Fired before the attention window is rendered.
* @event AttentionWindow#attentionwillrender
*/
/**
* Fired when the attention window is rendered to the DOM tree.
* @event AttentionWindow#attentionrendered
*/
var CSORIGIN =
window.location.origin.replace('system', 'callscreen') + '/';
var CallscreenWindow = function CallscreenWindow() {
this.config = {
manifestURL: CSORIGIN + 'manifest.webapp',
url: CSORIGIN + 'index.html',
origin: CSORIGIN
};
this.isCallscreenWindow = true;
this.reConfig(this.config);
this.render();
if (this._DEBUG) {
CallscreenWindow[this.instanceID] = this;
}
this.publish('created');
};
CallscreenWindow.prototype = Object.create(AttentionWindow.prototype);
CallscreenWindow.prototype.constructor = CallscreenWindow;
/**
* Turn on this flag to dump debugging messages for all attention windows.
* @type {Boolean}
*/
CallscreenWindow.prototype._DEBUG = false;
CallscreenWindow.prototype.CLASS_LIST =
'appWindow attentionWindow callscreenWindow';
CallscreenWindow.prototype.CLASS_NAME = 'CallscreenWindow';
CallscreenWindow.prototype.openAnimation = 'slide-from-top';
CallscreenWindow.prototype.closeAnimation = 'slide-to-top';
CallscreenWindow.SUB_COMPONENTS = {
'transitionController': 'AppTransitionController',
'attentionToaster': 'AttentionToaster'
};
CallscreenWindow.REGISTERED_EVENTS =
AttentionWindow.REGISTERED_EVENTS;
CallscreenWindow.prototype.render = function cw_render() {
this.publish('willrender');
this.containerElement.insertAdjacentHTML('beforeend', this.view());
this.element =
document.getElementById(this.instanceID);
// XXX: Use BrowserFrame
var iframe = document.createElement('iframe');
iframe.setAttribute('name', 'call_screen');
iframe.setAttribute('mozbrowser', 'true');
iframe.setAttribute('remote', 'false');
iframe.setAttribute('mozapp', this.config.manifestURL);
iframe.src = this.config.url;
this.browser = {
element: iframe
};
this.browserContainer = this.element.querySelector('.browser-container');
this.browserContainer.insertBefore(this.browser.element, null);
this.frame = this.element;
this.iframe = this.browser.element;
this._registerEvents();
this.installSubComponents();
this.publish('rendered');
};
CallscreenWindow.prototype.ensure = function() {
if (!this.browser || !this.browser.element) {
return;
}
var timestamp = new Date().getTime();
var src = this.config.url + '#' +
(Service.query('locked') ? 'locked' : '');
src = src + '&timestamp=' + timestamp;
this.browser.element.src = src;
this._terminated = false;
this.show();
};
CallscreenWindow.prototype._handle_mozbrowserclose = function() {
if (this._terminated) {
return;
}
this._terminated = true;
this.publish('terminated');
if (this.isActive() || this._requestedOpen) {
var self = this;
this.element.addEventListener('_closed', function onclosed() {
// XXX: We will have trouble if the second call comes during closing.
self.element.removeEventListener('_closed', onclosed);
self.hide();
self.reloadWindow();
});
this.requestClose();
} else {
this.hide();
this.reloadWindow();
}
// XXX: We are leaving the focus in the callscreen iframe
if (document.activeElement === this.browser.element) {
document.activeElement.blur();
}
};
CallscreenWindow.prototype.reloadWindow = function() {
var src = this.browser.element.src.split('#')[0];
this.browser.element.src = ''; // cocotte
setTimeout(function nextTick() {
this.browser.element.src = src;
}.bind(this));
this.setVisible(false);
};
CallscreenWindow.prototype.free = function() {
if (this.browser.element) {
this.browser.element.src = '';
}
};
exports.CallscreenWindow = CallscreenWindow;
}(window));

View file

@ -0,0 +1,35 @@
/* globals BaseModule, MozActivity, Service */
'use strict';
(function(exports) {
var CameraTrigger = function() {};
CameraTrigger.EVENTS = [
'holdcamera'
];
BaseModule.create(CameraTrigger, {
name: 'CameraTrigger',
DEBUG: false,
TRACE: false,
_handle_holdcamera: function(event) {
// Avoid triggering the Camera app multiple times if the lockscreen is
// locked, and prevent bailing during FTU
if (Service.query('locked') || Service.query('isFtuRunning')) {
return;
}
this.debug('Received holdcamera');
/* jshint unused:false */
var activity = new MozActivity({
name: 'record',
data: {
type: 'photos'
}
});
}
});
}());

View file

@ -0,0 +1,155 @@
/* -*Mode: js; js-indent-level: 2; indent-tabs-mode: nil -**/
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* global BrowserFrame,
EntrySheet,
Notification,
NotificationHelper,
MozActivity,
Service,
LazyLoader
*/
'use strict';
var CaptivePortal = {
eventId: null,
settings: null,
notification: null,
notificationPrefix: 'captivePortal:',
captiveNotification_onClick: null,
handleLogin: function cp_handleLogin(id, url) {
var wifiManager = window.navigator.mozWifiManager;
var settings = window.navigator.mozSettings;
var icon = window.location.origin +
'/style/icons/captivePortal.png';
//captive portal login needed
this.eventId = id;
var currentNetwork = wifiManager.connection.network;
var networkName = (currentNetwork && currentNetwork.ssid) ?
currentNetwork.ssid : '';
var message = { id: 'captive-wifi-available',
args: { networkName: networkName }
};
if (Service.query('isFtuRunning')) {
settings.createLock().set({'wifi.connect_via_settings': false});
LazyLoader.load(['js/entry_sheet.js']).then(function() {
this.entrySheet = new EntrySheet(
document.getElementById('screen'),
// Prefix url with LRM character
// This ensures truncation occurs correctly in an RTL document
// We can remove this when bug 1154438 is fixed.
'\u200E' + url,
new BrowserFrame({url: url})
);
this.entrySheet.open();
}.bind(this)).catch((err) => {
console.error(err);
});
return;
}
this.captiveNotification_onClick = () => {
this.notification.removeEventListener('click',
this.captiveNotification_onClick);
this.captiveNotification_onClick = null;
var activity = new MozActivity({
name: 'view',
data: { type: 'url', url: url }
});
activity.onerror = function() {
console.error('CaptivePortal Activity error: ' + this.error);
};
};
var options = {
bodyL10n: message,
icon: icon,
tag: this.notificationPrefix + networkName,
mozbehavior: {
showOnlyOnce: true
}
};
NotificationHelper.send('', options).then(notification => {
this.notification = notification;
notification.addEventListener('click',
this.captiveNotification_onClick);
notification.addEventListener('close', () => {
this.notification = null;
});
});
},
dismissNotification: function dismissNotification(id) {
if (id === this.eventId) {
if (this.notification) {
if (this.captiveNotification_onClick) {
this.notification.removeEventListener('click',
this.captiveNotification_onClick);
this.captiveNotification_onClick = null;
}
this.notification.close();
}
if (this.entrySheet) {
this.entrySheet.close();
this.entrySheet = null;
}
}
},
handleLoginAbort: function handleLoginAbort(id) {
this.dismissNotification(id);
},
handleLoginSuccess: function handleLoginSuccess(id) {
this.dismissNotification(id);
},
handleEvent: function cp_handleEvent(evt) {
switch (evt.detail.type) {
case 'captive-portal-login':
this.handleLogin(evt.detail.id, evt.detail.url);
break;
case 'captive-portal-login-abort':
this.handleLoginAbort(evt.detail.id);
break;
case 'captive-portal-login-success':
this.handleLoginSuccess(evt.detail.id);
break;
}
},
init: function cp_init() {
var promise = Notification.get();
var prefix = this.notificationPrefix;
promise.then(function(notifications) {
notifications.forEach(function(notification) {
if (!notification) {
return;
}
// We just care about notification with tag 'captivePortal:'
if (!notification.tag || !notification.tag.startsWith(prefix)) {
return;
}
notification.close();
});
}).then((function() {
window.addEventListener('mozChromeEvent', this);
}).bind(this));
return promise;
}
};
// unit tests call init() manually
if (navigator.mozL10n) {
navigator.mozL10n.once(CaptivePortal.init.bind(CaptivePortal));
}

View file

@ -0,0 +1,326 @@
/* globals TaskManagerUtils, Sanitizer */
'use strict';
(function(exports) {
/**
* A `Card` represents an individual app in the Task Manager.
*
* The `Card` class itself is inert; because TaskManager only updates its
* display on launch, Card doesn't need to update its state in real time.
* Card's swipe-up-to-delete behavior is handled by `SwipeToKillMotion`,
* also included in this file.
*
* @param {AppWindow} app
* @param {boolean} opts.disableScreenshots
* Legacy option. If true, we will use a small icon preview on the card
* rather than showing a full screenshot and/or moz-element of the app.
* Originally introduced on 128MB Tarako devices to save memory.
* @param {boolean} opts.stayInvisible
* If true (because we're just using this card as a placeholder since we're
* immediately launching the app), don't actually render the card.
*/
function Card(app, { disableScreenshots, stayInvisible } = {}) {
var el = document.createElement('li');
this.app = app;
this.element = el;
if (stayInvisible) {
this.element.style.display = 'none';
return;
}
this.title = (app.isBrowser() && app.title) ? app.title : app.name;
this.subTitle = TaskManagerUtils.getDisplayUrlForApp(app);
this.titleId = 'card-title-' + app.instanceID;
el.classList.add('card');
el.classList.toggle('private', !!app.isPrivate);
el.classList.toggle('show-subtitle', !!this.subTitle);
el.classList.toggle('browser', !!app.isBrowser());
el.dataset.appInstanceId = app.instanceID;
el.dataset.origin = app.origin; // for ease of testing
el.dataset.ssl = app.getSSLState() || ''; // indiciate security when available
el.setAttribute('aria-labelledby', this.titleId);
el.setAttribute('role', 'presentation'); // The card is not semantic.
el.innerHTML = Sanitizer.unwrapSafeHTML(this.getHtmlTemplate());
var topMostWindow = app.getTopMostWindow();
if (topMostWindow && topMostWindow.CLASS_NAME === 'TrustedWindow') {
this.title = topMostWindow.name || '';
el.classList.add('trustedui');
} else if (!app.killable()) {
el.querySelector('.close-button').style.visibility = 'hidden';
}
if (!disableScreenshots) {
el.querySelector('.screenshotView').style.backgroundImage =
`-moz-element(#${app.instanceID})`;
} else {
el.classList.add('appIconPreview');
var iconId = 'card-appIcon-' + app.instanceID;
el.querySelector('.appIcon').id = iconId;
el.querySelector('.appIconView').style.backgroundImage =
`-moz-element(#${iconId})`;
}
this.swipeAction = new SwipeToKillMotion(this.element, {
setTranslateY: (y) => {
this.translate({ y: y });
}
});
}
exports.Card = Card;
Card.prototype = {
/**
* To ensure animations run smoothly, cards are positioned exclusively with
* CSS transforms. Unfortunately, since translateX and translateY must be set
* simultaneously, we need a little additional boilerplate here: TaskManager
* itself positions the cards on the x-axis, but each card's SwipeToKillMotion
* maintains the y-axis positioning.
*
* @param {object} props
* An object with 'x' and/or 'y' properties. Each property MUST include
* units, e.g. 'px'.
*/
translate(props) {
if ('x' in props) {
this._translateX = props.x;
}
if ('y' in props) {
this._translateY = props.y;
}
// XXX: Due to a Flame-specific Gecko layout/rendering bug, we cannot use
// "top: 25%" to render the card at the appropriate Y position; we must
// instead include the desired Y offset here until that bug is fixed.
// In the metrics of this calculation, that's calc(50% + y).
// See <https://bugzil.la/1209194>; we can probably relocate the offset
// when that bug is fixed or when we don't care about Flame any more.
var newTransform =
`translate(${this._translateX}, calc(50% + ${this._translateY}))`;
// Preventing an extra restyle when the value doesn't need to change
if (this.element.style.transform !== newTransform) {
this.element.style.transform = newTransform;
}
},
loadIcon() {
const ICON_SIZE = 40;
TaskManagerUtils.loadAppIcon(
this.app, this.element.querySelector('.appIcon'), ICON_SIZE);
},
_translateX: '0px',
_translateY: '0px',
getHtmlTemplate() {
return Sanitizer.createSafeHTML `
<div class="titles">
<h1 id="${this.titleId}" dir="auto" class="title">${this.title}</h1>
<p class="subtitle">
<span class="subtitle-url">${this.subTitle}</span>
</p>
</div>
<div class="screenshotView bb-button" data-l10n-id="openCard" role="link">
</div>
<div class="privateOverlay"></div>
<div class="appIconView"></div>
<footer class="card-tray">
<button class="appIcon pending" data-l10n-id="openCard"
data-button-action="select" aria-hidden="true"></button>
<menu class="buttonbar">
<button class="close-button bb-button" data-l10n-id="closeCard"
data-button-action="close" role="button">
</button>
<button class="favorite-button bb-button"
data-button-action="favorite" role="button"
style="visibility: hidden"></button>
</menu>
</footer>
`;
},
};
/**
* Track vertical swipes across this element. If the element is flung with
* enough upward velocity, make the element fly offscreen and emit an event
* that indicates that the app should be killed.
*
* Extra care is taken here to allow a potential swipe-up to be interrupted.
* For instance, if the parent container is scrolling horizontally, we must
* prohibit this element from being flung (i.e. poor-man's axis lock).
*
* Emits:
* - "card-will-drag" when the element is grabbed and wants to be flung
* upward; this event is cancelable.
* - "card-dropped" with detail: { willKill: Boolean }
*
* @param {Element} el
* The element (i.e. card.element) to be made draggable.
* @param {function} opts.setTranslateY
* This class will call setTranslateY whenever the Y-translation changes.
* See `Card.prototype.translate` for rationale on this unfortunate grossness.
*/
function SwipeToKillMotion(el, { setTranslateY }) {
el.addEventListener('touchstart', this);
el.addEventListener('touchmove', this);
el.addEventListener('touchend', this);
el.addEventListener('click', this);
this.setTranslateY = setTranslateY;
this.el = el;
}
exports.SwipeToKillMotion = SwipeToKillMotion;
// Anything less than MIN_DISTANCE will be ignored for swiping purposes.
const MIN_DISTANCE = 4;
// Track MAX_DRAG_DELTA_COUNT latest drag events, so that we can compute the
// current fling velocity.
const MAX_DRAG_DELTA_COUNT = 3;
// Even if the user flings upward, if they only fling a tiny amound, ignore it.
// Note that velocity is more important than swipe distance.
const MIN_SWIPE_DISTANCE = 10;
SwipeToKillMotion.prototype = {
handleEvent(evt) {
if (/touch/.test(evt.type)) {
return this.handleTouchEvent(evt);
} else if (evt.type === 'click') {
// Ignore clicks if the element is already being dragged upward.
if (this.activelyDragging) {
evt.preventDefault();
evt.stopPropagation();
}
}
},
beginSwipe(evt) {
this.firstTouch = evt.targetTouches[0];
this.firstTouchTimestamp = Date.now();
this.dragDeltas = [];
this.activelyDragging = false;
this.prohibitDragging = false;
this.el.style.transition = 'none';
},
endSwipe(evt) {
this.el.style.removeProperty('transition');
// If we haven't moved far enough to cause a drag, there's nothing to do.
if (!this.activelyDragging) {
return;
}
var willKill = false;
if (this.dragDeltas.length > 2) {
var dy1 = this.dragDeltas[0];
var dy2 = this.dragDeltas[this.dragDeltas.length - 1];
// If the newer delta is substantially more negative than the older one,
// it's time to kill the card.
if (dy2 < dy1 - MIN_SWIPE_DISTANCE) {
willKill = true;
}
}
this.activelyDragging = false;
this.el.dispatchEvent(new CustomEvent('card-dropped', {
bubbles: true,
detail: { willKill }
}));
if (willKill) {
this.setTranslateY('-200%');
} else {
this.setTranslateY('0px');
}
},
dragSwipe(evt) {
var currentTouch = evt.targetTouches[0];
var dy = (currentTouch.clientY - this.firstTouch.clientY);
var dx = (currentTouch.clientX - this.firstTouch.clientX);
// If they're swiping downward, clamp the card to the bottom.
if (dy > 0) {
dy = 0;
}
// If we're dragging, track the most recent few swipes so that we can
// calculate a rough estimate of velocity, which is important in deciding
// if we should swipe up to kill the app or return the app to y=0.
if (this.activelyDragging) {
this.dragDeltas.push(dy);
if (this.dragDeltas.length > MAX_DRAG_DELTA_COUNT) {
this.dragDeltas.shift();
}
}
// If we're not actively dragging yet...
else {
// If we swiped along the X axis, scroll sideways instead.
if (Math.abs(dx) > MIN_DISTANCE) {
dy = 0;
this.prohibitDragging = true;
}
// If we swiped along the Y axis, check to see if the scrollable container
// thinks we should be allowed to swipe up right now. (If the container
// has been scrolling recently, it'll tell us to hold off by canceling
// the "card-will-drag" event).
else if (Math.abs(dy) > MIN_DISTANCE) {
if (this.el.dispatchEvent(new CustomEvent('card-will-drag', {
bubbles: true,
cancelable: true,
detail: { firstTouchTimestamp: this.firstTouchTimestamp }}))) {
this.activelyDragging = true;
}
// If the container told us we can't swipe, give up for now.
else {
dy = 0;
this.prohibitDragging = true;
}
}
// If we aren't sure that we should be swiping, clamp 'dy' to zero to
// avoid looking janky if we decide to scroll.
else if (Math.abs(dy) < MIN_DISTANCE) {
dy = 0;
}
}
this.setTranslateY(dy + 'px');
},
handleTouchEvent(evt) {
evt.stopPropagation();
// 'touchstart' and 'touchend' events aren't entirely relevant here;
// we're touching the element if and only if evt.targetTouches.length > 0.
var wasTouching = this.isTouching;
this.isTouching = evt.targetTouches.length > 0;
if (!wasTouching && this.isTouching) {
this.beginSwipe(evt);
} else if (wasTouching && !this.isTouching) {
this.endSwipe(evt);
}
if (this.isTouching && !this.prohibitDragging) {
this.dragSwipe(evt);
}
}
};
})(window);

View file

@ -0,0 +1,85 @@
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
/* global ModalDialog */
/* global NotificationScreen */
(function(exports) {
var CarrierInfoNotifier = {
_sound: 'style/notifications/ringtones/notifier_firefox.opus',
// A random starting point
// that is unlikely to be used by other notifications
_notificationId: 6000 + Math.floor(Math.random() * 999),
start: function() {
// CDMA record information
navigator.mozSetMessageHandler('cdma-info-rec-received',
this.showCDMA.bind(this));
},
showCDMA: function cin_showCDMA(message) {
if (message.display) {
this.show(message.display, 'cdma-record-info');
}
if (message.extendedDisplay) {
var text = message.extendedDisplay.records.map(function(elem) {
return elem.content;
}).join(' ');
this.show(text, 'cdma-record-info');
}
},
show: function cin_show(message, title) {
var showDialog = function cin_showDialog() {
ModalDialog.showWithPseudoEvent({
title: title,
text: { raw: message },
type: 'alert'
});
};
// If we are not inside the lockscreen, show the dialog
// immediately, dispatch an event to hide
if (!window.Service.query('locked')) {
this.dispatchEvent('emergencyalert');
this.playNotification();
showDialog();
return;
}
// If we are on the lock screen then create a notification
// that invokes the dialog
var notificationId = ++this._notificationId;
var notification = NotificationScreen.addNotification({
id: notificationId,
title: title,
text: message
});
notification.addEventListener('tap', function showDialogAndDismiss() {
showDialog();
NotificationScreen.removeNotification(notificationId);
});
},
playNotification: function cin_playNotification() {
var ringtonePlayer = new Audio();
ringtonePlayer.src = this._sound;
ringtonePlayer.mozAudioChannelType = 'notification';
ringtonePlayer.play();
window.setTimeout(function smsRingtoneEnder() {
ringtonePlayer.pause();
ringtonePlayer.src = '';
}, 2000);
},
dispatchEvent: function cin_dispatchEvent(name, detail) {
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(name, true, true, detail);
window.dispatchEvent(evt);
}
};
exports.CarrierInfoNotifier = CarrierInfoNotifier;
}(window));

View file

@ -0,0 +1,130 @@
'use strict';
/* global CarrierInfoNotifier */
/* global MobileOperator */
/* global Service */
(function(exports) {
/**
* CellBroadcastSystem
* @class CellBroadcastSystem
* @requires CarrierInfoNotifier
* @requires MobileOperator
*/
function CellBroadcastSystem() {}
CellBroadcastSystem.prototype = {
name: 'CellBroadcastSystem',
/**
* Whether or not the cellbroadcast setting is enabled or disabled.
* @memberof CellBroadcastSystem.prototype
* @type {Array}
*/
_settingsDisabled: [],
/**
* The cell broadcast settings key.
* @memberof CellBroadcastSystem.prototype
* @type {String}
*/
_settingsKey: 'ril.cellbroadcast.disabled',
/**
* Starts listening for events and settings changes.
* @memberof CellBroadcastSystem.prototype
*/
start: function cbs_start() {
var self = this;
if (navigator && navigator.mozCellBroadcast) {
navigator.mozCellBroadcast.onreceived = this.show.bind(this);
}
var settings = window.navigator.mozSettings;
var req = settings.createLock().get(this._settingsKey);
req.onsuccess = function() {
self._settingsDisabled = req.result[self._settingsKey];
};
settings.addObserver(
this._settingsKey, this.settingsChangedHandler.bind(this));
Service.register('show', this);
},
/**
* Called when the cellbroadcast setting is changed.
* @memberof CellBroadcastSystem.prototype
*/
settingsChangedHandler: function cbs_settingsChangedHandler(event) {
this._settingsDisabled = event.settingValue;
if (this._hasCBSDisabled()) {
var evt = new CustomEvent('cellbroadcastmsgchanged', { detail: null });
window.dispatchEvent(evt);
}
},
/**
* Shows the cell broadcast notification.
* @memberof CellBroadcastSystem.prototype
*/
show: function cbs_show(event) {
var msg = event.message;
var serviceId = msg.serviceId || 0;
var conn = window.navigator.mozMobileConnections[serviceId];
var id = msg.messageId;
var cdmaCategory = msg.cdmaServiceCategory;
// Early return CMAS messsage and let network alert app handle it. Please
// ref http://www.etsi.org/deliver/etsi_ts/123000_123099/123041/
// 11.06.00_60/ts_123041v110600p.pdf, chapter 9.4.1.2.2 Message identifier
// for GSM and http://www.3gpp2.org/public_html/specs/
// C.R1001-G_v1.0_Param_Administration.pdf for CDMA.
// GSM Message id from range 4370 to 4399(1112 hex to 112f hex) and
// CDMA service category from range 4096 to 4351(1000 hex to 10ff hex)
// should be CMAS and network alert will display detail information.
var isGSM = cdmaCategory === null;
var isGSMCmas = isGSM && (id >= 4370 && id < 4400);
var isCDMACmas = !isGSM &&
(cdmaCategory >= 0x1000 && cdmaCategory <= 0x10FF);
if (isGSMCmas || isCDMACmas) {
return;
}
if (conn && conn.voice && conn.voice.network &&
conn.voice.network.mcc === MobileOperator.BRAZIL_MCC &&
id === MobileOperator.BRAZIL_CELLBROADCAST_CHANNEL) {
var evt = new CustomEvent('cellbroadcastmsgchanged',
{ detail: msg.body });
window.dispatchEvent(evt);
return;
}
var body = msg.body;
// XXX: 'undefined' test until bug-1021177 lands
if (msg.etws && (!body || (body == 'undefined'))) {
body = navigator.mozL10n.get('cb-etws-warningType-' +
(msg.etws.warningType ? msg.etws.warningType : 'other'));
}
CarrierInfoNotifier.show(body,
navigator.mozL10n.get('cb-channel', { channel: id }));
},
/**
* To make sure there is any CBS pref is disabled
* @memberof CellBroadcastSystem.prototype
*/
_hasCBSDisabled: function cbs__getDisabledCBSIndex() {
var index =
this._settingsDisabled.findIndex(disabled => (disabled === true));
return (index >= 0);
}
};
exports.CellBroadcastSystem = CellBroadcastSystem;
}(window));

View file

@ -0,0 +1,332 @@
'use strict';
/* global AppWindow, PopupWindow, ActivityWindow, SettingsListener, Service,
AttentionWindow, MozActivity, GlobalOverlayWindow, TrustedWindow */
(function(exports) {
var ENABLE_IN_APP_SHEET = false;
SettingsListener.observe('in-app-sheet.enabled', false, function(value) {
ENABLE_IN_APP_SHEET = value;
});
/**
* ChildWindowFactory is a submodule of AppWindow,
* its responsbility is to:
*
* (1) deal with window.open request
* from mozbrowser iframe to open a proper new window.
* (2) deal with launchactivity request to open activity window.
*
* When an app is opening a page within same origin via window.open,
* the generated new app window would be linked to its caller.
* You could refer to the opener via <code>this.previousWindow</code>
* and refer the openee via <code>this.nextWindow</code>.
*
* On the other hand, if <code>"dialog"</code> feature is specified in
* window.open, we will open a front window, which is an instance of
* PopupWindow, on the caller and set it as the rear window of the callee.
* You could access the dialog's opener via <code>this.rearWindow</code>
* and access the openee via <code>this.frontWindow</code> in the opener.
*
* At most an appWindow instance could have one front window and
* one next window.
*
* @param {AppWindow} app The ordering window of this factory.
*/
var ChildWindowFactory = function ChildWindowFactory(app) {
this.app = app;
this.app.element.addEventListener('mozbrowseropenwindow', this);
this.app.element.addEventListener('_launchactivity',
this.createActivityWindow.bind(this));
this.app.element.addEventListener('_launchtrusted',
this.createTrustedWindow.bind(this));
};
ChildWindowFactory.prototype.handleEvent =
function cwf_handleEvent(evt) {
this.app.debug('[cwf] handling ' + evt.type);
// Prevent Gecko's default handler from opening the window.
evt.preventDefault();
// Handle event from child window.
if (evt.detail && evt.detail.instanceID &&
evt.detail.instanceID !== this.app.instanceID) {
if (this['_handle_child_' + evt.type]) {
this['_handle_child_' + evt.type](evt);
}
return;
}
// Skip to wrapperWindowFactory.
if (this.isHomescreen) {
// XXX: Launch wrapper window here.
return;
}
// <a href="" target="_blank"> should never be part of the app
// except while FTU is running: windows must be closable & parented by FTU
if (evt.detail.name == '_blank' &&
!Service.query('isFtuRunning') &&
evt.detail.features !== 'attention' &&
evt.detail.features !== 'global-clickthrough-overlay') {
this.createNewWindow(evt);
evt.stopPropagation();
return;
}
// Check if this is a call to open the url in an known app.
if (evt.detail.isApp) {
this.app.publish('openwindow',
{ manifestURL: evt.detail.name,
url: evt.detail.url,
timestamp: Date.now() });
evt.stopPropagation();
return;
}
var caught = false;
switch (evt.detail.features) {
case 'dialog':
// Only open popupWindow by app/http/https prefix
if (/^(app|http|https):\/\//i.test(evt.detail.url)) {
caught = this.createPopupWindow(evt);
} else {
caught = this.launchActivity(evt);
}
break;
case 'alwaysLowered':
if (this.app.hasPermission('open-hidden-window')) {
caught = this.createPopupWindow(evt);
}
break;
case 'attention':
// Open attentionWindow
if (!this.createAttentionWindow(evt)) {
this.createPopupWindow(evt);
}
break;
case 'global-clickthrough-overlay':
// Open GlobalOverlayWindow.
this.createGlobalOverlayWindow(evt);
break;
case 'mozhaidasheet':
// This feature is for internal usage only
// before we have final API to open an inner sheet.
caught = this.createChildWindow(evt);
break;
default:
if (ENABLE_IN_APP_SHEET) {
caught = this.createChildWindow(evt);
} else {
caught = this.createPopupWindow(evt);
}
break;
}
if (caught) {
evt.stopPropagation();
}
};
ChildWindowFactory.prototype.createPopupWindow = function(evt) {
if (this.app.frontWindow &&
(this.app.frontWindow.isTransitioning() ||
this.app.frontWindow.isActive())) {
return false;
}
var stayBackground = evt.detail.features.indexOf('alwaysLowered') >= 0;
var configObject = {
url: evt.detail.url,
name: this.app.name,
iframe: evt.detail.frameElement,
origin: this.app.origin,
rearWindow: this.app,
stayBackground: stayBackground
};
var childWindow = new PopupWindow(configObject);
if (!stayBackground) {
childWindow.element.addEventListener('_opened', this);
childWindow.element.addEventListener('_closing', this);
}
childWindow.open();
return true;
};
ChildWindowFactory.prototype._sameOrigin = function(url1, url2) {
var a = url1.split('/');
var b = url2.split('/');
return (a[0] === b[0] && a[2] === b[2]);
};
ChildWindowFactory.prototype.createNewWindow = function(evt) {
if (!this.app.isActive() || this.app.isTransitioning()) {
return false;
}
var parentAllowFullscreenAttr =
evt.target.getAttribute('mozallowfullscreen');
var iframe = evt.detail.frameElement;
// This new window should be allowed to go full screen.
if (parentAllowFullscreenAttr) {
iframe.setAttribute('mozallowfullscreen', parentAllowFullscreenAttr);
}
var configObject = {
url: evt.detail.url,
name: evt.detail.name,
iframe: iframe,
isPrivate: this.app.isPrivateBrowser()
};
window.dispatchEvent(new CustomEvent('openwindow', {
detail: configObject
}));
return true;
};
ChildWindowFactory.prototype.createChildWindow = function(evt) {
if (!this.app.isActive() || this.app.isTransitioning()) {
return false;
}
var configObject = {
url: evt.detail.url,
name: this.app.name,
iframe: evt.detail.frameElement,
origin: this.app.origin,
isPrivate: this.app.isPrivateBrowser()
};
if (this._sameOrigin(this.app.origin, evt.detail.url)) {
configObject.manifestURL = this.app.manifestURL;
configObject.previousWindow = this.app;
} else {
configObject.name = '';
configObject.origin = evt.detail.url;
}
var childWindow = new AppWindow(configObject);
childWindow.requestOpen();
return true;
};
ChildWindowFactory.prototype.createAttentionWindow = function(evt) {
if (!this.app || !this.app.hasPermission('attention')) {
console.error('Cannot create attention window. ' +
'Invalid of underprivileged app');
return false;
}
// Canceling any full screen web content
if (document.mozFullScreen) {
document.mozCancelFullScreen();
}
var attentionFrame = evt.detail.frameElement;
var attention = new AttentionWindow({
iframe: attentionFrame,
url: evt.detail.url,
name: evt.detail.name,
manifestURL: this.app.manifestURL,
origin: this.app.origin,
parentWindow: this.app
});
this.app.attentionWindow = attention;
attention.requestOpen();
return true;
};
ChildWindowFactory.prototype.createGlobalOverlayWindow = function(evt) {
if (!this.app || !this.app.hasPermission('global-clickthrough-overlay')) {
console.error('Cannot create global overlay window. ' +
'Invalid of underprivileged app');
return false;
}
var overlayFrame = evt.detail.frameElement;
var overlay = new GlobalOverlayWindow({
iframe: overlayFrame,
url: evt.detail.url,
name: evt.detail.name,
manifestURL: this.app.manifestURL,
origin: this.app.origin,
parentWindow: this.app
});
this.app.overlayWindow = overlay;
overlay.requestOpen();
return true;
};
ChildWindowFactory.prototype._handle_child__opened = function(evt) {
// Do nothing if we are not active or we are being killing.
if (!this.app.isVisible() || this.app._killed) {
return;
}
this.app.setVisible(false, true);
};
ChildWindowFactory.prototype._handle_child__closing = function(evt) {
// Do nothing if we are not active or we are being killing.
if (!this.app.isVisible() || this.app._killed) {
return;
}
this.app.setOrientation();
if (Service.query('getTopMostWindow').getBottomMostWindow() ===
this.app.getBottomMostWindow()) {
this.app.setVisible(true, true);
}
// An activity handled by ActivityWindow is always an inline activity.
// All window activities are handled by AppWindow. All inline
// activities have a rearWindow. Once this inline activity is killed,
// make rear window visible to screen reader and the focus should be
// transfered to its rear window.
evt.detail.rearWindow._setVisibleForScreenReader(true);
evt.detail.rearWindow.focus();
};
ChildWindowFactory.prototype.createActivityWindow = function(evt) {
var configuration = evt.detail;
var top = this.app.getTopMostWindow();
// If the lastActivity is the same as launch request, we don't need to
// create another activity.
if (top.manifestURL == configuration.manifestURL &&
top.url == configuration.url) {
return;
}
// We need to blur the opener before opening the new one.
top.setNFCFocus(false);
var activity = new ActivityWindow(configuration, top);
activity.element.addEventListener('_closing', this);
activity.element.addEventListener('_opened', this);
activity.open();
// Make topmost window browser element invisibile to screen reader.
top._setVisibleForScreenReader(false);
};
ChildWindowFactory.prototype.createTrustedWindow = function(evt) {
var configuration = evt.detail;
var top = this.app.getTopMostWindow();
var trusted = new TrustedWindow(configuration, top);
trusted.element.addEventListener('_opened', this);
trusted.element.addEventListener('_closing', this);
trusted.open();
};
ChildWindowFactory.prototype.launchActivity = function(evt) {
var activity = new MozActivity({
name: 'view',
data: {
type: 'url',
url: evt.detail.url
}
});
activity.onerror = function() {
this.app.debug(
'view activity error:' + activity.error.name + evt.detail.url);
};
return true;
};
exports.ChildWindowFactory = ChildWindowFactory;
}(window));

View file

@ -0,0 +1,3 @@
;'use strict';(function(exports){function Clock(){this.timeoutID=null;this.timerID=null;this.start=function cl_start(refresh){var date=new Date();var self=this;refresh(date);if(this.timeoutID==null){this.timeoutID=window.setTimeout(function cl_setClockInterval(){refresh(new Date());if(self.timerID==null){self.timerID=window.setInterval(function cl_clockInterval(){refresh(new Date());},60000);}},(60-date.getSeconds())*1000);}};this.stop=function cl_stop(){if(this.timeoutID!=null){window.clearTimeout(this.timeoutID);this.timeoutID=null;}
if(this.timerID!=null){window.clearInterval(this.timerID);this.timerID=null;}};}
exports.Clock=Clock;})(window);

View file

@ -0,0 +1,156 @@
/* exported ContextMenuView */
/* global BaseUI, Service, eventSafety */
(function(exports) {
'use strict';
var _id = 0;
/**
* The ContextMenu UI for BrowserContextMenu.
*
* @class ContextMenuView
* @param {BaseModule} parentModule The BrowserContextMenu instance
* where this dialog should popup.
* @extends BaseUI
*/
function ContextMenuView(parentModule) {
this.parentModule = parentModule;
this.containerElement = parentModule.containerElement;
this.instanceID = _id++;
this._injected = false;
}
ContextMenuView.prototype = Object.create(BaseUI.prototype);
ContextMenuView.prototype.CLASS_NAME = 'BrowserContextMenu';
ContextMenuView.prototype.ELEMENT_PREFIX = 'contextmenu-';
ContextMenuView.prototype.TRANSITION_TIMEOUT = 350;
ContextMenuView.prototype.customID = function am_customID() {
return 'context-menu';
};
ContextMenuView.prototype._fetchElements = function bcm__fetchElements() {
this.element = this.containerElement.querySelector('#' + this.CLASS_NAME +
this.instanceID);
this.elements = {};
var toCamelCase = function toCamelCase(str) {
return str.replace(/\-(.)/g, function replacer(str, p1) {
return p1.toUpperCase();
});
};
this.elementClasses = ['header', 'list'];
// Loop and add element with camel style name to Modal Dialog attribute.
this.elementClasses.forEach(function createElementRef(name) {
this.elements[toCamelCase(name)] =
this.element.querySelector('.' + this.ELEMENT_PREFIX + name);
}, this);
var cancel = document.createElement('button');
cancel.id = 'ctx-cancel-button';
cancel.dataset.action = 'cancel';
cancel.setAttribute('data-l10n-id', 'cancel');
this.elements.cancel = cancel;
};
ContextMenuView.prototype._registerEvents = function() {
this.elements.cancel.addEventListener('click', this.hide.bind(this));
};
ContextMenuView.prototype.view = function() {
var id = this.CLASS_NAME + this.instanceID;
var content = `<form class="contextmenu" role="dialog" tabindex="-1"
data-type="action" id="${id}">
<header class="contextmenu-header"></header>
<menu class="contextmenu-list"></menu>
</form>`;
return content;
};
ContextMenuView.prototype.kill = function() {
this.containerElement.removeChild(this.element);
};
ContextMenuView.prototype.show = function(menu) {
if (!this._injected) {
this.render();
this._injected = true;
}
this.buildMenu(menu);
this.element.classList.add('visible');
this._requestFocus();
};
ContextMenuView.prototype.hide = function(evt) {
if (!this.element) {
return Promise.resolve();
}
if (evt) {
evt.preventDefault();
}
return new Promise((resolve, reject) => {
this.element.classList.remove('visible');
this._requestFocus();
eventSafety(this.element, 'transitionend', resolve, 400);
});
};
ContextMenuView.prototype.buildMenu = function(items) {
this.elements.list.innerHTML = '';
items.forEach(function traveseItems(item) {
var action = document.createElement('button');
action.dataset.id = item.id;
action.dataset.value = item.value;
action.textContent = item.label;
if (item.icon) {
action.classList.add(item.iconClass || 'icon');
action.style.backgroundImage = 'url(' + item.icon + ')';
}
action.addEventListener('click', function(evt) {
this.hide(evt).then(() => {
item.callback();
});
}.bind(this));
this.elements.list.appendChild(action);
}, this);
var lastButtonWrapper = document.createElement('span');
lastButtonWrapper.className = 'last-button-container';
this.elements.list.appendChild(lastButtonWrapper);
lastButtonWrapper.appendChild(this.elements.cancel);
};
ContextMenuView.prototype.focus = function() {
document.activeElement.blur();
this.elements.cancel.focus();
};
ContextMenuView.prototype._requestFocus = function() {
var transited = function() {
this.element.removeEventListener('transitionend', transited);
if (this.transitionTimeout) {
clearTimeout(this.transitionTimeout);
this.transitionTimeout = null;
}
// The focus switch should be after this module is already shown and
// transited.
Service.request('focus');
}.bind(this);
this.element.addEventListener('transitionend', transited);
this.transitionTimeout = setTimeout(transited, this.TRANSITION_TIMEOUT);
};
exports.ContextMenuView = ContextMenuView;
})(window);

View file

@ -0,0 +1,181 @@
/* global BaseModule, LazyLoader, RemoteDebugger, DeveloperHud, DevToolsAuth */
'use strict';
(function(exports) {
/**
* This is the bootstrap module of the system app.
* It is responsible to instantiate and start the other core modules
* and sub systems per API.
*/
var Core = function() {
};
Core.SUB_MODULES = [
'SleepMenu',
'OrientationManager',
'HierarchyManager',
'FeatureDetector',
'SystemDialogManager',
'WallpaperManager',
'LayoutManager',
'SoftwareButtonManager',
'AppCore',
'ScreenManager'
];
Core.SERVICES = [
'getAPI'
];
BaseModule.create(Core, {
DEBUG: false,
name: 'Core',
REGISTRY: {
'mozTelephony': 'TelephonyMonitor',
'mozBluetooth': 'BluetoothCore',
'mozMobileConnections': 'MobileConnectionCore',
'mozNfc': 'NfcCore',
'battery': 'BatteryOverlay',
'mozWifiManager': 'Wifi',
'mozVoicemail': 'Voicemail'
},
getAPI: function(api) {
for (var key in this.REGISTRY) {
if (api === BaseModule.lowerCapital(key.replace('moz', ''))) {
return navigator[key];
}
}
return false;
},
_start: function() {
// We need to be sure to get the focus in order to wake up the screen
// if the phone goes to sleep before any user interaction.
// Apparently it works because no other window
// has the focus at this point.
window.focus();
// With all important event handlers in place, we can now notify
// Gecko that we're ready for certain system services to send us
// messages (e.g. the radio).
// Note that shell.js starts listen for the mozContentEvent event at
// mozbrowserloadstart, which sometimes does not happen till
// window.onload.
this.publish('mozContentEvent', {
type: 'system-message-listener-ready'
}, true);
return this.loadWhenIdle([
'Statusbar',
'HardwareButtons',
'CameraTrigger',
'NotificationScreen',
'AirplaneMode',
'NotificationsSystemMessage',
'Accessibility',
'AlarmMonitor',
'DebuggingMonitor',
'TimeCore',
'GeolocationCore',
'TetheringMonitor',
'UsbCore',
'AppTextSelectionDialog',
'ExternalStorageMonitor',
'DeviceStorageWatcher',
'AppUsageMetrics',
'AdvancedTelemetry',
'CellBroadcastSystem',
// This should be loaded by MobileConnectionCore.
// However, the integration test is testing this on desktop b2g
// which has no navigator.mozMobileConnections.
'CpuManager',
'HomeGesture',
'ViewSource',
'TtlView',
'MediaRecording',
'QuickSettings',
'UsbStorage',
'MobileIdManager',
'FindmydeviceLauncher',
'FxAccountsManager',
'FxAccountsUI',
'NetworkActivity',
'CrashReporter',
'Screenshot',
'SoundManager',
'CustomDialogService',
'CarrierInfoNotifier',
'AboutServiceWorkersProxy',
'MultiScreenController',
'PinsManager',
'SyncManager'
// XXX: We should move CarrierInfoNotifier into mobileConnectionCore,
// but integration tests running on desktop without mobileConnection
// is testing this.
]).then(() => {
return Promise.all([
this.startAPIHandlers(),
LazyLoader.load([
'js/download/download_manager.js',
'js/payment.js',
'js/identity.js',
'js/devtools/logshake.js',
'js/devtools/remote_debugger.js',
'js/devtools/developer_hud.js',
'js/devtools/devtools_auth.js',
'js/devtools/devtools_auth_dialog.js'
])
]).then(() => {
this.remoteDebugger = new RemoteDebugger();
this.developerHud = new DeveloperHud();
this.devToolsAuth = new DevToolsAuth();
return Promise.resolve(this.developerHud.start());
});
});
},
startAPIHandlers: function() {
var promises = [];
for (var api in this.REGISTRY) {
this.debug('Detecting API: ' + api +
' and corresponding module: ' + this.REGISTRY[api]);
if (navigator[api]) {
this.debug('API: ' + api + ' found, starting the handler.');
promises.push(this.startAPIHandler(api, this.REGISTRY[api]));
} else {
this.debug('API: ' + api + ' not found, skpping the handler.');
}
}
return Promise.all(promises);
},
startAPIHandler: function(api, handler) {
return BaseModule.lazyLoad([handler]).then(() => {
var moduleName = BaseModule.lowerCapital(handler);
if (window[handler] && typeof(window[handler]) === 'function') {
this[moduleName] = new window[handler](navigator[api], this);
} else if (BaseModule.defined(handler)) {
this[moduleName] =
BaseModule.instantiate(handler, navigator[api], this);
} else if (window[handler]) {
this[moduleName] = window[handler];
} else {
return Promise.reject('Cannot find handler for ' + api);
}
return this[moduleName].start();
});
},
_stop: function() {
for (var api in this.REGISTRY) {
var moduleName =
this.REGISTRY[api].charAt(0).toUpperCase() +
this.REGISTRY[api].slice(1);
this[moduleName] && this[moduleName].stop();
}
}
});
}(window));

View file

@ -0,0 +1,197 @@
'use strict';
/* global applications*/
(function(exports) {
const activityThreshold = 75;
const host = document.location.host;
const domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
const protocol = document.location.protocol + '//';
const origin = protocol + 'costcontrol.' + domain;
function CostControl() {}
CostControl.prototype = {
/**
* A reference to the widget container.
* @memberof CostControl.prototype
*/
widgetContainer: document.getElementById('cost-control-widget'),
/**
* A reference to the widget element.
* @memberof CostControl.prototype
*/
widgetFrame: null,
/**
* Current activity measure.
* @typeof {Integer}
* @memberof CostControl.prototype
*/
activityCounter: 0,
/**
* Starts listening to events to show/hide the widget.
* @memberof CostControl.prototype
*/
start: function() {
window.addEventListener('utilitytrayshow', this);
window.addEventListener('utilitytray-overlayopening', this);
window.addEventListener('utilitytrayhide', this);
},
/**
* General event handler interface.
* @memberof CostControl.prototype
*/
handleEvent: function(e) {
switch(e.type) {
case 'mozbrowsererror':
case 'mozbrowserclose':
this._onError();
break;
case 'moznetworkupload':
case 'moznetworkdownload':
this._onNetworkActivity();
break;
case 'mozbrowserlocationchange':
this._onUpdateDone();
break;
case 'utilitytrayshow':
this._showWidget();
break;
case 'utilitytray-overlayopening':
this._showWidgetIfLoaded();
break;
case 'utilitytrayhide':
this._hideWidget();
break;
}
},
/**
* Ensures the widget is loaded.
* @memberof CostControl.prototype
*/
_ensureWidget: function() {
if (!applications.ready) {
return;
}
if (!applications.getByManifestURL(origin + '/manifest.webapp')) {
return;
}
// Check widget is there
this.widgetFrame = this.widgetContainer.querySelector('iframe');
if (this.widgetFrame) {
return;
}
// Create the widget
if (!this.widgetFrame) {
this.widgetFrame = document.createElement('iframe');
this.widgetFrame.addEventListener('mozbrowsererror', this);
this.widgetFrame.addEventListener('mozbrowserclose', this);
}
this.widgetFrame.dataset.frameType = 'widget';
this.widgetFrame.dataset.frameOrigin = origin;
this.widgetFrame.setAttribute('mozbrowser', true);
this.widgetFrame.setAttribute('remote', 'true');
this.widgetFrame.setAttribute('mozapp', origin + '/manifest.webapp');
this.widgetFrame.src = origin + '/widget.html';
this.widgetContainer.appendChild(this.widgetFrame);
this._attachNetworkEvents();
},
/**
* Called when the widget fires an error event.
* @memberof CostControl.prototype
*/
_onError: function(e) {
this.widgetContainer.removeChild(this.widgetFrame);
this.widgetFrame = null;
},
/**
* Starts listening to network events.
* @memberof CostControl.prototype
*/
_attachNetworkEvents: function() {
window.removeEventListener('moznetworkupload', this);
window.removeEventListener('moznetworkdownload', this);
window.addEventListener('moznetworkupload', this);
window.addEventListener('moznetworkdownload', this);
},
/**
* Called when we detect some network activity.
* @memberof CostControl.prototype
*/
_onNetworkActivity: function() {
this.activityCounter++;
if (this.activityCounter === activityThreshold) {
this.activityCounter = 0;
window.removeEventListener('moznetworkupload', this);
window.removeEventListener('moznetworkdownload', this);
this.widgetFrame.addEventListener('mozbrowserlocationchange', this);
this.widgetFrame.src = origin + '/widget.html#update#' +
window.performance.now();
}
},
/**
* Called after the widget has updated its location.
* @memberof CostControl.prototype
*/
_onUpdateDone: function(evt) {
if (evt.detail.split('#')[1] === 'updateDone') {
this.widgetFrame.removeEventListener('mozbrowserlocationchange', this);
this._attachNetworkEvents();
}
},
/**
* Shows the Usage widget in the utility tray.
* @memberof CostControl.prototype
*/
_showWidget: function() {
this._ensureWidget();
// Ensure the widget is updated when is visible
this._attachNetworkEvents();
this.widgetFrame.setVisible(true);
},
/**
* Ensures we show the loaded widget.
* @memberof CostControl.prototype
*/
_showWidgetIfLoaded: function() {
if (this.widgetFrame) {
this.widgetFrame.setVisible(true);
}
},
/**
* Hides the widget when we close the utility tray.
* @memberof CostControl.prototype
*/
_hideWidget: function() {
// It's not necessary to update the widget when it is hidden.
window.removeEventListener('moznetworkupload', this);
window.removeEventListener('moznetworkdownload', this);
if (this.widgetFrame) {
this.widgetFrame.setVisible(false);
}
}
};
exports.CostControl = CostControl;
}(window));

View file

@ -0,0 +1,337 @@
/* global BaseModule, LogoLoader */
'use strict';
(function(exports) {
const DEFAULT_RESOURCES = {
poweron: {
video: '/resources/power/carrier_power_on.mp4',
image: '/resources/power/carrier_power_on.png'
},
poweroff: {
video: '/resources/power/carrier_power_off.mp4',
image: '/resources/power/carrier_power_off.png'
}
};
const ATT_PWRON_VIDEO = 'poweron.video';
const ATT_PWRON_IMG = 'poweron.image';
const ATT_PWROFF_VIDEO = 'poweroff.video';
const ATT_PWROFF_IMG = 'poweroff.image';
var CoverScreen = function() {};
CoverScreen.SETTINGS = [
'operatorResources.power'
];
CoverScreen.SERVICES = [
'poweroff',
'animatePoweronLogo'
];
BaseModule.create(CoverScreen, {
name: 'CoverScreen',
EVENT_PREFIX: 'initlogo',
ready: false,
animated: false,
readyCallBack: null,
DEBUG: false,
SETTING_POWER: 'operatorResources.power',
_start: function() {
// We need to lock the orientation as early as we could.
screen.mozLockOrientation('default');
this.carrierLogo = document.getElementById('carrier-logo');
this.osLogo = document.getElementById('os-logo');
this.service.request('registerHierarchy', this);
},
_removeCarrierPowerOn: function ilh_removeCarrierPowerOn() {
var self = this;
if (this.carrierLogo && this.carrierLogo.parentNode) {
this.carrierLogo.parentNode.removeChild(self.carrierLogo);
this._setReady();
} else {
document.addEventListener('DOMContentLoaded', function() {
if (self.carrierLogo) {
self.carrierLogo.parentNode.removeChild(self.carrierLogo);
}
self._setReady();
});
}
},
_appendCarrierPowerOn: function ilh_appendCarrierPowerOn() {
this.debug('appending power on logo');
if (this.carrierLogo) {
this.carrierLogo.appendChild(this.poweronLogoLoader.element);
this._setReady();
} else {
var self = this;
document.addEventListener('DOMContentLoaded', function() {
self.carrierLogo.appendChild(self.poweronLogoLoader.element);
self._setReady();
});
}
},
_setReady: function ilh_setReady() {
this.ready = true;
var elem = this.poweronLogoLoader.element;
if (elem && elem.tagName.toLowerCase() == 'video') {
// Play video just after the element is first painted.
window.addEventListener('mozChromeEvent', function startVideo(e) {
if (e.detail.type == 'system-first-paint') {
window.removeEventListener('mozChromeEvent', startVideo);
if (elem && elem.ended === false) {
elem.play();
}
}
});
}
if (this.readyCallBack) {
this.readyCallBack();
this.readyCallBack = null;
}
this.publish('-activated');
},
_waitReady: function ilh_waitReady(callback) {
this.readyCallBack = callback;
},
setDefaultValues: function () {
if (!this._poweron) {
this._poweron = {};
}
if (!this._poweroff) {
this._poweroff = {};
}
this._poweron.video = DEFAULT_RESOURCES.poweron.video;
this._poweron.image = DEFAULT_RESOURCES.poweron.image;
this._poweroff.video = DEFAULT_RESOURCES.poweroff.video;
this._poweroff.image = DEFAULT_RESOURCES.poweroff.image;
},
'_observe_operatorResources.power': function(powerValues) {
if (!powerValues) {
this.debug('No power on settings found, using default.');
this.setDefaultValues();
this.poweron();
return;
}
if (!this._poweron) {
this._poweron = {};
}
if (!this._poweroff) {
this._poweroff = {};
}
this._poweron.video = powerValues[ATT_PWRON_VIDEO] ||
DEFAULT_RESOURCES.poweron.video;
this._poweron.image = powerValues[ATT_PWRON_IMG] ||
DEFAULT_RESOURCES.poweron.image;
this._poweroff.video = powerValues[ATT_PWROFF_VIDEO] ||
DEFAULT_RESOURCES.poweroff.video;
this._poweroff.image = powerValues[ATT_PWROFF_IMG] ||
DEFAULT_RESOURCES.poweroff.image;
},
poweron: function() {
this.debug('start poweron logo: ' + JSON.stringify(this._poweron));
this.poweronLogoLoader = new LogoLoader(this._poweron);
this.poweronLogoLoader.onnotfound = this._removeCarrierPowerOn.bind(this);
this.poweronLogoLoader.onload = this._appendCarrierPowerOn.bind(this);
},
isActive: function() {
return this.ready && !this.animated;
},
animatePoweronLogo: function ilh_animate(callback) {
var self = this;
if (!this.ready) {
this._waitReady(this.animatePoweronLogo.bind(this, callback));
return;
}
if (this.animated) {
return;
}
this.debug('stopping poweron logo');
this.animated = true;
this.publish('-deactivated');
this.service.request('unregisterHierarchy', this);
// No carrier logo - Just animate OS logo.
if (!this.poweronLogoLoader.found) {
this.osLogo.classList.add('hide');
// Has carrier logo - Animate carrier logo, then OS logo.
} else {
// CarrierLogo is not transparent until now
// to prevent flashing.
this.carrierLogo.className = 'transparent';
var elem = this.poweronLogoLoader.element;
if (elem.tagName.toLowerCase() == 'video' && !elem.ended) {
// compability: ensure movie being played here in case
// system-first-paint is not supported by Gecko.
elem.play();
elem.onended = function() {
elem.classList.add('hide');
};
} else {
elem.classList.add('hide');
}
self.carrierLogo.addEventListener('transitionend',
function transCarrierLogo(evt) {
evt.stopPropagation();
self.carrierLogo.removeEventListener('transitionend',
transCarrierLogo);
if (elem.tagName.toLowerCase() == 'video') {
// XXX workaround of bug 831747
// Unload the video. This releases the video decoding hardware
// so other apps can use it.
elem.removeAttribute('src');
elem.load();
}
self.carrierLogo.parentNode.removeChild(self.carrierLogo);
delete self.carrierLogo; // Don't entrain the DOM nodes.
self.osLogo.classList.add('hide');
self.carrierPowerOnElement = null;
});
}
self.osLogo.addEventListener('transitionend', function transOsLogo() {
self.osLogo.removeEventListener('transitionend', transOsLogo);
self.osLogo.parentNode.removeChild(self.osLogo);
delete self.osLogo; // Don't entrain the DOM nodes.
window.performance.mark('osLogoEnd');
window.dispatchEvent(new CustomEvent('logohidden'));
if (callback) {
callback();
}
});
},
poweroff: function(reboot) {
this.debug('start poweroff logo');
var power = navigator.mozPower;
var self = this;
if (!power) {
return;
}
// Early return if we are already shutting down.
if (document.getElementById('poweroff-splash')) {
return;
}
this.publish('will-shutdown', null, true);
// Show shutdown animation before actually performing shutdown.
// * step1: fade-in poweroff-splash.
// * step2: - As default, 3-ring animation is performed on the screen.
// - Manufacturer can customize the animation using mp4/png
// file to replace the default.
var div = document.createElement('div');
div.dataset.zIndexLevel = 'poweroff-splash';
div.id = 'poweroff-splash';
this.poweroffLogoLoader = new LogoLoader(this._poweroff);
this.poweroffLogoLoader.onload = function(elem) {
// Perform customized animation.
div.appendChild(elem);
div.className = 'step1';
if (elem.tagName.toLowerCase() == 'video' && !elem.ended) {
elem.onended = function() {
elem.classList.add('hide');
// XXX workaround of bug 831747
// Unload the video. This releases the video decoding hardware
// so other apps can use it.
elem.removeAttribute('src');
elem.load();
};
elem.play();
} else {
div.addEventListener('animationend', function() {
elem.classList.add('hide');
if (elem.tagName.toLowerCase() == 'video') {
// XXX workaround of bug 831747
// Unload the video. This releases the video decoding hardware
// so other apps can use it.
elem.removeAttribute('src');
elem.load();
}
});
}
elem.addEventListener('transitionend', function() {
self._actualPowerOff(reboot);
});
document.getElementById('screen').appendChild(div);
};
this.poweroffLogoLoader.onnotfound = function() {
// - Perform OS default animation.
// The overall animation ends when the inner span of the bottom ring
// is animated, so we store it for detecting.
var inner;
for (var i = 1; i <= 3; i++) {
var outer = document.createElement('span');
outer.className = 'poweroff-ring';
outer.id = 'poweroff-ring-' + i;
div.appendChild(outer);
inner = document.createElement('span');
outer.appendChild(inner);
}
div.className = 'step1';
var nextAnimation = function nextAnimation(e) {
// Switch to next class
if (e.target == div) {
div.className = 'step2';
}
if (e.target != inner) {
return;
}
self._actualPowerOff(reboot);
};
div.addEventListener('animationend', nextAnimation);
document.getElementById('screen').appendChild(div);
};
},
/**
* Helper for powering the device off.
* @memberof SleepMenu.prototype
* @param {Boolean} isReboot Whether or not to reboot the phone.
*/
_actualPowerOff: function sm_actualPowerOff(isReboot) {
this.debug('stopping poweroff logo and poweroff/reboot');
var power = navigator.mozPower;
if (isReboot) {
power.reboot();
} else {
power.powerOff();
}
}
});
// We need to start as early as possible.
exports.CoverScreen = BaseModule.instantiate('CoverScreen');
exports.CoverScreen.start();
}(window));

View file

@ -0,0 +1,49 @@
'use strict';
/* global CpuWakeLockManager */
(function(exports) {
/**
* CpuManager is a tiny baby that does nothing except set the
* |cpuSleepAllowed| property on mozPower.
* But, it would probably grow up as all Gaia script do.
*/
var CpuManager = function CpuManager() {
this._wakeLockManager = null;
this._started = false;
};
CpuManager.prototype.start = function() {
if (this._started) {
throw new Error(
'CpuManager: should not be start()\'ed twice.');
}
if (!navigator.mozPower) {
return;
}
this._started = true;
this._wakeLockManager = new CpuWakeLockManager();
this._wakeLockManager.onwakelockchange =
this._handleWakeLockChange.bind(this);
this._wakeLockManager.start();
};
CpuManager.prototype.stop = function() {
if (!this._started) {
throw new Error('CpuManager: ' +
'Instance was never start()\'ed but stop() is called.');
}
this._started = false;
this._wakeLockManager.stop();
this._wakeLockManager = null;
};
CpuManager.prototype._handleWakeLockChange = function(isHeld) {
navigator.mozPower.cpuSleepAllowed = !isHeld;
};
exports.CpuManager = CpuManager;
}(window));

View file

@ -0,0 +1,178 @@
/* global SettingsListener, SystemBanner, LazyLoader */
/* exported CrashReporter */
'use strict';
// This file calls getElementById without waiting for an onload event, so it
// must have a defer attribute or be included at the end of the <body>.
var CrashReporter = (function() {
var _ = navigator.mozL10n.get;
var settings = navigator.mozSettings;
var screen = document.getElementById('screen');
// The name of the app that just crashed. We'll have special handling for
// when this remains null or is set to null.
var crashedAppName = null;
// Whether or not to show a "Report" button in the banner.
var showReportButton = false;
// Only show the "Report" button if the user hasn't set a preference to
// always/never report crashes.
SettingsListener.observe('app.reportCrashes', 'ask',
function handleCrashSetting(value) {
showReportButton = (value != 'always' && value != 'never');
});
// This function should only ever be called once.
function showDialog(crashID, isChrome) {
var elem = document.getElementById('crash-dialog-title');
if (isChrome) {
navigator.mozL10n.setAttributes(elem, 'crash-dialog2-os');
} else {
navigator.mozL10n.setAttributes(
elem,
'crash-dialog-app',
{ name: crashedAppName || _('crash-dialog-app-noname') }
);
}
// "Don't Send Report" button in dialog
var noButton = document.getElementById('dont-send-report');
noButton.addEventListener('click', function onNoButtonClick() {
settings.createLock().set({'app.reportCrashes': 'never'});
removeDialog();
});
// "Send Report" button in dialog
var yesButton = document.getElementById('send-report');
yesButton.addEventListener('click', function onYesButtonClick() {
submitCrash(crashID);
if (checkbox.checked) {
settings.createLock().set({'app.reportCrashes': 'always'});
}
removeDialog();
});
var checkbox = document.getElementById('always-send');
checkbox.addEventListener('click', function onCheckboxClick() {
// Disable the "Don't Send Report" button if the "Always send..."
// checkbox is checked
noButton.disabled = this.checked;
});
// "What's in a crash report?" link
var crashInfoLink = document.getElementById('crash-info-link');
crashInfoLink.addEventListener('click', function onLearnMoreClick() {
var dialog = document.getElementById('crash-dialog');
document.getElementById('crash-reports-header').
addEventListener('action', function onAction() {
this.removeEventListener('click', onAction);
dialog.classList.remove('learn-more');
});
dialog.classList.add('learn-more');
});
screen.classList.add('crash-dialog');
}
// We can get rid of the dialog after it is shown once.
function removeDialog() {
screen.classList.remove('crash-dialog');
var dialog = document.getElementById('crash-dialog');
dialog.parentNode.removeChild(dialog);
}
function showBanner(crashID, isChrome) {
var appName = crashedAppName || _('crash-dialog-app-noname');
var message = isChrome ? 'crash-banner-os2' :
{id: 'crash-banner-app', args: { name: appName }};
var button = null;
if (showReportButton) {
button = {
label: 'crash-banner-report',
callback: function reportCrash() {
submitCrash(crashID);
},
dismiss: function dismissCrash() {
deleteCrash(crashID);
}
};
}
LazyLoader.load(['js/system_banner.js']).then(() => {
var systemBanner = new SystemBanner();
systemBanner.show(message, button);
}).catch((err) => {
console.error(err);
});
}
function deleteCrash(crashID) {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentEvent', true, true, {
type: 'delete-crash',
crashID: crashID
});
window.dispatchEvent(event);
}
function submitCrash(crashID) {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentEvent', true, true, {
type: 'submit-crash',
crashID: crashID
});
window.dispatchEvent(event);
}
// - Show a dialog only the first time there's a crash to report.
// - On subsequent crashes, show a banner letting the user know there was a
// crash.
// - If the user hasn't set a pref, add a "Report" button to the banner.
function handleCrash(crashID, isChrome) {
// Check to see if we should show a dialog.
var dialogReq = settings.createLock().get('crashReporter.dialogShown');
dialogReq.onsuccess = function dialogShownSuccess() {
var dialogShown = dialogReq.result['crashReporter.dialogShown'];
if (!dialogShown) {
settings.createLock().set({'crashReporter.dialogShown': true});
showDialog(crashID, isChrome);
} else {
showBanner(crashID, isChrome);
}
};
}
// We depend on window_manager.js calling this function before
// we get a 'handle-crash' event from shell.js
function setAppName(name) {
crashedAppName = name;
}
// We will be notified of system crashes from shell.js
window.addEventListener('mozChromeEvent', function handleChromeEvent(e) {
if (e.detail.type == 'handle-crash') {
handleCrash(e.detail.crashID, e.detail.chrome);
}
});
function handleAppCrash(e) {
var app = e.detail;
// Only show crash reporter when the crashed app is active.
if (app.isActive()) {
setAppName(app.name);
}
}
window.addEventListener('appcrashed', handleAppCrash);
window.addEventListener('activitycrashed', handleAppCrash);
window.addEventListener('homescreencrashed', handleAppCrash);
window.addEventListener('searchcrashed', handleAppCrash);
return {
handleCrash: handleCrash,
handleAppCrash: handleAppCrash,
setAppName: setAppName
};
})();

View file

@ -0,0 +1,33 @@
/* global BaseModule, CustomDialog, LazyLoader */
'use strict';
(function() {
/**
* The module is grouping custom dialog request.
* It will lazy load CustomDialog once the first request comes.
* XXX: All custom dialogs in system app
* should be transfered into systemDialog in the future.
*/
var CustomDialogService = function() {};
CustomDialogService.SERVICES = [
'showCustomDialog',
'hideCustomDialog'
];
BaseModule.create(CustomDialogService, {
name: 'CustomDialogService',
showCustomDialog: function(title, msg, cancel, confirm) {
return LazyLoader.load(['shared/js/custom_dialog.js']).then(() => {
CustomDialog.show(title, msg, cancel, confirm,
document.getElementById('screen'))
.setAttribute('data-z-index-level', 'system-dialog');
}).catch((err) => {
console.error(err);
});
},
hideCustomDialog: function() {
if (window.CustomDialog) {
CustomDialog && CustomDialog.hide();
}
}
});
}());

View file

@ -0,0 +1,24 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var DebuggingIcon = function(manager) {
BaseIcon.call(this, manager);
};
DebuggingIcon.prototype = Object.create(BaseIcon.prototype);
DebuggingIcon.prototype.name = 'DebuggingIcon';
DebuggingIcon.prototype.shouldDisplay = function() {
return this.manager.enabled;
};
DebuggingIcon.prototype.view = function view() {
return `<div id="statusbar-debugging"
data-icon="bug"
data-l10n-id="statusbarDebugging"
class="sb-icon sb-icon-debugging" hidden
role="listitem">
</div>`;
};
exports.DebuggingIcon = DebuggingIcon;
}(window));

View file

@ -0,0 +1,24 @@
/* global BaseModule, DebuggingIcon, LazyLoader */
'use strict';
(function() {
var DebuggingMonitor = function() {};
DebuggingMonitor.SETTINGS = [
'debugger.remote-mode'
];
BaseModule.create(DebuggingMonitor, {
name: 'DebuggingMonitor',
_start: function() {
LazyLoader.load(['js/debugging_icon.js']).then(function() {
this.icon = new DebuggingIcon(this);
this.icon.start();
}.bind(this)).catch(function(err) {
console.error(err);
});
},
'_observe_debugger.remote-mode': function(value) {
this.enabled = (value !== 'disabled');
this.icon && this.icon.update();
}
});
}());

View file

@ -0,0 +1,184 @@
/* global MozActivity, LazyLoader, Service, SystemBanner */
/**
* DeviceStorageWatcher listens for nsIDOMDeviceStorage.onchange events
* notifying about low device storage situations (see Bug 861921). When a
* 'onchange' event containing a 'low-disk-space' reason is received, we show a
* banner for a few seconds and pin a notification in the notifications center.
* When the reason of the 'onchange' event is 'available-disk-space', we remove
* the pinned notification, if one exists.
*/
'use strict';
(function(exports) {
var DeviceStorageWatcher = {
LOW_DISK_NOTIF_ID: 'low-disk-space',
start: function dsw_init() {
this._lowDeviceStorage = false;
this._freeSpace = null;
this._ = navigator.mozL10n.get;
this._appStorage = navigator.getDeviceStorage('apps');
this._appStorage.addEventListener('change', this);
this._container = document.getElementById('storage-watcher-container');
this._container.onclick = this.containerClicked;
this._message = this._container.querySelector('.title-container');
this._availableSpace = this._container.querySelector('.detail');
window.addEventListener('appopening', this);
},
containerClicked: function dsw_containerClicked() {
// We redirect the user to the app permissions section of the settings app
// so she can uninstall 3rd party apps trying to recover from the low
// storage situation. This is not the best solution, but the only one that
// we have so far. In the future, we must expose how much storage each app
// consumes, so the user has a better idea of which apps are consuming
// more (see Bug 862408).
/* jshint nonew: false */
new MozActivity({
name: 'configure',
data: {
section: 'appPermissions'
}
});
},
hideNotification: function dsw_hideNotification() {
this._lowDeviceStorage = false;
if (this._container.classList.contains('displayed')) {
this._container.classList.remove('displayed');
Service.request('NotificationScreen:removeUnreadNotification',
this.LOW_DISK_NOTIF_ID);
}
},
displayNotification: function dsw_displayNotification() {
this._lowDeviceStorage = true;
if (!this._container.classList.contains('displayed')) {
this._container.classList.add('displayed');
Service.request('NotificationScreen:addUnreadNotification',
this.LOW_DISK_NOTIF_ID);
}
},
displayBanner: function dsw_displayBanner(space) {
var notification = ['low-device-storage'];
if (space && typeof space.size !== 'undefined' && space.unit) {
notification.push({
id: 'free-space',
args: {
value: space.size,
unit: space.unit
}
});
} else {
notification.push('unknown-free-space');
}
LazyLoader.load(['js/system_banner.js']).then(() => {
var systemBanner = new SystemBanner();
systemBanner.show(notification);
}).catch((err) => {
console.error(err);
});
},
lowDiskSpaceNotification: function dsw_lowDiskSpaceNotification(space) {
this.displayBanner(space);
this._message.setAttribute('data-l10n-id', 'low-device-storage');
this.updateAvailableSpace(space);
this.displayNotification();
},
updateAvailableSpace: function dsw_updateAvailableSpace(space) {
if (space && typeof space.size !== 'undefined' && space.unit) {
navigator.mozL10n.setAttributes(
this._availableSpace,
'free-space',
{
value: space.size,
unit: space.unit
}
);
} else {
this._availableSpace.setAttribute('data-l10n-id', 'unknown-free-space');
}
},
/**
* Helper function to format the value returned by the
* nsIDOMDeviceStorage.freeSpace call in a more readable way.
*/
formatSize: function dsw_formatSize(size) {
if (size === undefined || isNaN(size)) {
return;
}
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var i = 0;
while (size >= 1024 && i < (units.length) - 1) {
size /= 1024;
++i;
}
var sizeDecimal = i < 2 ? Math.round(size) : Math.round(size * 10) / 10;
return {
size: sizeDecimal,
unit: this._('byteUnit-' + units[i])
};
},
handleEvent: function dsw_handleEvent(evt) {
// Display low storage banner every time an app opens with low storage.
if (evt.type === 'appopening') {
if (this._lowDeviceStorage) {
this.displayBanner(this._freeSpace);
}
return;
}
// Otherwise if event is not a storage change event we ignore it.
if (evt.type !== 'change') {
return;
}
switch (evt.reason) {
// We get 'onchange' events with a 'low-disk-space' reason when a
// modification of a file is identified while the device is in a low
// storage situation. When we get the first notification, we have to
// show a system banner and pin a notification in the notifications
// center, containing the remaining free space. Consecutive events with
// a 'low-disk-space' reason will only update the remaining free space.
case 'low-disk-space':
var req = this._appStorage.freeSpace();
req.onsuccess = () => {
if (typeof req.result !== 'undefined') {
this._freeSpace = this.formatSize(req.result);
}
if (this._lowDeviceStorage) {
this.updateAvailableSpace(this._freeSpace);
return;
}
this.lowDiskSpaceNotification(this._freeSpace);
};
req.onerror = () => {
this.lowDiskSpaceNotification();
};
break;
case 'available-disk-space':
this.hideNotification();
break;
}
}
};
exports.DeviceStorageWatcher = DeviceStorageWatcher;
}(window));

View file

@ -0,0 +1,205 @@
/* global SettingsListener, Service */
(function(exports) {
'use strict';
/**
* The Developer HUD displays app metrics as an overlay on top of MozApps.
* @class DeveloperHud
*/
function DeveloperHud() {
SettingsListener.observe('hud.hide', false, this.hide.bind(this));
SettingsListener.observe('devtools.overlay.system',
false, this.toggleSystemHUD.bind(this));
}
DeveloperHud.prototype = {
name: 'DeveloperHud',
start: function() {
window.addEventListener('developer-hud-update', this);
Service.register('stop', this);
},
stop: function() {
window.removeEventListener('developer-hud-update', this);
},
hide: function(hidden) {
var screen = document.getElementById('screen');
screen.classList.toggle('hidden-developer-hud', hidden);
},
_showSystemHUD: false,
toggleSystemHUD: function(enabled) {
if (!enabled) {
this.display(window, {});
}
this._showSystemHUD = enabled;
},
handleEvent: function(e) {
this.display(e.target, e.detail);
e.preventDefault();
},
/**
* Display a HUD over an app to show its metrics' values.
* @param {Object} target The iframe of the app being tracked.
* @param {Object} data The metric values to display.
*/
display: function(target, data) {
if (!target) {
return;
}
// For regular mozbrowser iframes use the div container.
var appwindow = target.parentElement;
// For system messages, directly insert the hud into the #screen
// wrapper if needed.
if (!appwindow) {
if (target === window && this._showSystemHUD) {
appwindow = document.getElementById('screen');
} else {
return;
}
}
var overlay = appwindow.querySelector(':scope > .developer-hud');
if (!overlay) {
overlay = document.createElement('div');
overlay.classList.add('developer-hud');
appwindow.appendChild(overlay);
}
if (!data.metrics || data.metrics.length < 1) {
overlay.remove();
return;
}
var canvas = overlay.querySelector('.widgets');
if (!canvas) {
canvas = document.createElement('canvas');
canvas.classList.add('widgets');
canvas.height = 30;
overlay.appendChild(canvas);
}
// The width is always set to the size of the screen in case the screen
// has been rotated. This does not cause a reflow as long as the width
// is the same.
canvas.width = window.innerWidth;
var ctx = canvas.getContext('2d');
ctx.font = '18px sans-serif';
ctx.textBaseline = 'top';
ctx.save();
// Widgets are positioned starting from the right side of the screen.
ctx.translate(canvas.width, 0);
data.metrics.reverse().forEach((function(metric) {
var widget = this.widget(metric);
if (!widget) {
return;
}
// The size of a widget is comprise between 30px and the size of its
// content. There is also an additional padding of 5px on each side.
var textWidth = ctx.measureText(widget.value).width;
var widgetWidth = Math.max(30, textWidth) + (5 * 2);
// Position the widget relatively to the last position on the left
// of the screen.
ctx.translate(-widgetWidth, 0);
// Fill widget background-color.
ctx.fillStyle = widget.color;
ctx.fillRect(0, 0, widgetWidth, canvas.height);
// Draw widget text centered both horizontally and vertically.
ctx.fillStyle = 'white';
ctx.fillText(widget.value,
(widgetWidth - textWidth) / 2,
canvas.height / 4);
}).bind(this));
ctx.restore();
},
widget: function(metric) {
var value = metric.value;
if (!value) {
return null;
}
var color;
switch(metric.name) {
case 'warnings':
color = 'orange';
break;
case 'errors':
color = 'red';
break;
case 'security':
color = 'black';
break;
case 'reflows':
color = 'purple';
break;
case 'jank':
color = 'cornflowerblue';
value += 'ms';
break;
case 'uss':
color = 'dimgrey';
value = this.formatMemory(value);
break;
case 'memory':
color = 'lightslategrey';
value = this.formatMemory(value);
break;
default:
color = this.colorHash(metric.name);
break;
}
return {
'color': color,
'value': value
};
},
colorHash: function(name) {
var hue = 0;
for (var i = 0; i < name.length; i++) {
hue += name.charCodeAt(i);
}
return 'hsl(' + (hue % 360) + ', 75%, 50%)';
},
formatMemory: function(bytes) {
var prefix = ['','K','M','G','T','P','E','Z','Y'];
var i = 0;
for (; bytes > 1024 && i < prefix.length; ++i) {
bytes /= 1024;
}
return (Math.round(bytes * 100) / 100) + ' ' + prefix[i] + 'B';
}
};
exports.DeveloperHud = DeveloperHud;
}(window));

View file

@ -0,0 +1,167 @@
'use strict';
/* global Service, DevToolsAuthDialog */
(function(exports) {
/**
* DevToolsAuth displays a prompt asking the user to scan a QR code as part of
* pairing the device with a machine running Firefox desktop. This takes
* place when debugging via WiFi to exchange keys.
* @requires DevToolsAuthDialog
* @requires ScreenManager
* @class DevToolsAuth
*/
function DevToolsAuth() {
window.addEventListener('mozChromeEvent', this);
}
DevToolsAuth.prototype = {
/**
* General event handler interface.
* Displays the pair dialog when needed.
* @memberof DevToolsAuth.prototype
* @param {DOMEvent} evt The event.
*/
handleEvent: function(e) {
if (e.detail.type !== 'devtools-auth') {
return;
}
if (e.detail.action === 'start') {
this.start();
}
if (e.detail.action === 'capture') {
this.capture();
}
if (e.detail.action === 'stop') {
this.stop();
}
},
get dialog() {
if (!this._dialog) {
this._dialog = new DevToolsAuthDialog({
onHide: this.onHide.bind(this)
});
}
return this._dialog;
},
get canvas() {
if (!this._canvas) {
this._canvas = document.createElement('canvas');
}
return this._canvas;
},
/**
* Begin capturing images that DevTools will attempt to find a QR code in
* for authentication.
* @memberof DevToolsAuth.prototype
*/
start: function() {
// We want the user's attention, so we need to turn the screen on if it's
// off.
Service.request('turnScreenOn');
this.dialog.show();
var getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia;
getUserMedia = getUserMedia.bind(navigator);
getUserMedia({
video: {
width: { min: 480 },
height: { min: 480 }
},
audio: false
}, function(stream) {
var video = this.dialog.video;
video.addEventListener('loadedmetadata', function onMetadata() {
video.removeEventListener('loadedmetadata', onMetadata);
this._resizeCanvas();
this.capture();
}.bind(this));
video.mozSrcObject = stream;
video.play();
}.bind(this), function(e) {
console.error('Unable to access camera: ' + e);
});
},
/**
* Capture another image and send to DevTools.
* @memberof DevToolsAuth.prototype
*/
capture: function() {
var video = this.dialog.video;
var canvas = this.canvas;
var context = canvas.getContext('2d');
context.drawImage(video, 0, 0, canvas.width, canvas.height);
var url = canvas.toDataURL('image/jpeg');
this._checkImage(url);
},
/**
* An image was successfully decoded, so further capture can stop.
* @memberof DevToolsAuth.prototype
*/
stop: function() {
this.dialog.hide('complete');
},
/**
* Dialog is now hidden, perform cleanup.
* @memberof DevToolsAuth.prototype
*/
onHide: function(reason) {
var video = this.dialog.video;
video.pause();
if (video.mozSrcObject) {
video.mozSrcObject.stop();
video.mozSrcObject = null;
}
this._canvas = null;
if (reason !== 'complete') {
this.abort();
}
},
/**
* User chose to abort the auth process.
* @memberof DevToolsAuth.prototype
*/
abort: function() {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentEvent', true, true,
{ type: 'devtools-auth',
action: 'abort' });
window.dispatchEvent(event);
},
_resizeCanvas: function() {
var video = this.dialog.video;
this.canvas.width = video.videoWidth;
this.canvas.height = video.videoHeight;
},
/**
* Dispatches an event to check a possible pairing candidate image.
* @memberof DevToolsAuth.prototype
* @param {String} url Object URL of the image to check
*/
_checkImage: function(url) {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentEvent', true, true,
{ type: 'devtools-auth',
action: 'capture',
url: url });
window.dispatchEvent(event);
}
};
exports.DevToolsAuth = DevToolsAuth;
}(window));

View file

@ -0,0 +1,53 @@
'use strict';
(function(exports) {
var DevToolsAuthDialog = function DevToolsAuthDialog(options) {
window.SystemDialog.call(this, options);
};
DevToolsAuthDialog.prototype = Object.create(window.SystemDialog.prototype);
DevToolsAuthDialog.prototype.customID = 'devtools-auth-dialog';
DevToolsAuthDialog.prototype.DEBUG = false;
DevToolsAuthDialog.prototype.view = function() {
return '<div id="' + this.instanceID + '" role="dialog" ' +
'class="generic-dialog"' +
'data-z-index-level="devtools-auth-dialog" hidden>' +
'<div class="container">' +
'<header data-l10n-id="devtools-auth-scan2"></header>' +
'<div class="video-container">' +
'<video></video>' +
'</div>' +
'<menu data-items="1">' +
'<button data-l10n-id="cancel" type="cancel"></button>' +
'</menu>' +
'</div>' +
'</div>';
};
// Get all elements when inited.
DevToolsAuthDialog.prototype._fetchElements = function() {
this.video =
document.querySelector('#devtools-auth-dialog video');
this.videoContainer =
document.querySelector('#devtools-auth-dialog .video-container');
this.buttonCancel =
document.querySelector('#devtools-auth-dialog button[type="cancel"]');
};
// Register events when all elements are got.
DevToolsAuthDialog.prototype._registerEvents = function() {
this.buttonCancel.onclick = this.cancelHandler.bind(this);
};
DevToolsAuthDialog.prototype.cancelHandler = function() {
this.hide('cancel');
return true;
};
exports.DevToolsAuthDialog = DevToolsAuthDialog;
}(window));

View file

@ -0,0 +1,307 @@
/* global dump,
ModalDialog,
MozActivity,
Notification,
NotificationHelper,
Service
*/
(function(exports) {
'use strict';
const DEBUG = false;
/**
* This developer system module captures a snapshot of the current device
* logs as displayed by logcat using DeviceStorage to persist the file for
* future access. It communicates with gecko code running in
* b2g/chrome/content/shell.js using a SystemAppProxy custom event based API.
* It requires the preference 'devtools.logshake.enabled' to be enabled
*
* @class LogShake
*/
function LogShake() {
}
function debug(str) {
if (DEBUG) {
dump('LogShake: ' + str + '\n');
}
}
LogShake.prototype = {
/**
* Start existing, observing for capture-logs events caused by Gecko
* LogShake
*/
start: function() {
Service.request('handleSystemMessageNotification', 'logshake', this);
window.addEventListener('volumeup+volumedown', this);
this.startCaptureLogsListener();
},
/**
* Stop the component, removing all listeners if necessary
*/
stop: function() {
Service.request('unhandleSystemMessageNotification', 'logshake', this);
this.stopCaptureLogsListener();
},
startCaptureLogsListener: function() {
debug('starting captureLogs listener');
window.addEventListener('capture-logs-start', this);
window.addEventListener('capture-logs-success', this);
window.addEventListener('capture-logs-error', this);
},
stopCaptureLogsListener: function() {
debug('stopping captureLogs listener');
window.removeEventListener('capture-logs-start', this);
window.removeEventListener('capture-logs-success', this);
window.removeEventListener('capture-logs-error', this);
},
/**
* Handle a capture-logs-start, capture-logs-success or capture-logs-error
* event, dispatching to the appropriate handler
*/
handleEvent: function(event) {
debug('handling event ' + event.type);
switch(event.type) {
case 'volumeup+volumedown':
this.requestSystemLogs();
break;
case 'capture-logs-start':
this.handleCaptureLogsStart(event);
break;
case 'capture-logs-success':
this.handleCaptureLogsSuccess(event);
break;
case 'capture-logs-error':
this.handleCaptureLogsError(event);
break;
}
},
_shakeId: null,
handleCaptureLogsStart: function(event) {
debug('handling capture-logs-start');
this._shakeId = Date.now();
this._notify('logsSaving');
},
requestSystemLogs: function() {
window.dispatchEvent(new CustomEvent('requestSystemLogs'));
},
/**
* Handle an event of type capture-logs-success where event.detail.logPaths
* is an array of absolute paths to the saved log files.
*/
handleCaptureLogsSuccess: function(event) {
debug('handling capture-logs-success');
navigator.vibrate(100);
this._notify('logsSavedHeader', 'logsSavedBody',
this.triggerShareLogs.bind(this, event.detail.logPaths),
event.detail);
this._shakeId = null;
},
handleCaptureLogsError: function(event) {
debug('handling capture logs error');
var error = event ? event.detail.error : '';
var errorMsg = this.formatError(error);
this._notify('logsSaveError', errorMsg,
this.showErrorMessage.bind('logsSaveError', this, error),
event.detail);
this._shakeId = null;
},
getDeviceStorage: function() {
var storageName = 'sdcard';
var storages = navigator.getDeviceStorages(storageName);
for (var i = 0; i < storages.length; i++) {
if (storages[i].storageName === storageName) {
return storages[i];
}
}
return navigator.getDeviceStorage('sdcard');
},
triggerShareLogs: function(logPaths, notif) {
var logFiles = [];
var storage = this.getDeviceStorage();
var requestsRemaining = logPaths.length;
var self = this;
function onSuccess() {
/* jshint validthis: true */
logFiles.push(this.result);
requestsRemaining -= 1;
if (requestsRemaining === 0) {
var logNames = logFiles.map(function(file) {
// For some reason file.name contains the full path despite
// File's documentation explicitly stating the opposite.
var pathComponents = file.name.split('/');
return pathComponents[pathComponents.length - 1];
});
var activity = new MozActivity({
name: 'share',
data: {
type: 'application/vnd.moz-systemlog',
blobs: logFiles,
filenames: logNames
}
});
activity.onsuccess = function() {
if (!notif) {
return;
}
if ('close' in notif) {
notif.close();
} else {
self.closeSystemMessageNotification(notif);
}
};
}
}
// Only report error one time.
var inError = false;
function onError() {
if (!inError) {
inError = true;
/* jshint validthis: true */
self.showErrorMessage('logsShareError', this.error);
}
}
for (var logPath of logPaths) {
var req = storage.get(logPath);
req.onsuccess = onSuccess;
req.onerror = onError;
}
},
ERRNO_TO_MSG: {
0: 'logsGenericError',
30: 'logsSDCardMaybeShared', // errno: filesystem ro-mounted
'NotFoundError': 'logsNotFoundError'
},
formatError: function(error) {
if (typeof error === 'string') {
return error;
}
if (typeof error === 'object') {
if ('operation' in error) {
return { id: 'logsOperationFailed',
args: { operation: error.operation }
};
}
if ('name' in error && error.name === 'NotFoundError') {
return 'logsNotFoundError';
}
}
return '';
},
showErrorMessage: function(title, error, notif) {
if (notif) {
notif.close();
}
// Do nothing for error string
if (typeof error === 'string') {
return;
}
if (typeof error !== 'object') {
console.warn('Unexpected error type: ' + typeof error);
return;
}
var errorKey;
// Small heuristic for some frequent unix error cases
if ('unixErrno' in error) {
errorKey = error.unixErrno;
} else if ('name' in error) {
errorKey = error.name;
}
debug('errorKey: ' + errorKey);
// Gracefully fallback to a generic error messages if we don't know
// this errno code.
if (!this.ERRNO_TO_MSG[errorKey]) {
errorKey = 0;
}
ModalDialog.alert(title, this.ERRNO_TO_MSG[errorKey], { title: 'ok' });
},
_notify: function(titleL10n, bodyL10n, onclick, dataPayload) {
var payload = {
bodyL10n: bodyL10n,
icon: '/style/notifications/images/bug.png',
tag: 'logshake:' + this._shakeId,
data: {
systemMessageTarget: 'logshake',
logshakePayload: dataPayload || undefined
}
};
NotificationHelper.send(titleL10n, payload).then(notification => {
if (onclick) {
notification.onclick = onclick.bind(this, notification);
}
});
},
handleSystemMessageNotification: function(message) {
debug('Received system message: ' + JSON.stringify(message));
if (!('logshakePayload' in message.data)) {
console.warn('Received logshake system message notification without ' +
'payload, silently discarding.');
return;
}
debug('Message payload: ' + message.data.logshakePayload);
var payload = message.data.logshakePayload;
if ('error' in payload) {
this.showErrorMessage('logsSaveError', payload.error);
} else if ('logPaths' in payload) {
this.triggerShareLogs(payload.logPaths, message);
} else {
console.warn('Unidentified payload: ' + JSON.stringify(payload));
}
},
closeSystemMessageNotification: function(msg) {
Notification.get({ tag: msg.tag }).then(notifs => {
notifs.forEach(notif => {
if (notif.tag) {
// Close notification with the matching tag
if (notif.tag === msg.tag) {
notif.close && notif.close();
}
}
});
});
}
};
exports.LogShake = LogShake;
// XXX: See issue described in screenshot.js
exports.logshake = new LogShake();
exports.logshake.start();
})(window);

View file

@ -0,0 +1,160 @@
'use strict';
/* global Service, ModalDialog */
(function(exports) {
/**
* Note: Values copied from Gecko:
* toolkit/devtools/security/auth.js
*/
var AuthenticationResult = {
/**
* Close all listening sockets, and disable them from opening again.
*/
DISABLE_ALL: 'DISABLE_ALL',
/**
* Deny the current connection.
*/
DENY: 'DENY',
/**
* Additional data needs to be exchanged before a result can be determined.
*/
PENDING: 'PENDING',
/**
* Allow the current connection.
*/
ALLOW: 'ALLOW',
/**
* Allow the current connection, and persist this choice for future
* connections from the same client. This requires a trustable mechanism to
* identify the client in the future, such as the cert used during OOB_CERT.
*/
ALLOW_PERSIST: 'ALLOW_PERSIST'
};
/**
* RemoteDebugger displays a prompt asking the user if they want to enable
* remote debugging on their device. This is generally called when the user
* attempts to access the device from the App Manager.
* @requires ModalDialog
* @requires Service
* @class RemoteDebugger
*/
function RemoteDebugger() {
window.addEventListener('mozChromeEvent', this);
}
RemoteDebugger.prototype = {
/**
* General event handler interface.
* Displays the modal dialog when we needed.
* @memberof RemoteDebugger.prototype
* @param {DOMEvent} evt The event.
*/
handleEvent: function(e) {
if (e.detail.type !== 'remote-debugger-prompt') {
return;
}
// We want the user attention, so we need to turn the screen on
// if it's off.
Service.request('turnScreenOn');
var session = e.detail.session;
var dialog;
if (!session.server.port) {
dialog = this._buildUSBDialog(session);
} else {
dialog = this._buildTCPDialog(session);
}
if (!dialog) {
// Session does not meet the requirements of this dialog, so deny the
// connection immediately.
this._dispatchEvent(AuthenticationResult.DENY);
return;
}
// Reusing the ModalDialog infrastructure.
ModalDialog.showWithPseudoEvent({
type: 'selectone',
title: dialog.title,
text: dialog.options,
callback: this._dispatchEvent.bind(this),
cancel: this._dispatchEvent.bind(this)
});
},
_buildUSBDialog: function(session) {
var dialog = {};
if (session.authentication !== 'PROMPT') {
// This dialog is not prepared for any other authentication method at
// this time.
return false;
}
dialog.title = 'remoteDebuggerPromptUSB';
dialog.options = [
{
id: AuthenticationResult.ALLOW,
text: 'remoteDebuggerPrompt-allow'
},
{
id: AuthenticationResult.DENY,
text: 'remoteDebuggerPrompt-deny'
}
];
return dialog;
},
_buildTCPDialog: function(session) {
var dialog = {};
if (session.authentication !== 'OOB_CERT' || !session.client.cert) {
// This dialog is not prepared for any other authentication method at
// this time.
return false;
}
dialog.title = {
id: 'remoteDebuggerPromptTCP2',
args: {
host: session.client.host,
port: session.client.port
}
};
dialog.options = [
{
id: AuthenticationResult.ALLOW,
text: 'remoteDebuggerPrompt-scan'
},
{
id: AuthenticationResult.ALLOW_PERSIST,
text: 'remoteDebuggerPrompt-scanAndRemember'
}
];
return dialog;
},
/**
* Dispatches an event based on the user selection of the modal dialog.
* @memberof RemoteDebugger.prototype
* @param {String} id ID of option chosen, or null if cancelled.
*/
_dispatchEvent: function(id) {
var authResult = id || AuthenticationResult.DENY;
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentEvent', true, true,
{ type: 'remote-debugger-prompt',
authResult: authResult });
window.dispatchEvent(event);
}
};
exports.RemoteDebugger = RemoteDebugger;
}(window));

View file

@ -0,0 +1,263 @@
'use strict';
/* global SettingsListener, SettingsURL, CallscreenWindow, applications */
/* global Service, LazyLoader, toneUpgrader */
/* r=? dialer+system peers for changes in this file. */
(function(exports) {
/**
*
* The delay between an incoming phone call and the first ring or vibration
* needs to be as short as possible.
*
* This simple module keeps the ringtone (blob) around and starts alerting the
* user as soon as a new incoming call is detected via the mozTelephony API.
* And it opens an AttentionWindow with the preloaded callscreen app inside.
*
* We also listen for the sleep and volumedown hardware buttons to provide
* the user with an easy way to stop the ringing.
*
* @example
* var dialerAgent = new DialerAgent();
* dialerAgent.start(); // Attach the event listeners.
* dialerAgent.stop(); // Deattach the event listeners.
*
* @class DialerAgent
* @requires SettingsListener
* @requires SettingsURL
* @requires Service
* @requires LazyLoader
*
**/
var DialerAgent = function DialerAgent() {
var telephony = navigator.mozTelephony;
if (!telephony) {
return;
}
this._telephony = telephony;
this._started = false;
this._shouldVibrate = true;
this._alerting = false;
this._vibrateInterval = null;
var player = new Audio();
this._player = player;
// XXX: This will need to be updated for bug 961967
// (audio competing in system app)
player.mozAudioChannelType = 'ringer';
player.preload = 'metadata';
player.loop = true;
this.freeCallscreenWindow = this.freeCallscreenWindow.bind(this);
this.makeFakeNotification = this.makeFakeNotification.bind(this);
};
DialerAgent.prototype.freeCallscreenWindow = function() {
/* Free the callscreen window unless there are active calls or the
* callscreen is visible. */
if (this._callscreenWindow && !this.onCall() &&
!this._callscreenWindow.isVisible()) {
this._callscreenWindow.free();
}
};
DialerAgent.prototype.start = function da_start() {
if (!this._telephony) {
return;
}
if (this._started) {
throw 'Instance should not be start()\'ed twice.';
}
this._started = true;
SettingsListener.observe('audio.volume.notification', 7, function(value) {
this._playRing();
}.bind(this));
SettingsListener.observe('dialer.ringtone', '', function(value) {
LazyLoader.load(['shared/js/settings_url.js']).then(function() {
var phoneSoundURL = new SettingsURL();
this._player.pause();
this._player.src = phoneSoundURL.set(value);
this._playRing();
}.bind(this)).catch((err) => {
console.error(err);
});
}.bind(this));
// We have new default ringtones in 2.0, so check if the version is upgraded
// then execute the necessary migration.
if (Service.query('justUpgraded')) {
LazyLoader.load('js/tone_upgrader.js').then(() => {
toneUpgrader.perform('ringtone');
});
}
SettingsListener.observe('vibration.enabled', true, function(value) {
this._shouldVibrate = !!value;
}.bind(this));
this._telephony.addEventListener('callschanged', this);
window.addEventListener('sleep', this);
window.addEventListener('wake', this);
window.addEventListener('volumedown', this);
window.addEventListener('mozmemorypressure', this.freeCallscreenWindow);
this._callscreenWindow = new CallscreenWindow();
this._callscreenWindow.hide();
if (applications && applications.ready) {
this.makeFakeNotification();
} else {
window.addEventListener('applicationready', this.makeFakeNotification);
}
Service.registerState('onCall', this);
return this;
};
DialerAgent.prototype.makeFakeNotification = function() {
window.removeEventListener('applicationready', this.makeFakeNotification);
this._callscreenWindow && this._callscreenWindow.makeNotification();
};
DialerAgent.prototype.stop = function da_stop() {
if (!this._started) {
return;
}
this._started = false;
this._telephony.removeEventListener('callschanged', this);
window.removeEventListener('sleep', this);
window.removeEventListener('wake', this);
window.removeEventListener('volumedown', this);
window.removeEventListener('mozmemorypressure', this.freeCallscreenWindow);
Service.unregisterState('onCall', this);
// TODO: should remove the settings listener once the helper
// allows it.
// See bug 981373.
};
DialerAgent.prototype.handleEvent = function da_handleEvent(evt) {
if (evt.type === 'volumedown') {
this._stopAlerting();
return;
}
if ((evt.type === 'sleep') && this._alerting) {
this._stopAlerting();
return;
}
if ((evt.type === 'sleep') && this.onCall()) {
// Hangup all calls
this._telephony.calls.forEach(call => call.hangUp());
if (this._telephony.conferenceGroup.calls.length > 0) {
this._telephony.conferenceGroup.hangUp();
}
return;
}
if ((evt.type === 'wake') && this.onCall()) {
Service.request('turnScreenOn');
return;
}
if (evt.type !== 'callschanged') {
return;
}
var calls = this._telephony.calls;
if (calls.length === 0) {
Service.request('turnScreenOn');
return;
}
var calling = calls.some(function(call) {
if (call.state === 'incoming' || call.state === 'dialing') {
return true;
}
});
if (calling) {
this.openCallscreen();
}
var incomingCall = calls[calls.length - 1];
if (this._alerting || incomingCall.state !== 'incoming') {
return;
}
var self = this;
self._startAlerting();
incomingCall.addEventListener('statechange', function callStateChange() {
incomingCall.removeEventListener('statechange', callStateChange);
self._stopAlerting();
});
};
DialerAgent.prototype._playRing = function da_playRing () {
// Notice that, even we are in the vibration mode, we would still play the
// silence ringer. That is because when the incoming call is coming, other
// playing sound should be paused. Therefore, we need to a silence ringer
// to compete for the audio output.
if (this._alerting) {
this._player.play();
}
};
DialerAgent.prototype._startAlerting = function da_startAlerting() {
this._alerting = true;
if ('vibrate' in navigator && this._shouldVibrate) {
this._vibrateInterval = window.setInterval(function vibrate() {
navigator.vibrate([200]);
}, 600);
navigator.vibrate([200]);
}
this._playRing();
};
DialerAgent.prototype._stopAlerting = function da_stopAlerting() {
var player = this._player;
this._alerting = false;
if (player && player.readyState > player.HAVE_NOTHING) {
player.pause();
player.currentTime = 0;
}
window.clearInterval(this._vibrateInterval);
};
DialerAgent.prototype.openCallscreen = function() {
if (this._callscreenWindow) {
this._callscreenWindow.ensure();
this._callscreenWindow.requestOpen();
}
};
DialerAgent.prototype.onCall = function() {
var numOpenLines = this._telephony.calls.length +
(this._telephony.conferenceGroup.calls.length ? 1 : 0);
return (numOpenLines > 0);
};
exports.DialerAgent = DialerAgent;
}(window));

View file

@ -0,0 +1,124 @@
/* global LazyLoader, DownloadFormatter, DownloadNotification, DownloadIcon,
Service */
'use strict';
/*
* The DownloadManager listens for new downloads when they start. This
* component will create notifications in the utility tray and will store them
* Also it will create an icon in statusbar
*
* WARNING: This library will load these resources:
*
* - style/notifications/downloads.css
* - shared/js/download/download_formatter.js
* - js/download/download_notification.js
*/
/* exported DownloadManager */
var DownloadManager = (function() {
var mozDownloadManager = navigator.mozDownloadManager;
if (!mozDownloadManager) {
console.error('navigator.mozDownloadManager not supported!');
return;
}
// XXX bug 1097435 -- This is the only safe place we can call this for now.
mozDownloadManager.clearAllDone();
// This object stores download notification objects by id
var notifications = {};
var started = false;
var icon = null;
// Define and set our download start handler.
function onDownloadStart(ev) {
if (started) {
createDownloadNotificationAndIcon(ev.download);
} else {
LazyLoader.load(['shared/js/download/download_formatter.js',
'shared/js/download/download_ui.js',
'shared/js/download/download_store.js',
'shared/js/download/download_helper.js',
'js/download/download_notification.js']).then(
function() {
started = true;
createDownloadNotificationAndIcon(ev.download);
window.addEventListener('notification-clicked', handleEvent);
// Bug 1102810 - After this bug gets fixed, this listener won't be
// needed anymore. The API itself should handle startTime when killing
// the process.
window.addEventListener('will-shutdown', function onShutdown() {
ev.download.pause();
});
})['catch'](function(err) { // XXX: workaround gjslint
console.error(err);
});
}
}
mozDownloadManager.addEventListener('downloadstart', onDownloadStart);
function createDownloadNotificationAndIcon(download) {
var id = DownloadFormatter.getUUID(download);
notifications[id] = new DownloadNotification(download);
loadDownloadIconIfNecessary().then(function() {
icon.handle(download);
});
}
var downloadManager = {
name: 'DownloadManager',
incDownloads: function() {
loadDownloadIconIfNecessary().then(function() {
icon.incDownloads();
})['catch'](function(err) { // XXX: workaround gjslint
console.error(err);
});
},
decDownloads: function() {
loadDownloadIconIfNecessary().then(function() {
icon.decDownloads();
})['catch'](function(err) { // XXX: workaround gjslint
console.error(err);
});
}
};
function loadDownloadIconIfNecessary() {
return new Promise(function(resolve, reject) {
if (!icon) {
LazyLoader.load(['js/download_icon.js']).then(function() {
icon = new DownloadIcon(this);
icon.start();
resolve();
}.bind(this))['catch'](function(err) { // XXX: workaround gjslint
console.error(err);
reject();
});
} else {
resolve();
}
});
}
Service.register('incDownloads', downloadManager);
Service.register('decDownloads', downloadManager);
function handleEvent(evt) {
var detail = evt.detail;
if (!detail || !detail.id) {
return;
}
var id = detail.id;
var notification = notifications[id];
if (notification) {
notification.onClick(function onRemovedNotification() {
notifications[id] = null;
});
}
}
}());

View file

@ -0,0 +1,266 @@
/* global DownloadFormatter, NotificationScreen, DownloadUI, DownloadHelper,
MozActivity, DownloadStore */
'use strict';
/**
* This is the constructor that will represent a download notification
* in the system
*
* @param {Object} download object provided by the API.
*/
function DownloadNotification(download) {
this.download = download;
this.fileName = DownloadFormatter.getFileName(download);
this.state = 'started';
this.id = DownloadFormatter.getUUID(download);
// We have to listen for state changes
this.listener = this._update.bind(this);
this.download.addEventListener('statechange', this.listener);
if (download.state === 'started') {
NotificationScreen.addNotification(this._getInfo());
} else {
// For adopted downloads, it is possible for the download to already be
// completed.
this._update();
}
}
DownloadNotification.prototype = {
/**
* This method knows when the toaster should be displayed. Basically
* the toaster shouldn't be displayed if the download state does not change
* or the download was stopped by the user or because of connectivity lost
*
* @return {boolean} True whether the toaster should be displayed.
*/
_wontNotify: function dn_wontNotify() {
var download = this.download;
return this.state === download.state ||
(download.state === 'stopped' && download.error === null);
},
/**
* It updates the notification when the download state changes.
*/
_update: function dn_update() {
if (this.download.state === 'finalized') {
// If the user delete the file, we will see this state and what we have to
// do, is to remove the notification
this._close();
return;
}
var noNotify = this._wontNotify();
this.state = this.download.state;
if (this.download.state === 'stopped') {
this._onStopped();
}
var info = this._getInfo();
if (noNotify) {
info.noNotify = true;
}
if (this.state === 'downloading') {
info.mozbehavior = {
noscreen: true
};
}
NotificationScreen.addNotification(info);
if (this.state === 'succeeded') {
this._onSucceeded();
}
},
_onStopped: function dn_onStopped() {
if (this.download.error !== null) {
// Error attr will be not null when a download is stopped because
// something failed
this.state = 'failed';
this._onError();
} else if (!window.navigator.onLine) {
// Remain downloading state when the connectivity was lost
this.state = 'downloading';
}
},
_onError: function dn_onError() {
var result = parseInt(this.download.error.message);
switch (result) {
case DownloadUI.ERRORS.NO_MEMORY:
DownloadUI.show(DownloadUI.TYPE.NO_MEMORY,
this.download,
true);
break;
case DownloadUI.ERRORS.NO_SDCARD:
DownloadUI.show(DownloadUI.TYPE.NO_SDCARD,
this.download,
true);
break;
case DownloadUI.ERRORS.UNMOUNTED_SDCARD:
DownloadUI.show(DownloadUI.TYPE.UNMOUNTED_SDCARD,
this.download,
true);
break;
default:
DownloadHelper.getFreeSpace((function gotFreeMemory(bytes) {
if (bytes === 0) {
DownloadUI.show(DownloadUI.TYPE.NO_MEMORY, this.download, true);
}
}).bind(this));
}
},
_onSucceeded: function dn_onSucceeded() {
this._storeDownload(this.download);
},
/**
* This method stores complete downloads to share them with the download list
* in settings app
*
* @param {Object} The download object provided by the API.
*/
_storeDownload: function dn_storeDownload(download) {
var req = DownloadStore.add(download);
req.onsuccess = (function _storeDownloadOnSuccess(request) {
// We don't care about any more state changes to the download.
this.download.removeEventListener('statechange', this.listener);
// Update the download object to the datastore representation.
this.download = req.result;
}).bind(this);
req.onerror = function _storeDownloadOnError(e) {
console.error('Exception storing the download', download.id, '(',
download.url, ').', e.target.error);
};
},
_ICONS_PATH: 'style/notifications/images/',
_ICONS_EXTENSION: '.png',
/**
* It returns the icon depending on current state
*
* @return {String} Icon path.
*/
_getIcon: function dn_getIcon() {
var icon = (this.state === 'downloading' ? 'downloading' : 'download');
return this._ICONS_PATH + icon + this._ICONS_EXTENSION;
},
/**
* This method returns an object to update the notification composed by the
* text, icon and type
*
* @return {object} Object descriptor.
*/
_getInfo: function dn_getInfo() {
var state = this.state;
var _ = navigator.mozL10n.get;
var info = {
id: this.id,
title: _('download_' + state),
icon: this._getIcon(),
type: 'download-notification-' + state
};
if (state === 'downloading') {
info.text = _('download_downloading_text_2', {
name: this.fileName,
percentage: DownloadFormatter.getPercentage(this.download)
});
} else {
info.text = _('download_text_by_default', {
name: this.fileName
});
}
return info;
},
/**
* Closes the notification
*/
_close: function dn_close() {
NotificationScreen.removeNotification(this.id);
this.onClose();
},
/**
* It performs the action when the notification is clicked by the user
* depending on state:
*
* - 'downloading' -> launch the download list
* - 'stopped' -> show confirmation to resume the download
* - 'finalized' -> show confirmation to retry the download
* - 'succeeded' -> open the downloaded file
*
* @param {function} Function that will be invoked when the notification
* is removed from utility tray.
*/
onClick: function dn_onClick(done) {
var cb = (function() {
this._close();
done();
}).bind(this);
var req;
switch (this.download.state) {
case 'downloading':
// Launching settings > download list
/* jshint nonew: false */
new MozActivity({
name: 'configure',
data: {
target: 'device',
section: 'downloads'
}
});
/* jshint nonew: true */
// The notification won't be removed when users open the download list
// activity.onsuccess = activity.onerror = cb;
break;
case 'stopped':
// Prompts the user if he wishes to retry the download
req = DownloadUI.show(null, this.download, true);
// The notification won't be removed when users decline to resume
// req.oncancel = cb;
req.onconfirm = this.download.resume.bind(this.download);
break;
case 'succeeded':
// Attempts to open the file
var download = this.download;
req = DownloadHelper.open(download);
req.onerror = function req_onerror() {
DownloadHelper.handlerError(req.error, download);
};
cb();
break;
}
},
/**
* This method releases memory destroying the notification object
*/
onClose: function dn_onClose() {
this.download.onstatechange = this.download = this.id = this.state = null;
}
};

View file

@ -0,0 +1,74 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var DownloadIcon = function(manager) {
this.systemDownloads = {};
this.downloadsCount = 0;
BaseIcon.call(this, manager);
};
DownloadIcon.prototype = Object.create(BaseIcon.prototype);
DownloadIcon.prototype.name = 'DownloadIcon';
DownloadIcon.prototype.handle = function(download) {
// New download, track it so we can show or hide the active downloads
// indicator. If you think this logic needs to change, think really hard
// about it and then come and ask @nullaus
download.onstatechange = function(downloadEvent) {
var download = downloadEvent.download;
switch (download.state) {
case 'downloading':
// If this download has not already been tracked as actively
// downloading we'll add it to our list and increment the
// downloads counter.
if (!this.systemDownloads[download.id]) {
this.incDownloads();
this.systemDownloads[download.id] = true;
}
break;
// Once the download is finalized, and only then, is it safe to
// remove our state change listener. If we remove it before then
// we are likely to miss paused or errored downloads being restarted
case 'finalized':
download.onstatechange = null;
this.update();
break;
// All other state changes indicate the download is no longer
// active, if we were previously tracking the download as active
// we'll decrement the counter now and remove it from active
// download status.
case 'stopped':
case 'succeeded':
if (this.systemDownloads[download.id]) {
this.decDownloads();
delete this.systemDownloads[download.id];
}
break;
default:
console.warn('Unexpected download state = ', download.state);
}
}.bind(this);
};
DownloadIcon.prototype.incDownloads = function() {
this.downloadsCount++;
this.update();
};
DownloadIcon.prototype.decDownloads = function() {
if (--this.downloadsCount < 0) {
this.downloadsCount = 0;
}
this.update();
};
DownloadIcon.prototype.shouldDisplay = function() {
return this.downloadsCount !== 0;
};
DownloadIcon.prototype.view = function view() {
return `<!-- See note on <img> above. -->
<img id="statusbar-download"
src="style/statusbar/images/system-downloads.png"
class="sb-icon-download" hidden role="listitem"
data-l10n-id="statusbarDownload">`;
};
exports.DownloadIcon = DownloadIcon;
}(window));

View file

@ -0,0 +1,123 @@
'use strict';
/* global KeyboardHelper */
/**
* DynamicInputRegistry takes mozChromeEvent, check it, and save/remove the
* dynamic input definition to mozSettings.
*
* InputAppListSettings will then pick up the change in mozSettings and update
* the available layouts to all apps -- we do not communicate to it directly
* here.
*/
(function(exports) {
var DynamicInputRegistry = function() {
this.taskQueue = null;
};
DynamicInputRegistry.prototype.SETTING_KEY = 'keyboard.dynamic-inputs';
DynamicInputRegistry.prototype.start = function() {
this.taskQueue = Promise.resolve();
navigator.mozInputMethod.mgmt.addEventListener('addinputrequest', this);
navigator.mozInputMethod.mgmt.addEventListener('removeinputrequest', this);
};
DynamicInputRegistry.prototype.stop = function() {
this.taskQueue = null;
navigator.mozInputMethod.mgmt.removeEventListener('addinputrequest', this);
navigator.mozInputMethod.mgmt.removeEventListener('removeinputrequest', this);
};
DynamicInputRegistry.prototype.handleEvent = function(evt) {
var detail = evt.detail;
var p = this.taskQueue.then(function() {
return KeyboardHelper.inputAppList.getList();
}).then(function(inputApps) {
var inputApp = (inputApps.filter(function(inputApp) {
return (inputApp.domApp.manifestURL === detail.manifestURL);
}) || [])[0];
if (!inputApp) {
throw 'App not installed';
}
var currentInputs = inputApp.getInputs();
var currentInputIds = Object.keys(currentInputs);
if (currentInputIds.indexOf(detail.inputId) !== -1 &&
!currentInputs[detail.inputId].isDynamic) {
throw 'Can\'t mutate a statically declared input.';
}
return this._updateSetting(evt.type, detail)
.catch(function(e) {
console.error(e);
throw 'Error updating input.';
}.bind(this));
}.bind(this));
// Push the Promise queue to Gecko so it would respond to app accordingly.
detail.waitUntil(p);
// Tell Gecko this event is handled
evt.preventDefault();
this.taskQueue = p
.catch(function(e) {
console.error('DynamicInputRegistry rejects', e);
});
};
DynamicInputRegistry.prototype._updateSetting = function(evtType, detail) {
if (!navigator.mozSettings) {
throw new Error('DynamicInputRegistry: No mozSettings?');
}
// We must mutate the setting with the same lock there.
// As much as I want to use Promise interface, chainning callbacks in
// DOMRequest#then does not keep the lock (transaction) alive;
// we therefore have to work with EventTarget interface directly.
return new Promise(function(resolve, reject) {
var lock = navigator.mozSettings.createLock();
var getReq = lock.get(this.SETTING_KEY);
getReq.onerror = function() { reject(getReq.error); };
getReq.onsuccess = (function() {
var dynamicInputs = getReq.result[this.SETTING_KEY] || {};
switch (evtType) {
case 'addinputrequest':
if (!(detail.manifestURL in dynamicInputs)) {
dynamicInputs[detail.manifestURL] = {};
}
dynamicInputs[detail.manifestURL][detail.inputId] =
detail.inputManifest;
break;
case 'removeinputrequest':
if (!(detail.manifestURL in dynamicInputs)) {
break;
}
delete dynamicInputs[detail.manifestURL][detail.inputId];
if (Object.keys(dynamicInputs[detail.manifestURL]).length === 0) {
delete dynamicInputs[detail.manifestURL];
}
break;
}
var toSet = {};
toSet[this.SETTING_KEY] = dynamicInputs;
var setReq = lock.set(toSet);
setReq.onerror = function() { reject(setReq.error); };
setReq.onsuccess = function() { resolve(); };
}).bind(this);
}.bind(this));
};
exports.DynamicInputRegistry = DynamicInputRegistry;
})(window);

View file

@ -0,0 +1,465 @@
'use strict';
/* global SettingsListener */
/* global Service */
/* global SheetsTransition */
/* global StackManager */
/* global TouchForwarder */
(function(exports) {
const kEdgeIntertia = 250;
const kEdgeThreshold = 0.3;
const kEdgeAngleThreshold = Math.PI / 6;
const kSignificant = 10;
/**
* Detects user gestures for moving between apps using edge gestures.
* Gestures are listened for on the previous/next elements, which is
* above the app container. Forwards events when they are not relevant to
* the app iframe.
* @class EdgeSwipeDetector
*/
function EdgeSwipeDetector() {}
EdgeSwipeDetector.prototype = {
leftPanel: document.getElementById('left-panel'),
rightPanel: document.getElementById('right-panel'),
screen: document.getElementById('screen'),
_touchForwarder: null,
/**
* Starts EdgeSwipeDetector and begins listening for events.
* @memberof EdgeSwipeDetector.prototype
*/
start: function esd_init() {
window.addEventListener('homescreenopened', this);
window.addEventListener('appopened', this);
window.addEventListener('cardviewclosed', this);
window.addEventListener('mozChromeEvent', this);
window.addEventListener('updatepromptshown', this);
window.addEventListener('updateprompthidden', this);
window.addEventListener('installpromptshown', this);
window.addEventListener('installprompthidden', this);
window.addEventListener('shrinking-start', this);
window.addEventListener('shrinking-stop', this);
window.addEventListener('hierarchychanged', this);
window.addEventListener('hierarchytopmostwindowchanged', this);
['touchstart', 'touchmove', 'touchend',
'mousedown', 'mousemove', 'mouseup'].forEach(function(e) {
this.leftPanel.addEventListener(e, this);
this.rightPanel.addEventListener(e, this);
}, this);
this._touchForwarder = new TouchForwarder();
SettingsListener.observe('edgesgesture.enabled', false,
this.settingUpdate.bind(this));
SettingsListener.observe('edgesgesture.debug', false,
this.debugUpdate.bind(this));
},
/**
* Whether or not the setting is enabled.
* @memberof EdgeSwipeDetector.prototype
*/
_settingEnabled: false,
/**
* Called when the edgesgesture.enabled setting is changed.
* @memberof EdgeSwipeDetector.prototype
*/
settingUpdate: function esd_settingUpdate(enabled) {
this._settingEnabled = enabled;
this._updateEnabled();
},
/**
* Called when the edgesgesture.debug setting is changed.
* @memberof EdgeSwipeDetector.prototype
*/
debugUpdate: function esd_debugUpdate(enabled) {
this.screen.classList.toggle('edges-debug', enabled);
},
_lifecycleEnabled: false,
get lifecycleEnabled() {
return this._lifecycleEnabled;
},
set lifecycleEnabled(enable) {
this._lifecycleEnabled = enable;
this._updateEnabled();
},
/**
* General event handler.
* @param {Object} e
* @memberof EdgeSwipeDetector.prototype
*/
handleEvent: function esd_handleEvent(e) {
switch (e.type) {
case 'hierarchychanged':
case 'hierarchytopmostwindowchanged':
// XXX: Use this.appWindowManager instead if
// we become part of appWindowManager submodules.
// i.e., Service.query('getTopMostUI') === this.parent
var topMostUI = Service.query('getTopMostUI');
var topMostWindow = Service.query('getTopMostWindow');
if (topMostUI && topMostUI.name === 'AppWindowManager') {
if (topMostWindow &&
!topMostWindow.getBottomMostWindow().isHomescreen &&
!Service.query('isFtuRunning')) {
this.lifecycleEnabled = true;
break;
}
}
this.lifecycleEnabled = false;
break;
case 'mousedown':
case 'mousemove':
case 'mouseup':
// Preventing gecko reflows until
// https://bugzilla.mozilla.org/show_bug.cgi?id=1005815 lands
e.preventDefault();
break;
case 'touchstart':
e.preventDefault();
this._touchStart(e);
break;
case 'touchmove':
e.preventDefault();
this._touchMove(e);
break;
case 'touchend':
e.preventDefault();
this._touchEnd(e);
break;
case 'appopened':
var app = e.detail;
if (!app.stayBackground) {
this.lifecycleEnabled =
(app.origin !== Service.query('getFtuOrigin'));
}
break;
case 'shrinking-start':
this.lifecycleEnabled = false;
break;
case 'cardviewclosed':
var targetApp = e.detail;
// Enable the edge gestures if we switched back into any app except
// the homescreen.
if (!targetApp.isHomescreen) {
this.lifecycleEnabled = true;
}
break;
case 'mozChromeEvent':
if (e.detail.type !== 'accessibility-control') {
break;
}
var details = JSON.parse(e.detail.details);
switch (details.eventType) {
case 'edge-swipe-right':
this.autoSwipe('ltr');
break;
case 'edge-swipe-left':
this.autoSwipe('rtl');
break;
}
break;
case 'updatepromptshown':
case 'installpromptshown':
this.lifecycleEnabled = false;
break;
// XXX: Move install/update dialog into system dialog
// and then we could remove this.
case 'updateprompthidden':
case 'installprompthidden':
case 'shrinking-stop':
if (Service.query('getTopMostWindow') &&
!Service.query('getTopMostWindow').isHomescreen) {
this.lifecycleEnabled = true;
}
break;
}
},
/**
* Enables or disables the previous/next triggers.
* @memberof EdgeSwipeDetector.prototype
*/
_updateEnabled: function esd_updateEnabled() {
var enabled = this._lifecycleEnabled && this._settingEnabled;
this.leftPanel.classList.toggle('disabled', !enabled);
this.rightPanel.classList.toggle('disabled', !enabled);
if (!enabled && this._touchStartEvt) {
this._touchStartEvt = null; // we ignore the rest of the gesture
SheetsTransition.snapInPlace();
}
},
_touchStartEvt: null,
_startDate: null,
_startX: null,
_deltaX: null,
_startY: null,
_deltaY: null,
_forwardTimeout: null,
_progress: null,
_winWidth: null,
_beganTransition: null,
_moved: null,
_direction: null,
_forwarding: null,
_redispatching: null,
_touchStart: function esd_touchStart(e) {
this._winWidth = window.innerWidth;
this._direction = (e.target == this.rightPanel) ? 'rtl' : 'ltr';
this._touchStartEvt = e;
this._startDate = Date.now();
var iframe = StackManager.getCurrent().getTopMostWindow().iframe;
this._touchForwarder.destination = iframe;
var touch = e.changedTouches[0];
this._startX = touch.clientX;
this._startY = touch.clientY;
this._deltaX = 0;
this._deltaY = 0;
this._beganTransition = false;
this._moved = false;
this._forwarding = false;
this._redispatching = false;
this._clearForwardTimeout();
this._forwardTimeout = setTimeout((function longTouch() {
// Didn't move for a while after the touchstart,
// this isn't a swipe
this._forwardTimeout = null;
this._forwarding = true;
this._forward(this._touchStartEvt);
}).bind(this), 300);
},
_touchMove: function esd_touchMove(e) {
if (!this._touchStartEvt) {
return;
}
var touch = e.touches[0];
this._updateProgress(touch);
var delta = Math.max(Math.abs(this._deltaX), Math.abs(this._deltaY));
// If we already started forwarding we just continue
if (this._forwarding) {
this._forward(e);
return;
}
// If it's a pinch gesture we start forwarding
if (e.touches.length > 1) {
this._startForwarding(e);
return;
}
// If the gesture isn't horizontal we start forwarding
if (delta > kSignificant && !this._horizontalGesture()) {
this._startForwarding(e);
return;
}
// After a small threshold...
if ((this._deltaX < kSignificant || this._outsideApp(e)) &&
!this._moved) {
return;
}
// preparing to move the sheets...
if (!this._beganTransition) {
SheetsTransition.begin(this._direction);
this._clearForwardTimeout();
this._beganTransition = true;
}
SheetsTransition.moveInDirection(this._direction, this._progress);
this._moved = true;
},
_touchEnd: function esd_touchEnd(e) {
if (!this._touchStartEvt) {
return;
}
// Edge gestures are never multi-touch
var touches = e.touches.length + e.changedTouches.length;
if (touches > 1 && !this._forwarding) {
this._touchStartEvt = null;
SheetsTransition.snapInPlace();
return;
}
var touch = e.changedTouches[0];
this._updateProgress(touch);
if (this._forwarding) {
this._forward(e);
} else if ((this._deltaX < kSignificant) &&
(this._deltaY < kSignificant)) {
setTimeout(function(self, touchstart, touchend) {
self._forward(touchstart);
setTimeout(function() {
self._forward(touchend);
}, 100);
}, 0, this, this._touchStartEvt, e);
this._forwarding = true;
}
this._clearForwardTimeout();
var deltaT = Date.now() - this._startDate;
var speed = this._progress / deltaT; // progress / ms
var inertia = speed * kEdgeIntertia; // ms of intertia
var adjustedProgress = (this._progress + inertia);
if (adjustedProgress < kEdgeThreshold || this._forwarding) {
SheetsTransition.snapInPlace();
return;
}
var direction = this._direction;
if (direction === 'ltr') {
SheetsTransition.snapLeft(speed);
} else {
SheetsTransition.snapRight(speed);
}
if (direction === document.documentElement.dir) {
StackManager.goPrev();
} else {
StackManager.goNext();
}
},
_updateProgress: function esd_updateProgress(touch) {
this._deltaX = touch.clientX - this._startX;
this._deltaY = touch.clientY - this._startY;
if (this._direction == 'rtl') {
this._deltaX *= -1;
this._deltaY *= -1;
}
this._progress = this._deltaX / this._winWidth;
},
_horizontalGesture: function esd_horizontalGesture() {
var angle = Math.atan2(this._deltaX, this._deltaY);
var horizontalAngle = Math.PI / 2;
return Math.abs(angle - horizontalAngle) < kEdgeAngleThreshold;
},
_clearForwardTimeout: function esd_clearForwardTimeout() {
if (this._forwardTimeout) {
clearTimeout(this._forwardTimeout);
this._forwardTimeout = null;
}
},
_startForwarding: function esd_startForwarding(e) {
this._clearForwardTimeout();
this._forwarding = true;
this._forward(this._touchStartEvt);
this._forward(e);
if (this._beganTransition) {
SheetsTransition.snapInPlace();
}
},
/**
* Once we identify the gesture as something other than an horizontal
* swipe we replay and start forwarding the touch events:
*
* * either sending them to the current app through the sendTouchEvent
* mozbrowser API
* * or redispatching the events to the system app if they were outside
* the app frame
*
* When redispatching, since we don't know on which element they should be
* dispatched (the target is always the gesture-panel) we send a custom
* event instead.
* @memberof EdgeSwipeDetector.prototype
*/
_forward: function esd_forward(e) {
if (this._moved) {
return;
}
// We continue dispatching/forwarding where we started.
if (this._outsideApp(e) || this._redispatching) {
window.dispatchEvent(new CustomEvent('edge-touch-redispatch', {
bubbles: true,
detail: e
}));
this._redispatching = true;
} else {
this._touchForwarder.forward(e);
}
},
/**
* Detects whether or not a touch event is outside of an app container.
* @param {Object} e The relevant touch event.
* @memberof EdgeSwipeDetector.prototype
*/
_outsideApp: function esd_outsideApp(e) {
var touch = (e.type === 'touchend') ? e.changedTouches[0] : e.touches[0];
var x = touch.pageX;
var y = touch.pageY;
// In fullscreen_layout mode the app frame will always be fullscreen
// but we still want to redispatch touch events to the "overlayed"
// software home button
var softwareButtonOverlayed =
Service.query('getTopMostWindow') &&
Service.query('getTopMostWindow').isFullScreenLayout();
var width = Service.query('LayoutManager.width');
var height = Service.query('LayoutManager.height');
if (softwareButtonOverlayed) {
var sbWidth = Service.query('SoftwareButtonManager.width');
var sbHeight = Service.query('SoftwareButtonManager.height');
return x > (width - sbWidth) || y > (height - sbHeight);
}
return (x > width || y > height);
},
/**
* Swipes a page when we receive an event from gecko.
* @param {String} direction
* @memberof EdgeSwipeDetector.prototype
*/
autoSwipe: function esd_autoSwipe(direction) {
if (!this._lifecycleEnabled) {
return;
}
SheetsTransition.begin(direction);
if (direction === 'ltr') {
SheetsTransition.snapLeft(1);
} else {
SheetsTransition.snapRight(1);
}
if (direction === document.documentElement.dir) {
StackManager.goPrev();
} else {
StackManager.goNext();
}
}
};
exports.EdgeSwipeDetector = EdgeSwipeDetector;
}(window));

View file

@ -0,0 +1,24 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var EmergencyCallbackIcon = function(manager) {
BaseIcon.call(this, manager);
};
EmergencyCallbackIcon.prototype = Object.create(BaseIcon.prototype);
EmergencyCallbackIcon.prototype.name = 'EmergencyCallbackIcon';
EmergencyCallbackIcon.prototype.shouldDisplay = function() {
return this.manager.active;
};
EmergencyCallbackIcon.prototype.view = function view() {
return `<div id="statusbar-emergency-callback"
class="sb-icon sb-icon-emergency-callback"
hidden role="listitem"
data-l10n-id="statusbarEmergencyCallback">
</div>`;
};
exports.EmergencyCallbackIcon = EmergencyCallbackIcon;
}(window));

View file

@ -0,0 +1,190 @@
/* global LazyLoader, EmergencyCallbackIcon, BaseModule, UtilityTray */
'use strict';
(function() {
/*
* This emergency callback mode manager is for:
* - Initialize the manager if network type is cdma and regist an event handler
* for emergency callback mode change.
* - Display a notification to showing the system is entered the callback
* mode with count down timer.
* - Popup a dialog for user to notify the mode will block data network and
* let user decide callback mode should exit or not.
*
*/
var EmergencyCallbackManager = function(core) {
if (core && core.mobileConnections) {
this._conn = core.mobileConnections[0];
}
};
BaseModule.create(EmergencyCallbackManager, {
name: 'EmergencyCallbackManager',
timer: 0,
timeoutController: null,
TOASTER_TIMEOUT: 2000,
EVENT_PREFIX: 'emergencycallback',
notification: null,
message: null,
toaster: null,
toasterMessage: null,
warningDialog: null,
okButton: null,
cancelButton: null,
/**
* Indicates the emergency callback state
* @type {Boolean}
*/
active: false,
_start: function ecm_init() {
// Dom elements
this.notification =
document.getElementById('emergency-callback-notification');
this.message = this.notification.querySelector('.title-container');
this.notificationTimer = this.notification.querySelector('.timer');
this.toaster = document.getElementById('emergency-callback-toaster');
this.toasterMessage = this.toaster.querySelector('.message');
this.toasterTimer = this.toaster.querySelector('.timer');
this.warningDialog = document.getElementById('emergency-callback-dialog');
this.dialogTimer = this.warningDialog.querySelector('small');
this.okButton = document.getElementById('emergency-callback-exit-button');
this.cancelButton =
document.getElementById('emergency-callback-stay-button');
this.warningTimer = this.warningDialog.querySelector('p');
LazyLoader.load(['js/emergency_callback_icon.js']).then(function() {
this.icon = new EmergencyCallbackIcon(this);
this.icon.start();
}.bind(this))['catch'](function(err) { // XXX: workaround gjslint
console.error(err);
});
// Event handler
this._conn.addEventListener('emergencycbmodechange',
this.onEmergencyCbModeChange.bind(this));
this.notification.addEventListener('click',
this.notificationClicked.bind(this));
this.toaster.addEventListener('click', this.toasterClicked.bind(this));
this.okButton.addEventListener('click',
this.exitEmergencyCbMode.bind(this));
this.cancelButton.addEventListener('click', this.cancelPrompt.bind(this));
},
exitEmergencyCbMode: function ecm_exitEmergencyCbMode() {
this.warningDialog.classList.remove('visible');
this.warningDialog.hidden = true;
this._conn.exitEmergencyCbMode();
},
notificationClicked: function ecm_notificationClicker() {
this.showEmergencyCbPrompt();
UtilityTray.hide();
},
toasterClicked: function ecm_toasterClicker() {
this.showEmergencyCbPrompt();
this.toaster.classList.remove('displayed');
this.toaster.hidden = true;
},
showEmergencyCbPrompt: function ecm_showEmergencyCbPrompt() {
// Add home key event handler to dismiss the prompt if home is pressed
// while prompt displayed.
var self = this;
window.addEventListener('home', function hidePrompt() {
window.removeEventListener('home', hidePrompt);
self.cancelPrompt();
});
this.warningDialog.classList.add('visible');
this.warningDialog.hidden = false;
},
cancelPrompt: function ecm_cancelPrompt() {
this.warningDialog.classList.remove('visible');
this.warningDialog.hidden = true;
},
displayNotificationAndToaster: function() {
this.displayNotificationIfHidden();
this.toasterTimer.textContent = this.timerString;
this.toaster.classList.add('displayed');
this.toaster.hidden = false;
setTimeout(function waitToHide() {
this.toaster.classList.remove('displayed');
this.toaster.hidden = true;
}.bind(this), this.TOASTER_TIMEOUT);
},
hideNotificationIfDisplayed: function() {
if (this.notification.classList.contains('displayed')) {
this.notification.classList.remove('displayed');
this.notification.hidden = true;
this.publish('statechanged', false);
}
},
displayNotificationIfHidden: function() {
if (!this.notification.classList.contains('displayed')) {
this.notification.classList.add('displayed');
this.notification.hidden = false;
this.publish('statechanged', true);
}
},
updateTimer: function ecm_updateTimer() {
this.timer -= 1000;
if (this.timer < 0) {
window.clearInterval(this.timeoutController);
this.timeoutController = null;
return;
}
this.notificationTimer.textContent = this.dialogTimer.textContent =
this.timerString;
},
onDataError: function(evt) {
// TODO: We shold be able to confirm the error is caused by
// emergency callback mode before showing prompt.
this.showEmergencyCbPrompt();
},
onEmergencyCbModeChange: function(evt) {
var active = evt.active;
this.active = active;
this.icon && this.icon.update();
if (active) {
if (this.timeoutController) {
window.clearInterval(this.timeoutController);
}
this.timer = evt.timeoutMs;
this.displayNotificationAndToaster();
this.timeoutController =
window.setInterval(this.updateTimer.bind(this), 1000);
this._conn.ondataerror = this.onDataError.bind(this);
} else {
this.hideNotificationIfDisplayed();
this.warningDialog.classList.remove('visible');
this.warningDialog.hidden = true;
window.clearInterval(this.timeoutController);
this.timeoutController = null;
this._conn.ondataerror = null;
}
}
}, {
timerString: {
configurable: false,
get: function() {
var totalSec = this.timer / 1000;
var min = Math.floor(totalSec / 60);
var secString = (totalSec % 60).toString();
var sec = secString.length > 1 ? secString : '0' + secString;
return (min + ':' + sec);
}
}
});
}());

View file

@ -0,0 +1,16 @@
;'use strict';var EntrySheet=(function invocation(){function EntrySheet(){render.apply(this,arguments);}
EntrySheet.className='entrySheet';EntrySheet.prototype.setTitle=function(title){this.titleElement.textContent=title;};EntrySheet.prototype.open=function(){setTimeout(function(){this.element.classList.add('active');}.bind(this));};EntrySheet.prototype.close=function(){this.element.addEventListener('transitionend',function onTransitionend(){this.element.removeEventListener('transitionend',onTransitionend);this.element.classList.remove('disappearing');this.element.classList.remove('active');this.container.removeChild(this.element);}.bind(this));this.element.classList.add('disappearing');};function view(){return`<div class="${EntrySheet.className}">
<section role="region" class="skin-organic header">
<gaia-header action="close">
<h1 class="title">
<bdi class="${EntrySheet.className}-title"></bdi>
</h1>
</gaia-header>
<div class="throbber"></div>
</section>
<div class="content">
</div>
</div>`;}
function render(container,title,content){this.container=container;this.title=title;this.container.insertAdjacentHTML('beforeend',view.apply(this));this.header=this.container.querySelector('.'+EntrySheet.className+' gaia-header');this.titleElement=this.container.querySelector('.'+EntrySheet.className+'-title');this.throbberElement=this.container.querySelector('.'+EntrySheet.className+' .throbber');this.content=this.container.querySelector('.'+EntrySheet.className+' .content');this.element=this.container.querySelector('.'+EntrySheet.className);this.element.dataset.zIndexLevel='dialog-overlay';var self=this;if(typeof(BrowserFrame)!='undefined'&&content instanceof BrowserFrame){content.element.addEventListener('mozbrowserloadstart',function onLoadStart(){self.throbberElement.dataset.loading=true;});content.element.addEventListener('mozbrowserloadend',function onLoadEnd(){delete self.throbberElement.dataset.loading;});this.content.appendChild(content.element);}else if(content&&content.nodeType==1){this.content.appendChild(content);}
this.setTitle(this.title);this.header.addEventListener('action',function(){self.close();});}
return EntrySheet;}());

View file

@ -0,0 +1,344 @@
/* globals SIMSlotManager, Notification, NotificationHelper, MozActivity,
Promise, LazyLoader, BaseModule */
'use strict';
(function() {
var EU_ROAMING_FILE_PATH = '/resources/eu-roaming.json';
/**
* EuRoamingManager observes the mobile codes of the current connected
* operator and display a notification when necessary.
*
* @class EuRoamingManager
*/
function EuRoamingManager(core) {
this._connections = Array.slice(core.mobileConnections || []);
this._simMobileCodes = [];
this._curNetworkMobileCodes = [];
this._homeOperatorList = null;
this._foreignOperatorList = null;
}
BaseModule.create(EuRoamingManager, {
name: 'EuRoamingManager',
TAG_PREFIX: 'euRoamingNotificaton',
EU_ROAMING_ENABLED_KEY: 'eu-roaming.enabled',
EU_ROAMING_NOTIFICATION_STATE_KEY: 'eu-roaming.notification.state',
NOTIFICATION_STATES: {
UNAVAILABLE: 'unavailable',
DISPLAYED: 'displayed',
OPENED: 'opened'
},
/**
* Starts the module.
*
* @memberof EuRoamingManager.prototype
*/
_start: function() {
// Clear existing notifications.
this._clearNotifications();
this._init();
},
/**
* Load the EU roaming file and register related listeners when necessary.
*
* @memberof EuRoamingManager.prototype
* @returns {Promise}
*/
_init: function() {
var that = this;
return this._loadJSON(EU_ROAMING_FILE_PATH)
.then(function(result) {
if (!result) {
return;
}
that._homeOperatorList = result.home;
that._foreignOperatorList = result.foreign;
if (that._homeOperatorList &&
that._foreignOperatorList &&
Object.keys(that._homeOperatorList).length > 0 &&
Object.keys(that._foreignOperatorList).length > 0) {
that._initValues();
that._addSimChangeListener();
that._addDataChangeListener();
// Check the current state ar first.
that._connections.forEach(
that._tryShowEuRoamingNotification.bind(that));
}
});
},
/**
* Initialzes values.
*
* @memberof EuRoamingManager.prototype
*/
_initValues: function() {
// Initialize the default values.
SIMSlotManager.getSlots().forEach(function(simSlot, index) {
this._curNetworkMobileCodes.push(null);
var iccInfo = simSlot && simSlot.simCard && simSlot.simCard.iccInfo;
if (iccInfo) {
this._simMobileCodes.push({ mcc: iccInfo.mcc, mnc: iccInfo.mnc });
} else {
this._simMobileCodes.push(null);
}
}, this);
},
/**
* Check if the mobile codes is a valid EU romaing home operator.
*
* @memberof EuRoamingManager.prototype
* @param {String} mcc
* @param {String} mnc
* @returns {Boolean}
*/
_isEURoamingHomeOperator: function(mcc, mnc) {
if (!this._homeOperatorList || !mcc || !mnc) {
return false;
}
return !!this._homeOperatorList[mcc][mnc];
},
/**
* Check if the mobile codes is a valid EU romaing foreign operator.
*
* @memberof EuRoamingManager.prototype
* @param {String} mcc
* @param {String} mnc
* @returns {Boolean}
*/
_isEURoamingForeignOperator: function(mcc, mnc) {
if (!this._foreignOperatorList || !mcc || !mnc) {
return false;
}
return !!this._foreignOperatorList[mcc][mnc];
},
/**
* Add listeners on events related to the change of the mobile codes stored
* on the icc card.
*
* @memberof EuRoamingManager.prototype
*/
_addSimChangeListener: function() {
['updated', 'iccinfochange', 'cardstatechange'].forEach(
function(eventName) {
window.addEventListener('simslot-' + eventName,
this._onSimChanged.bind(this));
}, this);
},
/**
* Add listeners on events related to the change of the mobile codes of the
* currently connected data network.
*
* @memberof EuRoamingManager.prototype
*/
_addDataChangeListener: function() {
this._connections.forEach(function(connection, index) {
connection.addEventListener('datachange', function() {
this._tryShowEuRoamingNotification(connection, index);
}.bind(this));
}, this);
},
/**
* The function stores the mobile codes of the currently used sim card and
* try to display the notification.
*
* @memberof EuRoamingManager.prototype
*/
_onSimChanged: function(event) {
var simSlot = event.detail;
var iccInfo = simSlot && simSlot.simCard && simSlot.simCard.iccInfo;
if (!iccInfo) {
return;
}
this._simMobileCodes[simSlot.index] =
{ mcc: iccInfo.mcc, mnc: iccInfo.mnc };
this._tryShowEuRoamingNotification(simSlot.conn, simSlot.index);
},
/**
* The function displays the EU roaming notification when:
* - Roaming on the newly connected data network.
* - The mobile codes of the operators of the sim card are listed in the
* "home" section of the EU roaming file.
* - The mobile codes of the operators of the connected data network are
* listed in the "foreign" section of the EU roaming file.
*
* @memberof EuRoamingManager.prototype
* @param {MobileConnection} connection
* @param {Number} index
* @returns {Promise}
*/
_tryShowEuRoamingNotification: function(connection, index) {
var data = connection.data;
var network = data && data.network;
if (!network) {
return;
}
var simMobileCode = this._simMobileCodes[index];
var simMcc = simMobileCode && simMobileCode.mcc;
var simMnc = simMobileCode && simMobileCode.mnc;
var curNetworkMobileCode = this._curNetworkMobileCodes[index];
var curNetworkMcc = curNetworkMobileCode && curNetworkMobileCode.mcc;
var curNetworkMnc = curNetworkMobileCode && curNetworkMobileCode.mnc;
if (network.mcc !== curNetworkMcc || network.mnc !== curNetworkMnc) {
// a new operator detected.
this._curNetworkMobileCodes[index] =
{ mcc: network.mcc, mnc: network.mnc };
if (data.roaming &&
// check if the operator of the sim is in the EU regulation list.
this._isEURoamingHomeOperator(simMcc, simMnc) &&
// check if the new operator is a supported foreign operator or not.
this._isEURoamingForeignOperator(network.mcc, network.mnc)) {
return this._getState(this.EU_ROAMING_NOTIFICATION_STATE_KEY + index)
.then(function(notificationState) {
this._setState(this.EU_ROAMING_ENABLED_KEY + index, true);
// display the notification if it has never been opened before.
if (notificationState !== this.NOTIFICATION_STATES.OPENED) {
this._showNotification(index);
return this._setState(
this.EU_ROAMING_NOTIFICATION_STATE_KEY + index,
this.NOTIFICATION_STATES.DISPLAYED);
}
}.bind(this)).catch(function(err) {
console.error(err);
});
} else {
return Promise.all([
this._setState(this.EU_ROAMING_ENABLED_KEY + index, false),
this._setState(this.EU_ROAMING_NOTIFICATION_STATE_KEY + index,
this.NOTIFICATION_STATES.UNAVAILABLE)
]).catch(function(err) {
console.error(err);
});
}
}
},
/**
* Clear all notifications of EU roaming.
*
* @memberof EuRoamingManager.prototype
* @returns {Promise}
*/
_clearNotifications: function() {
var that = this;
return Notification.get().then(function(notifications) {
notifications.forEach(function(notification) {
var tag = notification && notification.tag;
if (!tag || !tag.startsWith(that.TAG_PREFIX)) {
return;
}
notification.close();
});
});
},
/**
* Shows the EU roaming notification of a sim card.
*
* @memberof EuRoamingManager.prototype
* @param {Number} serviceId
*/
_showNotification: function(serviceId) {
var iconUrl = window.location.origin + '/style/eu_roaming_manager/' +
'eu_roaming.png';
var options = {
bodyL10n: 'euRoamingNotificationMsg',
icon: iconUrl,
tag: this.TAG_PREFIX + serviceId,
mozbehavior: {
showOnlyOnce: true
}
};
return NotificationHelper.send('euRoamingNotificationTitle',
options).then(
notification => {
notification.onclick = function() {
this._triggerSettingsActivity(serviceId);
}.bind(this);
notification.onclose = function() {
this._setState(this.EU_ROAMING_NOTIFICATION_STATE_KEY + serviceId,
this.NOTIFICATION_STATES.OPENED);
};
return notification;
});
},
/**
* Invokes an activity for setting the EU roaming apn.
*
* @memberof EuRoamingManager.prototype
* @param {Number} serviceId
*/
_triggerSettingsActivity: function(serviceId) {
var activity = new MozActivity({
name: 'configure',
data: {
target: 'device',
section: 'apn-list',
options: {
role: 'activity',
type: 'default',
serviceId: serviceId
}
}
});
activity.onsuccess = function() {};
},
_getState: function(key) {
return new Promise(function(resolve, reject) {
var req = navigator.mozSettings.createLock().get(key);
req.onsuccess = function() {
resolve(req.result[key]);
};
req.onerror = function() {
reject('get state ' + key + ' error');
};
});
},
_setState: function(key, state) {
return new Promise(function(resolve, reject) {
var obj = {};
obj[key] = state;
var req = navigator.mozSettings.createLock().set(obj);
req.onsuccess = resolve;
req.onerror = function() {
reject('set state ' + key + ' error');
};
});
},
_loadJSON: function(path) {
return LazyLoader.getJSON(path).then(function(json) {
return Promise.resolve(json);
}, function(error) {
return Promise.resolve(null);
});
}
});
}());

View file

@ -0,0 +1,540 @@
'use strict';
/* global NotificationHelper, MozActivity */
(function(exports) {
/**
* ExternalStorageMonitor listens to external storage(should be
* insterted/removed SD card slot) for the volume chagne event.
* According to the status activity, we use regular expression to indentify
* storage actions.
* @class ExternalStorageMonitor
*/
function ExternalStorageMonitor() {
// We will monitor external storage. And the external storage should be
// removable.
var storages = navigator.getDeviceStorages('sdcard');
// If there are two storages, the name of first storage name is 'sdcard'.
// But it's an internal storage. The storage name is mapping to
// a hard code name. Because name of some external storages are different.
// Such as, Flame: 'external', Helix: 'extsdcard'.
// XXX: Bug 1033952 - Implement "isRemovable" API for device storage
// Once "isRemovable" attribute is ready, we can remove the dirty check here
if (storages.length > 1) {
storages.some(function(storage) {
if (storage.storageName != 'sdcard' && storage.canBeMounted) {
this._storage = storage;
}
}.bind(this));
} else {
this._storage = storages[0];
}
}
const RESET_JUST_ENTER_IDLE_STATUS_MILLISECONDS = 1000;
ExternalStorageMonitor.prototype = {
/**
* ums.mode setting value when the automounter is disabled.
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
statusStack: [],
/**
* Volume state "Init".
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'Init': -1,
/**
* Volume state "NoMedia"(Removed).
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'NoMedia': 0,
/**
* Volume state "Idle"(Unmounted).
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'Idle': 1,
/**
* Volume state "Pending".
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'Pending': 2,
/**
* Volume state "Checking".
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'Checking': 3,
/**
* Volume state "Mounted".
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'Mounted': 4,
/**
* Volume state "Unmounting".
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'Unmounting': 5,
/**
* Volume state "Formatting".
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'Formatting': 6,
/**
* Volume state "Shared"(Unmounted in shared mode).
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'Shared': 7,
/**
* Volume state "Shared-Mounted".
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
// It's defined in android storage void. But never see the event comes.
// Shared-Mounted: 8,
/**
* Volume state "Mount-Fail".
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
'Mount-Fail': 9,
/**
* Volume RegExp "actionRecognised".
* If a user inserted formatted and raedable SD card,
* Volume State Machine will run in this flow.
* State flow:
* NoMedia --> Pending --> Idle(Unmounted) --> Checking --> Mounted
*
* Enumerate value:
* 0 --> 2 --> 1 --> 3 --> 4
*
* @memberof ExternalStorageMonitor.prototype
* @type {RegExp}
*/
actionRecognised: /02134$/,
/**
* Volume RegExp "actionUnrecognised".
* If there is an unformatted or format-not-readable SD card in slot,
* and the storage is not able
* to be mounted, Volume State Machine will run in this flow.
* State flow:
* Checking --> Mount-Fail(Unmounted)
*
* Enumerate value:
* 3 --> 9
*
* @memberof ExternalStorageMonitor.prototype
* @type {RegExp}
*/
actionUnrecognised: /39$/,
/**
* Volume RegExp "actionRemoved".
* If a user unmount SD card successfully,
* Volume State Machine will run in this flow.
* State flow:
* Mounted --> Unmounting --> Idle(Unmounted) --> NoMedia
*
* Enumerate value:
* 4 --> 5 --> 1 --> 0
*
* @memberof ExternalStorageMonitor.prototype
* @type {RegExp}
*/
actionRemoved: /4510$/,
/**
* Volume RegExp "actionUnrecognisedStorageRemoved".
* If a user unmount SD card successfully,
* Volume State Machine will run in this flow.
* State flow:
* Mount-Fail --> NoMedia
*
* Enumerate value:
* 9 --> 0
*
* @memberof ExternalStorageMonitor.prototype
* @type {RegExp}
*/
actionUnrecognisedStorageRemoved: /90$/,
/**
* A flag to identify the storage status has been into Idle(Unmounted).
* We will use the flag to distinguish removal SD card manually or not.
*
* @memberof ExternalStorageMonitor.prototype
* @type {Boolean}
*/
justEnterIdleLessThanOneSecond: null,
/**
* An integer with the ID value of the timer that is set.
* We will clean the timer if create a new timer.
*
* @memberof ExternalStorageMonitor.prototype
* @type {Integer}
*/
resetJustEnterIdleFlagTimer: null,
/**
* An variable for maintain the storage status.
*
* @memberof ExternalStorageMonitor.prototype
* @type {Object}
*/
_storage: null,
/**
* Listene to storage 'change' event.
* @memberof ExternalStorageMonitor.prototype
* @private
*/
start: function() {
if (!this._storage) {
return;
}
var statusReq = this._storage.storageStatus();
statusReq.onsuccess = statusReq.onerror = (evt) => {
var curStorageStatus = evt.target.result;
this.enableStorageUnrecognised(curStorageStatus === 'Mount-Fail');
this._storage.addEventListener('storage-state-change', this);
var msg = '[ExternalStorageMonitor] initEvent(): ' +
'monitor external storage name = ' +
this._storage.storageName + ', canBeMounted = ' +
this._storage.canBeMounted;
this.debug(msg);
};
},
/**
* Push latest storage status in statusStack
* @memberof ExternalStorageMonitor.prototype
* @param {string} str The value we are pushing in stack. The string is
* mapping to the storage status. And the status are defined in
* ExternalStorageMonitor property.
*/
pushStatus: function(status) {
this.statusStack.push(this[status]);
},
/**
* The function keep the latest storage status. And put the status be the
* the first element in a new array.
* @memberof ExternalStorageMonitor.prototype
*/
clearStatus: function() {
this.statusStack = [this.statusStack.pop()];
var actionsFlags = this.statusStack.join('');
var msg = '[ExternalStorageMonitor] clearStatus(): ' +
'actionsFlags = ' + actionsFlags;
this.debug(msg);
},
/**
* Trigger a timer to set flag 'justEnterIdleLessThanOneSecond' be true.
* We will use the flag to speculate the action of storage removal manually
* or not.
* @memberof ExternalStorageMonitor.prototype
* @param {} str The value we are pushing in stack.
*/
enableEnterIdleStateTimer: function() {
this.justEnterIdleLessThanOneSecond = true;
if (this.resetJustEnterIdleFlagTimer) {
clearTimeout(this.resetJustEnterIdleFlagTimer);
}
// reset the flag after 1000 milliseconds
this.resetJustEnterIdleFlagTimer = setTimeout(function() {
this.justEnterIdleLessThanOneSecond = false;
}.bind(this), RESET_JUST_ENTER_IDLE_STATUS_MILLISECONDS);
},
/**
* Recognise storage actions.
* @memberof ExternalStorageMonitor.prototype
* @private
*/
recogniseStorageActions: function() {
var actionsFlags = this.statusStack.join('');
var msg = '[ExternalStorageMonitor] recogniseStorageActions(): ' +
'actionsFlags = ' + actionsFlags;
this.debug(msg);
if (actionsFlags.match(this.actionRecognised)) {
this.clearStatus();
// reset settings key 'volume.external.unrecognised' to be false
this.enableStorageUnrecognised(false);
// notify SD card detected
this.createMessage('detected-recognised');
} else if (actionsFlags.match(this.actionUnrecognised)) {
this.clearStatus();
// XXX: Bug 1060252 - Request deviceStorages API pass volume storage
// activity event. Once API is ready to maintain state machine, it will
// pass storage activity immediately. Then, system and settings app is
// able to receive change event without maintain state machine. And we
// can remove set mozSettings key here.
// change settings key 'volume.external.unrecognised' to be true
this.enableStorageUnrecognised(true);
// notify unknown SD card detected
this.createMessage('detected-unrecognised');
} else if (actionsFlags.match(this.actionRemoved)) {
this.clearStatus();
// reset settings key 'volume.external.unrecognised' to be false
this.enableStorageUnrecognised(false);
// identify SD card removed expectedly or not
if (!this.justEnterIdleLessThanOneSecond) {
// notify SD card safely removed
this.createMessage('normally-removed');
} else {
// notify SD card unexpectedly removed
this.createMessage('unexpectedly-removed');
}
} else if (actionsFlags.match(this.actionUnrecognisedStorageRemoved)) {
this.clearStatus();
// reset settings key 'volume.external.unrecognised' to be false
this.enableStorageUnrecognised(false);
}
},
/**
* Set settings key 'volume.external.unrecognised' to be boolean.
* @memberof ExternalStorageMonitor.prototype
* @type {Boolean}
*/
enableStorageUnrecognised: function(enabled) {
var setRequest = navigator.mozSettings.createLock().set({
'volume.external.unrecognised': enabled
});
setRequest.onerror = function() {
var msg = '[ExternalStorageMonitor] set settings key error:' +
setRequest.error.name;
this.debug(msg);
}.bind(this);
setRequest.onsuccess = function() {
var msg = '[ExternalStorageMonitor] set settings key onsuccess';
this.debug(msg);
}.bind(this);
},
/**
* Create message for the storage activity.
* @memberof ExternalStorageMonitor.prototype
* @param {action} str The string we figure out the storage action
*/
createMessage: function(action) {
var msg = '[ExternalStorageMonitor] createMessage(): action = ' + action;
this.debug(msg);
// Prepare message for fire notification.
var title, body;
switch (action) {
case 'detected-recognised':
this.getTotalSpace(function(totalSpace) {
title = 'sdcard-detected-title';
body = { id: 'sdcard-total-size-body',
args: {
size: totalSpace.size,
unit: totalSpace.unit
}
};
this.fireNotification(title, body, true);
}.bind(this));
break;
case 'detected-unrecognised':
title = 'sdcard-detected-title';
body = 'sdcard-unknown-size-then-tap-to-format-body';
this.fireNotification(title, body, true);
break;
case 'normally-removed':
title = 'sdcard-removed-title';
body = 'sdcard-removed-ejected-successfully';
this.fireNotification(title, body);
break;
case 'unexpectedly-removed':
title = 'sdcard-removed-title';
body = 'sdcard-removed-not-ejected-properly';
this.fireNotification(title, body);
break;
}
},
/**
* Notify user what the storage is activate.
* @memberof ExternalStorageMonitor.prototype
* @param {title} str The string we show on the title of notification
* @param {body} str The string we show on the body of notification
*/
fireNotification: function(title, body, openSettings) {
var iconUrl = window.location.origin + '/style/storage_status/' +
'notification_sd_card.png';
// We always use tag "sdcard-storage-status" to manage these notifications
var notificationId = 'sdcard-storage-status';
var options = {
body: body,
icon: iconUrl,
tag: notificationId
};
NotificationHelper.send(title, options).then(notification => {
// set onclick handler for the notification
notification.onclick =
this.notificationHandler.bind(this, notification, openSettings);
});
},
/**
* Handle notification while it be triggered 'onclick' event.
* @memberof ExternalStorageMonitor.prototype
* @private
*/
notificationHandler: function(notification, openSettings) {
// close the notification
notification.close();
// request configure activity for settings media storage
if (!openSettings) {
return;
}
var activityReq = new MozActivity({
name: 'configure',
data: {
target: 'device',
section: 'mediaStorage'
}
});
activityReq.onerror = function(e) {
var msg = '[ExternalStorageMonitor] configure activity error:' +
activityReq.error.name;
this.debug(msg);
}.bind(this);
activityReq.onsuccess = function(e) {
var msg = '[ExternalStorageMonitor] configure activity onsuccess';
this.debug(msg);
}.bind(this);
},
/**
* General event handler interface.
* @memberof ExternalStorageMonitor.prototype
* @param {DOMEvent} evt The event.
*/
handleEvent: function(e) {
switch (e.type) {
case 'storage-state-change':
var storageStatus = e.reason;
var msg = '[ExternalStorageMonitor] received ' +
'"storage-state-change" event ' +
'storageStatus = ' + storageStatus;
this.debug(msg);
this.pushStatus(storageStatus);
// checking 'Idle' state for set flag on
if (storageStatus === 'Idle') {
this.enableEnterIdleStateTimer();
}
// recognise storage actions
this.recogniseStorageActions();
break;
}
},
/**
* Get total space via the sum of the used space and free space.
* @memberof ExternalStorageMonitor.prototype
* @param {callback} function The callback will be run while get total space
*/
getTotalSpace: function(callback) {
var usedSpace, freeSpace;
var self = this;
this._storage.usedSpace().onsuccess = function(e) {
usedSpace = e.target.result;
self._storage.freeSpace().onsuccess = function(e) {
freeSpace = e.target.result;
var totalSpace = self.formatSize(usedSpace + freeSpace);
if (callback) {
callback(totalSpace);
}
};
};
},
/**
* Helper function to format the value returned by the
* nsIDOMDeviceStorage.freeSpace call in a more readable way.
* @memberof ExternalStorageMonitor.prototype
* @param {size} bytes The size of specific storage space
*/
formatSize: function(size) {
if (size === undefined || isNaN(size)) {
return;
}
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var i = 0;
while (size >= 1024 && i < (units.length) - 1) {
size /= 1024;
++i;
}
var sizeDecimal = i < 2 ? Math.round(size) : Math.round(size * 10) / 10;
return {
size: sizeDecimal,
unit: 'byteUnit-' + units[i]
};
},
/**
* On/Off flag for enable debug message.
* @memberof ExternalStorageMonitor.prototype
*/
_debug: false,
/**
* Dump console log for debug message.
* @memberof ExternalStorageMonitor.prototype
* @param {string} str The error message for debugging.
*/
debug: function(msg) {
if (this._debug) {
console.log(msg);
}
}
};
exports.ExternalStorageMonitor = ExternalStorageMonitor;
}(window));

View file

@ -0,0 +1,65 @@
/* global BaseModule */
'use strict';
(function(exports) {
/**
* This is the feature detector module of the system app.
* It is responsible to retrieve device specific information.
*/
var FeatureDetector = function() {
};
FeatureDetector.SERVICES = [
'getDeviceMemory'
];
FeatureDetector.STATES = [
'getDeviceType'
];
BaseModule.create(FeatureDetector, {
name: 'FeatureDetector',
deviceType: 'phone',
_deviceMemoryPromise: null,
_start: function() {
this._getDeviceType();
this._getDeviceMemory();
},
_stop: function() {
this.deviceType = null;
this._deviceMemoryPromise = null;
},
_getDeviceType: function() {
// `_GAIA_DEVICE_TYPE_` is a placeholder and will be replaced by real
// device type in build time, `system/build/build.js` does the trick.
this.deviceType = 'phone';
},
getDeviceType: function() {
return this.deviceType;
},
_getDeviceMemory: function() {
if ('getFeature' in navigator) {
this._deviceMemoryPromise =
navigator.getFeature('hardware.memory').catch(() => {
this.error('Failed to retrieve total memory of the device.');
// Return the failsafe value.
return 0;
});
} else {
this.error('navigator.getFeature is not available.');
this._deviceMemoryPromise = Promise.resolve(0);
}
},
getDeviceMemory: function() {
return this._deviceMemoryPromise;
}
});
}(window));

View file

@ -0,0 +1,151 @@
/* global DUMP */
/* global SettingsHelper */
/* global wakeUpFindMyDevice */
/* global Notification */
/* global NotificationHelper */
/* global MozActivity */
/* global Service */
/* global LazyLoader */
/* global IAC_API_WAKEUP_REASON_ENABLED_CHANGED */
/* global IAC_API_WAKEUP_REASON_LOGIN */
/* global IAC_API_WAKEUP_REASON_LOGOUT */
/* global IAC_API_WAKEUP_REASON_STALE_REGISTRATION */
/* global IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED */
'use strict';
const FMD_MAX_REGISTRATION_RETRIES = 5;
const FMD_ENABLE_FAILURE_NOTIFICATION_TAG = 'findmydevice.enable-failed';
// Open settings at the findmydevice section
var FMDOpenSettings = function() {
var activity = new MozActivity(
{
name : 'configure',
data : {
target : 'device',
section: 'findmydevice'
}
});
activity.onerror = function (e) {
DUMP('There was a problem opening settings: '+e);
};
};
function FMDNotifications () {}
FMDNotifications.prototype = {
handleSystemMessageNotification: function(message) {
FMDOpenSettings();
this.closeSystemMessageNotification(message);
},
closeSystemMessageNotification: function(msg) {
Notification.get({ tag: msg.tag }).then(notifs => {
notifs.forEach(notif => {
if (notif.tag) {
// Close notification with the matching tag
if (notif.tag === msg.tag) {
notif.close && notif.close();
}
} else {
// If we have notification without a tag, check on the body
if (notif.body === msg.body) {
notif.close && notif.close();
}
}
});
});
},
};
var FMDNotificationsHandler = new FMDNotifications();
function FMDInit() {
Service.request('handleSystemMessageNotification',
'findmydevice', FMDNotificationsHandler);
navigator.mozSettings.addObserver('findmydevice.enabled', function(event) {
// make sure Find My Device is registered if it's enabled,
// and that it notifies the server if disabled
wakeUpFindMyDevice(IAC_API_WAKEUP_REASON_ENABLED_CHANGED);
});
var fmdEnabledHelper = SettingsHelper('findmydevice.enabled');
navigator.mozSettings.addObserver('findmydevice.retry-count',
function(event) {
// Make sure a notification is displayed if the retry count is exceeded
if (event.settingValue >= FMD_MAX_REGISTRATION_RETRIES) {
fmdEnabledHelper.set(false);
var icon = 'style/find_my_device/images/findMyDevice.png';
var titleL10n = 'unable-to-connect';
var bodyL10n = 'tap-to-check-settings';
NotificationHelper.send(titleL10n,
{
bodyL10n: bodyL10n,
icon: icon,
tag: FMD_ENABLE_FAILURE_NOTIFICATION_TAG,
data: {
systemMessageTarget: 'findmydevice'
}
}).then(notification => {
notification.onclick = function(evt) {
FMDOpenSettings();
};
});
}
});
navigator.mozSettings.addObserver('geolocation.enabled', function(event) {
// invalidate registration so we can report to the server
// whether tracking is enabled or not, which depends on
// geolocation
wakeUpFindMyDevice(IAC_API_WAKEUP_REASON_STALE_REGISTRATION);
});
var lockMessageHelper = SettingsHelper('lockscreen.lock-message');
window.addEventListener('lockscreen-request-unlock', function(event) {
if (event.detail && event.detail.activity &&
'record' === event.detail.activity.name) {
// Let's make sure we won't start FMD when user triggers Camera from
// the lockscreen.
return;
}
// clear the lockscreen lock message
lockMessageHelper.set('');
// stop the ringer if it's currently being rung
wakeUpFindMyDevice(IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED);
});
var loggedInHelper = SettingsHelper('findmydevice.logged-in');
window.addEventListener('mozFxAccountsUnsolChromeEvent', function(event) {
if (!event || !event.detail) {
return;
}
var eventName = event.detail.eventName;
DUMP('findmydevice received ' + eventName + ' FxA event in the System app');
if (eventName === 'onlogin' || eventName === 'onverified') {
loggedInHelper.set(true);
wakeUpFindMyDevice(IAC_API_WAKEUP_REASON_LOGIN);
} else if (eventName === 'onlogout') {
loggedInHelper.set(false);
wakeUpFindMyDevice(IAC_API_WAKEUP_REASON_LOGOUT);
}
});
}
window.FindmydeviceLauncher = function() {};
window.FindmydeviceLauncher.prototype.start = function() {
return LazyLoader.load([
'shared/js/findmydevice_iac_api.js',
'shared/js/settings_helper.js'
]).then(function() {
FMDInit();
});
};

View file

@ -0,0 +1,84 @@
/* global BaseModule */
'use strict';
/**
* Singleton object to recieve and make available the list of
* late customization apps
*/
(function(exports) {
var FtuLateCustomization = function(core) {
this.debug('ctor');
};
FtuLateCustomization.EVENTS = [
'iac-ftucomms'
];
FtuLateCustomization.STATES = [
'ftuCustomizationContains'
];
BaseModule.create(FtuLateCustomization, {
name: 'FtuLateCustomization',
appsToInstall: null,
DEBUG: false,
EVENT_PREFIX: 'ftucustomization',
_start: function() {
this.debug('_start');
},
_stop: function() {
this.debug('_stop');
this.publish('end');
// what was the outcome?
if (!this.appsToInstall) {
// We never got the data from FTU
return;
}
if (!this.appsToInstall.size) {
// manifest was empty
return;
}
var installedCount = Array.from(this.appsToInstall.values())
.filter(app => {
return (app && app.installState === 'installed');
}).length;
var remaining = this.appsToInstall.size - installedCount;
if (remaining) {
this.debug('_stop, ' + remaining + ' apps not installed');
}
this.appsToInstall = null;
},
ftuCustomizationContains: function (url) {
return this.appsToInstall ?
this.appsToInstall.has(url) : false;
},
gotInstallUrls: function(urls) {
if (this.appsToInstall) {
console.warn('handling iac-ftucomms, late-customization-apps '+
'message, appsToInstall already defined');
}
this.appsToInstall = new Map();
urls.forEach(url => {
this.appsToInstall.set(url, null);
});
this.debug('urls received');
this.publish('begin', {
manifestURLs: [...this.appsToInstall.keys()]
});
},
'_handle_iac-ftucomms': function(evt) {
var message = evt.detail;
this.debug('_handle_iac-ftucomms, got message: ' +
message.type, message.urls);
switch (message.type) {
case 'late-customization-apps':
this.gotInstallUrls(message.urls);
break;
}
}
});
exports.FtuLateCustomization = FtuLateCustomization;
})(window);

View file

@ -0,0 +1,271 @@
'use strict';
/* globals applications, VersionHelper, dump, FtuPing, BaseModule,
LazyLoader, SettingsMigrator, FtuLateCustomization */
/* This module deals with FTU stuff.
FTU is known as First Time Usage,
which is the first app the users would use, to configure their phone. */
(function() {
var FtuLauncher = function() {};
FtuLauncher.EVENTS = [
'iac-ftucomms',
'appterminated',
'lockscreen-appopened',
'appopened'
];
FtuLauncher.IMPORTS = [
'shared/js/uuid.js',
'js/ftu_ping.js'
];
FtuLauncher.STATES = [
'isFtuUpgrading',
'isFtuRunning',
'isFtuCustomizing',
'getFtuOrigin',
'isFinished'
];
FtuLauncher.SERVICES = [
'stepReady',
'skip',
'launch'
];
FtuLauncher.SETTINGS = [
'ftu.manifestURL',
'latecustomization.url'
];
FtuLauncher.SUB_MODULES = [];
BaseModule.create(FtuLauncher, {
name: 'FtuLauncher',
DEBUG: false,
EVENT_PREFIX: 'ftu',
/* The application object of ftu got from Application module */
_ftu: null,
/* The manifest URL of FTU */
_ftuManifestURL: '',
/* The origin of FTU */
_ftuOrigin: '',
/* The FTU ping service instance */
_ftuPing: null,
/* Store that if FTU is currently running */
_isRunningFirstTime: false,
_isUpgrading: false,
/* The Late Customization service instance */
_lateCustomization: null,
/* Store that the Late Customization is/not currently underway */
_isFtuCustomizing: false,
_bypassHomeEvent: false,
_start: function() {
this._stepsList = [];
this._storedStepRequest = [];
return Promise.all([
this.readSetting('latecustomization.url'),
LazyLoader.load(['js/ftu_ping.js', 'js/ftu_late_customization.js'])
]).then((results) => {
var [customizationURL] = results;
this._ftuPing = new FtuPing();
this._ftuPing.ensurePing();
if (customizationURL) {
this._isFtuCustomizing = true;
this._lateCustomization =
new FtuLateCustomization();
this._lateCustomization.start();
}
}).catch(err => {
console.warn(err);
});
},
isFtuRunning: function fl_isFtuRunning() {
return this._isRunningFirstTime;
},
isFtuUpgrading: function fl_isFtuUpgrading() {
return this._isUpgrading;
},
isFtuCustomizing: function fl_isFtuCustomizing() {
return this._isFtuCustomizing;
},
getFtuOrigin: function fl_getFtuOrigin() {
return this._ftuOrigin;
},
getFtuPing: function fl_getFtuPing() {
return this._ftuPing;
},
setBypassHome: function fl_setBypassHome(value) {
this._bypassHomeEvent = value;
},
updateStep: function(step) {
if (this._stepsList.indexOf(step) < 0) {
this._stepsList.push(step);
}
var remainingRequest = [];
this._storedStepRequest.forEach(function(request, index) {
if (this.isStepFinished(request.step)) {
request.resolve();
} else {
remainingRequest.push(request);
}
}, this);
this._storedStepRequest = remainingRequest;
},
isFinished: function() {
return this._done || this._skipped;
},
isStepFinished: function(step) {
return this._done || this._skipped ||
this._stepsList.indexOf(step) >= 0;
},
_handle_home: function() {
if (this._isRunningFirstTime) {
// Because tiny devices have its own exit button,
// this check is for large devices
if (!this._bypassHomeEvent) {
return false;
} else {
this.publish('killapp', { origin: this._ftuOrigin }, true);
}
}
return true;
},
_handle_holdhome: function() {
if (this._isRunningFirstTime) {
return false;
}
return true;
},
/**
* The function will be requested by the global Launcher
* when it gets the ftu manifestURL.
* @param {String} manifestURL The manifest URL of ftu
*/
launch: function(manifestURL) {
this._ftuManifestURL = manifestURL;
if (!manifestURL) {
dump('FTU manifest cannot be found, skipping.\n');
this.skip();
return;
}
var ftu = this._ftu = applications.getByManifestURL(manifestURL);
if (!ftu) {
dump('Opps, bogus FTU manifest.\n');
this.skip();
return;
}
this._isRunningFirstTime = true;
this._ftuOrigin = ftu.origin;
// Open FTU
ftu.launch();
},
respondToHierarchyEvent: function(evt) {
if (this['_handle_' + evt.type]) {
return this['_handle_' + evt.type](evt);
}
return true;
},
stepReady: function(step) {
return new Promise(function(resolve) {
if (this.isStepFinished(step)) {
resolve();
} else {
this._storedStepRequest.push({
resolve: resolve,
step: step
});
}
}.bind(this));
},
_handle_appopened: function(evt) {
if (evt.detail.origin == this._ftuOrigin && this._isRunningFirstTime) {
this.publish('open');
}
},
'_handle_iac-ftucomms': function(evt) {
var message = evt.detail;
switch (message) {
case 'done':
this.setBypassHome(true);
break;
default:
if (message.type === 'step') {
this.updateStep(evt.detail.hash);
this.publish('step');
}
break;
}
},
_handle_appterminated: function(evt) {
if (evt.detail.origin == this._ftuOrigin) {
this.close();
}
},
close: function fl_close() {
this._isRunningFirstTime = false;
this._isUpgrading = false;
this._done = true;
window.asyncStorage.setItem('ftu.enabled', false);
// update the previous_os setting (asyn)
// so we dont try and handle upgrade again
LazyLoader.load(['shared/js/version_helper.js']).then(function() {
VersionHelper.updatePrevious();
}).catch((err) => {
console.error(err);
});
this.updateStep('done');
this.publish('done');
this.finish();
},
skip: function fl_skip() {
window.performance.mark('ftuSkip');
this._isRunningFirstTime = false;
this._isUpgrading = false;
this._skipped = true;
this.updateStep('done');
this.publish('skip');
this.finish();
},
finish: function() {
this.loadWhenIdle(['NewsletterManager']);
this._isFtuCustomizing = false;
this._lateCustomization && this._lateCustomization.stop();
this.writeSetting({'gaia.system.checkForUpdates': true});
// XXX: remove after bug 1109451 is fixed
LazyLoader.load(['js/migrators/settings_migrator.js']).then(function() {
var settingsMigrator = new SettingsMigrator();
settingsMigrator.start();
}).catch((err) => {
console.error(err);
});
}
});
}());

View file

@ -0,0 +1,445 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
/* global MobileOperator, SIMSlotManager, dump, TelemetryRequest,
LazyLoader */
/**
* A simple ping that is kicked off on first time use
*/
(function(exports) {
var DEBUG = false;
const FTU_PING_ACTIVATION = 'ftu.pingActivation';
const FTU_PING_ENABLED = 'ftu.pingEnabled';
const FTU_PING_MAX_NETWORK_FAILS = 'ftu.pingMaxNetworkFails';
const FTU_PING_NETWORK_FAIL_COUNT = 'ftu.pingNetworkFailCount';
const FTU_PING_URL = 'ftu.pingURL';
const FTU_PING_TIMEOUT = 'ftu.pingTimeout';
const FTU_PING_TRY_INTERVAL = 'ftu.pingTryInterval';
const DEFAULT_TRY_INTERVAL = 60 * 60 * 1000;
const DEFAULT_MAX_NETWORK_FAILS = 24;
const DEFAULT_PING_TIMEOUT = 60 * 1000;
// Used by the telemetry server to help identify the payload format
const TELEMETRY_VERSION = 1;
const TELEMETRY_REASON = 'ftu';
// Settings to observe value changes for while the ping has not been sent
const OBSERVE_SETTINGS = ['deviceinfo.os',
'deviceinfo.software',
'deviceinfo.product_model',
'deviceinfo.firmware_revision',
'deviceinfo.hardware'];
const URL_SETTINGS = ['deviceinfo.platform_build_id',
'deviceinfo.platform_version',
'app.update.channel'];
function FtuPing() {
}
FtuPing.prototype = {
// whether or not required properties have been loaded
_pingReady: false,
_pingReadyCallback: null,
// interval timer for first time ping
_pingTimer: null,
// time between ping tries
_tryInterval: DEFAULT_TRY_INTERVAL,
// URL for first time ping
_pingURL: null,
// Whether or not FTU ping is enabled
_pingEnabled: true,
// The number of times to wait for SIM / voice network data before sending
// the ping anyway
_maxNetworkFails: DEFAULT_MAX_NETWORK_FAILS,
// Number of network failures accrued
_networkFailCount: 0,
// Data used in ping
_pingData: {},
// Data used in info
_infoData: {},
// Timeout for ping requests
_pingTimeout: DEFAULT_PING_TIMEOUT,
_settingObserver: null,
debug: function fp_debug(msg) {
if (DEBUG) {
dump('[FtuPing] ' + msg + '\n');
}
},
reset: function fp_reset() {
this._pingReady = false;
this._pingReadyCallback = null;
this._pingTimer = null;
this._tryInterval = DEFAULT_TRY_INTERVAL;
this._pingTimeout = DEFAULT_PING_TIMEOUT;
this._settingObserver = null;
this._pingURL = null;
this._pingEnabled = true;
this._maxNetworkFails = DEFAULT_MAX_NETWORK_FAILS;
this._networkFailCount = 0;
this._pingData = {};
},
initSettings: function fp_initSettings() {
var self = this;
function getAsyncItems() {
return new Promise(function(resolve, reject) {
self.getAsyncStorageItems([FTU_PING_ACTIVATION,
FTU_PING_ENABLED,
FTU_PING_NETWORK_FAIL_COUNT],
function(items) {
self._pingData.activationTime = items[FTU_PING_ACTIVATION];
self._pingEnabled = items[FTU_PING_ENABLED];
if (!self._pingData.activationTime) {
self._pingData.activationTime = Date.now();
window.asyncStorage.setItem(FTU_PING_ACTIVATION,
self._pingData.activationTime);
}
if (self._pingEnabled === null) {
self._pingEnabled = true;
}
if (typeof(items[FTU_PING_NETWORK_FAIL_COUNT]) === 'number') {
self._networkFailCount = items[FTU_PING_NETWORK_FAIL_COUNT];
}
resolve();
});
});
}
function getSettings() {
return new Promise(function(resolve, reject) {
var allSettings = [FTU_PING_URL, FTU_PING_TRY_INTERVAL,
FTU_PING_TIMEOUT, FTU_PING_MAX_NETWORK_FAILS].
concat(OBSERVE_SETTINGS);
allSettings = allSettings.concat(URL_SETTINGS);
self.getSettings(allSettings, function(settings) {
self._pingURL = settings[FTU_PING_URL];
self._tryInterval = settings[FTU_PING_TRY_INTERVAL] ||
self._tryInterval;
self._pingTimeout = settings[FTU_PING_TIMEOUT] || self._pingTimeout;
self._maxNetworkFails = settings[FTU_PING_MAX_NETWORK_FAILS] ||
self._maxNetworkFails;
var mozSettings = window.navigator.mozSettings;
if (!self._settingObserver) {
self._settingObserver = self.onSettingChanged.bind(self);
}
OBSERVE_SETTINGS.forEach(function(observeSetting) {
self._pingData[observeSetting] = settings[observeSetting];
mozSettings.addObserver(observeSetting, self._settingObserver);
});
URL_SETTINGS.forEach(function(urlSetting) {
self._infoData[urlSetting] = settings[urlSetting];
mozSettings.addObserver(urlSetting, self._settingObserver);
});
resolve();
});
});
}
self.reset();
self._pingData.screenHeight = window.screen.height;
self._pingData.screenWidth = window.screen.width;
self._pingData.devicePixelRatio = window.devicePixelRatio;
return Promise.all([getAsyncItems(), getSettings()]);
},
initPreinstalledApps: function fp_initPreinstalledApps(callback) {
var preinstalled = this._pingData.preinstalled = {};
var self = this;
return new Promise(function(resolve, reject) {
window.navigator.mozApps.mgmt.getAll().onsuccess = function(evt) {
var apps = evt.target.result;
apps.forEach(function(app) {
var url;
try {
url = new URL(app.manifestURL);
} catch (e) {
// Don't die if somehow the manifestURL is invalid
}
// Only report non-gaia apps
if (!url || url.hostname.indexOf('gaiamobile.org') === -1) {
preinstalled[app.manifestURL] = app.manifest.name;
}
});
self.debug(self._pingData.preinstalled);
resolve();
};
});
},
getAsyncStorageItems: function fp_getAsyncStorageItems(itemKeys, callback) {
var itemsLeft = itemKeys.length;
var items = {};
itemKeys.forEach(function(key) {
window.asyncStorage.getItem(key, function(value) {
itemsLeft--;
items[key] = value;
if (itemsLeft === 0 && callback) {
callback(items);
}
});
});
},
getSettings: function fp_getSettings(settingKeys, callback) {
var settingsLeft = settingKeys.length;
var settings = {};
var lock = window.navigator.mozSettings.createLock();
settingKeys.forEach(function(key) {
var request = lock.get(key);
request.onsuccess = function(evt) {
var value = request.result[key];
settingsLeft--;
settings[key] = value;
if (settingsLeft === 0 && callback) {
callback(settings);
}
};
});
},
ensurePing: function fp_ensurePing() {
var self = this;
LazyLoader.load('shared/js/telemetry.js').then(function() {
return Promise.all([self.initSettings(), self.initPreinstalledApps()])
.then(self.startPing.bind(self));
});
},
onSettingChanged: function fp_onSettingChanged(evt) {
this._pingData[evt.settingName] = evt.settingValue;
},
startPing: function fp_startPing() {
if (this._pingEnabled === false) {
this.debug('FTU ping disabled');
return;
}
if (!this._pingURL) {
this.debug('No FTU ping URL');
return;
}
if (this._pingTimer !== null) {
return;
}
this.debug('Starting FTU ping');
this._pingTimer = setInterval(this.tryPing.bind(this), this._tryInterval);
},
maybeThrowNetworkFailure: function fp_maybeThrowNetworkFailure(message) {
this._networkFailCount++;
window.asyncStorage.setItem(FTU_PING_NETWORK_FAIL_COUNT,
this._networkFailCount);
if (this._networkFailCount < this._maxNetworkFails) {
throw message + ' (' + this._networkFailCount + ' of ' +
this._maxNetworkFails + ' failures)';
} else {
this.debug('Warning: ' + message);
}
},
checkMobileNetwork: function fp_checkMobileNetwork() {
// Wait until we have a valid network connection
if (SIMSlotManager.noSIMCardConnectedToNetwork()) {
this.maybeThrowNetworkFailure('No SIM cards connected to a network');
}
var conns = window.navigator.mozMobileConnections;
if (!conns || conns.length === 0) {
this.maybeThrowNetworkFailure('No mobile connections');
return;
}
var slots = SIMSlotManager.getSlots().filter(function(slot) {
return !slot.isAbsent() && !slot.isLocked();
});
if (slots.length === 0) {
this.maybeThrowNetworkFailure('No unlocked or active SIM cards found');
return;
}
var conn = slots[0].conn;
var iccObj = navigator.mozIccManager.getIccById(conn.iccId);
var iccInfo = iccObj ? iccObj.iccInfo : null;
var voiceNetwork = conn.voice ? conn.voice.network : null;
if (!iccInfo && !voiceNetwork) {
this.maybeThrowNetworkFailure('No voice network or ICC info');
return;
}
this._pingData.network = MobileOperator.userFacingInfo(conn);
if (voiceNetwork) {
this._pingData.network.mnc = voiceNetwork.mnc;
this._pingData.network.mcc = voiceNetwork.mcc;
}
if (iccInfo) {
this._pingData.icc = {
mnc: iccInfo.mnc,
mcc: iccInfo.mcc,
spn: iccInfo.spn
};
}
},
tryPing: function fp_tryPing() {
var self = this;
try {
this.checkMobileNetwork();
if (this._networkFailCount >= this._maxNetworkFails) {
this.debug('Max voice network failures reached, pinging anyway!');
if (this._pingData.network === undefined) {
this._pingData.network = null;
}
if (this._pingData.icc === undefined) {
this._pingData.icc = null;
}
}
if (!this._pingData['deviceinfo.os']) {
this.debug('No OS information, holding off');
return false;
}
var promise = TelemetryRequest.getDeviceID();
promise.then(function(deviceID) {
self._pingData.pingID = deviceID;
self.ping();
}).catch(function(error) {
self.debug('Generating deviceID: ' + error);
});
} catch (e) {
this.debug('Error while trying FTU ping: ' + e);
return false;
}
},
ping: function fp_ping() {
var pingData = this.assemblePingData();
this._pingData.pingTime = Date.now();
var request = new TelemetryRequest({
reason: TELEMETRY_REASON,
deviceID: pingData.pingID,
ver: TELEMETRY_VERSION,
url: this._pingURL,
appUpdateChannel: this._infoData['app.update.channel'],
appVersion: this._infoData['deviceinfo.platform_version'],
appBuildID: this._infoData['deviceinfo.platform_build_id']
}, pingData);
var self = this;
request.send({
timeout: this._pingTimeout,
onload: function() {
self.pingSuccess(this.responseText);
},
ontimeout: function() {
self.pingError('Timed out');
},
onerror: function() {
self.pingError(this.statusText);
}
});
},
pingSuccess: function fp_pingSuccess(result) {
if (result !== 'OK') {
this.debug('Ping response unexpected: ' + result);
return;
}
this._pingEnabled = false;
window.asyncStorage.setItem(FTU_PING_ENABLED, false);
var settings = window.navigator.mozSettings;
OBSERVE_SETTINGS.forEach(function(setting) {
settings.removeObserver(setting, this._settingObserver);
}, this);
URL_SETTINGS.forEach(function(setting) {
settings.removeObserver(setting, this._settingObserver);
}, this);
clearInterval(this._pingTimer);
},
pingError: function fp_pingError(message) {
this.debug('Ping error: ' + message);
},
isEnabled: function fp_isEnabled() {
return this._pingEnabled;
},
getPingURL: function fp_getPingURL() {
return this._pingURL;
},
assemblePingData: function fp_assemblePingData() {
this._pingData.pingTime = Date.now();
this._pingData.locale = window.navigator.language;
return this._pingData;
},
getInfoData: function fp_getInfoData() {
return this._infoData;
},
getNetworkFailCount: function fp_getNetworkFailCount() {
return this._networkFailCount;
},
getTryInterval: function fp_getTryInterval() {
return this._tryInterval;
},
getPingTimeout: function fp_getPingTimeout() {
return this._pingTimeout;
},
getMaxNetworkFails: function fp_getMaxNetworkFails() {
return this._maxNetworkFails;
}
};
exports.FtuPing = FtuPing;
}(window));

View file

@ -0,0 +1,160 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/**
* Check fxa_manager.js for a further explanation about Firefox Accounts and
* its architecture in Firefox OS.
*/
/* exported FxAccountsClient */
'use strict';
var FxAccountsClient = function FxAccountsClient() {
var listening;
var callbacks = {};
var sendMessage = function sendMessage(message, successCb, errorCb) {
if (!listening) {
window.addEventListener('mozFxAccountsChromeEvent', onChromeEvent);
listening = true;
}
var id = getUUID();
callbacks[id] = {
successCb: successCb,
errorCb: errorCb
};
var details = {
id: id,
data: message
};
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozFxAccountsContentEvent', true, true, details);
window.dispatchEvent(event);
};
var onChromeEvent = function onChromeEvent(event) {
var message = event.detail;
if (!message.id) {
console.warn('Got mozFxAccountsChromeEvent with no id');
return;
}
var callback = callbacks[message.id];
if (callback && typeof message.data !== 'undefined' && callback.successCb) {
callback.successCb(message.data);
delete callbacks[message.id];
} else if (callback && message.error && callback.errorCb) {
callback.errorCb(message.error);
delete callbacks[message.id];
}
};
var getUUID = function getUUID() {
var s4 = function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
};
// === API ===
var getAccount = function getAccount(successCb, errorCb) {
sendMessage({
method: 'getAccount'
}, successCb, errorCb);
};
var getAssertion = function getAssertion(options, successCb, errorCb) {
sendMessage({
method: 'getAssertion',
silent: options ? options.silent : null,
audience: options ? options.audience : null
}, successCb, errorCb);
};
var getKeys = function getKeys(successCb, errorCb) {
sendMessage({
method: 'getKeys'
}, successCb, errorCb);
};
var logout = function logout(successCb, errorCb) {
sendMessage({
method: 'logout'
}, successCb, errorCb);
};
var queryAccount = function queryAccount(email, successCb, errorCb) {
sendMessage({
method: 'queryAccount',
email: email
}, successCb, errorCb);
};
var resendVerificationEmail = function resendVerificationEmail(email,
successCb, errorCb) {
sendMessage({
method: 'resendVerificationEmail',
email: email
}, successCb, errorCb);
};
var signIn = function signIn(email, password, successCb, errorCb) {
var fetchKeys = false;
fetchKeys = true;
sendMessage({
method: 'signIn',
email: email,
password: password,
fetchKeys: fetchKeys
}, successCb, errorCb);
};
var signUp = function signUp(email, password, successCb, errorCb) {
var fetchKeys = false;
fetchKeys = true;
sendMessage({
method: 'signUp',
email: email,
password: password,
fetchKeys: fetchKeys
}, successCb, errorCb);
};
var verificationStatus = function verificationStatus(email, successCb,
errorCb) {
sendMessage({
method: 'verificationStatus',
email: email
}, successCb, errorCb);
};
return {
'getAccount': getAccount,
'getAssertion': getAssertion,
'getKeys': getKeys,
'logout': logout,
'queryAccount': queryAccount,
'resendVerificationEmail': resendVerificationEmail,
'signIn': signIn,
'signUp': signUp,
'verificationStatus': verificationStatus
};
}();

View file

@ -0,0 +1,189 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/**
* Firefox Accounts is a database of users who have opted in to services in
* the Mozilla cloud. Firefox Accounts holds Mozilla specific information
* (which of your devices are attached right now?), as well as one (and
* eventually multiple) verified identities for a user.
*
* Firefox Accounts is a web scale service with a REST API, that allows a user
* agent to authenticate to the service (by verifying the user's identity),
* and in exchange to get credentials that allow the user agent to assert the
* identity of the user to other services.
*
* FxAccountsManager is mostly a proxy between certified apps that wants to
* manage Firefox Accounts (FTU and Settings so far) and the platform.
* It handles the communication via IAC and redirects the requests coming from
* these apps to the platform in the form of mozContentEvents. It also handles
* sign-in requests done via the mozId API from RPs and triggers FxAccountsUI
* to show the appropriate UI in each case.
*
*
* =FTU= =Settings= =RP=
* | / |
* |-IAC IAC-/ |
* \ / |
* ========================System======================= |
* \ / |
* \ / FxAccountsUI |
* FxAccountsManager ______/ | |-mozId API
* | \ | |
* | FxAccountsClient |
* | | |
* | | |
* =============================Gecko======================================
* | | |
* | |-moz(Chrome/Content)Event |
* | | |
* FxAccountsUIGlue FxAccountsMgmtService |
* | ^ |
* | | |
* | | |
* |__________FxAccountsManager |
* | |
* | |
* DOM Identity API impl. _______________|
*/
/* global IACHandler, LazyLoader, FxAccountsClient, FxAccountsUI */
'use strict';
var FxAccountsManager = {
start: function fxa_mgmt_init() {
// Set up the listener for IAC API connection requests.
window.addEventListener('iac-fxa-mgmt', this.onPortMessage);
// Listen for unsolicited chrome events coming from the implementation o
// RP DOM API or the Fx Accounts UI glue.
window.addEventListener('mozFxAccountsUnsolChromeEvent', this);
},
sendPortMessage: function fxa_mgmt_sendPortMessage(message) {
var port = IACHandler.getPort('fxa-mgmt');
if (port) {
port.postMessage(message);
}
},
onPortMessage: function fxa_mgmt_onPortMessage(event) {
if (!event || !event.detail) {
console.error('Wrong event');
return;
}
var self = FxAccountsManager;
var methodName = event.detail.name;
switch (methodName) {
case 'getAccount':
case 'logout':
(function(methodName) {
LazyLoader.load('js/fx_accounts_client.js', function() {
FxAccountsClient[methodName](function(data) {
self.sendPortMessage({ methodName: methodName, data: data });
}, function(error) {
self.sendPortMessage({ methodName: methodName, error: error });
});
});
})(methodName);
break;
case 'resendVerificationEmail':
(function(methodName) {
var email = event.detail.email;
if (!email) {
self.sendPortMessage({ methodName: methodName,
error: 'NO_VALID_EMAIL' });
return;
}
LazyLoader.load('js/fx_accounts_client.js', function() {
FxAccountsClient[methodName](email, function(data) {
self.sendPortMessage({ methodName: methodName, data: data });
}, function(error) {
self.sendPortMessage({ methodName: methodName, error: error });
});
});
})(methodName);
break;
case 'openFlow':
(function(methodName) {
FxAccountsUI.login(function(data) {
self.sendPortMessage({ methodName: methodName, data: data });
}, function(error) {
self.sendPortMessage({ methodName: methodName, error: error });
});
})(methodName);
break;
case 'refreshAuthentication':
(function(methodName) {
var email = event.detail.email;
if (!email) {
self.sendPortMessage({ methodName: methodName,
error: 'NO_VALID_EMAIL' });
return;
}
FxAccountsUI.refreshAuthentication(email, function(data) {
self.sendPortMessage({ methodName: methodName, data: data });
}, function(error) {
self.sendPortMessage({ methodName: methodName, error: error });
});
})(methodName);
break;
}
},
_sendContentEvent: function fxa_mgmt_sendContentEvent(msg) {
var event = new CustomEvent('mozFxAccountsRPContentEvent', {detail: msg});
window.dispatchEvent(event);
},
handleEvent: function fxa_mgmt_handleEvent(event) {
if (!event || !event.detail) {
console.error('Wrong event');
return;
}
var message = event.detail;
switch (message.eventName) {
case 'openFlow':
FxAccountsUI.login(function(result) {
this._sendContentEvent({
id: message.id,
result: result
});
}.bind(this), function(error) {
this._sendContentEvent({
id: message.id,
error: error
});
}.bind(this));
break;
case 'refreshAuthentication':
var email = message.data.email;
if (!email) {
console.error('No account id specified');
return;
}
FxAccountsUI.refreshAuthentication(email, function(result) {
this._sendContentEvent({
id: message.id,
result: result
});
}.bind(this), function(error) {
this._sendContentEvent({
id: message.id,
error: error
});
}.bind(this));
break;
case 'onlogin':
case 'onverified':
case 'onlogout':
FxAccountsManager.sendPortMessage({ eventName: message.eventName });
break;
}
}
};

View file

@ -0,0 +1,114 @@
/* global FxAccountsDialog, Service, LazyLoader */
'use strict';
(function(exports) {
var FxAccountsUI = {
name: 'FxAccountsUI',
dialog: null,
panel: null,
iframe: null,
onerrorCb: null,
onsuccessCb: null,
start: function() {
var dialogOptions = {
onHide: this.reset.bind(this)
};
if (!this.dialog) {
LazyLoader.load('js/fxa_dialog.js').then(() => {
this.dialog = new FxAccountsDialog(dialogOptions);
this.panel = this.dialog.getView();
}).catch((err) => {
console.error(err);
});
}
this.iframe = document.createElement('iframe');
this.iframe.id = 'fxa-iframe';
Service.register('login', this);
Service.register('close', this);
},
// Sign in/up flow.
login: function fxa_ui_login(onsuccess, onerror) {
this.onsuccessCb = onsuccess;
this.onerrorCb = onerror;
this.loadFlow('login');
},
// Logout flow.
logout: function fxa_ui_logout(onsuccess, onerror) {
this.onsuccessCb = onsuccess;
this.onerrorCb = onerror;
this.loadFlow('logout');
},
// Delete flow.
delete: function fxa_ui_delete(onsuccess, onerror) {
this.onsuccessCb = onsuccess;
this.onerrorCb = onerror;
this.loadFlow('delete');
},
// Refresh authentication flow.
refreshAuthentication: function fxa_ui_refreshAuth(email,
onsuccess,
onerror) {
this.onsuccessCb = onsuccess;
this.onerrorCb = onerror;
this.loadFlow('refresh_auth', ['email=' + email]);
},
// Method which close the dialog.
close: function fxa_ui_close() {
var self = this;
this.panel.addEventListener('animationend', function closeAnimationEnd() {
self.panel.removeEventListener('animationend',
closeAnimationEnd, false);
self.panel.classList.remove('closing');
self.dialog.hide();
}, false);
this.panel.classList.add('closing');
},
// Method for reseting the panel.
reset: function fxa_ui_reset(reason) {
this.panel.removeChild(this.iframe);
this.dialog.browser = null;
if (reason == 'home' || reason == 'holdhome') {
this.onerrorCb && this.onerrorCb('DIALOG_CLOSED_BY_USER');
}
this.onerrorCb = null;
this.onsuccessCb = null;
},
// Method for loading the iframe with the flow required.
loadFlow: function fxa_ui_loadFlow(flow, params) {
var url = '../fxa/fxa_module.html#' + flow;
if (Service.query('isFtuRunning')) {
params = params || [];
params.push('isftu=true');
}
if (params && Array.isArray(params)) {
url += '?' + params.join('&');
}
this.iframe.setAttribute('src', url);
this.panel.appendChild(this.iframe);
this.dialog.browser = { element: this.iframe };
this.dialog.show();
},
// Method for sending the result of the FxAccounts flow to the caller app.
done: function fxa_ui_done(data) {
// Proccess data retrieved.
this.onsuccessCb && this.onsuccessCb(data);
this.close();
},
error: function fxa_ui_error(error) {
this.onerrorCb && this.onerrorCb(error);
this.close();
}
};
exports.FxAccountsUI = FxAccountsUI;
}(window));

View file

@ -0,0 +1,53 @@
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
(function(exports) {
/**
* @class FxAccountsDialog
* @param {options} object for attributes `onShow`, `onHide` callback.
* @extends SystemDialog
*/
var FxAccountsDialog = function FxAccountsDialog(options) {
if (options) {
this.options = options;
}
/**
* render the dialog
*/
this.render();
this.publish('created');
};
FxAccountsDialog.prototype = Object.create(window.SystemDialog.prototype);
FxAccountsDialog.prototype.customID = 'fxa-dialog';
FxAccountsDialog.prototype.DEBUG = false;
FxAccountsDialog.prototype.view = function fxad_view() {
return `<div id="${this.instanceID}" role="dialog"
class="generic-dialog" data-z-index-level="system-dialog"
hidden></div>`;
};
FxAccountsDialog.prototype.getView = function fxad_view() {
return document.getElementById(this.instanceID);
};
// Get all elements when inited.
FxAccountsDialog.prototype._fetchElements =
function fxad__fetchElements() {
};
// Register events when all elements are got.
FxAccountsDialog.prototype._registerEvents =
function fxad__registerEvents() {
};
exports.FxAccountsDialog = FxAccountsDialog;
}(window));

View file

@ -0,0 +1,28 @@
/* global BaseModule, GeolocationIcon, LazyLoader */
'use strict';
(function() {
var GeolocationCore = function() {};
GeolocationCore.EVENTS = [
'mozChromeEvent'
];
BaseModule.create(GeolocationCore, {
name: 'GeolocationCore',
_start: function() {
LazyLoader.load(['js/geolocation_icon.js']).then(function() {
this.icon = new GeolocationIcon(this);
this.icon.start();
}.bind(this)).catch(function(err) {
console.error(err);
});
},
_handle_mozChromeEvent: function(evt) {
switch (evt.detail.type) {
case 'geolocation-status':
this.active = evt.detail.active;
this.icon && this.icon.update();
break;
}
}
});
}());

Some files were not shown because too many files have changed in this diff Show more