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.
1
gaia/apps/system/.gitignore
vendored
Executable file
|
|
@ -0,0 +1 @@
|
|||
/camera
|
||||
8
gaia/apps/system/blank.html
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
162
gaia/apps/system/build/build.js
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
'use strict';
|
||||
|
||||
/* jshint node: true */
|
||||
|
||||
var utils = require('utils');
|
||||
var preprocessor = require('preprocessor');
|
||||
|
||||
var SystemAppBuilder = function() {
|
||||
};
|
||||
|
||||
SystemAppBuilder.prototype.setOptions = function(options) {
|
||||
this.stageDir = utils.getFile(options.STAGE_APP_DIR);
|
||||
this.appDir = utils.getFile(options.APP_DIR);
|
||||
this.distDirPath = options.GAIA_DISTRIBUTION_DIR;
|
||||
};
|
||||
|
||||
SystemAppBuilder.prototype.addCustomizeFiles = function() {
|
||||
if (!utils.getFile(this.distDirPath, 'power').exists()) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
var fileDir = utils.getFile(this.distDirPath, 'power');
|
||||
var files = utils.ls(fileDir);
|
||||
files.forEach(function(file) {
|
||||
utils.copyFileTo(file.path,
|
||||
utils.joinPath(self.stageDir.path, 'resources', 'power'), file.leafName);
|
||||
});
|
||||
};
|
||||
|
||||
SystemAppBuilder.prototype.initConfigJsons = function() {
|
||||
var iccDefault = {
|
||||
'defaultURL': 'http://www.mozilla.org/en-US/firefoxos/'
|
||||
};
|
||||
var wapuaprofDefault = {
|
||||
};
|
||||
var euRoamingDefault = {
|
||||
};
|
||||
var iccFile = utils.getFile(this.stageDir.path, 'resources', 'icc.json');
|
||||
var wapFile = utils.getFile(this.stageDir.path, 'resources',
|
||||
'wapuaprof.json');
|
||||
var euRoamingFile = utils.getFile(this.stageDir.path, 'resources',
|
||||
'eu-roaming.json');
|
||||
|
||||
utils.writeContent(iccFile,
|
||||
utils.getDistributionFileContent('icc', iccDefault, this.distDirPath));
|
||||
|
||||
utils.writeContent(wapFile,
|
||||
utils.getDistributionFileContent('wapuaprof',
|
||||
wapuaprofDefault, this.distDirPath));
|
||||
|
||||
utils.writeContent(euRoamingFile,
|
||||
utils.getDistributionFileContent('eu-roaming',
|
||||
euRoamingDefault, this.distDirPath));
|
||||
};
|
||||
|
||||
/**
|
||||
* XXX: Before we can pull LockScreen out, we need this to split
|
||||
* LockScreen and System app while still merge them into one file.
|
||||
* (Bug 1057198).
|
||||
*/
|
||||
SystemAppBuilder.prototype.integrateLockScreen = function(options) {
|
||||
var stagePath = options.STAGE_APP_DIR;
|
||||
var lockscreenFrameElement = '<div id="lockscreen-frame-placeholder"></div>';
|
||||
// Paths must indicate to the files in build stage directory.
|
||||
var lockscreenFramePath = [stagePath, 'lockscreen', 'lockscreen.html'];
|
||||
var systemIndexPath = [stagePath, 'index.html'];
|
||||
var systemIndexFile = utils.getFile.apply(utils, systemIndexPath);
|
||||
var lockscreenContent = utils.getFileContent(
|
||||
utils.getFile.apply(utils, lockscreenFramePath));
|
||||
var systemIndexContent = utils.getFileContent(
|
||||
systemIndexFile);
|
||||
var replacedIndexContent = systemIndexContent.replace(lockscreenFrameElement,
|
||||
lockscreenContent);
|
||||
utils.writeContent(systemIndexFile, replacedIndexContent);
|
||||
};
|
||||
|
||||
/**
|
||||
* XXX: Before we can pull LockScreenInputpad out, we need this to split
|
||||
* LockScreenInputpad and LockScreen while still merge them into one file.
|
||||
* (Bug 1053680).
|
||||
*/
|
||||
SystemAppBuilder.prototype.integrateLockScreenInputpad = function(options) {
|
||||
var stagePath = options.STAGE_APP_DIR;
|
||||
var lockscreenInputpadFrameElement =
|
||||
'<div id="lockscreen-inputpad-frame-placeholder"></div>';
|
||||
// Paths must indicate to the files in build stage directory.
|
||||
var lockscreenInputpadFramePath =
|
||||
[stagePath, 'lockscreen', 'lockscreen_inputpad_frame.html'];
|
||||
var systemIndexPath = [stagePath, 'index.html'];
|
||||
var systemIndexFile = utils.getFile.apply(utils, systemIndexPath);
|
||||
var lockscreenInputpadContent = utils.getFileContent(
|
||||
utils.getFile.apply(utils, lockscreenInputpadFramePath));
|
||||
var systemIndexContent = utils.getFileContent(
|
||||
systemIndexFile);
|
||||
var replacedIndexContent = systemIndexContent.replace(
|
||||
lockscreenInputpadFrameElement,
|
||||
lockscreenInputpadContent);
|
||||
utils.writeContent(systemIndexFile, replacedIndexContent);
|
||||
};
|
||||
|
||||
SystemAppBuilder.prototype.inlineDeviceType = function(options) {
|
||||
var stagePath = options.STAGE_APP_DIR;
|
||||
var deviceType = options.GAIA_DEVICE_TYPE;
|
||||
var basemodulePath = [stagePath, 'js', 'base_module.js'];
|
||||
var basemoduleFile = utils.getFile.apply(utils, basemodulePath);
|
||||
var basemoduleContent = utils.getFileContent(basemoduleFile);
|
||||
var featureDetectorPath = [stagePath, 'js', 'feature_detector.js'];
|
||||
var featureDetectorFile = utils.getFile.apply(utils, featureDetectorPath);
|
||||
var featureDetectorContent = utils.getFileContent(featureDetectorFile);
|
||||
|
||||
// `this.deviceType = '_GAIA_DEVICE_TYPE_';` will be replaced by real device
|
||||
// type, take phone for example, result will be: this.deviceType = 'phone';
|
||||
// Only override necessary modules below in build time.
|
||||
// For common case, using standard method Service.query('getDeviceType') to
|
||||
// get device type
|
||||
utils.writeContent(
|
||||
basemoduleFile,
|
||||
basemoduleContent.replace(
|
||||
/this\.deviceType = \'_GAIA_DEVICE_TYPE_\';/,
|
||||
'this.deviceType = \'' + deviceType + '\';')
|
||||
);
|
||||
|
||||
utils.writeContent(
|
||||
featureDetectorFile,
|
||||
featureDetectorContent.replace(
|
||||
/this\.deviceType = \'_GAIA_DEVICE_TYPE_\';/,
|
||||
'this.deviceType = \'' + deviceType + '\';')
|
||||
);
|
||||
};
|
||||
|
||||
SystemAppBuilder.prototype.enableFirefoxSync = function(options) {
|
||||
var fileList = {
|
||||
process: [
|
||||
['js', 'core.js'],
|
||||
['js', 'fx_accounts_client.js']
|
||||
],
|
||||
remove: [
|
||||
['js', 'sync_manager.js'],
|
||||
['js', 'sync_state_machine.js'],
|
||||
['test', 'unit', 'sync_manager_test.js'],
|
||||
['test', 'unit', 'sync_state_machine_test.js']
|
||||
]
|
||||
};
|
||||
preprocessor.execute(options, 'FIREFOX_SYNC', fileList);
|
||||
};
|
||||
|
||||
SystemAppBuilder.prototype.execute = function(options) {
|
||||
utils.copyToStage(options);
|
||||
this.setOptions(options);
|
||||
this.initConfigJsons();
|
||||
if (this.distDirPath) {
|
||||
this.addCustomizeFiles();
|
||||
}
|
||||
this.enableFirefoxSync(options);
|
||||
this.integrateLockScreen(options);
|
||||
this.integrateLockScreenInputpad(options);
|
||||
this.inlineDeviceType(options);
|
||||
};
|
||||
|
||||
exports.execute = function(options) {
|
||||
(new SystemAppBuilder()).execute(options);
|
||||
};
|
||||
16
gaia/apps/system/elements/net_error_action_menu.html
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
<element name="net-error-action-menu" extends="section">
|
||||
<template>
|
||||
<form role="dialog" data-type="action">
|
||||
<section>
|
||||
<h1 id="error-title" class="title"></h1>
|
||||
<span id="failed-logo"></span>
|
||||
</section>
|
||||
<menu id="actions">
|
||||
<button id="settings-btn" type="button" class="icon icon-settings" data-l10n-id="check-settings"></button>
|
||||
<span class="last-button-container">
|
||||
<button id="cancel-btn" type="button" class="cancel" data-l10n-id="cancel"></button>
|
||||
</span>
|
||||
</menu>
|
||||
</form>
|
||||
</template>
|
||||
</element>
|
||||
17
gaia/apps/system/elements/net_error_confirm_dialog.html
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
<element name="net-error-confirm-dialog" extends="section">
|
||||
<template>
|
||||
<form role="dialog" data-type="confirm">
|
||||
<section>
|
||||
<h1 id="error-title" class="title"></h1>
|
||||
<p>
|
||||
<progress class="still" id="retry-icon"></progress>
|
||||
<span id="error-message" class="message"></span>
|
||||
</p>
|
||||
</section>
|
||||
<menu id="actions">
|
||||
<button id="close-btn" type="button" class="close" data-l10n-id="close"></button>
|
||||
<button id="retry-btn" type="button" class="reload recommend" data-l10n-id="try-again"></button>
|
||||
</menu>
|
||||
</form>
|
||||
</template>
|
||||
</element>
|
||||
19
gaia/apps/system/fxa/elements/fxa-coppa.html
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
<element name="fxa-coppa" extends="section">
|
||||
<template>
|
||||
<section class="vertical">
|
||||
<h3 data-l10n-id="fxa-age-verification">
|
||||
Age verification
|
||||
</h3>
|
||||
<ul class="fxa-verification-wrapper">
|
||||
<li>
|
||||
<span class="button icon icon-dialog">
|
||||
<select id="fxa-age-select">
|
||||
<option value="0" data-l10n-id="fxa-year-of-birth">Year of birth</option>
|
||||
</select>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<script src='js/screens/fxam_coppa.js'></script>
|
||||
</template>
|
||||
</element>
|
||||
16
gaia/apps/system/fxa/elements/fxa-email.html
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
<element name="fxa-email" extends="section">
|
||||
<template>
|
||||
<section class="vertical">
|
||||
<div id="fxa-logo" class="fxa-logo"></div>
|
||||
<p class="fxa-intro" data-l10n-id="fxa-use-email-to-sign-in">
|
||||
</p>
|
||||
<input id="fxa-email-input" data-l10n-id="fxa-enter-email-input" type="email" placeholder="Enter your email" required="" x-inputmode="verbatim">
|
||||
|
||||
<p id="fxa-notice" class="fine-print" data-l10n-id="fxa-notice2">
|
||||
<a id="fxa-terms" href="https://accounts.firefox.com/legal/terms"></a>
|
||||
<a id="fxa-privacy" href="https://accounts.firefox.com/legal/privacy"></a>
|
||||
</p>
|
||||
</section>
|
||||
<script src='js/screens/fxam_enter_email.js'></script>
|
||||
</template>
|
||||
</element>
|
||||
18
gaia/apps/system/fxa/elements/fxa-enter-password.html
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
<element name="fxa-enter-password" extends="section">
|
||||
<template>
|
||||
<section class="vertical">
|
||||
<h3 data-l10n-id="fxa-enter-password-title">Enter Your Password</h3>
|
||||
<p id="fxa-hello-known-user">Hello, <a id="fxa-known-user-email">holaENTER@email.com</a></p>
|
||||
<input id="fxa-pw-input" type="password" placeholder="Password" required pattern=".{8,}" data-l10n-id="fxa-password" x-inputmode="verbatim">
|
||||
<p class="fine-print"><a id="fxa-forgot-password" data-l10n-id="fxa-forgot-your-password">Forgot your password?</a></p>
|
||||
<div id="show-pw-wrapper">
|
||||
<label class="pack-checkbox" for="fxa-show-pw">
|
||||
<input id="fxa-show-pw" type="checkbox">
|
||||
<span></span>
|
||||
</label>
|
||||
<p data-l10n-id="fxa-show-password">Show password</p>
|
||||
</div>
|
||||
<script src='js/screens/fxam_enter_password.js'></script>
|
||||
</section>
|
||||
</template>
|
||||
</element>
|
||||
16
gaia/apps/system/fxa/elements/fxa-error-overlay.html
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
<element name="fxa-error-overlay" extends="form">
|
||||
<template>
|
||||
<section>
|
||||
<h1 id="fxa-error-title"></h1>
|
||||
<p>
|
||||
<small id="fxa-error-msg"></small>
|
||||
<small id="fxa-error-msg-coppa">
|
||||
<a id="coppa-link" href="#"></a>
|
||||
</small>
|
||||
</p>
|
||||
<menu>
|
||||
<button class="full" data-l10n-id="fxa-ok" id="fxa-error-ok"></button>
|
||||
</menu>
|
||||
</section>
|
||||
</template>
|
||||
</element>
|
||||
13
gaia/apps/system/fxa/elements/fxa-pp.html
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
<element name="fxa-pp" extends="section">
|
||||
<template>
|
||||
<section role="region">
|
||||
<h3 data-l10n-id="fxa-pp-header">Privacy Policy</h3>
|
||||
<p data-l10n-id="fxa-pp1">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eius mod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequ at. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
</p>
|
||||
<p data-l10n-id="fxa-pp2">
|
||||
Excepteur sint occaecat qui of fi cia deserunt mollit anim id est lab orum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. D uis aute irure dolor in.
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
</element>
|
||||
17
gaia/apps/system/fxa/elements/fxa-refresh-auth.html
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
<element name="fxa-refresh-auth" extends="section">
|
||||
<template>
|
||||
<section class="vertical">
|
||||
<p id="fxa-password-refresh" data-l10n-id="fxa-password-refresh">Please enter your Firefox Account password to continue.</p>
|
||||
<input id="fxa-pw-input-refresh" type="password" placeholder="Password" required="" pattern=".{8,}" data-l10n-id="fxa-password">
|
||||
<p class="fine-print"><a id="fxa-forgot-password-refresh" data-l10n-id="fxa-forgot-your-password">Forgot your password?</a></p>
|
||||
<div id="show-pw-wrapper">
|
||||
<label class="pack-checkbox" for="fxa-show-pw-refresh">
|
||||
<input id="fxa-show-pw-refresh" type="checkbox">
|
||||
<span></span>
|
||||
</label>
|
||||
<p data-l10n-id="fxa-show-password">Show password</p>
|
||||
</div>
|
||||
<script src='js/screens/fxam_refresh_auth.js'></script>
|
||||
</section>
|
||||
</template>
|
||||
</element>
|
||||
19
gaia/apps/system/fxa/elements/fxa-set-password.html
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
<element name="fxa-set-password" extends="section">
|
||||
<template>
|
||||
<section class="vertical">
|
||||
<h3 data-l10n-id="fxa-create-password-title"></h3>
|
||||
<p id="fxa-hello-user"><a id="fxa-user-set-email"></a></p>
|
||||
<input id="fxa-pw-set-input" type="password" placeholder="Password" required="" pattern=".{8,}" data-l10n-id="fxa-password" x-inputmode="verbatim">
|
||||
<p class="fine-print" data-l10n-id="fxa-password-length-requirement">
|
||||
</p>
|
||||
<div id="show-pw-wrapper">
|
||||
<label class="pack-checkbox" for="fxa-show-pw-set">
|
||||
<input id="fxa-show-pw-set" type="checkbox">
|
||||
<span></span>
|
||||
</label>
|
||||
<p data-l10n-id="fxa-show-password"></p>
|
||||
</div>
|
||||
<script src='js/screens/fxam_set_password.js'></script>
|
||||
</section>
|
||||
</template>
|
||||
</element>
|
||||
19
gaia/apps/system/fxa/elements/fxa-signin-success.html
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
<element name="fxa-signin-success" extends="section">
|
||||
<template>
|
||||
<section class="vertical">
|
||||
<h3 data-l10n-id="fxa-success">
|
||||
Success!
|
||||
</h3>
|
||||
<p data-l10n-id="fxa-ready-to-go">
|
||||
Your Firefox Account is ready to go!
|
||||
</p>
|
||||
<p data-l10n-id="fxa-use-features">
|
||||
Now you can use features like Find My Device and apps like the Firefox Marketplace.
|
||||
</p>
|
||||
<p data-l10n-id="fxa-access-manage-account-settings">
|
||||
In the future, you can access and manage your account from Settings.
|
||||
</p>
|
||||
<script src='js/screens/fxam_signin_success.js'></script>
|
||||
</section>
|
||||
</template>
|
||||
</element>
|
||||
19
gaia/apps/system/fxa/elements/fxa-signup-success.html
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
<element name="fxa-signup-success" extends="section">
|
||||
<template>
|
||||
<section class="vertical">
|
||||
<h3 data-l10n-id="fxa-confirm-account">
|
||||
Confirm Your Account
|
||||
</h3>
|
||||
<p id="fxa-will-send-email" class="fxa-envelope">
|
||||
We will send an email to: <strong id="fxa-summary-email">email</strong>
|
||||
</p>
|
||||
<p data-l10n-id="fxa-signup-click-verification-link-in-email">
|
||||
Next time you check your email, click the verification link to confirm your Firefox Account.
|
||||
</p>
|
||||
<p data-l10n-id="fxa-access-manage-account-settings">
|
||||
In the future, you can access and manage your account from Settings.
|
||||
</p>
|
||||
<script src='js/screens/fxam_signup_success.js'></script>
|
||||
</section>
|
||||
</template>
|
||||
</element>
|
||||
13
gaia/apps/system/fxa/elements/fxa-tos.html
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
<element name="fxa-tos" extends="section">
|
||||
<template>
|
||||
<section role="region">
|
||||
<h3 data-l10n-id="fxa-tos-header">Terms of Service</h3>
|
||||
<p data-l10n-id="fxa-tos1">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eius mod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequ at. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
</p>
|
||||
<p data-l10n-id="fxa-tos2">
|
||||
Excepteur sint occaecat qui of fi cia deserunt mollit anim id est lab orum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. D uis aute irure dolor in.
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
</element>
|
||||
104
gaia/apps/system/fxa/fxa_module.html
Executable file
|
|
@ -0,0 +1,104 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link href="../shared/style/switches.css" rel="stylesheet" type="text/css">
|
||||
<link href="../shared/style/buttons.css" rel="stylesheet" type="text/css">
|
||||
<link href="../shared/style/confirm.css" rel="stylesheet" type="text/css">
|
||||
<link href="../shared/style/input_areas.css" rel="stylesheet" type="text/css">
|
||||
<link href="../shared/style/progress_activity.css" rel="stylesheet" type="text/css">
|
||||
<link href="style/fxa.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- Gaia Components -->
|
||||
<link href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css" rel="stylesheet" type="text/css">
|
||||
<link href="../shared/elements/gaia-icons/gaia-icons.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="locales/fxa.{locale}.properties">
|
||||
|
||||
<link rel="import" href="elements/fxa-email.html">
|
||||
<link rel="import" href="elements/fxa-set-password.html">
|
||||
<link rel="import" href="elements/fxa-signup-success.html">
|
||||
<link rel="import" href="elements/fxa-enter-password.html">
|
||||
<link rel="import" href="elements/fxa-signin-success.html">
|
||||
<link rel="import" href="elements/fxa-tos.html">
|
||||
<link rel="import" href="elements/fxa-pp.html">
|
||||
<link rel="import" href="elements/fxa-refresh-auth.html">
|
||||
<link rel="import" href="elements/fxa-coppa.html">
|
||||
|
||||
<script defer src="../js/service.js"></script>
|
||||
<script defer src="../js/browser_frame.js"></script>
|
||||
<script defer src="../js/entry_sheet.js"></script>
|
||||
<script defer src="../shared/js/lazy_loader.js"></script>
|
||||
<script defer src="../shared/js/l10n.js"></script>
|
||||
<script defer src="../shared/js/settings_helper.js"></script>
|
||||
<script defer src="../shared/js/utilities.js"></script>
|
||||
|
||||
<!-- Gaia Components -->
|
||||
<script defer src="../shared/elements/config.js"></script>
|
||||
<script defer src="../shared/elements/gaia-header/dist/gaia-header.js"></script>
|
||||
|
||||
<script defer src="js/fxam_server_request.js"></script>
|
||||
<script defer src="js/fxam_overlay.js"></script>
|
||||
<script defer src="js/fxam_error_overlay.js"></script>
|
||||
<script defer src="js/fxam_navigation.js"></script>
|
||||
<script defer src="js/fxam_ui.js"></script>
|
||||
<script defer src="js/fxam_manager.js"></script>
|
||||
<script defer src="js/fxam_states.js"></script>
|
||||
<script defer src="js/fxam_module.js"></script>
|
||||
|
||||
</head>
|
||||
<body class="theme-settings">
|
||||
<section role="region">
|
||||
<gaia-header id="fxa-module-header" action="close">
|
||||
<h1 data-l10n-id="firefox-accounts-title"></h1>
|
||||
</gaia-header>
|
||||
<div class="bb-steps">
|
||||
<progress id="fxa-progress" value="" max="100"></progress>
|
||||
</div>
|
||||
<article id="step-container">
|
||||
<section id="fxa-email" is="fxa-email" class="screen"></section>
|
||||
<section id="fxa-set-password" is="fxa-set-password" class="screen fxa-password"></section>
|
||||
<section id="fxa-enter-password" is="fxa-enter-password" class="screen fxa-password"></section>
|
||||
<section id="fxa-signup-success" is="fxa-signup-success" class="screen"></section>
|
||||
<section id="fxa-signin-success" is="fxa-signin-success" class="screen"></section>
|
||||
<section id="fxa-tos" is="fxa-tos" class="screen" data-navigation="back"></section>
|
||||
<section id="fxa-pp" is="fxa-pp" class="screen" data-navigation="back"></section>
|
||||
<section id="fxa-refresh-auth" is="fxa-refresh-auth" class="screen fxa-password"></section>
|
||||
<section id="fxa-coppa" is="fxa-coppa" class="screen fxa-coppa"></section>
|
||||
</article>
|
||||
<nav role="navigation" class="horizontal navigation navigation-single-button" id="fxa-module-navigation">
|
||||
<button id="fxa-module-back" class="start" data-l10n-id="fxa-back">
|
||||
Back
|
||||
</button>
|
||||
<button id="fxa-module-next" class="recommend end" data-l10n-id="fxa-next">
|
||||
Next
|
||||
</button>
|
||||
<button id="fxa-module-done" class="recommend end" data-l10n-id="fxa-done">
|
||||
Done
|
||||
</button>
|
||||
</nav>
|
||||
</section>
|
||||
<!-- Progress overlay -->
|
||||
<section id="fxa-overlay" role="dialog">
|
||||
<progress></progress>
|
||||
<p id="fxa-overlay-msg" data-l10n-id="fxa-connecting"></p>
|
||||
</section>
|
||||
<!-- Error message overlay -->
|
||||
<form role="dialog" data-type="confirm" id="fxa-error-overlay" class="fade-in">
|
||||
<section>
|
||||
<h1 id="fxa-error-title"></h1>
|
||||
<p>
|
||||
<small id="fxa-error-msg"></small>
|
||||
<small id="fxa-error-msg-coppa">
|
||||
<a id="coppa-link" href="#"></a>
|
||||
</small>
|
||||
</p>
|
||||
</section>
|
||||
<menu>
|
||||
<button class="full" data-l10n-id="fxa-ok" id="fxa-error-ok"></button>
|
||||
</menu>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
64
gaia/apps/system/fxa/js/fxam_error_overlay.js
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global HtmlHelper */
|
||||
/* exported FxaModuleErrorOverlay */
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* FxaModuleErrorOverlay shows an error prompt to the user with the info
|
||||
* retrieved after getting an error.
|
||||
* As params, we can define Title and Message.
|
||||
*/
|
||||
|
||||
var FxaModuleErrorOverlay = {
|
||||
init: function fxam_error_overlay_init() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
HtmlHelper.importElements(this,
|
||||
'fxa-error-overlay',
|
||||
'fxa-error-title',
|
||||
'fxa-error-msg',
|
||||
'fxa-error-msg-coppa',
|
||||
'fxa-error-ok'
|
||||
);
|
||||
|
||||
this.fxaErrorOk.addEventListener('click', this.hide.bind(this));
|
||||
this.fxaErrorOverlay.addEventListener('submit', this.prevent);
|
||||
|
||||
this.initialized = true;
|
||||
},
|
||||
|
||||
show: function fxam_error_overlay_show(titleL10n, messageL10n, resp) {
|
||||
this.init();
|
||||
|
||||
this.fxaErrorTitle.setAttribute('data-l10n-id', titleL10n);
|
||||
|
||||
if (resp && resp.error === 'COPPA_ERROR') {
|
||||
this.fxaErrorMsg.style.display = 'none';
|
||||
this.fxaErrorMsgCoppa.style.display = 'inline';
|
||||
this.fxaErrorMsgCoppa.setAttribute('data-l10n-id', messageL10n);
|
||||
} else {
|
||||
this.fxaErrorMsg.style.display = 'inline';
|
||||
this.fxaErrorMsgCoppa.style.display = 'none';
|
||||
this.fxaErrorMsg.setAttribute('data-l10n-id', messageL10n);
|
||||
}
|
||||
|
||||
|
||||
this.fxaErrorOverlay.classList.add('show');
|
||||
},
|
||||
|
||||
hide: function fxam_overlay_hide() {
|
||||
this.init();
|
||||
|
||||
this.fxaErrorOverlay.classList.remove('show');
|
||||
},
|
||||
|
||||
prevent: function fxam_prevent(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
58
gaia/apps/system/fxa/js/fxam_errors.js
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
var Errors = {
|
||||
CONNECTION_ERROR: {
|
||||
title: 'fxa-connection-error-title',
|
||||
message: 'fxa-connection-error-message'
|
||||
},
|
||||
RESET_PASSWORD_ERROR: {
|
||||
title: 'fxa-reset-password-error-title',
|
||||
message: 'fxa-reset-password-error-message'
|
||||
},
|
||||
INVALID_EMAIL: {
|
||||
title: 'fxa-invalid-email-title',
|
||||
message: 'fxa-invalid-email-message'
|
||||
},
|
||||
INVALID_PASSWORD: {
|
||||
title: 'fxa-invalid-password-title',
|
||||
message: 'fxa-invalid-password-message'
|
||||
},
|
||||
COPPA_ERROR: {
|
||||
title: 'fxa-coppa-failure-error-title',
|
||||
message: 'fxa-coppa-failure-error-message3'
|
||||
},
|
||||
COPPA_FTU_ERROR: {
|
||||
title: 'fxa-coppa-failure-error-title',
|
||||
message: 'fxa-coppa-ftu-error-message'
|
||||
},
|
||||
OFFLINE: {
|
||||
title: 'fxa-offline-error-title',
|
||||
message: 'fxa-offline-error-message'
|
||||
},
|
||||
UNKNOWN: {
|
||||
title: 'fxa-unknown-error-title',
|
||||
message: 'fxa-unknown-error-message'
|
||||
}
|
||||
};
|
||||
|
||||
function _getError(error) {
|
||||
var l10nKeys = Errors[error] || Errors.UNKNOWN;
|
||||
return {
|
||||
title: l10nKeys.title,
|
||||
message: l10nKeys.message
|
||||
};
|
||||
}
|
||||
|
||||
var FxaModuleErrors = {
|
||||
responseToParams: function fxame_responseToParams(response) {
|
||||
return response && response.error ?
|
||||
_getError(response.error) : _getError('GENERIC_ERROR');
|
||||
}
|
||||
};
|
||||
|
||||
exports.FxaModuleErrors = FxaModuleErrors;
|
||||
}(window));
|
||||
58
gaia/apps/system/fxa/js/fxam_manager.js
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global FxaModuleUI */
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* FxaModuleManager is in charge of communication with the core of
|
||||
* FxAccounts, sending the info retrieved during the process if the
|
||||
* process was succeeded.
|
||||
*/
|
||||
|
||||
var FxaModuleManager = {
|
||||
paramsRetrieved: null,
|
||||
init: function fxamm_init() {
|
||||
// Some FxA UI flows require us to get some data from the platform, like
|
||||
// the account information. This data is given by FxAccountsUI in the form
|
||||
// of URL query parameters.
|
||||
var hash = window.location.hash.split('?');
|
||||
var flow = hash[0].replace('#', '');
|
||||
if (hash.length > 1) {
|
||||
var queryParams = hash[1].split('&');
|
||||
for (var i = 0; i < queryParams.length; i++) {
|
||||
var param = queryParams[i].split('=');
|
||||
if (param.length != 2) {
|
||||
console.warning('Wrong query parameter');
|
||||
continue;
|
||||
}
|
||||
this.setParam(param[0], param[1]);
|
||||
}
|
||||
}
|
||||
FxaModuleUI.init(flow);
|
||||
},
|
||||
setParam: function fxamm_setParam(key, value) {
|
||||
if (!this.paramsRetrieved) {
|
||||
this.paramsRetrieved = {};
|
||||
}
|
||||
this.paramsRetrieved[key] = value;
|
||||
},
|
||||
done: function fxamm_done() {
|
||||
window.parent.FxAccountsUI.done(FxaModuleManager.paramsRetrieved);
|
||||
},
|
||||
close: function fxamm_close(error) {
|
||||
// If there is no params retrieved, or the flow is not complete ()
|
||||
if (!this.paramsRetrieved ||
|
||||
!this.paramsRetrieved.done) {
|
||||
window.parent.FxAccountsUI.error(error);
|
||||
return;
|
||||
}
|
||||
window.parent.FxAccountsUI.done(this.paramsRetrieved);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('load', function managerLoaded() {
|
||||
window.removeEventListener('load', managerLoaded);
|
||||
FxaModuleManager.init();
|
||||
});
|
||||
97
gaia/apps/system/fxa/js/fxam_module.js
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global HtmlHelper, FxaModuleOverlay, LazyLoader, FxaModuleErrors,
|
||||
FxaModuleErrorOverlay, BrowserFrame, EntrySheet */
|
||||
/* exported FxaModule */
|
||||
|
||||
'use strict';
|
||||
|
||||
var FxaModule = (function() {
|
||||
|
||||
var Module = {
|
||||
initialized: false,
|
||||
init: function fxam_init() {
|
||||
// override this to do initialization
|
||||
// l10n note: this method is called after mozL10n.once has fired. It is
|
||||
// safe to assume strings have loaded and mozL10n.readyState is 'complete'
|
||||
// inside this function and other Module functions.
|
||||
},
|
||||
|
||||
onNext: function fxam_onNext(gotoNextStepCallback) {
|
||||
// override this to take care of when the user clicks on the "next"
|
||||
// button. Validate any inputs, talk with the backend, do processing.
|
||||
// When complete, call gotoNextStepCallback with the next state from
|
||||
// fxam_states.js
|
||||
},
|
||||
|
||||
onBack: function fxam_onBack() {
|
||||
// handle "back" button presses.
|
||||
},
|
||||
|
||||
onDone: function fxam_onDone(doneCallback) {
|
||||
doneCallback();
|
||||
},
|
||||
|
||||
importElements: function fxam_importElements() {
|
||||
var args = [].slice.call(arguments);
|
||||
// context to import into is the first argument to importElements
|
||||
args.unshift(this);
|
||||
HtmlHelper.importElements.apply(null, args);
|
||||
},
|
||||
|
||||
showErrorResponse: function fxam_showErrorResponse(response) {
|
||||
// special case: if a network error occurs during FTE,
|
||||
// we show a slightly different error message
|
||||
var resp = response;
|
||||
if (window.location.search.indexOf('isftu') != -1 &&
|
||||
resp == 'OFFLINE_ERROR') {
|
||||
resp = 'CONNECTION_ERROR';
|
||||
}
|
||||
|
||||
FxaModuleOverlay.hide();
|
||||
LazyLoader.load('js/fxam_errors.js', () => {
|
||||
var config = FxaModuleErrors.responseToParams(resp);
|
||||
FxaModuleErrorOverlay.show(config.title, config.message, resp);
|
||||
if (resp && resp.error === 'COPPA_ERROR') {
|
||||
var link =
|
||||
FxaModuleErrorOverlay.fxaErrorMsgCoppa.querySelector('#coppa-link');
|
||||
link.addEventListener('click', this.onCopaLinkClick);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onCopaLinkClick: function(e) {
|
||||
e.preventDefault();
|
||||
if (this.entrySheet) {
|
||||
this.entrySheet.close();
|
||||
this.entrySheet = null;
|
||||
}
|
||||
var coppaUrl = 'http://www.ftc.gov/news-events/media-resources/' +
|
||||
'protecting-consumer-privacy/kids-privacy-coppa';
|
||||
|
||||
this.entrySheet = new EntrySheet(
|
||||
window.top.document.getElementById('screen'),
|
||||
'URL:' + coppaUrl,
|
||||
new BrowserFrame({
|
||||
url: coppaUrl,
|
||||
oop: true
|
||||
})
|
||||
);
|
||||
|
||||
this.entrySheet.open();
|
||||
},
|
||||
|
||||
hideErrorResponse: function fxam_hideErrorResponse() {
|
||||
var link =
|
||||
FxaModuleErrorOverlay.fxaErrorMsgCoppa.querySelector('#coppa-link');
|
||||
link.removeEventListener('click', this.onCopaLinkClick);
|
||||
FxaModuleErrorOverlay.hide();
|
||||
FxaModuleOverlay.hide();
|
||||
}
|
||||
};
|
||||
|
||||
return Module;
|
||||
|
||||
}());
|
||||
|
||||
106
gaia/apps/system/fxa/js/fxam_navigation.js
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global LazyLoader, FxaModuleUI, View, FxaModuleManager, FxaModuleStates */
|
||||
/* exported FxaModuleNavigation */
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* This code controls the navigation between modules. When tapping on
|
||||
* next/back, we load the next/previous module, loading resources and
|
||||
* updating the UI. Transitions are based on hash change.
|
||||
*/
|
||||
|
||||
var FxaModuleNavigation = {
|
||||
stepCount: 0,
|
||||
currentModule: null,
|
||||
init: function fxam_nav_init(flow) {
|
||||
// Listen on hash changes for panel changes
|
||||
window.addEventListener(
|
||||
'hashchange',
|
||||
this.hashchangeHandler.bind(this),
|
||||
false
|
||||
);
|
||||
|
||||
// Load view
|
||||
LazyLoader.load('view/view_' + flow + '.js', function loaded() {
|
||||
FxaModuleUI.setMaxSteps(View.length);
|
||||
window.location.hash = View.start.id;
|
||||
}.bind(this));
|
||||
},
|
||||
hashchangeHandler: function fxam_nav_hashchangeHandler() {
|
||||
if (!location.hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
var panel = document.querySelector(location.hash);
|
||||
if (!panel || !panel.classList.contains('screen')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.backAnim) {
|
||||
this.stepCount--;
|
||||
this.loadStep(panel, true);
|
||||
} else {
|
||||
this.stepCount++;
|
||||
this.loadStep(panel);
|
||||
}
|
||||
},
|
||||
loadStep: function fxam_nav_loadStep(panel, back) {
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
FxaModuleUI.loadScreen({
|
||||
panel: panel,
|
||||
count: this.stepCount,
|
||||
back: this.backAnim,
|
||||
onload: function() {
|
||||
this.currentModule = window[this.moduleById(panel.id)];
|
||||
this.currentModule.init &&
|
||||
this.currentModule.init(FxaModuleManager.paramsRetrieved);
|
||||
}.bind(this),
|
||||
onanimate: function() {
|
||||
this.backAnim = false;
|
||||
this.updatingStep = false;
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
back: function fxam_nav_back() {
|
||||
// Avoid multiple taps on 'back' if screen transition is not over.
|
||||
if (this.updatingStep) {
|
||||
return;
|
||||
}
|
||||
this.updatingStep = true;
|
||||
// Execute module back (if is defined)
|
||||
this.currentModule.onBack && this.currentModule.onBack();
|
||||
// Go to previous step
|
||||
this.backAnim = true;
|
||||
|
||||
window.history.back();
|
||||
},
|
||||
loadNextStep: function fxam_loadNextStep(nextStep) {
|
||||
if (!nextStep || !nextStep.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
location.hash = nextStep.id;
|
||||
},
|
||||
next: function fxam_nav_next() {
|
||||
this.currentModule.onNext(this.loadNextStep.bind(this));
|
||||
},
|
||||
moduleById: function fxam_nav_moduleById(id) {
|
||||
var moduleKey = Object.keys(FxaModuleStates).filter(function(module) {
|
||||
return FxaModuleStates[module] &&
|
||||
FxaModuleStates[module].id &&
|
||||
FxaModuleStates[module].id === id;
|
||||
}).pop();
|
||||
if (moduleKey) {
|
||||
return FxaModuleStates[moduleKey].module;
|
||||
}
|
||||
},
|
||||
done: function fxam_nav_done() {
|
||||
this.currentModule.onDone(FxaModuleManager.done);
|
||||
}
|
||||
};
|
||||
41
gaia/apps/system/fxa/js/fxam_overlay.js
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global HtmlHelper */
|
||||
/* exported FxaModuleOverlay */
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* FxaModuleOverlay shows an loading prompt to the user.
|
||||
* As params, we can only a message which will be shown with
|
||||
* a spinner of 'loading'.
|
||||
*/
|
||||
|
||||
var FxaModuleOverlay = {
|
||||
init: function fxam_overlay_init() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
HtmlHelper.importElements(this,
|
||||
'fxa-overlay',
|
||||
'fxa-overlay-msg'
|
||||
);
|
||||
|
||||
this.initialized = true;
|
||||
},
|
||||
|
||||
show: function fxam_overlay_show(l10nId) {
|
||||
this.init();
|
||||
|
||||
this.fxaOverlayMsg.setAttribute('data-l10n-id', l10nId);
|
||||
this.fxaOverlay.classList.add('show');
|
||||
},
|
||||
|
||||
hide: function fxam_overlay_hide() {
|
||||
this.init();
|
||||
|
||||
this.fxaOverlay.classList.remove('show');
|
||||
}
|
||||
};
|
||||
107
gaia/apps/system/fxa/js/fxam_server_request.js
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global SettingsHelper, FxaModuleManager, MozActivity */
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* FxModuleServerRequest wraps the functionality exposed by the server,
|
||||
* letting our code to be shielded against changes in the API of FxA.
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var pref = 'identity.fxaccounts.reset-password.url';
|
||||
var fxaSettingsHelper = SettingsHelper(pref);
|
||||
var fxaURL;
|
||||
|
||||
fxaSettingsHelper.get(function on_fxa_get_settings(url) {
|
||||
fxaURL = url;
|
||||
});
|
||||
|
||||
function _setAccountDetails(response) {
|
||||
if(response && response.user && response.user.email) {
|
||||
FxaModuleManager.setParam('done', true);
|
||||
FxaModuleManager.setParam('verified', response.user.verified);
|
||||
}
|
||||
}
|
||||
|
||||
function _ensureFxaClient(callback) {
|
||||
window.parent.LazyLoader.load('../js/fx_accounts_client.js', function() {
|
||||
callback && callback();
|
||||
});
|
||||
}
|
||||
|
||||
var FxModuleServerRequest = {
|
||||
checkEmail: function fxmsr_checkEmail(email, onsuccess, onerror) {
|
||||
_ensureFxaClient(function() {
|
||||
window.parent.FxAccountsClient.queryAccount(
|
||||
email,
|
||||
onsuccess,
|
||||
onerror);
|
||||
});
|
||||
},
|
||||
signIn: function fxmsr_signIn(email, password, onsuccess, onerror) {
|
||||
|
||||
function successHandler(response) {
|
||||
_setAccountDetails(response);
|
||||
var authenticated =
|
||||
(response && response.user && response.user.verified) || false;
|
||||
onsuccess && onsuccess({
|
||||
authenticated: authenticated
|
||||
});
|
||||
}
|
||||
|
||||
function errorHandler(response) {
|
||||
onerror && onerror(response);
|
||||
}
|
||||
|
||||
_ensureFxaClient(function signIn() {
|
||||
window.parent.FxAccountsClient.signIn(
|
||||
email,
|
||||
password,
|
||||
successHandler,
|
||||
errorHandler);
|
||||
});
|
||||
|
||||
},
|
||||
signUp: function fxmsr_signUp(email, password, onsuccess, onerror) {
|
||||
function successHandler(response) {
|
||||
_setAccountDetails(response);
|
||||
onsuccess && onsuccess(response);
|
||||
}
|
||||
|
||||
_ensureFxaClient(function signUp() {
|
||||
window.parent.FxAccountsClient.signUp(
|
||||
email,
|
||||
password,
|
||||
successHandler,
|
||||
onerror);
|
||||
});
|
||||
},
|
||||
requestPasswordReset:
|
||||
function fxmsr_requestPasswordReset(email, onsuccess, onerror) {
|
||||
var url = email ? fxaURL + '?email=' + email : fxaURL;
|
||||
var activity = new MozActivity({
|
||||
name: 'view',
|
||||
data: {
|
||||
type: 'url',
|
||||
url: url
|
||||
}
|
||||
});
|
||||
activity.onsuccess = function on_reset_success() {
|
||||
// TODO When the browser loads, it is *behind* the system app. So we
|
||||
// need to dismiss this app in order to let the user reset their
|
||||
// password.
|
||||
onsuccess && onsuccess();
|
||||
FxaModuleManager.close('DIALOG_CLOSED_BY_USER');
|
||||
};
|
||||
activity.onerror = function on_reset_error(err) {
|
||||
console.error(err);
|
||||
onerror && onerror(err);
|
||||
};
|
||||
}
|
||||
};
|
||||
exports.FxModuleServerRequest = FxModuleServerRequest;
|
||||
}(window));
|
||||
64
gaia/apps/system/fxa/js/fxam_states.js
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global FxaModuleNavigation */
|
||||
/* exported FxaModuleStates */
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* Define the states of the firefox accounts signup/signin flow.
|
||||
* The object key defines the state name, the value is the
|
||||
* URL hash of the screen to show. done is a special state that has no
|
||||
* corresponding screen.
|
||||
*/
|
||||
|
||||
var FxaModuleStates = (function() {
|
||||
return {
|
||||
ENTER_EMAIL: {
|
||||
id: 'fxa-email',
|
||||
module: 'FxaModuleEnterEmail'
|
||||
},
|
||||
SET_PASSWORD: {
|
||||
id: 'fxa-set-password',
|
||||
module: 'FxaModuleSetPassword'
|
||||
},
|
||||
ENTER_PASSWORD: {
|
||||
id: 'fxa-enter-password',
|
||||
module: 'FxaModuleEnterPassword'
|
||||
},
|
||||
SIGNUP_SUCCESS: {
|
||||
id: 'fxa-signup-success',
|
||||
module: 'FxaModuleSignupSuccess'
|
||||
},
|
||||
SIGNIN_SUCCESS: {
|
||||
id: 'fxa-signin-success',
|
||||
module: 'FxaModuleSigninSuccess'
|
||||
},
|
||||
REFRESH_AUTH: {
|
||||
id: 'fxa-refresh-auth',
|
||||
module: 'FxaModuleRefreshAuth'
|
||||
},
|
||||
COPPA: {
|
||||
id: 'fxa-coppa',
|
||||
module: 'FxaModuleCoppa'
|
||||
},
|
||||
TOS: {
|
||||
id: 'fxa-tos'
|
||||
},
|
||||
PP: {
|
||||
id: 'fxa-pp'
|
||||
},
|
||||
DONE: null,
|
||||
back: function() {
|
||||
FxaModuleNavigation.back();
|
||||
},
|
||||
setState: function setState(state) {
|
||||
if ( !(state in this) || typeof state === 'function') {
|
||||
return;
|
||||
}
|
||||
document.location.hash = state.id;
|
||||
}
|
||||
};
|
||||
}());
|
||||
|
||||
168
gaia/apps/system/fxa/js/fxam_ui.js
Executable file
|
|
@ -0,0 +1,168 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global HtmlHelper, FxaModule, FxaModuleManager, FxaModuleNavigation,
|
||||
LazyLoader */
|
||||
/* exported FxaModuleUI */
|
||||
|
||||
'use strict';
|
||||
|
||||
const OFFLINE_TIMEOUT = 30 * 1000;
|
||||
|
||||
var FxaModuleUI = {
|
||||
maxSteps: null,
|
||||
init: function(flow) {
|
||||
// Add listeners to the main elements
|
||||
HtmlHelper.importElements(this,
|
||||
'fxa-module-header',
|
||||
'fxa-module-back',
|
||||
'fxa-module-next',
|
||||
'fxa-module-navigation',
|
||||
'fxa-module-done',
|
||||
'fxa-progress'
|
||||
);
|
||||
|
||||
this.fxaModuleHeader.addEventListener('action', function() {
|
||||
FxaModuleManager.close('DIALOG_CLOSED_BY_USER');
|
||||
});
|
||||
|
||||
this.fxaModuleBack.addEventListener('mousedown', function() {
|
||||
FxaModuleNavigation.back();
|
||||
});
|
||||
|
||||
this.fxaModuleNext.addEventListener('mousedown', function() {
|
||||
FxaModuleNavigation.next();
|
||||
});
|
||||
|
||||
this.fxaModuleDone.addEventListener('click', function() {
|
||||
FxaModuleNavigation.done();
|
||||
});
|
||||
|
||||
// Give up if the network goes offline for more than 30 seconds straight.
|
||||
var offlineTimer;
|
||||
window.addEventListener('online', function onOnline(evt) {
|
||||
clearTimeout(offlineTimer);
|
||||
FxaModule.hideErrorResponse();
|
||||
});
|
||||
|
||||
window.addEventListener('offline', function onOffline(evt) {
|
||||
if (offlineTimer) { return; }
|
||||
offlineTimer = setTimeout(function onOfflineTimeout() {
|
||||
return FxaModule.showErrorResponse({
|
||||
error: 'OFFLINE'
|
||||
});
|
||||
}, OFFLINE_TIMEOUT);
|
||||
});
|
||||
|
||||
FxaModuleNavigation.init(flow);
|
||||
},
|
||||
setMaxSteps: function(num) {
|
||||
this.maxSteps = num;
|
||||
},
|
||||
increaseMaxStepsBy: function(inc) {
|
||||
this.maxSteps = this.maxSteps + inc;
|
||||
},
|
||||
decreaseMaxStepsBy: function(dec) {
|
||||
this.maxSteps = this.maxSteps - dec;
|
||||
},
|
||||
loadScreen: function(params) {
|
||||
var currentScreen = document.querySelector('.current');
|
||||
var nextScreen = params.panel;
|
||||
// Set progress width
|
||||
this.fxaProgress.style.width = (100 / this.maxSteps) + '%';
|
||||
// Lazy load current panel
|
||||
LazyLoader.load(nextScreen, function() {
|
||||
// If the panel contains any new script elements,
|
||||
// lazy load those as well.
|
||||
var scripts = [].slice.call(nextScreen.querySelectorAll('script'))
|
||||
.map(function(script) { return script.getAttribute('src'); });
|
||||
|
||||
// Once all scripts are loaded, load the modules/UI
|
||||
LazyLoader.load(scripts, function() {
|
||||
if (params.count > 1 && params.count < this.maxSteps) {
|
||||
this.fxaModuleNavigation.classList.remove('navigation-single-button');
|
||||
this.fxaModuleNavigation.classList.remove('navigation-back-only');
|
||||
|
||||
if (nextScreen.getAttribute('data-navigation') === 'back') {
|
||||
this.fxaModuleNavigation.classList.add('navigation-back-only');
|
||||
}
|
||||
} else {
|
||||
this.fxaModuleNavigation.classList.add('navigation-single-button');
|
||||
if (params.count === this.maxSteps) {
|
||||
this.fxaModuleNavigation.classList.add('navigation-done');
|
||||
}
|
||||
}
|
||||
this.setProgressBar(params.count);
|
||||
|
||||
navigator.mozL10n.once(function() {
|
||||
// fire module's init method
|
||||
params.onload && params.onload();
|
||||
|
||||
// animate it into view - TODO unclear how nextScreen could be falsy
|
||||
if (nextScreen) {
|
||||
this._animate(currentScreen,
|
||||
nextScreen,
|
||||
params.back,
|
||||
params.onanimate);
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
},
|
||||
_animate: function(from, to, back, callback) {
|
||||
if (!to) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!from) {
|
||||
to.classList.add('current');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._inTransition(from) || this._inTransition(to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
from.addEventListener('animationend', function fromAnimEnd() {
|
||||
from.removeEventListener('animationend', fromAnimEnd, false);
|
||||
from.classList.remove(back ? 'currentToNext' : 'currentToPrev');
|
||||
from.classList.remove('current');
|
||||
from.classList.remove('back');
|
||||
}, false);
|
||||
|
||||
to.addEventListener('animationend', function toAnimEnd() {
|
||||
to.removeEventListener('animationend', toAnimEnd, false);
|
||||
to.classList.remove(back ? 'prevToCurrent' : 'nextToCurrent');
|
||||
to.classList.add('current');
|
||||
callback && callback();
|
||||
}, false);
|
||||
|
||||
from.classList.add(back ? 'currentToNext' : 'currentToPrev');
|
||||
to.classList.add(back ? 'prevToCurrent' : 'nextToCurrent');
|
||||
},
|
||||
_inTransition: function(elem) {
|
||||
return elem.classList.contains('currentToNext') ||
|
||||
elem.classList.contains('currentToPrev') ||
|
||||
elem.classList.contains('nextToCurrent') ||
|
||||
elem.classList.contains('prevToCurrent') || false;
|
||||
},
|
||||
setProgressBar: function(value) {
|
||||
this.fxaProgress.value = 100 * value / this.maxSteps;
|
||||
this.fxaProgress.style.transform = 'translateX(' + 100 * (value - 1) + '%)';
|
||||
},
|
||||
setNextText: function(l10n) {
|
||||
this.fxaModuleNext.textContent = l10n;
|
||||
},
|
||||
disableNextButton: function() {
|
||||
this.fxaModuleNext.setAttribute('disabled', 'disabled');
|
||||
},
|
||||
enableNextButton: function() {
|
||||
this.fxaModuleNext.removeAttribute('disabled');
|
||||
},
|
||||
disableDoneButton: function() {
|
||||
this.fxaModuleDone.setAttribute('disabled', 'disabled');
|
||||
},
|
||||
enableDoneButton: function() {
|
||||
this.fxaModuleDone.removeAttribute('disabled');
|
||||
}
|
||||
};
|
||||
89
gaia/apps/system/fxa/js/screens/fxam_coppa.js
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This module allow the user to select her age from a date picker and checks
|
||||
* that she is old enough to create a Firefox account. Otherwise, it shows an
|
||||
* error.
|
||||
*/
|
||||
|
||||
/* global FxaModuleUI, FxaModule, FxaModuleStates, FxaModuleNavigation */
|
||||
/* exported FxaModuleCoppa */
|
||||
|
||||
var FxaModuleCoppa = (function() {
|
||||
|
||||
var MINIMUM_AGE = 13;
|
||||
|
||||
function _populateSelect(select) {
|
||||
var currentYear = new Date().getFullYear();
|
||||
var start = currentYear - MINIMUM_AGE;
|
||||
for (var year = start; year <= currentYear; year++) {
|
||||
var option = document.createElement('option');
|
||||
option.value= year;
|
||||
if (year == start) {
|
||||
option.setAttribute('data-l10n-id', 'fxa-age-earlier');
|
||||
option.setAttribute('data-l10n-args', '{ "year": "' + year + '" }');
|
||||
} else {
|
||||
option.textContent = year;
|
||||
}
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function _enableNext(age) {
|
||||
if (age === '0') {
|
||||
FxaModuleUI.disableNextButton();
|
||||
return;
|
||||
}
|
||||
FxaModuleUI.enableNextButton();
|
||||
}
|
||||
|
||||
var Module = Object.create(FxaModule);
|
||||
Module.init = function init(options) {
|
||||
this.importElements('fxa-age-select');
|
||||
|
||||
this.isFTU = !!(options && options.isftu);
|
||||
|
||||
// Initially we only have one value ("Year of birth") in the age
|
||||
// selection element.
|
||||
if (this.fxaAgeSelect.length == 1) {
|
||||
_populateSelect(this.fxaAgeSelect);
|
||||
}
|
||||
|
||||
_enableNext(this.fxaAgeSelect.value);
|
||||
|
||||
// If COPPA is part of the sign up flow, then we need to add a step to the
|
||||
// progress bar. Don't increment if the user is moving backwards, though.
|
||||
if (!FxaModuleNavigation.backAnim) {
|
||||
FxaModuleUI.increaseMaxStepsBy(1);
|
||||
}
|
||||
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fxaAgeSelect.addEventListener('change', () => {
|
||||
_enableNext(this.fxaAgeSelect.value);
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
};
|
||||
|
||||
Module.onNext = function onNext(gotoNextStepCallback) {
|
||||
if (new Date().getFullYear() - this.fxaAgeSelect.value < MINIMUM_AGE) {
|
||||
this.showErrorResponse({
|
||||
error: this.isFTU ? 'COPPA_FTU_ERROR' : 'COPPA_ERROR'
|
||||
});
|
||||
return;
|
||||
}
|
||||
gotoNextStepCallback(FxaModuleStates.SET_PASSWORD);
|
||||
};
|
||||
|
||||
Module.onBack = function onBack() {
|
||||
FxaModuleUI.decreaseMaxStepsBy(1);
|
||||
};
|
||||
|
||||
return Module;
|
||||
})();
|
||||
193
gaia/apps/system/fxa/js/screens/fxam_enter_email.js
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global FxaModuleStates, FxaModuleUI, FxaModule, FxaModuleNavigation,
|
||||
FxModuleServerRequest, FxaModuleOverlay, FxaModuleManager, EntrySheet,
|
||||
BrowserFrame */
|
||||
/* exported FxaModuleEnterEmail */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This module checks the validity of an email address, and if valid,
|
||||
* determines which screen to go next.
|
||||
*/
|
||||
|
||||
var FxaModuleEnterEmail = (function() {
|
||||
|
||||
function _isEmailValid(emailEl) {
|
||||
return emailEl && emailEl.value && emailEl.validity.valid;
|
||||
}
|
||||
|
||||
function _loadSignIn(done) {
|
||||
done(FxaModuleStates.ENTER_PASSWORD);
|
||||
}
|
||||
|
||||
function _loadSignUp(done) {
|
||||
done(FxaModuleStates.SET_PASSWORD);
|
||||
}
|
||||
|
||||
function _loadCoppa(done) {
|
||||
done(FxaModuleStates.COPPA);
|
||||
}
|
||||
|
||||
function _enableNext(emailEl) {
|
||||
if (_isEmailValid(emailEl)) {
|
||||
FxaModuleUI.enableNextButton();
|
||||
} else {
|
||||
FxaModuleUI.disableNextButton();
|
||||
}
|
||||
}
|
||||
|
||||
var Module = Object.create(FxaModule);
|
||||
Module.init = function init(options) {
|
||||
// Cache static HTML elements
|
||||
this.importElements(
|
||||
'fxa-email-input',
|
||||
'fxa-logo',
|
||||
'fxa-notice'
|
||||
);
|
||||
|
||||
// Blocks the navigation until check the condition
|
||||
_enableNext(this.fxaEmailInput);
|
||||
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fxaPrivacy = document.getElementById('fxa-privacy');
|
||||
this.fxaPrivacy.setAttribute('data-l10n-id', 'fxa-pn');
|
||||
this.fxaTerms = document.getElementById('fxa-terms');
|
||||
this.fxaTerms.setAttribute('data-l10n-id', 'fxa-tos');
|
||||
|
||||
this.isFTU = !!(options && options.isftu);
|
||||
|
||||
// Add listeners
|
||||
this.fxaEmailInput.addEventListener(
|
||||
'input',
|
||||
function onInput(event) {
|
||||
_enableNext(event.target);
|
||||
}
|
||||
);
|
||||
this.fxaEmailInput.addEventListener(
|
||||
'focus',
|
||||
function onFocus() {
|
||||
this.fxaLogo.setAttribute('hidden', true);
|
||||
}.bind(this)
|
||||
);
|
||||
this.fxaEmailInput.addEventListener(
|
||||
'blur',
|
||||
function onBlur() {
|
||||
this.fxaLogo.removeAttribute('hidden');
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this.fxaTerms.addEventListener('click', onExternalLinkClick.bind(this));
|
||||
this.fxaPrivacy.addEventListener('click', onExternalLinkClick.bind(this));
|
||||
|
||||
function onExternalLinkClick(e) {
|
||||
/*jshint validthis:true */
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!navigator.onLine) {
|
||||
return this.showErrorResponse({error: 'OFFLINE'});
|
||||
}
|
||||
var url = e.target.href;
|
||||
if (this.entrySheet) {
|
||||
this.entrySheet.close();
|
||||
this.entrySheet = null;
|
||||
}
|
||||
this.entrySheet = new EntrySheet(
|
||||
window.top.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,
|
||||
oop: true
|
||||
})
|
||||
);
|
||||
this.entrySheet.open();
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
'visibilitychange',
|
||||
onVisibilityChange.bind(this)
|
||||
);
|
||||
|
||||
function onVisibilityChange() {
|
||||
/*jshint validthis:true */
|
||||
if (document.hidden) {
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
if (this.entrySheet) {
|
||||
this.entrySheet.close();
|
||||
this.entrySheet = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('holdhome', hideEntrySheet.bind(this));
|
||||
window.addEventListener('home', hideEntrySheet.bind(this));
|
||||
window.addEventListener('activityrequesting', hideEntrySheet.bind(this));
|
||||
|
||||
function hideEntrySheet() {
|
||||
/*jshint validthis:true */
|
||||
if (this.entrySheet) {
|
||||
this.entrySheet.close();
|
||||
this.entrySheet = null;
|
||||
}
|
||||
window.removeEventListener('holdhome', hideEntrySheet);
|
||||
window.removeEventListener('home', hideEntrySheet);
|
||||
window.removeEventListener('activityrequesting', hideEntrySheet);
|
||||
}
|
||||
|
||||
// Ensure that pressing 'ENTER' (keycode 13) we send the form
|
||||
// as expected
|
||||
this.fxaEmailInput.addEventListener(
|
||||
'keypress',
|
||||
function onKeypress(event) {
|
||||
if (_isEmailValid(this.fxaEmailInput) && event.keyCode === 13) {
|
||||
document.activeElement.blur();
|
||||
FxaModuleNavigation.next();
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
// Avoid to add listener twice
|
||||
this.initialized = true;
|
||||
};
|
||||
|
||||
Module.onNext = function onNext(gotoNextStepCallback) {
|
||||
FxaModuleOverlay.show('fxa-connecting');
|
||||
|
||||
var email = this.fxaEmailInput.value;
|
||||
|
||||
FxModuleServerRequest.checkEmail(
|
||||
email,
|
||||
function onSuccess(response) {
|
||||
FxaModuleOverlay.hide();
|
||||
FxaModuleManager.setParam('email', email);
|
||||
if (response && response.registered) {
|
||||
_loadSignIn(gotoNextStepCallback);
|
||||
} else if (this.isFTU) {
|
||||
// XXX Skip COPPA verification during FTU: if a child has a mobile
|
||||
// device, we assume a parent/guardian has given it to them, which
|
||||
// implies parental consent. So, we skip to the next step in the
|
||||
// signup flow, the set-password screen. See also bug 1010598.
|
||||
_loadSignUp(gotoNextStepCallback);
|
||||
} else {
|
||||
_loadCoppa(gotoNextStepCallback);
|
||||
}
|
||||
}.bind(this),
|
||||
this.showErrorResponse);
|
||||
};
|
||||
|
||||
Module.onBack = function onBack() {
|
||||
FxaModuleUI.enableNextButton();
|
||||
};
|
||||
|
||||
return Module;
|
||||
|
||||
}());
|
||||
|
||||
166
gaia/apps/system/fxa/js/screens/fxam_enter_password.js
Executable file
|
|
@ -0,0 +1,166 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global FxaModuleStates, FxaModuleUI, FxaModule, FxaModuleNavigation,
|
||||
FxModuleServerRequest, FxaModuleOverlay, FxaModuleManager */
|
||||
/* exported FxaModuleEnterPassword */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This module checks the validity of password given email address, and if
|
||||
* valid, determine which screen to go next.
|
||||
*/
|
||||
|
||||
var FxaModuleEnterPassword = (function() {
|
||||
|
||||
function _isPasswordValid(passwordEl) {
|
||||
var passwordValue = passwordEl.value;
|
||||
return passwordValue && passwordEl.validity.valid;
|
||||
}
|
||||
|
||||
function _enableNext(passwordEl) {
|
||||
if (_isPasswordValid(passwordEl)) {
|
||||
FxaModuleUI.enableNextButton();
|
||||
} else {
|
||||
FxaModuleUI.disableNextButton();
|
||||
}
|
||||
}
|
||||
|
||||
function _cleanForm(passwordEl, passwordCheck) {
|
||||
passwordEl.value = '';
|
||||
passwordCheck.checked = false;
|
||||
passwordEl.setAttribute('type', 'password');
|
||||
}
|
||||
|
||||
function _loadSigninSuccess(done) {
|
||||
done(FxaModuleStates.SIGNIN_SUCCESS);
|
||||
}
|
||||
|
||||
function _notVerifiedUser(done) {
|
||||
done(FxaModuleStates.SIGNUP_SUCCESS);
|
||||
}
|
||||
|
||||
function _togglePasswordVisibility() {
|
||||
/*jshint validthis:true*/
|
||||
var passwordFieldType = !!this.fxaShowPw.checked ? 'text' : 'password';
|
||||
this.fxaPwInput.setAttribute('type', passwordFieldType);
|
||||
}
|
||||
|
||||
function _forgotPassword() {
|
||||
/*jshint validthis:true*/
|
||||
var self = this;
|
||||
if (this.isFTU) {
|
||||
return this.showErrorResponse({
|
||||
error: 'RESET_PASSWORD_ERROR'
|
||||
});
|
||||
}
|
||||
// Note: we don't need to pass a success callback, but we do need an errback
|
||||
FxModuleServerRequest.requestPasswordReset(
|
||||
this.email,
|
||||
null,
|
||||
function on_reset_error() {
|
||||
self.showErrorResponse({
|
||||
error: 'UNKNOWN_ERROR'
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var Module = Object.create(FxaModule);
|
||||
Module.init = function init(options) {
|
||||
|
||||
if (!this.initialized) {
|
||||
// Cache DOM elements
|
||||
this.importElements(
|
||||
'fxa-hello-known-user',
|
||||
'fxa-pw-input',
|
||||
'fxa-show-pw',
|
||||
'fxa-forgot-password'
|
||||
);
|
||||
// Add listeners
|
||||
this.fxaPwInput.addEventListener(
|
||||
'input',
|
||||
function onInput(event) {
|
||||
_enableNext(event.target);
|
||||
}
|
||||
);
|
||||
// Ensure that pressing 'ENTER' (keycode 13) we send the form
|
||||
// as expected
|
||||
this.fxaPwInput.addEventListener(
|
||||
'keypress',
|
||||
function onKeypress(event) {
|
||||
if (_isPasswordValid(this.fxaPwInput) && event.keyCode === 13) {
|
||||
document.activeElement.blur();
|
||||
FxaModuleNavigation.next();
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this.fxaShowPw.addEventListener(
|
||||
'change',
|
||||
_togglePasswordVisibility.bind(this),
|
||||
false
|
||||
);
|
||||
|
||||
this.fxaForgotPassword.addEventListener(
|
||||
'click',
|
||||
_forgotPassword.bind(this),
|
||||
false
|
||||
);
|
||||
// Avoid repeated initialization
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
if (!options || !options.email) {
|
||||
console.error('Options are not sent properly. Email not available');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isFTU = !!(options && options.isftu);
|
||||
this.email = options.email;
|
||||
|
||||
navigator.mozL10n.setAttributes(this.fxaHelloKnownUser, 'fxa-hello-user2', {
|
||||
email: this.email
|
||||
});
|
||||
|
||||
_cleanForm(
|
||||
this.fxaPwInput,
|
||||
this.fxaShowPw
|
||||
);
|
||||
|
||||
_enableNext(this.fxaPwInput);
|
||||
|
||||
};
|
||||
|
||||
Module.onNext = function onNext(gotoNextStepCallback) {
|
||||
FxaModuleOverlay.show('fxa-connecting');
|
||||
|
||||
FxaModuleManager.setParam('success', true);
|
||||
FxModuleServerRequest.signIn(
|
||||
this.email,
|
||||
this.fxaPwInput.value,
|
||||
function onServerResponse(response) {
|
||||
FxaModuleOverlay.hide();
|
||||
|
||||
if (!response.authenticated) {
|
||||
_notVerifiedUser(gotoNextStepCallback);
|
||||
return;
|
||||
}
|
||||
|
||||
_loadSigninSuccess(gotoNextStepCallback);
|
||||
}.bind(this),
|
||||
function onError(response) {
|
||||
_cleanForm(
|
||||
this.fxaPwInput,
|
||||
this.fxaShowPw
|
||||
);
|
||||
this.showErrorResponse(response);
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
return Module;
|
||||
|
||||
}());
|
||||
|
||||
121
gaia/apps/system/fxa/js/screens/fxam_refresh_auth.js
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global FxaModuleStates, FxaModuleUI, FxaModule, FxModuleServerRequest,
|
||||
FxaModuleOverlay */
|
||||
/* exported FxaModuleRefreshAuth */
|
||||
/* jshint unused:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var FxaModuleRefreshAuth = (function() {
|
||||
|
||||
function _isPasswordValid(passwordEl) {
|
||||
var passwordValue = passwordEl.value;
|
||||
return passwordValue && passwordEl.validity.valid;
|
||||
}
|
||||
|
||||
function _enableDone(passwordEl) {
|
||||
if (_isPasswordValid(passwordEl)) {
|
||||
FxaModuleUI.enableDoneButton();
|
||||
} else {
|
||||
FxaModuleUI.disableDoneButton();
|
||||
}
|
||||
}
|
||||
|
||||
function _cleanForm(passwordEl, passwordCheck) {
|
||||
passwordEl.value = '';
|
||||
passwordCheck.checked = false;
|
||||
passwordEl.setAttribute('type', 'password');
|
||||
FxaModuleUI.disableDoneButton();
|
||||
}
|
||||
|
||||
function _requestPasswordReset(email, done) {
|
||||
/*jshint validthis:true */
|
||||
FxModuleServerRequest.requestPasswordReset(
|
||||
email,
|
||||
function onSuccess(response) {
|
||||
done();
|
||||
},
|
||||
function onError(response) {
|
||||
this._showCouldNotResetPassword();
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
function _showCouldNotResetPassword() {
|
||||
/*jshint validthis:true */
|
||||
this.showErrorResponse({
|
||||
error: 'RESET_PASSWORD_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
function _forgotPassword() {
|
||||
/*jshint validthis:true */
|
||||
FxaModuleOverlay.show('fxa-requesting-password-reset');
|
||||
_requestPasswordReset.call(
|
||||
this,
|
||||
this.email,
|
||||
function() {
|
||||
FxaModuleOverlay.hide();
|
||||
FxaModuleStates.setState(FxaModuleStates.PASSWORD_RESET_SUCCESS);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var Module = Object.create(FxaModule);
|
||||
Module.init = function init(options) {
|
||||
if (!this.initialized) {
|
||||
// Cache DOM elements.
|
||||
this.importElements(
|
||||
'fxa-pw-input-refresh',
|
||||
'fxa-show-pw-refresh',
|
||||
'fxa-forgot-password-refresh'
|
||||
);
|
||||
|
||||
this.fxaPwInputRefresh.addEventListener('input', (function(event) {
|
||||
_enableDone(event.target);
|
||||
}).bind(this));
|
||||
|
||||
this.fxaShowPwRefresh.addEventListener('change', (function() {
|
||||
var passwordFieldType = !!this.fxaShowPwRefresh.checked ? 'text' :
|
||||
'password';
|
||||
}).bind(this), false);
|
||||
|
||||
this.fxaForgotPasswordRefresh.addEventListener('click',
|
||||
_forgotPassword.bind(this),
|
||||
false);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
if (!options || !options.email) {
|
||||
console.error('Options are not sent properly. Email not available');
|
||||
return;
|
||||
}
|
||||
|
||||
this.email = options.email;
|
||||
|
||||
_cleanForm(this.fxaPwInputRefresh, this.fxaShowPwRefresh);
|
||||
_enableDone(this.fxaPwInputRefresh);
|
||||
};
|
||||
|
||||
Module.onDone = function onDone(callback) {
|
||||
FxaModuleOverlay.show('fxa-connecting');
|
||||
|
||||
FxModuleServerRequest.signIn(
|
||||
this.email,
|
||||
this.fxaPwInputRefresh.value,
|
||||
function onServerResponse(response) {
|
||||
FxaModuleOverlay.hide();
|
||||
callback();
|
||||
}, function onError(response) {
|
||||
_cleanForm(this.fxaPwInputRefresh,
|
||||
this.fxaShowPwRefresh);
|
||||
this.showErrorResponse(response);
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
return Module;
|
||||
})();
|
||||
123
gaia/apps/system/fxa/js/screens/fxam_set_password.js
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global FxaModuleUI, FxaModuleOverlay, FxaModule, FxModuleServerRequest,
|
||||
FxaModuleManager, FxaModuleStates */
|
||||
/* exported FxaModuleSetPassword */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Takes care of a new user's set password screen. If password is valid,
|
||||
* attempt to stage the user.
|
||||
*/
|
||||
var FxaModuleSetPassword = (function() {
|
||||
|
||||
function _isPasswordValid(passwordEl) {
|
||||
var passwordValue = passwordEl.value;
|
||||
return passwordValue && passwordEl.validity.valid;
|
||||
}
|
||||
|
||||
function _enableNext(passwordEl) {
|
||||
if (_isPasswordValid(passwordEl)) {
|
||||
FxaModuleUI.enableNextButton();
|
||||
} else {
|
||||
FxaModuleUI.disableNextButton();
|
||||
}
|
||||
}
|
||||
|
||||
function _cleanForm(passwordEl, passwordCheck) {
|
||||
passwordEl.value = '';
|
||||
passwordCheck.checked = false;
|
||||
passwordEl.setAttribute('type', 'password');
|
||||
}
|
||||
|
||||
function _showRegistering() {
|
||||
FxaModuleOverlay.show('fxa-connecting');
|
||||
}
|
||||
|
||||
function _hideRegistering() {
|
||||
FxaModuleOverlay.hide();
|
||||
}
|
||||
|
||||
function _showUserNotCreated() {
|
||||
/*jshint validthis:true */
|
||||
this.showErrorResponse({
|
||||
error: 'CANNOT_CREATE_ACCOUNT'
|
||||
});
|
||||
}
|
||||
|
||||
function _togglePasswordVisibility() {
|
||||
/*jshint validthis:true */
|
||||
var passwordFieldType = !!this.fxaShowPwSet.checked ? 'text' : 'password';
|
||||
this.fxaPwSetInput.setAttribute('type', passwordFieldType);
|
||||
}
|
||||
|
||||
var Module = Object.create(FxaModule);
|
||||
Module.init = function init(options) {
|
||||
|
||||
if (!this.initialized) {
|
||||
// Cache DOM elements
|
||||
this.importElements(
|
||||
'fxa-pw-set-input',
|
||||
'fxa-show-pw-set',
|
||||
'fxa-hello-user'
|
||||
);
|
||||
// Add Listeners
|
||||
this.fxaPwSetInput.addEventListener(
|
||||
'input',
|
||||
function onInput(event) {
|
||||
_enableNext(event.target);
|
||||
}
|
||||
);
|
||||
|
||||
this.fxaShowPwSet.addEventListener(
|
||||
'change',
|
||||
_togglePasswordVisibility.bind(this),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
this.email = options.email;
|
||||
|
||||
navigator.mozL10n.setAttributes(this.fxaHelloUser, 'fxa-hello-user2', {
|
||||
email: this.email
|
||||
});
|
||||
|
||||
_cleanForm(
|
||||
this.fxaPwSetInput,
|
||||
this.fxaShowPwSet
|
||||
);
|
||||
|
||||
_enableNext(this.fxaPwSetInput);
|
||||
};
|
||||
|
||||
Module.onNext = function onNext(gotoNextStepCallback) {
|
||||
_showRegistering();
|
||||
FxModuleServerRequest.signUp(
|
||||
this.email,
|
||||
this.fxaPwSetInput.value,
|
||||
function(response) {
|
||||
_hideRegistering();
|
||||
|
||||
FxaModuleManager.setParam('success', true);
|
||||
|
||||
var isAccountCreated = response.accountCreated;
|
||||
|
||||
if (!isAccountCreated) {
|
||||
_showUserNotCreated.call(this);
|
||||
return;
|
||||
}
|
||||
|
||||
gotoNextStepCallback(FxaModuleStates.SIGNUP_SUCCESS);
|
||||
}.bind(this),
|
||||
this.showErrorResponse
|
||||
);
|
||||
};
|
||||
|
||||
return Module;
|
||||
|
||||
}());
|
||||
|
||||
23
gaia/apps/system/fxa/js/screens/fxam_signin_success.js
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global FxaModule, FxaModuleStates */
|
||||
/* exported FxaModuleSigninSuccess */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Display the signup success message to the user.
|
||||
*/
|
||||
var FxaModuleSigninSuccess = (function() {
|
||||
|
||||
var Module = Object.create(FxaModule);
|
||||
|
||||
Module.onNext = function onNext(done) {
|
||||
done(FxaModuleStates.DONE);
|
||||
};
|
||||
|
||||
return Module;
|
||||
|
||||
}());
|
||||
|
||||
34
gaia/apps/system/fxa/js/screens/fxam_signup_success.js
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/* global FxaModule, FxaModuleStates */
|
||||
/* exported FxaModuleSignupSuccess */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Display the signup success message to the user.
|
||||
*/
|
||||
var FxaModuleSignupSuccess = (function() {
|
||||
|
||||
var Module = Object.create(FxaModule);
|
||||
Module.init = function init(options) {
|
||||
options = options || {};
|
||||
this.importElements(
|
||||
'fxa-will-send-email'
|
||||
);
|
||||
|
||||
navigator.mozL10n.setAttributes(this.fxaWillSendEmail,
|
||||
'fxa-will-send-email2', {
|
||||
email: options.email
|
||||
});
|
||||
};
|
||||
|
||||
Module.onNext = function onNext(done) {
|
||||
done(FxaModuleStates.DONE);
|
||||
};
|
||||
|
||||
return Module;
|
||||
|
||||
}());
|
||||
|
||||
103
gaia/apps/system/fxa/locales/fxa.en-US.properties
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
|
||||
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
# Navigation
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
|
||||
firefox-accounts-title = Firefox Accounts
|
||||
fxa-next = Next
|
||||
fxa-back = Back
|
||||
fxa-done = Done
|
||||
fxa-ok = OK
|
||||
|
||||
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
# Errors
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
# 'spec' references below refer to the spec attached to bug 960130
|
||||
|
||||
# this is a real error screen (7.2 in spec)
|
||||
fxa-connection-error-title = Unable to Connect
|
||||
fxa-connection-error-message = You can create an account or sign in from Settings.
|
||||
|
||||
# this is a real error screen (5.4 in spec)
|
||||
fxa-reset-password-error-title = Reset Password
|
||||
fxa-reset-password-error-message = You can reset your password from Settings.
|
||||
|
||||
# this is a real error screen (19.4 in spec)
|
||||
fxa-coppa-failure-error-title = Cannot Create Account
|
||||
fxa-coppa-failure-error-message3 = You must meet certain age requirements to create a Firefox Account. <a>Learn more</a>
|
||||
|
||||
# this is a real error screen (13.2 and 17.2 in spec)
|
||||
fxa-offline-error-title = Unable to Connect
|
||||
fxa-offline-error-message = Please connect to a network from Settings.
|
||||
|
||||
# this is a real error screen (3.2 in spec)
|
||||
fxa-invalid-email-title = Error
|
||||
fxa-invalid-email-message = Please enter a valid email.
|
||||
|
||||
# this is a real error screen (5.2 / 11.1 in spec)
|
||||
fxa-invalid-password-title = Invalid Password
|
||||
fxa-invalid-password-message = Please check your password and try again.
|
||||
|
||||
# this is the fallback error
|
||||
fxa-unknown-error-title = Unable to Connect
|
||||
fxa-unknown-error-message = System unavailable, try again soon.
|
||||
|
||||
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
# Interstitial screen
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
fxa-connecting = Connecting
|
||||
|
||||
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
# Enter Email
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
fxa-enter-email-input.placeholder = Enter your email
|
||||
fxa-use-email-to-sign-in = Use your email address to create an account or sign in.
|
||||
fxa-notice2 = By proceeding, I agree to the <a>{{tos}}</a> and <a>{{pn}}</a> of Firefox cloud services.
|
||||
# LOCALIZATION NOTE(fxa-tos): This string corresponds to the {{tos}} placeholder
|
||||
# in fxa-notice.
|
||||
fxa-tos = Terms of Service
|
||||
# LOCALIZATION NOTE(fxa-pn): This string corresponds to the {{pn}} placeholder
|
||||
# in fxa-notice.
|
||||
fxa-pn = Privacy Notice
|
||||
|
||||
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
# Set & Enter Password
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
fxa-create-password-title = Create a Password
|
||||
fxa-enter-password-title = Enter Your Password
|
||||
fxa-hello-user2 = Hello, <a>{{email}}</a>
|
||||
fxa-password-refresh = Please enter your Firefox Account password to continue.
|
||||
fxa-password.placeholder = Password
|
||||
fxa-forgot-your-password = Forgot your password?
|
||||
fxa-show-password = Show password
|
||||
fxa-password-length-requirement = Your password must be at least 8 characters long.
|
||||
|
||||
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
# Signin success
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
fxa-success = Success!
|
||||
fxa-ready-to-go = Your Firefox Account is ready to go!
|
||||
fxa-use-features = Now you can use features like Find My Device and apps like the Firefox Marketplace.
|
||||
fxa-access-manage-account-settings = In the future, you can access and manage your account from Settings.
|
||||
|
||||
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
# Signup success
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
fxa-confirm-account = Confirm Your Account
|
||||
fxa-will-send-email2 = We will send an email to: <strong>{{email}}</strong>
|
||||
fxa-signup-click-verification-link-in-email = Next time you check your email, click the verification link to confirm your Firefox Account.
|
||||
|
||||
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
# COPPA
|
||||
#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#
|
||||
fxa-age-verification = Age Verification
|
||||
fxa-age-earlier = {{year}} or earlier
|
||||
fxa-year-of-birth = Year of Birth
|
||||
374
gaia/apps/system/fxa/style/fxa.css
Executable file
|
|
@ -0,0 +1,374 @@
|
|||
/*
|
||||
* Basic and shared styles of the structure
|
||||
* */
|
||||
html, body {
|
||||
font-size: 62.5%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
color: #000;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
html, body * {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
input {
|
||||
-moz-user-select: text;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section[role="region"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body {
|
||||
background: url(bg_light.png);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.7rem;
|
||||
font-weight: 400;
|
||||
line-height: 2.3rem;
|
||||
margin: 0 0 1rem;
|
||||
text-align: start;
|
||||
}
|
||||
h3:-moz-dir(rtl) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
label {
|
||||
width:100%;
|
||||
display:block;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Navigation bar based on a flex model
|
||||
* * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
.navigation {
|
||||
position: absolute;
|
||||
background: #f4f4f4;
|
||||
border-top: 0.1rem solid #a6a6a6;
|
||||
padding: .75rem 1.5rem;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
right:0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Overwrite this property from Building Blocks */
|
||||
html[dir="ltr"] .navigation button { margin-left: 0.5rem; }
|
||||
html[dir="rtl"] .navigation button { margin-right: 0.5rem; }
|
||||
|
||||
html[dir="ltr"] .navigation button.start { margin-left: 0.5rem; }
|
||||
html[dir="rtl"] .navigation button.start { margin-right: 0.5rem; }
|
||||
|
||||
.navigation button.start,
|
||||
.navigation button.end {
|
||||
width: calc((100% / 2) - 0.5rem);
|
||||
position: relative;
|
||||
}
|
||||
html[dir="ltr"] .navigation button.start,
|
||||
html[dir="ltr"] .navigation button.end { float: left; }
|
||||
html[dir="rtl"] .navigation button.start,
|
||||
html[dir="rtl"] .navigation button.end { float: right; }
|
||||
|
||||
/* When there is only one option, we remove the margin */
|
||||
|
||||
.navigation-single-button button.start {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navigation.navigation-single-button button.end {
|
||||
margin: 0;
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.navigation-back-only button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navigation-back-only button.end {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navigation-done #fxa-module-back,
|
||||
.navigation-done #fxa-module-next {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#fxa-module-done {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navigation-done #fxa-module-done {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Steps & Container
|
||||
* All steps are contained in several views inside
|
||||
* 'step-container'. We will use HTML 'import' polyfill
|
||||
* and lazy load of the views. A view is a set of steps.
|
||||
* * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
.bb-steps {
|
||||
margin: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
#step-container {
|
||||
height: calc(100% - 6.3rem - 5rem);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ff_account,
|
||||
.screen {
|
||||
font-family: sans-serif;
|
||||
font-size: 1.7rem;
|
||||
line-height:2.3rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
overflow: scroll;
|
||||
display: none;
|
||||
padding: 1.5rem 2.5rem;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screen p.fxa-intro {
|
||||
text-align: start;
|
||||
line-height: 2.3rem;
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
.screen p {
|
||||
text-align: start;
|
||||
line-height: 1.9rem;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.current {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Intro screen styles
|
||||
*/
|
||||
|
||||
#fxa-hello-user {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#fxa-user-email,
|
||||
#fxa-user-set-email {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p.fine-print {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* Verification Screen Styles
|
||||
*/
|
||||
|
||||
ul.fxa-verification-wrapper {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.fxa-verification-wrapper li {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Password screen styles
|
||||
*/
|
||||
|
||||
#show-pw-wrapper {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 0 -1.5rem;
|
||||
}
|
||||
|
||||
#show-pw-wrapper p {
|
||||
display: block;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
html[dir="ltr"] #show-pw-wrapper p { float: left; }
|
||||
html[dir="rtl"] #show-pw-wrapper p { float: right; }
|
||||
|
||||
html[dir="ltr"] #show-pw-wrapper > label { float: left; }
|
||||
html[dir="rtl"] #show-pw-wrapper > label { float: right; }
|
||||
|
||||
/* Animate next screen */
|
||||
.currentToPrev {
|
||||
display:block;
|
||||
}
|
||||
html[dir="ltr"] .currentToPrev {
|
||||
animation: currentToLeft var(--transition-duration) forwards;
|
||||
}
|
||||
html[dir="rtl"] .currentToPrev {
|
||||
animation: currentToRight var(--transition-duration) forwards;
|
||||
}
|
||||
|
||||
.nextToCurrent {
|
||||
display:block;
|
||||
}
|
||||
html[dir="ltr"] .nextToCurrent {
|
||||
animation: rightToCurrent var(--transition-duration) forwards;
|
||||
}
|
||||
html[dir="rtl"] .nextToCurrent {
|
||||
animation: leftToCurrent var(--transition-duration) forwards;
|
||||
}
|
||||
|
||||
/* Animate prev screen */
|
||||
.prevToCurrent {
|
||||
display: block;
|
||||
}
|
||||
html[dir="ltr"] .prevToCurrent {
|
||||
animation: leftToCurrent var(--transition-duration) forwards;
|
||||
}
|
||||
html[dir="rtl"] .prevToCurrent {
|
||||
animation: rightToCurrent var(--transition-duration) forwards;
|
||||
}
|
||||
|
||||
.currentToNext {
|
||||
display: block;
|
||||
}
|
||||
html[dir="ltr"] .currentToNext {
|
||||
animation: currentToRight var(--transition-duration) forwards;
|
||||
}
|
||||
html[dir="rtl"] .currentToNext {
|
||||
animation: currentToLeft var(--transition-duration) forwards;
|
||||
}
|
||||
|
||||
@keyframes currentToLeft {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
@keyframes rightToCurrent {
|
||||
0% { transform: translateX(100%); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
@keyframes leftToCurrent {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
@keyframes currentToRight {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.current {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.screen a {
|
||||
text-decoration: underline;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.fxa-logo {
|
||||
background-image: url('fxa_logo.png');
|
||||
background-size: 14rem auto;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0 auto;
|
||||
width: 14rem;
|
||||
height: 15rem;
|
||||
}
|
||||
|
||||
p.fxa-envelope {
|
||||
background-image: url('fxa_envelope.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 7rem;
|
||||
padding-inline-start: 8rem;
|
||||
padding-top: 1rem;
|
||||
line-height: 2rem;
|
||||
min-height: 6.4rem;
|
||||
max-width:100%;
|
||||
overflow: hidden; /* short email addresses will only take up one line*/
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
html[dir="ltr"] p.fxa-envelope { background-position: left top; }
|
||||
html[dir="rtl"] p.fxa-envelope { background-position: right top; }
|
||||
|
||||
#fxa-error-overlay {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Overlay:
|
||||
* IMPORTANT: Avoid to use 'visibility' or 'opacity' due
|
||||
* to performance issues. For hiding, use 'display:none;''
|
||||
* * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#fxa-overlay {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
font-size: 2rem;
|
||||
background-color: rgba(200,200,200,0.9);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#fxa-overlay.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
form[role=dialog] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
form[role=dialog].show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Classes to ensure functionality
|
||||
*/
|
||||
|
||||
a.disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button.icon-dialog select {
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
BIN
gaia/apps/system/fxa/style/fxa_envelope.png
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
gaia/apps/system/fxa/style/fxa_envelope@1.5x.png
Executable file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
gaia/apps/system/fxa/style/fxa_envelope@2.25x.png
Executable file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
gaia/apps/system/fxa/style/fxa_envelope@2x.png
Executable file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
gaia/apps/system/fxa/style/fxa_logo.png
Executable file
|
After Width: | Height: | Size: 31 KiB |
BIN
gaia/apps/system/fxa/style/fxa_logo@1.5x.png
Executable file
|
After Width: | Height: | Size: 56 KiB |
BIN
gaia/apps/system/fxa/style/fxa_logo@2.25x.png
Executable file
|
After Width: | Height: | Size: 155 KiB |
BIN
gaia/apps/system/fxa/style/fxa_logo@2x.png
Executable file
|
After Width: | Height: | Size: 86 KiB |
18
gaia/apps/system/fxa/test/marionette/.jshintrc
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "../../../../../.jshintrc",
|
||||
"node": true,
|
||||
"predef": [
|
||||
"suite",
|
||||
"test",
|
||||
"setup",
|
||||
"teardown",
|
||||
"suiteSetup",
|
||||
"suiteTeardown",
|
||||
"marionette",
|
||||
"marionetteScriptFinished",
|
||||
"require",
|
||||
"define",
|
||||
"requireApp",
|
||||
"sinon"
|
||||
]
|
||||
}
|
||||
55
gaia/apps/system/fxa/test/marionette/fxa_launch_test.js
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
'use strict';
|
||||
|
||||
var FxA = require('./lib/fxa');
|
||||
|
||||
marionette('Firefox Accounts Launch Tests', function() {
|
||||
var app,
|
||||
selectors,
|
||||
client = marionette.client({
|
||||
profile: {
|
||||
prefs: {
|
||||
'focusmanager.testmode': true
|
||||
},
|
||||
settings: {
|
||||
'ftu.manifestURL': FxA.FTU_ORIGIN + '/manifest.webapp'
|
||||
},
|
||||
apps: {
|
||||
'test-fxa-client.gaiamobile.org': __dirname + '/test-fxa-client'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
app = new FxA(client);
|
||||
selectors = FxA.Selectors;
|
||||
});
|
||||
|
||||
//If we can enter email on first screen, that should prove successful launch
|
||||
suite('Should launch FxA flow from FxA-consuming apps:', function () {
|
||||
test('Settings app', function () {
|
||||
app.launch(FxA.SETTINGS_ORIGIN);
|
||||
app.runSettingsMenu();
|
||||
app.enterEmailNew();
|
||||
});
|
||||
|
||||
test('UITest app', function () {
|
||||
app.launch(FxA.UITEST_ORIGIN);
|
||||
app.runUITestMenu();
|
||||
app.enterEmailNew();
|
||||
});
|
||||
|
||||
test('FTU menu', function () {
|
||||
app.launch(FxA.FTU_ORIGIN);
|
||||
app.runFTUMenu();
|
||||
app.enterEmailNew();
|
||||
});
|
||||
|
||||
// This is an old version which uses the IAC API.
|
||||
// TODO: INTERMITTENT failures?
|
||||
test.skip('test-fxa-client app', function () {
|
||||
app.launch(FxA.TEST_FXA_CLIENT_ORIGIN);
|
||||
app.runFxAClientTestMenu();
|
||||
app.enterEmailNew();
|
||||
});
|
||||
});
|
||||
});
|
||||
62
gaia/apps/system/fxa/test/marionette/fxa_screen_flow_test.js
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
'use strict';
|
||||
|
||||
var FxA = require('./lib/fxa'),
|
||||
FxAUser = require('./lib/fxa_user'),
|
||||
Server = require('./lib/server'),
|
||||
config = require('./lib/config.json');
|
||||
|
||||
marionette('Firefox Accounts Screen Flow Test (UITest app)', function() {
|
||||
var app,
|
||||
selectors,
|
||||
server,
|
||||
fxaUser,
|
||||
client = marionette.client({
|
||||
profile: {
|
||||
prefs: {
|
||||
'focusmanager.testmode': true,
|
||||
'identity.fxaccounts.auth.uri': 'http://' +
|
||||
config.SERVER_HOST + ':' +
|
||||
config.SERVER_PORT + '/' +
|
||||
config.SERVER_PATH
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setup(function(done) {
|
||||
fxaUser = new FxAUser();
|
||||
app = new FxA(client);
|
||||
Server.create(FxA.SERVER_ARGS, function (err, _server) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
server = _server;
|
||||
done();
|
||||
});
|
||||
selectors = FxA.Selectors;
|
||||
app.launch(FxA.UITEST_ORIGIN);
|
||||
app.runUITestMenu();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
server.stop();
|
||||
});
|
||||
|
||||
test('should walk screen flow for new user', function () {
|
||||
app.enterEmailNew();
|
||||
app.clickNext();
|
||||
app.enterCOPPANew();
|
||||
app.clickNext();
|
||||
app.enterPasswordNew();
|
||||
app.clickNext();
|
||||
app.clickDone();
|
||||
});
|
||||
|
||||
test('should walk screen flow for existing user', function () {
|
||||
app.enterEmailExisting();
|
||||
app.clickNext();
|
||||
app.enterPasswordExisting();
|
||||
app.clickNext();
|
||||
app.clickDone();
|
||||
});
|
||||
});
|
||||
17
gaia/apps/system/fxa/test/marionette/lib/config.json
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"MARIONETTE_TIMEOUT": 20000,
|
||||
"USER_NEW": 0,
|
||||
"USER_EXISTING": 1,
|
||||
"USER_EXISTING_EMAIL": "kilroy_exists",
|
||||
"USER_EXISTING_PW": "123456789",
|
||||
"USERNAME_LEN": "24",
|
||||
"USER_NEW_PW": "123456789",
|
||||
"PASSWORD_HASH": "059b7beebaf4824e5395fb9eba6e8424b6ee27b43675130608502790e4af2178",
|
||||
"MAIL_HOST": "restmail.net",
|
||||
"SERVER_HOST": "localhost",
|
||||
"SERVER_PORT": 8081,
|
||||
"SERVER_PATH": "v1",
|
||||
"AUTH_SERVER_URL": "https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format",
|
||||
"DEBUG": false
|
||||
}
|
||||
|
||||
235
gaia/apps/system/fxa/test/marionette/lib/fxa.js
Executable file
|
|
@ -0,0 +1,235 @@
|
|||
'use strict';
|
||||
|
||||
var FxAUser = require('./fxa_user'),
|
||||
config = require('./config.json');
|
||||
|
||||
/**
|
||||
* Abstraction around FxA app.
|
||||
* 'Entrance point' app URL is passed in since FxA acts more like a
|
||||
* component than a standalone app
|
||||
* (launched by Settings, Marketplace, FTU, FMD, dev_apps, etc.)
|
||||
*
|
||||
* @param client
|
||||
* @constructor
|
||||
*/
|
||||
function FxA(client) {
|
||||
this.config = config;
|
||||
this.fxaUser = new FxAUser();
|
||||
this.client = client.scope({ searchTimeout: config.MARIONETTE_TIMEOUT});
|
||||
}
|
||||
|
||||
FxA.SETTINGS_ORIGIN = 'app://settings.gaiamobile.org';
|
||||
FxA.UITEST_ORIGIN = 'app://uitest.gaiamobile.org';
|
||||
FxA.TEST_FXA_CLIENT_ORIGIN = 'app://test-fxa-client.gaiamobile.org';
|
||||
FxA.FTU_ORIGIN = 'app://ftu.gaiamobile.org';
|
||||
FxA.SERVER_PATH = 'http://' +
|
||||
config.SERVER_HOST + ':' +
|
||||
config.SERVER_PORT + '/' +
|
||||
config.SERVER_PATH;
|
||||
FxA.SERVER_ARGS = [
|
||||
config.SERVER_HOST,
|
||||
config.SERVER_PORT,
|
||||
config.SERVER_PATH
|
||||
];
|
||||
|
||||
FxA.COPPA_YEAR = new Date().getFullYear() - 13;
|
||||
|
||||
FxA.Selectors = {
|
||||
body: 'body',
|
||||
bodyReady: 'body .view-body',
|
||||
// dev_apps/uitest
|
||||
apiFxaFrame: '#test-iframe',
|
||||
fxaFrame: '#fxa-iframe',
|
||||
tabAPI: '#API',
|
||||
fxaButton: '#mozId-fxa',
|
||||
requestButton: '#request',
|
||||
// dev_apps/test-fxa-client
|
||||
tabOpenFlow: '#openFlow',
|
||||
// apps/settings
|
||||
menuItemFxa: '#menuItem-fxa',
|
||||
menuItemFmd: '#menuItem-findmydevice',
|
||||
fxaLogin: '#fxa-login',
|
||||
fmdLogin: '#findmydevice-login',
|
||||
fxaCancelAccountConfirmation: '#fxa-cancel-confirmation',
|
||||
// apps/ftu
|
||||
forward: '#forward',
|
||||
createAccountOrLogin: '#fxa-create-account',
|
||||
// apps/findmydevice
|
||||
emailInput: '#fxa-email-input',
|
||||
fxaForgotPassword: 'fxa-forgot-password',
|
||||
pwInput: '#fxa-pw-input',
|
||||
//COPPA (age-restriction menu no longer appears,
|
||||
// but password input tag is still different)
|
||||
pwInputPostCOPPA: '#fxa-pw-set-input',
|
||||
pwSetInput: '#fxa-pw-set-input',
|
||||
pwRefresh: '#fxa-pw-input-refresh',
|
||||
COPPAElementId: '#fxa-coppa',
|
||||
COPPASelectId: '#fxa-age-select',
|
||||
COPPAOptionVal: FxA.COPPA_YEAR + ' or earlier',
|
||||
COPPAOptionValToFail: FxA.COPPA_YEAR.toString(),
|
||||
moduleNext: '#fxa-module-next',
|
||||
moduleDone: '#fxa-module-done',
|
||||
errorOK: '#fxa-error-ok'
|
||||
};
|
||||
|
||||
FxA.prototype = {
|
||||
/**
|
||||
* Launches FxA app and focuses on frame.
|
||||
*/
|
||||
launch: function (origin) {
|
||||
// do unless FTU
|
||||
if (origin.search('ftu') === -1) {
|
||||
this.client.apps.launch(origin);
|
||||
}
|
||||
this.client.apps.switchToApp(origin);
|
||||
},
|
||||
|
||||
enterInput: function (inputId, inputString) {
|
||||
this.client.helper
|
||||
.waitForElement(inputId)
|
||||
.sendKeys(inputString);
|
||||
},
|
||||
|
||||
enterCOPPANew: function () {
|
||||
var sel = FxA.Selectors.COPPASelectId;
|
||||
var inputString = FxA.Selectors.COPPAOptionVal;
|
||||
this.client.helper.tapSelectOption(sel, inputString);
|
||||
},
|
||||
|
||||
enterEmailNew: function () {
|
||||
var sel = FxA.Selectors.emailInput;
|
||||
var inputString = this.fxaUser.email(config.USER_NEW);
|
||||
this.client.helper.fillInputField(sel, inputString);
|
||||
},
|
||||
enterPasswordNew: function () {
|
||||
var sel = FxA.Selectors.pwInputPostCOPPA;
|
||||
var inputString = this.fxaUser.password(config.USER_NEW);
|
||||
this.client.helper.fillInputField(sel, inputString);
|
||||
},
|
||||
enterEmailExisting: function () {
|
||||
var sel = FxA.Selectors.emailInput;
|
||||
var inputString = this.fxaUser.email(config.USER_EXISTING);
|
||||
this.client.helper.fillInputField(sel, inputString);
|
||||
},
|
||||
enterPasswordExisting: function () {
|
||||
var sel = FxA.Selectors.pwInput;
|
||||
var inputString = this.fxaUser.password(config.USER_EXISTING);
|
||||
this.client.helper.fillInputField(sel, inputString);
|
||||
},
|
||||
onClick: function(searchSelector) {
|
||||
var element = this.client.findElement(searchSelector);
|
||||
this.client.helper
|
||||
.waitForElement(element)
|
||||
.tap();
|
||||
},
|
||||
clickNext: function() {
|
||||
var element = this.client.findElement(FxA.Selectors.moduleNext);
|
||||
var client = this.client;
|
||||
|
||||
client.waitFor(function () {
|
||||
return client.findElement(FxA.Selectors.moduleNext)
|
||||
.scriptWith(function (el) {
|
||||
return el.getAttribute('disabled') !== 'disabled';
|
||||
});
|
||||
});
|
||||
|
||||
client.helper
|
||||
.waitForElement(element)
|
||||
.tap();
|
||||
},
|
||||
clickDone: function() {
|
||||
var element = this.client.findElement(FxA.Selectors.moduleDone);
|
||||
this.client.helper
|
||||
.waitForElement(element)
|
||||
.tap();
|
||||
},
|
||||
clickTabOpenFlow: function() {
|
||||
var element = this.client.findElement(FxA.Selectors.tabOpenFlow);
|
||||
this.client.helper
|
||||
.waitForElement(element)
|
||||
.tap();
|
||||
},
|
||||
clickFxaLogin: function() {
|
||||
var element = this.client.findElement(FxA.Selectors.fxaLogin);
|
||||
this.client.helper
|
||||
.waitForElement(element)
|
||||
.tap();
|
||||
},
|
||||
clickMenuItemFxa: function() {
|
||||
var element = this.client.findElement(FxA.Selectors.menuItemFxa);
|
||||
this.client.helper
|
||||
.waitForElement(element)
|
||||
.tap();
|
||||
},
|
||||
clickRequestButton: function() {
|
||||
var element = this.client.findElement(FxA.Selectors.requestButton);
|
||||
this.client.helper
|
||||
.waitForElement(element)
|
||||
.tap();
|
||||
},
|
||||
clickTabAPI: function() {
|
||||
var element = this.client.findElement(FxA.Selectors.tabAPI);
|
||||
this.client.helper
|
||||
.waitForElement(element)
|
||||
.tap();
|
||||
},
|
||||
clickFxaButton: function() {
|
||||
var element = this.client.findElement(FxA.Selectors.fxaButton);
|
||||
this.client.helper
|
||||
.waitForElement(element)
|
||||
.tap();
|
||||
},
|
||||
switchFrame: function(frameId) {
|
||||
this.client.switchToFrame();
|
||||
var frame = this.client.findElement(frameId);
|
||||
this.client.switchToFrame(frame);
|
||||
},
|
||||
switchFrameDirect: function(frameId) {
|
||||
var frame = this.client.findElement(frameId);
|
||||
this.client.switchToFrame(frame);
|
||||
},
|
||||
clickThruPanel: function(panelId, buttonId) {
|
||||
if (panelId === '#wifi') {
|
||||
// The wifi panel will bring up a screen to show it is scanning for
|
||||
// networks. Not waiting for this to clear will blow test timing
|
||||
// and cause things to fail.
|
||||
this.client.helper.waitForElementToDisappear('#loading-overlay');
|
||||
}
|
||||
// waitForElement is used to make sure animations and page changes have
|
||||
// finished, and that the panel is displayed.
|
||||
this.client.helper.waitForElement(panelId);
|
||||
|
||||
if (buttonId) {
|
||||
var button = this.client.helper.waitForElement(buttonId);
|
||||
button.click();
|
||||
}
|
||||
},
|
||||
runSettingsMenu: function() {
|
||||
this.clickMenuItemFxa();
|
||||
this.clickFxaLogin();
|
||||
this.switchFrame(FxA.Selectors.fxaFrame);
|
||||
},
|
||||
runUITestMenu: function() {
|
||||
this.clickTabAPI();
|
||||
this.clickFxaButton();
|
||||
this.switchFrameDirect(FxA.Selectors.apiFxaFrame);
|
||||
this.clickRequestButton();
|
||||
this.switchFrame(FxA.Selectors.fxaFrame);
|
||||
},
|
||||
runFTUMenu: function() {
|
||||
this.clickThruPanel('#languages', '#forward');
|
||||
this.clickThruPanel('#wifi', '#forward');
|
||||
this.clickThruPanel('#date_and_time', '#forward');
|
||||
this.clickThruPanel('#geolocation', '#forward');
|
||||
this.clickThruPanel('#import_contacts', '#forward');
|
||||
this.clickThruPanel('#firefox_accounts',
|
||||
FxA.Selectors.createAccountOrLogin);
|
||||
this.switchFrame(FxA.Selectors.fxaFrame);
|
||||
},
|
||||
runFxAClientTestMenu: function() {
|
||||
this.clickTabOpenFlow();
|
||||
this.switchFrame(FxA.Selectors.fxaFrame);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FxA;
|
||||
54
gaia/apps/system/fxa/test/marionette/lib/fxa_user.js
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
'use strict';
|
||||
|
||||
var config = require('./config.json');
|
||||
|
||||
/**
|
||||
* FxA user handler for email, password & string generations
|
||||
* @constructor
|
||||
*/
|
||||
function FxAUser() {
|
||||
}
|
||||
|
||||
FxAUser.prototype = {
|
||||
_randString: function(len, charSet){
|
||||
charSet = charSet || 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
var rand = '';
|
||||
for (var i = 0; i < len; i++) {
|
||||
var randomPoz = Math.floor(Math.random() * charSet.length);
|
||||
rand += charSet.substring(randomPoz, randomPoz + 1);
|
||||
}
|
||||
return rand;
|
||||
},
|
||||
|
||||
get _uniqueUserName() {
|
||||
return this._randString(config.USERNAME_LEN);
|
||||
},
|
||||
|
||||
/**
|
||||
* @TODO bug 1052267
|
||||
* add ability to generate variety of "invalid" email strings
|
||||
*/
|
||||
email: function(userType) {
|
||||
switch(userType) {
|
||||
case config.USER_NEW:
|
||||
return this._uniqueUserName + '@' + config.MAIL_HOST;
|
||||
default:
|
||||
return config.USER_EXISTING_EMAIL + '@' + config.MAIL_HOST;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @TODO bug 1052267
|
||||
* add ability to generate variety of "invalid" password strings
|
||||
*/
|
||||
password: function(userType) {
|
||||
switch(userType) {
|
||||
case config.USER_NEW:
|
||||
return config.USER_NEW_PW;
|
||||
default:
|
||||
return config.USER_EXISTING_PW;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FxAUser;
|
||||
39
gaia/apps/system/fxa/test/marionette/lib/server.js
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
function Server(child) {
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
Server.prototype = {
|
||||
/**
|
||||
* Sends signal to stop child process and stop server.
|
||||
*
|
||||
* TODO: Bug 1056186:
|
||||
* [FxA] Create unit tests for Firefox Accounts mock server process
|
||||
*/
|
||||
stop: function() {
|
||||
this.child.kill('SIGTERM');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Spawn the child process where the FxA mock server lives.
|
||||
*
|
||||
* @param args - [ host, port, path ]
|
||||
* @param callback
|
||||
*/
|
||||
function create(args, callback) {
|
||||
var child = require('child_process')
|
||||
.fork(__dirname + '/server_mock_fxa.js', args)
|
||||
.on('message', function(data) {
|
||||
if (Array.isArray(data) && data[0] === 'start') {
|
||||
callback(null, new Server(child));
|
||||
}
|
||||
})
|
||||
.on('error', function (err) {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
Server.create = create;
|
||||
module.exports = Server;
|
||||
434
gaia/apps/system/fxa/test/marionette/lib/server_mock_fxa.js
Executable file
|
|
@ -0,0 +1,434 @@
|
|||
'use strict';
|
||||
|
||||
var restify = require('restify'),
|
||||
ecstatic = require('ecstatic'),
|
||||
config = require('./config.json');
|
||||
|
||||
var server = restify.createServer({
|
||||
name: 'Mock Firefox Accounts Server'
|
||||
});
|
||||
|
||||
server.hostname = process.argv[2];
|
||||
server.port = process.argv[3];
|
||||
server.version = process.argv[4];
|
||||
|
||||
process.on('uncaughtException', gracefulShutdown);
|
||||
server.on('uncaughtException', gracefulShutdown);
|
||||
|
||||
function shutdown(cb) {
|
||||
try {
|
||||
server.close(cb);
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function gracefulShutdown(err) {
|
||||
if (err) {
|
||||
console.error(err.stack);
|
||||
}
|
||||
shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* authPW = 123456789
|
||||
* client accepts any hex Token the server kindly grants it
|
||||
* Note: other user types could be added: i.e. verified user
|
||||
*/
|
||||
var account = {
|
||||
'new': {
|
||||
'authPW': config.PASSWORD_HASH,
|
||||
'uid': randomString(32),
|
||||
'sessionToken': randomString(64),
|
||||
'keyFetchToken': randomString(64),
|
||||
'passwordChangeToken': randomString(64),
|
||||
'verified': false,
|
||||
'authAt': Date.now()
|
||||
},
|
||||
|
||||
'exists': {
|
||||
'email': config.USER_EXISTING_EMAIL + '@' + config.MAIL_HOST,
|
||||
'authPW': config.PASSWORD_HASH,
|
||||
'uid': randomString(32),
|
||||
'sessionToken': randomString(64),
|
||||
'keyFetchToken': randomString(64),
|
||||
'passwordChangeToken': randomString(64),
|
||||
'verified': false,
|
||||
'authAt': Date.now()
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock Firefox Accounts Server
|
||||
*
|
||||
* Purpose:
|
||||
* Mimic behavior of FxA server v1
|
||||
* (SHA: 29dc9abbacb3fe1b6de763f3c7bd85e63215297c)
|
||||
* with mostly 'canned' responses
|
||||
* Some APIs (i.e. for authentication) provide limited logical checking
|
||||
* for testing B2G
|
||||
*
|
||||
* Reference
|
||||
* https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md
|
||||
*/
|
||||
var Server = {
|
||||
|
||||
//set to true for verbose logging
|
||||
debug: config.DEBUG || false,
|
||||
|
||||
// allows for terminating process directly when calling server standalone
|
||||
// when calling as child process, use child.kill() from parent
|
||||
stop: function() {
|
||||
gracefulShutdown();
|
||||
console.log('SERVER STOPPED');
|
||||
},
|
||||
|
||||
start: function() {
|
||||
server.use(restify.acceptParser(server.acceptable));
|
||||
server.use(restify.queryParser());
|
||||
server.use(restify.bodyParser());
|
||||
server.use(ecstatic({ root: __dirname + '/' }));
|
||||
server.get('/', ecstatic({ root: __dirname }));
|
||||
server.listen(server.port, function() {
|
||||
var debug = Server.debug || false;
|
||||
if (debug) {
|
||||
console.log(' %s running on port: %s', server.name, server.port);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
(function(err, port) {
|
||||
Server.start();
|
||||
process.send(['start']);
|
||||
})();
|
||||
|
||||
// handle process messages
|
||||
process.on('message', function(data) {
|
||||
switch (data) {
|
||||
case 'stop':
|
||||
console.log('STOPPING MOCK SERVER');
|
||||
Server.stop();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
server.post('/v1/account/create', postAccountCreate);
|
||||
server.get('/v1/account/status', getAccountStatus);
|
||||
server.get('/v1/account/devices', getAccountDevices);
|
||||
server.get('/v1/account/keys', getAccountKeys);
|
||||
server.post('/v1/account/reset', respondEmpty);
|
||||
server.post('/v1/account/destroy', respondEmpty);
|
||||
server.post('/v1/account/login', postAccountLogin);
|
||||
server.get('/v1/session/status', respondEmpty);
|
||||
server.post('/v1/session/destroy', respondEmpty);
|
||||
server.get('/v1/recovery_email/status', getRecoveryEmailStatus);
|
||||
server.post('/v1/recovery_email/resend_code', respondEmpty);
|
||||
server.post('/v1/recovery_email/verify_code', respondEmpty);
|
||||
server.post('/v1/certificate/sign', postCertificateSign);
|
||||
server.post('/v1/password/change/start', postPasswordChangeStart);
|
||||
server.post('/v1/password/change/finish', respondEmpty);
|
||||
server.post('/v1/password/forgot/send_code', postPasswordForgotSendCode);
|
||||
server.post('/v1/password/forgot/resend_code', postPasswordForgotResendCode);
|
||||
server.post('/v1/password/forgot/verify_code', postPasswordForgotVerifyCode);
|
||||
server.get('/v1/forgot/status', getPasswordForgotStatus);
|
||||
server.post('/v1/get_random_bytes', postGetRandomBytes);
|
||||
|
||||
//catch any undefined requests
|
||||
server.get(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, anyUndefined);
|
||||
server.post(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, anyUndefined);
|
||||
|
||||
/************************************
|
||||
* API: Account
|
||||
************************************/
|
||||
|
||||
function postAccountCreate(req,res,next) {
|
||||
var accountSelected = getAccountType(req.body.email);
|
||||
var jsonResponse = getRespAccountCreate(accountSelected);
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
function getAccountStatus(req,res,next) {
|
||||
var jsonResponse = { 'exists': false };
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
function getAccountDevices(req,res,next) {
|
||||
var jsonResponse = {
|
||||
'devices': [
|
||||
{
|
||||
'id': '4c352927-cd4f-4a4a-a03d-7d1893d950b8',
|
||||
'type': 'computer',
|
||||
'name': 'Foxys Macbook'
|
||||
}
|
||||
]
|
||||
};
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
function getAccountKeys(req,res,next) {
|
||||
var jsonResponse = {
|
||||
'bundle': randomString(256)
|
||||
};
|
||||
printLog('GET /account/keys', jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
/************************************
|
||||
* API: Authentication
|
||||
************************************/
|
||||
|
||||
function postAccountLogin(req,res,next) {
|
||||
var jsonResponse;
|
||||
var accountSelected = getAccountType(req.body.email);
|
||||
|
||||
if (req.body.email === account.exists.email) {
|
||||
if (req.body.authPW === account.exists.authPW) {
|
||||
jsonResponse = getRespAccountLogin(accountSelected);
|
||||
res.send(200, jsonResponse);
|
||||
} else {
|
||||
//status code 400, errno 103: incorrect password
|
||||
jsonResponse = getError(103,accountSelected);
|
||||
res.send(jsonResponse);
|
||||
}
|
||||
} else {
|
||||
//status code 400, errno 102: attempt to access a non-existent account
|
||||
jsonResponse = getError(102, accountSelected);
|
||||
res.send(jsonResponse);
|
||||
}
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
}
|
||||
|
||||
/************************************
|
||||
* API: Recovery Email
|
||||
************************************/
|
||||
|
||||
function getRecoveryEmailStatus(req,res,next) {
|
||||
var accountSelected = getAccountType(req.body.email);
|
||||
var jsonResponse = getRespRecoveryEmail(accountSelected);
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
/************************************
|
||||
* API: Certificate Signing
|
||||
************************************/
|
||||
|
||||
function postCertificateSign(req,res,next) {
|
||||
var jsonResponse = {
|
||||
'cert': randomString(520,
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz0123456789')
|
||||
};
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
/************************************
|
||||
* API: Password
|
||||
************************************/
|
||||
|
||||
/**
|
||||
* should verify request of format:
|
||||
* {
|
||||
* "email": "me@example.com",
|
||||
* "oldAuthPW":
|
||||
* "d486e79c9f3214b0010fe31bfb50fa6c12e1d093f7770c81c6b1c19c7ee375a6"
|
||||
* }
|
||||
*/
|
||||
function postPasswordChangeStart(req,res,next) {
|
||||
|
||||
var accountSelected = getAccountType(req.body.email);
|
||||
var jsonResponse;
|
||||
if (req.body.email === account.exists.email) {
|
||||
jsonResponse = getRespPasswordChangeStart(accountSelected);
|
||||
res.send(200, jsonResponse);
|
||||
} else {
|
||||
//status code 400, errno 102: attempt to access non-existent account
|
||||
jsonResponse = getError(102, accountSelected);
|
||||
res.send(jsonResponse);
|
||||
}
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
function postPasswordForgotSendCode(req,res,next) {
|
||||
var jsonResponse = getRespPasswordForgotSendCode();
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
function postPasswordForgotResendCode(req,res,next) {
|
||||
var jsonResponse = getRespPasswordForgotSendCode();
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
function postPasswordForgotVerifyCode(req,res,next) {
|
||||
var jsonResponse = {
|
||||
'accountResetToken': randomString(128)
|
||||
};
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
function getPasswordForgotStatus(req,res,next) {
|
||||
var jsonResponse = { 'tries': 3, 'ttl': 420 };
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
/************************************
|
||||
* API: Miscellaneous
|
||||
************************************/
|
||||
|
||||
function postGetRandomBytes(req,res,next) {
|
||||
var randomData = getRandomBytes();
|
||||
var jsonResponse = { 'data': randomData };
|
||||
printLog(req.method + ' ' + req.url, jsonResponse);
|
||||
res.send(200,jsonResponse);
|
||||
}
|
||||
|
||||
/************************************
|
||||
* API: Undefined
|
||||
* any other requests land here
|
||||
************************************/
|
||||
|
||||
function anyUndefined(req,res,next) {
|
||||
printLog(req.method + ' ' + req.url + ' (UNDEFINED)',
|
||||
'value of req is: ' + req);
|
||||
}
|
||||
|
||||
function randomString(len, charSet) {
|
||||
charSet = charSet || 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
var rand = '';
|
||||
for (var i = 0; i < len; i++) {
|
||||
var randomPoz = Math.floor(Math.random() * charSet.length);
|
||||
rand += charSet.substring(randomPoz,randomPoz+1);
|
||||
}
|
||||
return rand;
|
||||
}
|
||||
|
||||
/**
|
||||
* mock API will random 64 char length hex strings
|
||||
* @returns {string}
|
||||
*/
|
||||
function getRandomBytes() {
|
||||
return randomString(64);
|
||||
}
|
||||
|
||||
function getAccountType(returnType) {
|
||||
switch(returnType) {
|
||||
case account.exists.email:
|
||||
return account.exists;
|
||||
default:
|
||||
return account.new;
|
||||
}
|
||||
}
|
||||
|
||||
function respondEmpty(req,res,next) {
|
||||
printLog(req.method + ' ' + req.url, {});
|
||||
res.send(200, {});
|
||||
}
|
||||
|
||||
function getRespAccountLogin(accountSelected) {
|
||||
return {
|
||||
'uid': accountSelected.uid,
|
||||
'sessionToken': accountSelected.sessionToken,
|
||||
'verified': accountSelected.verified,
|
||||
'authAt': Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
function getRespRecoveryEmail(accountSelected) {
|
||||
return {
|
||||
'email': accountSelected.email,
|
||||
'verified': accountSelected.verified
|
||||
};
|
||||
}
|
||||
|
||||
function getRespAccountCreate(accountSelected) {
|
||||
return {
|
||||
'uid': accountSelected.uid,
|
||||
'sessionToken': accountSelected.sessionToken,
|
||||
'keyFetchToken': accountSelected.keyFetchToken,
|
||||
'authAt': Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
function getRespPasswordChangeStart(accountSelected) {
|
||||
return {
|
||||
'keyFetchToken': accountSelected.keyFetchToken,
|
||||
'passwordChangeToken': accountSelected.passwordChangeToken,
|
||||
'verified': accountSelected.verified
|
||||
};
|
||||
}
|
||||
|
||||
function getRespPasswordForgotSendCode() {
|
||||
return {
|
||||
'passwordForgotToken': randomString(128),
|
||||
'ttl': 900,
|
||||
'codeLength': 8,
|
||||
'tries': 3
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Use:
|
||||
* var errNum = req.socket._httpMessage.statusCode;
|
||||
* @param errNum
|
||||
* @param accountSelected
|
||||
* @returns {*}
|
||||
*/
|
||||
function getError(errNum,accountSelected) {
|
||||
var info = config.AUTH_SERVER_URL;
|
||||
switch(errNum) {
|
||||
case 102:
|
||||
return {
|
||||
'code': 400,
|
||||
'errno': errNum,
|
||||
'error': 'Bad Request',
|
||||
'message': 'Attempt to access an account that does not exist',
|
||||
'info': info,
|
||||
'email': accountSelected.email,
|
||||
'log': []
|
||||
};
|
||||
|
||||
case 103:
|
||||
return {
|
||||
'code': 400,
|
||||
'errno': errNum,
|
||||
'error': 'Bad Request',
|
||||
'message': 'Incorrect password',
|
||||
'info': info,
|
||||
'email': accountSelected.email,
|
||||
'log': []
|
||||
};
|
||||
|
||||
case 999:
|
||||
return {
|
||||
'code': errNum,
|
||||
'errno': 999,
|
||||
'error': 'Not Found',
|
||||
'message': 'Unspecified error',
|
||||
'info': info,
|
||||
'log': []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log req/res to stdout
|
||||
*/
|
||||
function printLog(title,msg) {
|
||||
if (config.DEBUG) {
|
||||
console.log('\n========================');
|
||||
console.log(title);
|
||||
console.log('========================');
|
||||
console.log(msg);
|
||||
console.log('\n\n');
|
||||
}
|
||||
}
|
||||
37
gaia/apps/system/fxa/test/marionette/test-fxa-client/index.html
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>FxAccounts testing tools</title>
|
||||
<!-- We don't need localization for this app. -->
|
||||
<script defer type="text/javascript" src="/js/fxa_iac_client.js"></script>
|
||||
<script defer type="text/javascript" src="/js/app.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="style/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<section role="dialog">
|
||||
<menu>
|
||||
<button id="getAccount">Get Fx Account</button>
|
||||
<button id="openFlow">Launch FxAccounts flow</button>
|
||||
<button id="logout">Logout</button>
|
||||
<div class="refresh-auth">
|
||||
<button id="refreshAuthentication">Refresh Authentication UI</button>
|
||||
<input id="accountId" type="text" value="dummy@account" x-inputmode="verbatim">
|
||||
</div>
|
||||
</menu>
|
||||
|
||||
<section id="result">
|
||||
<p id="result-text"></p>
|
||||
</section>
|
||||
|
||||
<section id="events">
|
||||
<p id="events-text"></p>
|
||||
</section>
|
||||
</section>
|
||||
<!-- Progress overlay -->
|
||||
<section id="overlay" role="dialog">
|
||||
<progress></progress>
|
||||
<p id="overlay-msg">Connecting to Firefox</p>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
124
gaia/apps/system/fxa/test/marionette/test-fxa-client/js/app.js
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
'use strict';
|
||||
/* global FxAccountsIACHelper */
|
||||
|
||||
var hideTimer;
|
||||
var Overlay = {
|
||||
show: function fxam_overlay_show() {
|
||||
if (hideTimer) {
|
||||
return;
|
||||
}
|
||||
var overlay = document.querySelector('#overlay');
|
||||
overlay.classList.add('show');
|
||||
hideTimer = setTimeout(Overlay.hide, 2000);
|
||||
},
|
||||
hide: function fxam_overlay_hide() {
|
||||
hideTimer = clearTimeout(hideTimer);
|
||||
var overlay = document.querySelector('#overlay');
|
||||
if (!overlay) {
|
||||
return;
|
||||
}
|
||||
overlay.classList.remove('show');
|
||||
}
|
||||
};
|
||||
|
||||
var TestFxAClient = function TestFxAClient() {
|
||||
|
||||
var getFxAccountsButton, launchFxAFlowButton, logoutButton,
|
||||
refreshAuthButton, resultTextEl, resultEl, timer, eventsEl,
|
||||
eventsTextEl, eventsTimer, accountIdInput;
|
||||
|
||||
var init = function init() {
|
||||
resultEl = document.getElementById('result');
|
||||
resultTextEl = document.getElementById('result-text');
|
||||
getFxAccountsButton = document.getElementById('getAccount');
|
||||
launchFxAFlowButton = document.getElementById('openFlow');
|
||||
logoutButton = document.getElementById('logout');
|
||||
refreshAuthButton = document.getElementById('refreshAuthentication');
|
||||
eventsEl = document.getElementById('events');
|
||||
eventsTextEl = document.getElementById('events-text');
|
||||
accountIdInput = document.getElementById('accountId');
|
||||
|
||||
getFxAccountsButton.addEventListener('click', handler);
|
||||
launchFxAFlowButton.addEventListener('click', handler);
|
||||
logoutButton.addEventListener('click', handler);
|
||||
refreshAuthButton.addEventListener('click', handler);
|
||||
|
||||
FxAccountsIACHelper.addEventListener('onlogin', function() {
|
||||
showEvent('onlogin');
|
||||
});
|
||||
|
||||
FxAccountsIACHelper.addEventListener('onlogout', function() {
|
||||
showEvent('onlogout');
|
||||
});
|
||||
|
||||
FxAccountsIACHelper.addEventListener('onverifiedlogin', function() {
|
||||
showEvent('onverifiedlogin');
|
||||
});
|
||||
};
|
||||
|
||||
function _setResponse(response, errorDebug) {
|
||||
resultTextEl.innerHTML = '';
|
||||
|
||||
var text = document.createTextNode((errorDebug)? 'Error ' : 'Success :');
|
||||
resultTextEl.appendChild(text);
|
||||
var responseBeautyfied =
|
||||
document.createTextNode(JSON.stringify(response || 'No params', null, 4));
|
||||
resultTextEl.appendChild(responseBeautyfied);
|
||||
|
||||
clearTimeout(timer);
|
||||
resultEl.classList.add('show');
|
||||
timer = setTimeout(function() {
|
||||
resultEl.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function showEvent(eventName) {
|
||||
eventsTextEl.innerHTML = '';
|
||||
|
||||
var text = document.createTextNode('Event received: ' + eventName);
|
||||
eventsTextEl.appendChild(text);
|
||||
|
||||
clearTimeout(timer);
|
||||
eventsEl.classList.add('show');
|
||||
eventsTimer = setTimeout(function() {
|
||||
eventsEl.classList.remove('show');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
var showResponse = function showResponse(response) {
|
||||
Overlay.hide();
|
||||
_setResponse(response);
|
||||
};
|
||||
|
||||
var showError = function showResponse(response) {
|
||||
Overlay.hide();
|
||||
_setResponse(response, true);
|
||||
};
|
||||
|
||||
var handler = function handler(evt) {
|
||||
var method = evt.target.id;
|
||||
switch (method) {
|
||||
case 'openFlow':
|
||||
FxAccountsIACHelper[method](showResponse, showError);
|
||||
break;
|
||||
case 'getAccount':
|
||||
case 'logout':
|
||||
Overlay.show();
|
||||
FxAccountsIACHelper[method](showResponse, showError);
|
||||
break;
|
||||
case 'refreshAuthentication':
|
||||
FxAccountsIACHelper[method](accountIdInput.value,
|
||||
showResponse, showError);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
return {
|
||||
'init': init
|
||||
};
|
||||
|
||||
}();
|
||||
|
||||
TestFxAClient.init();
|
||||
13
gaia/apps/system/fxa/test/marionette/test-fxa-client/manifest.webapp
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Test FxA Client",
|
||||
"launch_path": "/index.html",
|
||||
"type": "certified",
|
||||
"developer": {
|
||||
"name": "The Gaia Team",
|
||||
"url": "https://github.com/mozilla-b2g/gaia"
|
||||
},
|
||||
"icons": {
|
||||
"120": "/style/icons/icon.png"
|
||||
},
|
||||
"orientation": "default"
|
||||
}
|
||||
BIN
gaia/apps/system/fxa/test/marionette/test-fxa-client/style/icons/icon.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
97
gaia/apps/system/fxa/test/marionette/test-fxa-client/style/style.css
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
/* XXX: The below overflow property is being ignored by B2G Phone
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=777672
|
||||
*/
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 6rem;
|
||||
line-height: 6rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
menu {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#result {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#result.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#events {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#events.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding: 1rem 1.5rem;
|
||||
-moz-box-sizing: border-box;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 1rem 1.5rem;
|
||||
-moz-box-sizing: border-box;
|
||||
font-size: 1.6rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Overlay:
|
||||
* IMPORTANT: Avoid to use 'visibility' or 'opacity' due
|
||||
* to performance issues. For hiding, use 'display:none;''
|
||||
* * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#overlay {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
font-size: 2rem;
|
||||
background-color: rgba(200,200,200,0.9);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#overlay.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 4rem;
|
||||
line-height: 6rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.refresh-auth {
|
||||
border-top: 0.1rem solid;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
19
gaia/apps/system/fxa/view/view_login.js
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
/* global FxaModuleStates */
|
||||
/* exported View */
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
*
|
||||
* "View" object defines the number of steps to follow and the
|
||||
* first step of the flow. Then entire flow is self-contained, so
|
||||
* once the first step is loaded the next ones are loaded taking into
|
||||
* consideration the decision taken by the user.
|
||||
* Number of steps is needed for showing to the user the progress bar.
|
||||
*
|
||||
*/
|
||||
|
||||
var View = {
|
||||
length: 3,
|
||||
start: FxaModuleStates.ENTER_EMAIL
|
||||
};
|
||||
18
gaia/apps/system/fxa/view/view_refresh_auth.js
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
/* global FxaModuleStates */
|
||||
/* exported View */
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
*
|
||||
* "View" object defines the number of steps to follow and the
|
||||
* first step of the flow. Then entire flow is self-contained, so
|
||||
* once the first step is loaded the next ones are loaded taking into
|
||||
* consideration the decision taken by the user.
|
||||
* Number of steps is needed for showing to the user the progress bar.
|
||||
*
|
||||
*/
|
||||
|
||||
var View = {
|
||||
length: 1,
|
||||
start: FxaModuleStates.REFRESH_AUTH
|
||||
};
|
||||
838
gaia/apps/system/index.html
Executable file
|
|
@ -0,0 +1,838 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="style/system/initlogo.css">
|
||||
|
||||
<!-- Shared code -->
|
||||
<script src="shared/js/l10n.js"></script>
|
||||
<script src="shared/js/moz_intl.js"></script>
|
||||
<script src="shared/js/dump.js"></script>
|
||||
|
||||
<link rel="resource" type="image/png" href="shared/resources/branding/browser_84.png"/>
|
||||
<link rel="resource" type="image/png" href="shared/resources/branding/browser_126.png"/>
|
||||
<!-- applications.js must not be deferred
|
||||
in order to catch webapps-registry-ready mozChromeEvent -->
|
||||
<script src="js/applications.js"></script>
|
||||
<!-- Not deferring the following to show startup logo/animation as soon as possible. -->
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style/battery_overlay/battery_overlay.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/wrapper/wrapper.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/chrome/chrome.css">
|
||||
<link rel="stylesheet" href="shared/elements/gaia_overflow_menu/style/external.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/definitions.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/system/system.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/system/keyboard.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/system/global-overlays.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/ime_menu.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/confirm.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/input_areas.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/value_selector.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/date_selector.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/time_selector.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/object_menu.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/elements/gaia_progress/style/external.css">
|
||||
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/elements/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/fake-notification.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="style/view_source.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="style/tracking_notice/tracking_notice.css"/>
|
||||
|
||||
<script defer src="shared/elements/config.js"></script>
|
||||
<script defer src="shared/js/component_utils.js"></script>
|
||||
<!-- <script defer src="/shared/js/addons/match_pattern.js"></script> -->
|
||||
<!-- <script defer src="/shared/js/homescreens/confirm_dialog_helper.js"></script> -->
|
||||
<!-- <script defer src="/shared/js/gzip/gzip.js"></script> -->
|
||||
<!-- <script defer src="/shared/js/gzip/gzip_worker.js"></script> -->
|
||||
<!-- <script defer src="/shared/js/gzip/gzip.min.js"></script> -->
|
||||
<!-- <script defer src="/shared/elements/gaia_confirm/script.js"></script> -->
|
||||
<!-- <script defer src="/shared/elements/gaia_menu/script.js"></script> -->
|
||||
<!-- <script defer src="/shared/elements/gaia_switch/script.js"></script> -->
|
||||
<!-- <script defer src="shared/elements/gaia-dialog/gaia-dialog.js"></script> -->
|
||||
<!-- <script defer src="shared/elements/gaia-button/gaia-button.js"></script> -->
|
||||
<!-- <script defer src="shared/elements/gaia-text-input/gaia-text-input.js"></script> -->
|
||||
<script defer src="shared/elements/gaia_buttons/script.js"></script>
|
||||
<script defer src="shared/elements/gaia_checkbox/script.js"></script>
|
||||
<script defer src="shared/elements/gaia_progress/script.js"></script>
|
||||
<script defer src="shared/elements/gaia_subheader/script.js"></script>
|
||||
<script defer src="shared/elements/gaia_grid/js/grid_icon_renderer.js"></script>
|
||||
<script defer src="shared/js/homescreens/icon.js"></script>
|
||||
<script defer src="shared/elements/gaia_pin_card/script.js"></script>
|
||||
<script defer src="shared/elements/gaia-header/dist/gaia-header.js"></script>
|
||||
|
||||
<!-- Include shared resources
|
||||
<link rel="resource" type="application/json" href="shared/resources/apn.json"/>
|
||||
<link rel="resource" type="image/png" href="shared/resources/branding/initlogo.png"/>
|
||||
<link rel="resource" type="image/png" href="shared/resources/branding/initlogo_large.png"/>
|
||||
<link rel="resource" type="audio/ogg" href="shared/resources/media/ringtones/ringer_firefox.opus"/>
|
||||
<link rel="resource" type="audio/ogg" href="shared/resources/media/notifications/notifier_firefox.opus"/>
|
||||
-->
|
||||
|
||||
<link rel="resource" type="image/png" href="shared/resources/branding/browser_84.png"/>
|
||||
<link rel="resource" type="image/png" href="shared/resources/branding/browser_126.png"/>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style/ttlview/ttlview.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/accessibility/accessibility.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/sleep_menu/sleep_menu.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/utility_tray/utility_tray.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/action_menu.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/action_menu/action_menu_extended.css">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style/date_selector/date_selector_extended.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/time_selector/time_selector_extended.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/statusbar/statusbar.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/software_button_manager/software_button_manager.css">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style/icc.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/textselection_dialog/textselection_dialog.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/modal_dialog/prompt.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/modal_dialog/modal_dialog.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/authentication_dialog.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/value_selector/value_selector.css">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style/trusted_ui/trusted_ui.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/permission_manager/permission_manager.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/app_install_manager/app_install_manager.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/cost_control/cost_control.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/quick_settings/quick_settings.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/media_playback/media_playback.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/notifications/notifications.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/notifications/downloads.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/cards_view/cards_view.css">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style/bluetooth_transfer/bluetooth_transfer.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/update_manager/update_manager.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/media_recording/media_recording.css">
|
||||
|
||||
<!-- Theme and localization -->
|
||||
<link rel="stylesheet" type="text/css" href="style/themes/default/system.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/themes/default/core.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/themes/default/menus.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/themes/default/buttons.css">
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/shared/locales/branding/branding.{locale}.properties">
|
||||
<link rel="localization" href="locales/system.{locale}.properties">
|
||||
<link rel="localization" href="locales/accessibility.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/date/date.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/permissions/permissions.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/download/download.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/voicemail/voicemail.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/lockscreen/lockscreen.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/activities/activities.{locale}.properties">
|
||||
<link rel="localization" href="/shared/elements/gaia_menu/locales/gaia_menu.{locale}.properties">
|
||||
<link rel="localization" href="/shared/elements/gaia_progress/locales/gaia_progress.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/shrinking_ui/shrinking_ui.{locale}.properties">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style/crash_reporter/crash_reporter.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/browser_frame/browser_frame.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/emergency_callback_manager/emergency_callback_manager.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/shrinking_ui.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/window_layout.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/window.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/app_titlebar.css">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style/edges/edges.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/entry_sheet/entry_sheet.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/devtools/developer_hud.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/devtools/devtools_auth.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/zindex.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/storage_watcher/storage_watcher.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/rocketbar/rocketbar.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/pin_page_system_dialog.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/simcard.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/nfcconnect.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/firefox_account/firefox_account_container.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/mobileid/mobileid.css">
|
||||
|
||||
<script defer src="js/clock.js"></script>
|
||||
<!--Shared
|
||||
<!-- Lazy loaded shared --
|
||||
<script defer src="shared/js/settings_listener.js"></script>
|
||||
<script defer src="shared/js/async_storage.js"></script>
|
||||
<script defer src="shared/js/manifest_helper.js"></script>
|
||||
<script defer src="shared/js/web_manifest_helper.js"></script>
|
||||
<script defer src="shared/js/findmydevice_iac_api.js"></script>
|
||||
<script defer src="shared/js/settings_helper.js"></script>
|
||||
<script defer src="shared/js/default_activity_helper.js"></script>
|
||||
<script defer src="shared/js/async_semaphore.js"></script>
|
||||
<script defer src="shared/js/custom_dialog.js"></script>
|
||||
<script defer src="shared/js/screen_layout.js"></script>
|
||||
<script defer src="shared/js/mobile_operator.js"></script>
|
||||
<script defer src="shared/js/utilities.js"></script>
|
||||
<script defer src="shared/js/sanitizer.js"></script>
|
||||
<script defer src="shared/js/settings_url.js"></script>
|
||||
<script defer src="shared/js/version_helper.js"></script>
|
||||
<script defer src="shared/js/telemetry.js"></script>
|
||||
<script defer src="shared/js/idletimer.js"></script>
|
||||
<script defer src="shared/js/event_safety.js"></script>
|
||||
<script defer src="shared/js/notification_helper.js"></script>
|
||||
<script defer src="shared/js/nfc_utils.js"></script>
|
||||
<script defer src="shared/js/shrinking_ui.js"></script>
|
||||
<script defer src="shared/js/operator_variant_helper.js"></script>
|
||||
<script defer src="/shared/js/icc_helper.js">
|
||||
<script defer src="/shared/js/simslot.js">
|
||||
<script defer src="/shared/js/simslot_manager.js">
|
||||
<script defer src="/shared/js/stk_helper.js">
|
||||
<script defer src="/shared/js/mime_mapper.js">
|
||||
<script defer src="/shared/js/advanced_timer.js">
|
||||
<script defer src="/shared/js/iac_handler.js">
|
||||
<script defer src="/shared/js/apn_helper.js">
|
||||
<script defer src="/shared/js/input_mgmt/input_app_list.js">
|
||||
<script defer src="/shared/js/keyboard_helper.js">
|
||||
<script defer src="/shared/js/bookmarks_database.js">
|
||||
<script defer src="/shared/elements/gaia_overflow_menu/script.js">
|
||||
<script defer src="/shared/elements/gaia-component/gaia-component.js"></script>
|
||||
<script defer src="/shared/elements/gaia-toast/gaia-toast.js"></script>
|
||||
<script defer src="/shared/elements/gaia-site-icon/script.js"></script>
|
||||
<script defer src="/shared/js/homescreens/confirm_dialog_helper.js">
|
||||
<script defer src="/shared/js/image_utils.js">
|
||||
<script defer src="/shared/js/uuid.js">
|
||||
<script defer src="/shared/js/basket_client.js">
|
||||
<script defer src="/shared/js/download/download_helper.js">
|
||||
<script defer src="/shared/js/download/download_ui.js">
|
||||
<script defer src="/shared/js/download/download_formatter.js">
|
||||
<script defer src="/shared/js/download/download_store.js">
|
||||
<script defer src="shared/js/web_manifest_helper.js"></script>
|
||||
<script defer src="/shared/js/icons_helper.js">
|
||||
<script defer src="shared/js/url_helper.js"></script>
|
||||
<script defer src="shared/js/date_time_helper.js"></script>
|
||||
<script defer src="shared/js/input_parser.js"></script>
|
||||
<script defer src="shared/js/lockscreen_slide.js"></script>
|
||||
<script defer src="shared/js/lockscreen_connection_info_manager.js"></script>
|
||||
<script defer src="shared/js/achievements-service.js"></script>
|
||||
<script defer src="/shared/js/passcode_helper.js"></script>
|
||||
<script defer src="/shared/js/sync/errors.js"></script>
|
||||
<script defer src="/shared/js/sync/telemetry.js"></script>
|
||||
<script defer src="/shared/js/advanced_telemetry_helper.js"></script>
|
||||
-->
|
||||
</head>
|
||||
|
||||
<body class="theme-settings" role="application">
|
||||
<div id="screen" aria-hidden="true">
|
||||
<div id="os-logo" data-z-index-level="initlogo">
|
||||
<div id="carrier-logo"></div>
|
||||
</div>
|
||||
|
||||
<!-- Statusbar -->
|
||||
<div id="statusbar" data-z-index-level="statusbar">
|
||||
<div id="statusbar-wrapper">
|
||||
<!-- Statusbar icons -->
|
||||
<div id="statusbar-icons" class="statusbar" role="group" data-l10n-id="statusbar" aria-describedby="ambient-indicator">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="windows" data-z-index-level="app">
|
||||
<!-- application windows are added here -->
|
||||
</div>
|
||||
|
||||
<!-- Rocketbar-->
|
||||
<div id="rocketbar" data-z-index-level="statusbar">
|
||||
<form id="rocketbar-form" class="hidden">
|
||||
<input id="rocketbar-input" type="search" x-inputmode="verbatim"
|
||||
placeholder="Search or enter address" data-l10n-id="search-or-enter-address" value="" />
|
||||
<button type="button" id="rocketbar-clear" data-l10n-id="clear" aria-label="Clear"></button>
|
||||
<button type="button" id="rocketbar-cancel" data-l10n-id="close"></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Rocketbar Results -->
|
||||
<div id="rocketbar-backdrop" class="hidden"
|
||||
data-z-index-level="rocketbar-backdrop">
|
||||
</div>
|
||||
<div id="rocketbar-results" class="hidden" data-z-index-level="rocketbar-results"></div>
|
||||
|
||||
<div id="utility-tray-motion" data-z-index-level="utility-tray" class="tray utility-tray-loading">
|
||||
<div class="tray-content" aria-hidden="true" id="utility-tray">
|
||||
<!-- Notifications -->
|
||||
<div id="utility-tray-notifications" data-l10n-id="utilityTray" role="dialog" aria-label="Utility tray" data-z-index-level="utility-tray-notifications">
|
||||
<div id="statusbar-tray">
|
||||
</div>
|
||||
<div id="notification-bar" class="fake-notification">
|
||||
<div data-icon="notifications" aria-hidden="true"></div>
|
||||
<div class="title-container">
|
||||
<div id="notification-some" class="title" data-l10n-id="statusbarNotifications" data-l10n-args='{"n": "0"}'></div>
|
||||
<button id="notification-clear" data-l10n-id="clear-all" disabled></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="notifications-nested-scroll-interceptor">
|
||||
<div id="notifications-container">
|
||||
<!-- Attention Windows -->
|
||||
<div id="attention-window-notifications-container"></div>
|
||||
<!-- Update Manager -->
|
||||
<div id="update-manager-container" class="fake-notification" role="link">
|
||||
<div data-icon="download-circle" aria-hidden="true"></div>
|
||||
<div class="title-container"></div>
|
||||
<progress></progress>
|
||||
</div>
|
||||
<!-- Emergency callback manager -->
|
||||
<div id="emergency-callback-notification" class="fake-notification" hidden role="link">
|
||||
<div data-icon="call-emergency" class="alert" aria-hidden="true"></div>
|
||||
<div class="title-container" data-l10n-id="emergency-callback-mode"></div>
|
||||
<div class="timer detail"></div>
|
||||
</div>
|
||||
<!-- Storage watcher notification -->
|
||||
<div id="storage-watcher-container" class="fake-notification" role="link">
|
||||
<div data-icon="storage-circle" aria-hidden="true"></div>
|
||||
<div class="title-container"></div>
|
||||
<div class="detail"></div>
|
||||
</div>
|
||||
<!-- App install manager notification -->
|
||||
<div id="install-manager-notification-container">
|
||||
</div>
|
||||
<!-- Bluetooth transfer -->
|
||||
<div id="bluetooth-transfer-status-list">
|
||||
</div>
|
||||
<!-- IME list -->
|
||||
<div id="keyboard-show-ime-list">
|
||||
<div class="fake-notification" role="link">
|
||||
<div data-icon="keyboard-circle" aria-hidden="true"></div>
|
||||
<div class="title-container"></div>
|
||||
<div class="detail" data-l10n-id="ime-switching-tip"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Media recording status -->
|
||||
<div id="media-recording-status-list" role="list">
|
||||
</div>
|
||||
<!-- Notifications container -->
|
||||
<div id="desktop-notifications-container">
|
||||
<div class="priority-notifications"></div>
|
||||
<div class="other-notifications"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tray-footer-container" class="tray-footer">
|
||||
<div id="utility-tray-footer" class="animate-out">
|
||||
<!-- Media playback notification -->
|
||||
<div id="media-playback-container" class="media-playback-container fake-notification" hidden>
|
||||
<div class="media-playback-nowplaying">
|
||||
<bdi class="track" data-l10n-id="mediaPlaybackTrack"></bdi>
|
||||
</div>
|
||||
<div class="media-playback-controls" role="toolbar" data-l10n-id="mediaPlaybackControls">
|
||||
<!-- XXX: These used to be button elements for prev/pause/next buttons but we are not able to receive contextmenu event from them. Change to div elements to make it work and able to fast seek from it (Bug 1017294) -->
|
||||
<div data-icon="skip-back" class="previous" data-l10n-id="mediaPlaybackPrevious"></div>
|
||||
<div data-icon="play" class="play-pause" data-l10n-id="mediaPlaybackPlay"></div>
|
||||
<div data-icon="skip-forward" class="next" data-l10n-id="mediaPlaybackNext"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credit module -->
|
||||
<div id="cost-control-widget">
|
||||
<div id="cost-control-touch-overlay"></div>
|
||||
</div>
|
||||
|
||||
<!-- Quick settings -->
|
||||
<div id="quick-settings" role="toolbar">
|
||||
<ul>
|
||||
<li><a href="#" id="quick-settings-wifi" class="icon bb-button" data-icon="wifi-4" role="button" data-l10n-id="quick-settings-wifiButton-off"></a></li>
|
||||
<li><a href="#" id="quick-settings-airplane-mode" class="icon bb-button" data-icon="airplane" role="button" data-l10n-id="quick-settings-airplaneMode-off"></a></li>
|
||||
<li><a href="#" id="quick-settings-bluetooth" class="icon bb-button" data-icon="bluetooth" role="button" data-l10n-id="quick-settings-bluetoothButton-off"></a></li>
|
||||
<li><a href="#" id="quick-settings-data" class="icon bb-button" data-icon="data" role="button" data-l10n-id="quick-settings-dataButton-off"></a></li>
|
||||
<li><a href="#" id="quick-settings-full-app" class="icon bb-button" data-icon="settings" role="button" data-l10n-id="settingsButton"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="utility-tray-grippy"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ambient-indicator" id="ambient-indicator" data-z-index-level="notification-toaster">
|
||||
<div class="handle handle1"></div>
|
||||
<div class="handle handle2"></div>
|
||||
</div>
|
||||
<div id="tray-invisible-gripper">
|
||||
</div>
|
||||
<div class="tray-dead-area"></div>
|
||||
</div>
|
||||
|
||||
<!-- XXX: Before we can make LockScreen as a iframe or app, we need this to split LockScreen and System part in this way (Bug 1057198) -->
|
||||
<div id="lockscreen-frame-placeholder"></div>
|
||||
|
||||
<!-- This overlay is used for dialogs that need to be in top of the statusbar, but below
|
||||
other divs like the lockscreen or sleep-menu -->
|
||||
<div id="fullscreen-dialog-overlay" data-z-index-level="fullscreen-dialog-overlay"></div>
|
||||
|
||||
<div id="dialog-overlay" data-z-index-level="dialog-overlay">
|
||||
<!-- Unlock SIM Pin dialog -->
|
||||
<!-- Add the dialog via SystemDialog dynamically. -->
|
||||
|
||||
<!-- Crash reporter -->
|
||||
<div id="crash-dialog">
|
||||
<form role="dialog" class="generic-dialog" data-type="confirm">
|
||||
<section id="crash-dialog-contents">
|
||||
<h1 id="crash-dialog-title"></h1>
|
||||
<p data-l10n-id="crash-dialog-message"></p>
|
||||
<p><a id="crash-info-link" data-l10n-id="crash-info-link"></a></p>
|
||||
<p>
|
||||
<input id="always-send" type="checkbox" checked="true" />
|
||||
<label for="always-send" data-l10n-id="crash-always-report"></label>
|
||||
<p>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button id="dont-send-report" disabled="true" data-l10n-id="crash-dont-send"></button>
|
||||
<button id="send-report" class="recommend" data-l10n-id="crash-end"></button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
<!-- "Crash Reports" information page -->
|
||||
<section role="region">
|
||||
<gaia-header id="crash-reports-header" action="close">
|
||||
<h1 data-l10n-id="crashReports"></h1>
|
||||
</gaia-header>
|
||||
<div>
|
||||
<ul>
|
||||
<li class="description" data-l10n-id="crash-reports-description-1"></li>
|
||||
<li class="description" data-l10n-id="crash-reports-description-2"></li>
|
||||
<li class="description">
|
||||
<span data-l10n-id="crash-reports-description-3-start"></span><span data-l10n-id="crash-reports-description-3-privacy"></span><span data-l10n-id="crash-reports-description-3-end"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="trustedui-container" class="up theme-settings">
|
||||
<div id="trustedui-inner">
|
||||
<section role="region" class="title-container skin-organic">
|
||||
<gaia-header id="trustedui-header" action="close">
|
||||
<h1 id="trustedui-title"></h1>
|
||||
</gaia-header>
|
||||
<div id="trustedui-throbber"></div>
|
||||
</section>
|
||||
<div id="trustedui-frame-container">
|
||||
<div id="trustedui-error-dialog" role="dialog" class="generic-dialog">
|
||||
<div class="inner">
|
||||
<h3 id="trustedui-error-title">Error</h3>
|
||||
<p>
|
||||
<span id="trustedui-error-message">The app has encountered an error.</span>
|
||||
</p>
|
||||
</div>
|
||||
<menu data-items="1">
|
||||
<button id="trustedui-error-close" data-l10n-id="close">Close</button>
|
||||
</menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Firefox Account Dialog -->
|
||||
<!-- <div id="fxa-dialog"></div> Add the dialog via SystemDialog dynamically. -->
|
||||
|
||||
<!-- Mobile ID -->
|
||||
<!-- <div id="mobileid-dialog"></div> Add the dialog via SystemDialog dynamically. -->
|
||||
|
||||
<div id="modal-dialog">
|
||||
<div id="modal-dialog-alert" role="dialog" class="generic-dialog" tabindex="-1">
|
||||
<div class="modal-dialog-message-container inner">
|
||||
<h3 id="modal-dialog-alert-title"></h3>
|
||||
<p>
|
||||
<span id="modal-dialog-alert-message"></span>
|
||||
</p>
|
||||
</div>
|
||||
<menu>
|
||||
<button id="modal-dialog-alert-ok" data-l10n-id="ok"></button>
|
||||
</menu>
|
||||
</div>
|
||||
|
||||
<div id="modal-dialog-confirm" role="dialog" class="generic-dialog" tabindex="-1">
|
||||
<div class="modal-dialog-message-container inner">
|
||||
<h3 id="modal-dialog-confirm-title"></h3>
|
||||
<p>
|
||||
<span id="modal-dialog-confirm-message"></span>
|
||||
</p>
|
||||
</div>
|
||||
<menu data-items="2">
|
||||
<button id="modal-dialog-confirm-cancel" data-l10n-id="cancel">Cancel</button>
|
||||
<button id="modal-dialog-confirm-ok" data-l10n-id="ok" class="affirmative"></button>
|
||||
</menu>
|
||||
</div>
|
||||
|
||||
<div id="modal-dialog-prompt" role="dialog" class="generic-dialog" tabindex="-1">
|
||||
<div class="modal-dialog-message-container inner">
|
||||
<h3 id="modal-dialog-prompt-title"></h3>
|
||||
<p>
|
||||
<span id="modal-dialog-prompt-message"></span>
|
||||
<input id="modal-dialog-prompt-input" />
|
||||
</p>
|
||||
</div>
|
||||
<menu data-items="2">
|
||||
<button id="modal-dialog-prompt-cancel" data-l10n-id="cancel">Cancel</button>
|
||||
<button id="modal-dialog-prompt-ok" data-l10n-id="ok" class="affirmative"></button>
|
||||
</menu>
|
||||
</div>
|
||||
|
||||
<div id="modal-dialog-select-one" role="dialog" class="generic-dialog" tabindex="-1">
|
||||
<div class="modal-dialog-message-container inner">
|
||||
<h3 id="modal-dialog-select-one-title"></h3>
|
||||
<ul id="modal-dialog-select-one-menu"></ul>
|
||||
</div>
|
||||
<menu>
|
||||
<button id="modal-dialog-select-one-cancel" data-l10n-id="cancel">Cancel</button>
|
||||
</menu>
|
||||
</div>
|
||||
</div> <!-- end of #modal-dialog -->
|
||||
</div> <!-- end of #overlay-dialog -->
|
||||
|
||||
<div id="permission-screen" data-z-index-level="permission-screen">
|
||||
<div id="permission-dialog" role="dialog" class="generic-dialog">
|
||||
<div class="inner">
|
||||
<h1 id="permission-title"></h1>
|
||||
<h2 id="permission-message"></h2>
|
||||
<div id="permission-more-info" class="hidden">
|
||||
<a id="permission-more-info-link" data-l10n-id="more-info" href="#"></a>
|
||||
<a id="permission-hide-info-link" data-l10n-id="hide-info" href="#" class="hidden"></a>
|
||||
<div id="permission-more-info-box" class="hidden"> </div>
|
||||
</div>
|
||||
<div id="permission-remember-section">
|
||||
<gaia-checkbox id="permission-remember-checkbox" class="inline">
|
||||
<label data-l10n-id="remember-my-choice" id="permission-remember-label"></label>
|
||||
</label>
|
||||
</div>
|
||||
<div id="permission-device-selector"></div>
|
||||
<ul id="permission-devices">
|
||||
</ul>
|
||||
<menu id="permission-buttons" data-items="2">
|
||||
<button id="permission-no"></button>
|
||||
<button id="permission-yes" class="affirmative"></button>
|
||||
</menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app-uninstall-dialog" data-z-index-level="app-uninstall-dialog">
|
||||
</div>
|
||||
|
||||
<form id="app-install-dialog" class="app-install-dialog generic-dialog"
|
||||
data-type="confirm" role="dialog"
|
||||
data-z-index-level="app-install-dialog">
|
||||
<section>
|
||||
<h1 id="app-install-message"></h1>
|
||||
<dl>
|
||||
<dt data-l10n-id="size">Size</dt>
|
||||
<dd id="app-install-size"></dd>
|
||||
<dt data-l10n-id="author">Author</dt>
|
||||
<dd>
|
||||
<div id="app-install-author-name"></div>
|
||||
<div id="app-install-author-url"></div>
|
||||
</dd>
|
||||
</dl>
|
||||
<div id="system-addon-warning" hidden>
|
||||
<div id="system-addon-icon">
|
||||
<img src="style/app_install_manager/images/alert-icon.svg" />
|
||||
</div>
|
||||
<p data-l10n-id="addon-affects-system"></p>
|
||||
<p data-l10n-id="addon-disable-instructions2"></p>
|
||||
</div>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button id="app-install-cancel-button" data-l10n-id="cancel">Cancel</button>
|
||||
<button id="app-install-install-button" class="recommend" data-l10n-id="install">Install</button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
<form id="app-install-cancel-dialog" class="app-install-dialog generic-dialog"
|
||||
data-type="confirm" role="dialog"
|
||||
data-z-index-level="app-install-dialog">
|
||||
<section>
|
||||
<h1 data-l10n-id="cancel-install">Cancel Install</h1>
|
||||
<p>
|
||||
<small data-l10n-id="cancelling-will-not-refund">Cancelling will not refund a purchase. Refunds for paid content are provided by the original seller.</small>
|
||||
<small data-l10n-id="apps-can-be-installed-later">Apps can be installed later from the original installation source.</small>
|
||||
</p>
|
||||
<p data-l10n-id="are-you-sure-you-want-to-cancel">Are you sure you want to cancel this install?</p>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button id="app-install-confirm-cancel-button" type="reset" data-l10n-id="cancel-install-button">Cancel Install</button>
|
||||
<button id="app-install-resume-button" type="submit" data-l10n-id="resume">Resume</button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
<form id="app-download-cancel-dialog" class="app-install-dialog generic-dialog"
|
||||
data-type="confirm" role="dialog"
|
||||
data-z-index-level="app-install-dialog">
|
||||
<section>
|
||||
<h1></h1>
|
||||
<p data-l10n-id="app-download-can-be-restarted">The download can
|
||||
be restarted later.</p>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button id="app-download-stop-button" class="danger confirm" data-l10n-id="app-download-stop-button">Stop Download</button>
|
||||
<button id="app-download-continue-button" class="cancel" type="reset" data-l10n-id="continue">Continue</button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
<form id="updates-download-dialog" data-type="confirm" role="dialog"
|
||||
data-z-index-level="updates-download-dialog" data-nowifi="false"
|
||||
data-data-connection-inline-warning="false" class="generic-dialog">
|
||||
<section>
|
||||
<h1>
|
||||
Updates
|
||||
</h1>
|
||||
<ul>
|
||||
</ul>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button id="updates-later-button" type="reset" data-l10n-id="later">Later</button>
|
||||
<button id="updates-download-button" type="submit" data-l10n-id="download">Download</button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
<form id="emergency-callback-dialog" role="dialog" data-type="confirm"
|
||||
data-z-index-level="emergency-callback-dialog" class="generic-dialog" hidden>
|
||||
<section>
|
||||
<h1 data-l10n-id="confirm">Confirm</h1>
|
||||
<p id="emergency-callback-dialog-wrapper">
|
||||
<img id="emergency-callback-icon"/>
|
||||
<strong data-l10n-id="emergency-callback-mode">Emergency call back mode</strong>
|
||||
<small id="emergency-callback-timer"></small>
|
||||
</p>
|
||||
<p data-l10n-id="emergency-callback-message">You can not make calls, send messages or go online because emergency callback mode is enabled. Would you like to turn it off?</p>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button type="reset" id="emergency-callback-stay-button" data-l10n-id="cancel">Cancel</button>
|
||||
<button class="recommend" type="button" id="emergency-callback-exit-button" data-l10n-id="ok">OK</button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
<form id="updates-viaDataConnection-dialog" data-type="confirm" role="dialog" data-z-index-level="updates-viaDataConnection-dialog" class="generic-dialog">
|
||||
<section>
|
||||
<h1 data-l10n-id="downloadUpdatesViaDataConnection">
|
||||
Download updates via data connection?
|
||||
</h1>
|
||||
<p data-l10n-id="downloadUpdatesViaDataConnectionMessage3">
|
||||
Updates are downloaded via data connection when Wi-Fi is not available. When using data connection, phone calls may be blocked and additional charges may also apply.
|
||||
</p>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button id="updates-viaDataConnection-notnow-button" type="reset" data-l10n-id="notNow">Not Now</button>
|
||||
<button id="updates-viaDataConnection-download-button" class="recommend" type="submit" data-l10n-id="download">Download</button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
<form id="setup-installed-app-dialog" class="setup-installed-app-dialog"
|
||||
data-type="confirm" role="dialog"
|
||||
data-z-index-level="setup-installed-app-dialog">
|
||||
<section>
|
||||
<h1 id="setup-app-name"></h1>
|
||||
<p id="setup-app-description"></p>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button id="setup-cancel-button" type="button" data-l10n-id="later">Later</button>
|
||||
<button id="setup-confirm-button" class="recommend" type="button" data-l10n-id="setup">Setup</button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
<form id="ime-layout-dialog" class="ime-layout-dialog"
|
||||
data-type="confirm" role="dialog"
|
||||
data-z-index-level="ime-layout-dialog">
|
||||
<section>
|
||||
<h1 data-l10n-id="ime-addkeyboards">Add keyboards</h1>
|
||||
<ul id="ime-list">
|
||||
</ul>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button id="ime-cancel-button" type="button" data-l10n-id="cancel">Cancel</button>
|
||||
<button id="ime-confirm-button" class="recommend" type="button" data-l10n-id="confirm">Confirm</button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
<form data-type="confirm" role="dialog" id="quick-setting-data-enabled-dialog" class="generic-dialog" data-z-index-level="quick-setting-data-enabled-dialog">
|
||||
<section>
|
||||
<h1 data-l10n-id="data-roaming-enabled-title"></h1>
|
||||
<p data-l10n-id="data-roaming-enabled-warning2"></p>
|
||||
</section>
|
||||
<menu data-items="2">
|
||||
<button type="button" class="quick-setting-data-cancel-btn recommend" data-l10n-id="notNow"></button>
|
||||
<button type="button" class="quick-setting-data-ok-btn" data-l10n-id="turnOn"></button>
|
||||
</menu>
|
||||
</form>
|
||||
|
||||
|
||||
<div id="update-manager-toaster" data-z-index-level="notification-toaster" aria-live="assertive" aria-atomic="true">
|
||||
<div class="icon">
|
||||
</div>
|
||||
<div class="message">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="emergency-callback-toaster" data-z-index-level="notification-toaster" hidden role="link" aria-live="assertive" aria-atomic="true">
|
||||
<div class="icon">
|
||||
</div>
|
||||
<div class="message" data-l10n-id="emergency-callback-mode">
|
||||
</div>
|
||||
<div class="timer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="notification-toaster" data-z-index-level="notification-toaster" role="link" aria-live="assertive" aria-atomic="true" class="notification-toaster">
|
||||
<img id="toaster-icon" role="presentation" class="toaster-icon"/>
|
||||
<div id="notification-container">
|
||||
<div id="toaster-title" class="toaster-title" dir="auto"></div>
|
||||
<div id="toaster-detail" class="toaster-detail" dir="auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- keyboard -->
|
||||
<div id="keyboards" data-z-index-level="keyboards"></div>
|
||||
|
||||
<!-- Global overlays -->
|
||||
<div id="global-overlays" data-z-index-level="global-overlays"></div>
|
||||
|
||||
<!-- cards view -->
|
||||
<div id="task-manager" data-z-index-level="cards-view">
|
||||
<div id="cards-view">
|
||||
<ul id="cards-list"></ul>
|
||||
<span id="cards-no-recent-windows" class="no-recent-apps" data-l10n-id="no-recent-app-windows"></span>
|
||||
</div>
|
||||
<div id="task-manager-buttons">
|
||||
<button id="task-manager-new-private-sheet-button"></button>
|
||||
<button id="task-manager-new-sheet-button"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- icc / stk -->
|
||||
<div id="icc-view" class="generic-dialog" data-z-index-level="icc-view" role="dialog">
|
||||
<section role="region" class="skin-organic" id="icc-alert" data-icons="false">
|
||||
<gaia-header>
|
||||
<h1 id="icc-alert-maintitle" data-l10n-id="icc-message-maintitle"></h1>
|
||||
</gaia-header>
|
||||
<gaia-subheader skin="organic"><span id="icc-alert-subtitle"></span></gaia-subheader>
|
||||
<span id="icc-alert-icons" class="icons"></span>
|
||||
<p id="icc-alert-msg"></p>
|
||||
<menu>
|
||||
<button type="button" data-l10n-id="close" id="icc-alert-btn" class="recommend full"></button>
|
||||
</menu>
|
||||
</section>
|
||||
<section role="region" class="skin-organic" id="icc-confirm" data-icons="false">
|
||||
<gaia-header id="icc-confirm-header" action="back">
|
||||
<h1 id="icc-confirm-maintitle" data-l10n-id="icc-message-maintitle"></h1>
|
||||
</gaia-header>
|
||||
<gaia-subheader><span id="icc-confirm-subtitle"></span></gaia-subheader>
|
||||
<span id="icc-confirm-icons" class="icons"></span>
|
||||
<p id="icc-confirm-msg"></p>
|
||||
<menu data-items="2">
|
||||
<button type="button" data-l10n-id="close" id="icc-confirm-btn_close"></button>
|
||||
<button type="button" data-l10n-id="ok" id="icc-confirm-btn" class="recommend"></button>
|
||||
</menu>
|
||||
</section>
|
||||
<section role="region" class="skin-organic" id="icc-asyncconfirm" data-icons="false">
|
||||
<gaia-header>
|
||||
<h1 id="icc-asyncconfirm-maintitle" data-l10n-id="icc-message-maintitle"></h1>
|
||||
</gaia-header>
|
||||
<gaia-subheader skin="organic">
|
||||
<span id="icc-asyncconfirm-subtitle"></span>
|
||||
</gaia-subheader>
|
||||
<span id="icc-asyncconfirm-icons" class="icons"></span>
|
||||
<p id="icc-asyncconfirm-msg"></p>
|
||||
<menu data-items="2">
|
||||
<button type="button" data-l10n-id="no" id="icc-asyncconfirm-btn-no"></button>
|
||||
<button type="button" data-l10n-id="yes" id="icc-asyncconfirm-btn-yes" class="recommend"></button>
|
||||
</menu>
|
||||
</section>
|
||||
<section role="region" class="skin-organic" id="icc-input" data-icons="false">
|
||||
<gaia-header id="icc-input-header" action="back">
|
||||
<h1 id="icc-input-maintitle" data-l10n-id="icc-message-maintitle"></h1>
|
||||
<button type="button" data-l10n-id="help" id="icc-input-btn_help"></button>
|
||||
</gaia-header>
|
||||
<gaia-subheader skin="organic"><span id="icc-input-subtitle"></span></gaia-subheader>
|
||||
<form>
|
||||
<span id="icc-input-icons" class="icons"></span>
|
||||
<p id="icc-input-msg"></p>
|
||||
<input id="icc-input-box" required="" type="text" dir="auto" />
|
||||
</form>
|
||||
<menu>
|
||||
<button type="button" data-l10n-id="close" id="icc-input-btn_close"></button>
|
||||
<span class="yesnodisabled">
|
||||
<button type="button" data-l10n-id="ok" id="icc-input-btn" class="recommend"></button>
|
||||
</span>
|
||||
<span class="yesnoenabled">
|
||||
<button type="button" data-l10n-id="no" id="icc-input-btn_no"></button>
|
||||
<button type="button" data-l10n-id="yes" id="icc-input-btn_yes" class="recommend"></button>
|
||||
</span>
|
||||
</menu>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="sleep-menu" data-z-index-level="sleep-menu">
|
||||
<div id="sleep-menu-container" role="dialog" data-type="object">
|
||||
<section>
|
||||
<ul role="menu">
|
||||
</ul>
|
||||
</section>
|
||||
<gaia-buttons skin="dark">
|
||||
<button data-l10n-id="cancel"></button>
|
||||
</gaia-buttons>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="system-overlay" data-z-index-level="system-overlay">
|
||||
<div id="battery">
|
||||
<span class="icon-battery"></span>
|
||||
<div class="battery-notification">
|
||||
<span data-l10n-id="battery-almost-empty3"></span>
|
||||
<span data-l10n-id="plug-in-your-charger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="volume" class="vibration">
|
||||
<style scoped>
|
||||
@import url("style/sound_manager/sound_manager.css");
|
||||
</style>
|
||||
<span class="channel-type"></span>
|
||||
<section>
|
||||
<div class="active"></div>
|
||||
<div class="active"></div>
|
||||
<div class="active"></div>
|
||||
<div class="active"></div>
|
||||
<div class="active"></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="text-selection-dialog-root" data-z-index-level="text-selection-dialog"></div>
|
||||
|
||||
<div id="software-buttons" data-z-index-level="software-buttons">
|
||||
<div id="software-buttons-dead-space" class="dead-space-portrait"></div>
|
||||
<button id="software-home-button" data-l10n-id="softwareHomeButton">
|
||||
<div id="software-home-ring"></div>
|
||||
</button>
|
||||
</div>
|
||||
<button id="fullscreen-software-home-button" data-z-index-level="fullscreen-software-home-button">
|
||||
<div id="software-home-ring"></div>
|
||||
</button>
|
||||
<div id="software-buttons-fullscreen-layout" data-z-index-level="fullscreen-layout-software-home-button">
|
||||
<button id="fullscreen-layout-software-home-button" data-z-index-level="fullscreen-layout-software-home-button">
|
||||
<div id="software-home-ring"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="top-panel" class="gesture-panel" data-z-index-level="gesture-panel"></div>
|
||||
<div id="left-panel" class="gesture-panel disabled" data-z-index-level="gesture-panel"></div>
|
||||
<div id="right-panel" class="gesture-panel disabled" data-z-index-level="gesture-panel"></div>
|
||||
<div id="bottom-panel" class="gesture-panel" data-z-index-level="gesture-panel"></div>
|
||||
|
||||
</div> <!-- end of #screen -->
|
||||
<script defer src="shared/js/lazy_loader.js"></script>
|
||||
<script defer src="js/logo_loader.js"></script>
|
||||
<script defer src="js/service.js"></script>
|
||||
<script defer src="js/base_module.js"></script>
|
||||
<script defer src="js/cover_screen.js"></script>
|
||||
<script defer src="js/app.js"></script>
|
||||
<script defer src="js/bootstrap.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
17
gaia/apps/system/index_remote.html
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="style/remote/system_remote.css">
|
||||
<script defer src="shared/js/lazy_loader.js"></script>
|
||||
<script defer src="js/service.js"></script>
|
||||
<script defer src="js/base_module.js"></script>
|
||||
<script defer src="js/browser_frame.js"></script>
|
||||
<script defer src="js/remote/app.js"></script>
|
||||
<script defer src="js/remote/bootstrap.js"></script>
|
||||
</head>
|
||||
<body id="screen">
|
||||
<div id="windows"></div>
|
||||
</body>
|
||||
</html>
|
||||
121
gaia/apps/system/js/about_service_workers_proxy.js
Executable 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));
|
||||
838
gaia/apps/system/js/accessibility.js
Executable 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));
|
||||
108
gaia/apps/system/js/accessibility_quicknav_menu.js
Executable 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));
|
||||
162
gaia/apps/system/js/action_menu.js
Executable 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));
|
||||
320
gaia/apps/system/js/activities.js
Executable 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;
|
||||
}
|
||||
});
|
||||
|
||||
}());
|
||||
37
gaia/apps/system/js/activity_handler.js
Executable 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));
|
||||
264
gaia/apps/system/js/activity_window.js
Executable 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
|
||||
* 
|
||||
*
|
||||
* @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));
|
||||
144
gaia/apps/system/js/activity_window_manager.js
Executable 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));
|
||||
678
gaia/apps/system/js/advanced_telemetry.js
Executable 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));
|
||||
291
gaia/apps/system/js/airplane_mode.js
Executable 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
23
gaia/apps/system/js/airplane_mode_icon.js
Executable 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));
|
||||
185
gaia/apps/system/js/airplane_mode_service_helper.js
Executable 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}());
|
||||
23
gaia/apps/system/js/alarm_icon.js
Executable 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));
|
||||
24
gaia/apps/system/js/alarm_monitor.js
Executable 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();
|
||||
}
|
||||
});
|
||||
}());
|
||||
84
gaia/apps/system/js/app.js
Executable 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));
|
||||
215
gaia/apps/system/js/app_authentication_dialog.js
Executable 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));
|
||||
1294
gaia/apps/system/js/app_chrome.js
Executable file
66
gaia/apps/system/js/app_core.js
Executable 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'
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
}());
|
||||
872
gaia/apps/system/js/app_install_manager.js
Executable 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));
|
||||
75
gaia/apps/system/js/app_migrator.js
Executable 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);
|
||||
488
gaia/apps/system/js/app_modal_dialog.js
Executable 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));
|
||||
200
gaia/apps/system/js/app_statusbar.js
Executable 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));
|
||||
503
gaia/apps/system/js/app_text_selection_dialog.js
Executable 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));
|
||||
418
gaia/apps/system/js/app_transition_controller.js
Executable 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 #####
|
||||
* 
|
||||
*
|
||||
* ##### State machine #####
|
||||
* 
|
||||
*
|
||||
* @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));
|
||||
1134
gaia/apps/system/js/app_usage_metrics.js
Executable file
2635
gaia/apps/system/js/app_window.js
Executable file
298
gaia/apps/system/js/app_window_factory.js
Executable 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>.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* @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));
|
||||
1129
gaia/apps/system/js/app_window_manager.js
Executable file
210
gaia/apps/system/js/applications.js
Executable file
|
|
@ -0,0 +1,210 @@
|
|||
/* -*- 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 Applications, applications*/
|
||||
/** Application module handles the information of apps on behalf of other
|
||||
* modules
|
||||
* @class Applications
|
||||
*/
|
||||
(function(exports) {
|
||||
function Applications() {
|
||||
}
|
||||
|
||||
Applications.prototype = {
|
||||
/**
|
||||
* The installed APPs list
|
||||
* @access public
|
||||
* @type {Object}
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
installedApps: {},
|
||||
|
||||
/**
|
||||
* The statuts about get all installed Apps by mozApps API.
|
||||
* @access public
|
||||
* @type {boolean}
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
ready: false,
|
||||
|
||||
/**
|
||||
* Stop the Applications services,
|
||||
* un-register apps.mgmt.oninstall and apps.mgmt.uninstall handler.
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
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 the Applications to get all installed Apps and
|
||||
* register apps.mgmt.oninstall and apps.mgmt.uninstall handler.
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
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;
|
||||
// TODO Followup for retrieving homescreen & comms app
|
||||
});
|
||||
|
||||
self.ready = true;
|
||||
self.fireApplicationReadyEvent();
|
||||
self.waitingResolve && self.waitingResolve();
|
||||
};
|
||||
};
|
||||
|
||||
// We need to wait for the chrome shell to let us know when it's ok to
|
||||
// launch activities. This prevents race conditions.
|
||||
// The event does not fire again when we reload System app in on
|
||||
// B2G Desktop, so we save the information into sessionStorage.
|
||||
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);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get App by ManifestURL.
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
getByManifestURL: function a_getByManifestURL(manifestURL) {
|
||||
if (manifestURL in this.installedApps) {
|
||||
return this.installedApps[manifestURL];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcast ApplicationReadyEvent when mozApps.mgmt.getAll() done.
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
|
||||
fireApplicationReadyEvent: function a_fireAppReadyEvent() {
|
||||
var evt = new CustomEvent('applicationready',
|
||||
{ bubbles: true,
|
||||
cancelable: false,
|
||||
detail: { applications: this.installedApps } });
|
||||
window.dispatchEvent(evt);
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcast ApplicationInstallEvent when apps.mgmt.oninstall occured.
|
||||
* We need to dispatch the following events because
|
||||
* mozApps is not doing so right now.
|
||||
* ref: @link https://bugzilla.mozilla.org/show_bug.cgi?id=731746
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
|
||||
fireApplicationInstallEvent: function a_fireApplicationInstallEvent(app) {
|
||||
var evt = new CustomEvent('applicationinstall',
|
||||
{ bubbles: true,
|
||||
cancelable: false,
|
||||
detail: { application: app } });
|
||||
window.dispatchEvent(evt);
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcast ApplicationUninstallEvent when apps.mgmt.onuninstall occured.
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
|
||||
fireApplicationUninstallEvent:
|
||||
function a_fireApplicationUninstallEvent(app) {
|
||||
var evt = new CustomEvent('applicationuninstall',
|
||||
{ bubbles: true,
|
||||
cancelable: false,
|
||||
detail: { application: app } });
|
||||
window.dispatchEvent(evt);
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcast ApplicationEnabledEvent when apps.mgmt.onenabledstatechange
|
||||
* occured when the application was enabled.
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
|
||||
fireApplicationEnabledEvent:
|
||||
function a_fireApplicationEnabledEvent(app) {
|
||||
var evt = new CustomEvent('applicationenabled',
|
||||
{ bubbles: true,
|
||||
cancelable: false,
|
||||
detail: { application: app } });
|
||||
window.dispatchEvent(evt);
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcast ApplicationDisabledEvent when apps.mgmt.onenabledstatechange
|
||||
* occured when the application was enabled.
|
||||
* @memberof Applications.prototype
|
||||
*/
|
||||
|
||||
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();
|
||||
207
gaia/apps/system/js/attention_toaster.js
Executable 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));
|
||||
299
gaia/apps/system/js/attention_window.js
Executable 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));
|
||||
312
gaia/apps/system/js/attention_window_manager.js
Executable 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));
|
||||
255
gaia/apps/system/js/audio_channel_controller.js
Executable 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));
|
||||
288
gaia/apps/system/js/audio_channel_policy.js
Executable 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;
|
||||
}
|
||||
});
|
||||
}());
|
||||
266
gaia/apps/system/js/audio_channel_service.js
Executable 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}());
|
||||
208
gaia/apps/system/js/base_icon.js
Executable 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));
|
||||
787
gaia/apps/system/js/base_module.js
Executable 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));
|
||||
152
gaia/apps/system/js/base_ui.js
Executable 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));
|
||||
49
gaia/apps/system/js/battery_icon.js
Executable 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));
|
||||
147
gaia/apps/system/js/battery_overlay.js
Executable 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}());
|
||||
26
gaia/apps/system/js/bluetooth_core.js
Executable 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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}());
|
||||