VulpesOS/assets/webapps/clock.gaiamobile.org/js/onring.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

1287 lines
39 KiB
JavaScript

requirejs.config({
// waitSeconds is set to the default here; the build step rewrites
// it to 0 in build/require_config.jslike so that we never timeout
// waiting for modules in production. This is important when the
// device is under super-low-memory stress, as it may take a while
// for the device to get around to loading things like Clock's alarm
// ringing screen, and we absolutely do not want that to time out.
waitSeconds: 0,
paths: {
shared: '../shared'
},
shim: {
'shared/js/template': {
exports: 'Template'
},
'shared/js/gesture_detector': {
exports: 'GestureDetector'
},
'shared/js/async_storage': {
exports: 'asyncStorage'
},
'shared/js/accessibility_helper': {
exports: 'AccessibilityHelper'
}
}
});
define("require_config", function(){});
define('constants',['require','exports','module'],function(require, exports) {
'use strict';
// ---------------------------------------------------------
// Constants
// Random day that happens to be Sunday
exports.KNOWN_SUNDAY = new Date(2015, 8, 20);
});
define('utils',['require','constants'],function(require) {
'use strict';
/* global mozIntl, IntlHelper */
var constants = require('constants');
var Utils = {};
// Maintain references to millisecond multipliers
var dateMultipliers = {
days: 1000 * 60 * 60 * 24,
hours: 1000 * 60 * 60,
minutes: 1000 * 60,
seconds: 1000,
milliseconds: 1
};
var units = Object.keys(dateMultipliers);
IntlHelper.define('weekday-short', 'datetime', {
weekday: 'short'
});
IntlHelper.define('time-html', 'mozdatetime', {
dayperiod: true,
hour: 'numeric',
minute: 'numeric',
});
IntlHelper.define('time-text', 'datetime', {
hour: 'numeric',
minute: 'numeric',
});
/**
* Define a singleton method that returns a unified instance
* based on arguments.
*
* @param {function} constructor - A constructor function used
* to create a new instance.
* @param {function} [getKey] - A function called with (arguments),
* and returns a lookup key for this singleton.
* @return {object} - returns the instance either created or retrieved
* from the singleton-map by the key.
*/
Utils.singleton = function(constructor, getKey) {
var singletonMap = new Map();
return function() {
var arglist = Array.prototype.slice.call(arguments);
var key = (typeof getKey === 'function') ? getKey(arglist) : constructor;
var instance = singletonMap.get(key);
if (!instance) {
instance = Object.create(constructor.prototype);
constructor.apply(instance, arglist);
singletonMap.set(key, instance);
}
return instance;
};
};
Utils.memoizedDomPropertyDescriptor = function(selector) {
var memoizedValue = null;
return {
get: function() {
if (memoizedValue === null) {
memoizedValue = document.querySelector(selector);
}
return memoizedValue;
},
set: function(value) {
memoizedValue = value;
}
};
};
/**
* Extend the given prototype object with lazy getters.
* selectorMap is a mapping of { propertyName: selector }.
*/
Utils.extendWithDomGetters = function(proto, selectorMap) {
for (var property in selectorMap) {
Object.defineProperty(proto, property,
Utils.memoizedDomPropertyDescriptor(selectorMap[property]));
}
return proto;
};
Utils.dateMath = {
/**
* Convert object literals containing interval length to milliseconds
*
* @param {Object|Date|Number} interval An object literal containing days,
* hours, minutes etc.
* Optionally a number or date object.
* @param {Object} opts Options object with a unitsPartial property containing
* (if desired) a restriction on which properties will be
* searched on the interval.
* @return {Number} Millisecond value for interval length.
*/
toMS: function(interval, opts) {
var converted, sign, unitsPartial;
// if a millisecond interval or a Date is passed in, return that
if (interval instanceof Date || typeof interval === 'number') {
return +interval;
}
opts = opts || {};
unitsPartial = opts.unitsPartial || units;
// Allow for 'hours' or 'hour'
unitsPartial = unitsPartial.map(function(unit) {
// String.prototype.endsWith is available in FF17+
return unit.endsWith('s') ? unit : unit.concat('s');
});
// some will exit early when it returns a truthy value
sign = unitsPartial.some(function(unit) {
return interval[unit] < 0;
});
// Using as a multiplier later
sign = sign ? -1 : 1;
// collect passed in units and multiply by their millisecond/unit count
converted = unitsPartial.map(function(unit) {
var partial;
// we're storing the sign out of the iterator
partial = Math.abs(interval[unit]);
// A missing property and 0 should be treated the same
return partial ? partial * dateMultipliers[unit] : 0;
});
// add up each millisecond-converted term and multiply total by sign
return sign * converted.reduce(function(a, b) { return a + b; });
},
/**
* Convert millisecond values to object literals conformable to toMS()
*
* @param {Number} interval A millisecond value.
* @param {Object} [opts] Options object with a unitsPartial property
* containing (if desired) a restriction on which
* properties will be reated for the return value.
* @return {Object} Object literal with properties as deliniated by opts.
*/
fromMS: function(interval, opts) {
var times, sign, unitsPartial;
opts = opts || {};
unitsPartial = opts.unitsPartial || units;
// Allow for 'hours' or 'hour'
unitsPartial = unitsPartial.map(function(unit) {
// String.prototype.endsWith is available in FF17+
return unit.endsWith('s') ? unit : unit.concat('s');
});
// For negative intervals (time previous to now)
// update interval to absolute value and store the sign
// to apply to all units
if (interval < 0) {
sign = -1;
interval = Math.abs(interval);
} else {
sign = 1;
}
// divide the time interval by the highest millisecond multiplier
// store the truncated result and subtract that from the interval
// update the interval to be the remainder
times = unitsPartial.map(function(unit, index) {
var truncated, mult;
mult = dateMultipliers[unit];
truncated = Math.floor(interval / mult);
interval = interval - (truncated * mult);
// units are either all positive or negative
// only iterate to needed specificity
return sign * truncated;
});
// Populate the returned object using units as property names
// and times for values
return times.reduce(function(out, unitTime, index) {
out[unitsPartial[index]] = unitTime;
return out;
}, {});
}
};
Utils.extend = function(initialObject, extensions) {
// extend({}, a, b, c ... d) -> {...}
// rightmost properties (on 'd') take precedence
extensions = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < extensions.length; i++) {
var extender = extensions[i];
for (var prop in extender) {
var descriptor = Object.getOwnPropertyDescriptor(extender, prop);
if (descriptor && descriptor.value !== undefined) {
initialObject[prop] = extender[prop];
}
}
}
return initialObject;
};
Utils.escapeHTML = function(str, escapeQuotes) {
var span = document.createElement('span');
span.textContent = str;
if (escapeQuotes) {
return span.innerHTML.replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
}
return span.innerHTML;
};
Utils.getLocalizedTimeHtml = function(date) {
var f = IntlHelper.get('time-html');
return f.format(date, {
dayperiod: '<small>$&</small>'
});
};
Utils.getLocalizedTimeText = function(date) {
var f = IntlHelper.get('time-text');
return f.format(date);
};
Utils.changeSelectByValue = function(selectElement, value) {
var options = selectElement.options;
for (var i = 0; i < options.length; i++) {
if (options[i].value == value) {
if (selectElement.selectedIndex != i) {
selectElement.selectedIndex = i;
}
break;
}
}
};
Utils.getSelectedValueByIndex = function(selectElement) {
return selectElement.options[selectElement.selectedIndex].value;
};
var wakeTarget = {
requests: {
cpu: new Map(), screen: new Map(), wifi: new Map()
},
locks: {
cpu: null, screen: null, wifi: null
},
timeouts: {
cpu: null, screen: null, wifi: null
}
};
function getLongestLock(type) {
var max = 0;
for (var i of wakeTarget.requests[type]) {
var request = i[1];
if (request.time > max) {
max = request.time;
}
}
return {
time: max,
lock: wakeTarget.locks[type],
timeout: wakeTarget.timeouts[type]
};
}
Utils.safeWakeLock = function(opts, fn) {
/*
* safeWakeLock
*
* Create a Wake lock that is automatically released after
* timeoutMs. Locks are reentrant, and have no meaningful mutual
* exclusion behavior.
*
* @param {Object} options - an object containing
* [type] {string} a string passed to requestWakeLock
* default = 'cpu'. This string can be any
* resource exposed by the environment that
* this application was designed to run in.
* Gaia exposes three of them: 'cpu',
* 'screen', and 'wifi'. Certified apps may
* expose more.
* timeoutMs {number} number of milliseconds to hold
* the lock.
* @param {Function} callback - a function to be called after all other
* generated callbacks have been called.
* function ([err]) -> undefined.
*/
opts = opts || {};
var type = opts.type || 'cpu';
var timeoutMs = opts.timeoutMs | 0;
var now = Date.now();
var myKey = {};
wakeTarget.requests[type].set(myKey, {
time: now + timeoutMs
});
var max = getLongestLock(type);
var unlockFn = function() {
if (!myKey) {
return;
}
wakeTarget.requests[type]. delete(myKey);
var now = Date.now();
var max = getLongestLock(type);
if (max.time > now) {
clearTimeout(wakeTarget.timeouts[type]);
wakeTarget.timeouts[type] = setTimeout(unlockFn, max.time - now);
} else {
if (wakeTarget.locks[type]) {
wakeTarget.locks[type].unlock();
}
wakeTarget.locks[type] = null;
clearTimeout(wakeTarget.timeouts[type]);
wakeTarget.timeouts[type] = null;
}
myKey = null;
};
clearTimeout(wakeTarget.timeouts[type]);
wakeTarget.timeouts[type] = setTimeout(unlockFn, max.time - now);
try {
if (!wakeTarget.locks[type] && max.time > now) {
wakeTarget.locks[type] = navigator.requestWakeLock(type);
}
fn(unlockFn);
} catch (err) {
unlockFn();
throw err;
}
};
Utils.async = {
generator: function(latchCallback) {
/*
* Generator
*
* Create an async generator. Each time the generator is
* called, it will return a new callback. When all issued
* callbacks have been called, the latchCallback is called.
*
* If any of the callbacks are called with and error as
* the first argument, the latchCallback will be called
* immediately with that error.
*
* @latchCallback {Function} a function to be called after
* all other generated callbacks have been
* called
* function ([err]) -> undefined
*/
var tracker = new Map();
var issuedCallbackCount = 0;
var disabled = false;
var testFn = function(err) {
var trackerSize;
if (!disabled) {
// FF18 defines size to be a method, so we need to test here:
// Remove with FF18 support
if (typeof tracker.size === 'function') {
trackerSize = tracker.size();
} else {
trackerSize = tracker.size;
}
if (err || trackerSize === issuedCallbackCount) {
disabled = true;
latchCallback && latchCallback(err);
}
}
};
return function() {
return (function() {
var i = issuedCallbackCount++;
return function(err) {
tracker.set(i, true);
testFn(err);
};
})();
};
},
namedParallel: function(names, latchCallback) {
/*
* namedParallel
*
* Create an async namedParallel.
*
* The return value is an object containing the parameters
* specified in the names array. Each parameter is set to
* a callback. When all callbacks have been called, latchCallback
* is called.
*
* If any named callback is called with an error as the first
* parameter, latchCallback is immediately called with that
* error. Future calls to callbacks are then no-ops.
*
* @names {List<String>} - A list of strings to be used as
* parameter names for callbacks on the returned object.
*/
var generator = Utils.async.generator(latchCallback);
var done = generator();
var ret = {};
for (var i = 0; i < names.length; i++) {
ret[names[i]] = generator();
}
done();
return ret;
}
};
Utils.data = {
defaultCompare: function ud_defaultCompare(a, b) {
if (typeof a === 'number' && typeof b === 'number') {
var diff = a - b;
return diff !== 0 ? diff / Math.abs(diff) : diff;
} else if ((typeof a === 'string' || a instanceof String) &&
(typeof b === 'string' || b instanceof String)) {
return (a < b) ? -1 : ((a > b) ? 1 : 0);
} else if (Array.isArray(a) && Array.isArray(b)) {
var commonLength = Math.min(a.length, b.length);
for (var i = 0; i < commonLength; i++) {
var compareResult = Utils.data.defaultCompare(a[i], b[i]);
if (compareResult !== 0) {
return compareResult;
}
}
return b.length - a.length;
} else {
throw new Error('Cannot compare ' + JSON.stringify([a, b]));
}
},
keyedCompare: function ud_keyedCompare(key) {
return function internal_keyedCompare(a, b) {
return Utils.data.defaultCompare(a[key], b[key]);
};
},
binarySearch: function ud_binarySearch(key, list, compare) {
compare = compare || Utils.data.defaultCompare;
var botI = 0;
var topI = list.length;
var midI, comp, value;
if (list.length > 0) {
do {
midI = botI + ((topI - botI) / 2) | 0;
value = list[midI];
comp = compare(value, key);
if (comp < 0) {
botI = midI + 1;
} else if (comp > 0) {
topI = midI - 1;
}
} while (comp !== 0 && botI < topI);
}
midI = comp === 0 ? midI : topI;
value = list[midI];
if (comp === 0 || value && compare(value, key) === 0) {
return { match: true, index: midI, value: value };
} else {
var index;
if (0 > midI) {
index = 0;
} else if (midI >= list.length) {
index = list.length;
} else {
index = midI + (compare(value, key) < 0 ? 1 : 0);
}
return {
match: false,
index: index
};
}
},
sortedInsert: function ud_sortedInsert(item, list, compare, unique) {
compare = compare || Utils.data.defaultCompare;
var bs = Utils.data.binarySearch(item, list, compare);
var inserted = !bs.match || !unique;
if (inserted) {
list.splice(bs.index, 0, item);
}
return (inserted) ? bs.index : null;
},
sortedRemove: function ud_sortedRemove(item, list, compare, multiple) {
compare = compare || Utils.data.defaultCompare;
var removed = false;
if (!multiple) {
var bs = Utils.data.binarySearch(item, list, compare);
if (bs.match) {
list.splice(bs.index, 1);
removed = true;
}
} else {
var biasedCompare = function(target, result, ic) {
return function(a, b) {
if (ic(a, target) === 0) {
return result;
} else {
return ic(a, b);
}
};
};
var leftBound = Utils.data.binarySearch(item, list,
biasedCompare(item, 1, compare));
var rightBound = Utils.data.binarySearch(item, list,
biasedCompare(item, -1, compare));
if (leftBound.index < rightBound.index) {
list.splice(leftBound.index, rightBound.index - leftBound.index);
removed = true;
}
}
return removed;
}
};
Utils.addEventListenerOnce = function(element, type, fn, useCapture) {
var handler = function(evt) {
element.removeEventListener(type, handler, useCapture);
fn(evt);
};
element.addEventListener(type, handler, useCapture);
};
Utils.summarizeDaysOfWeek = function(repeat) {
var days = [];
if (repeat) {
for (var day in repeat) {
if (repeat[day]) {
days.push(day);
}
}
}
if (days.length === 7) {
return Promise.resolve('everyday');
} else if (days.length === 5 &&
days.indexOf('6') === -1 && // Saturday
days.indexOf('0') === -1) { // Sunday
return Promise.resolve('weekdays');
} else if (days.length === 2 &&
days.indexOf('6') !== -1 && // Saturday
days.indexOf('0') !== -1) { // Sunday
return Promise.resolve('weekends');
} else if (days.length === 0) {
return Promise.resolve('never');
} else {
var formatter = IntlHelper.get('weekday-short');
var repeatStrings = [];
for (var i = 0; i < 7; i++) {
if (days.indexOf(i.toString()) !== -1) {
var dayDate = new Date(constants.KNOWN_SUNDAY);
dayDate.setDate(constants.KNOWN_SUNDAY.getDate() + i);
repeatStrings.push(formatter.format(dayDate));
}
}
return mozIntl.formatList(repeatStrings).then(listString => {
return { raw: listString };
});
}
};
return Utils;
});
/* global define */
define('audio_manager',['require'],function(require) {
'use strict';
/**
* The Settings App stores volumes in the range [0, 15] inclusive.
* Whenever we need to play sounds, though, the Audio object
* requires a float between [0.0, 1.0]. The conversion has to happen
* somewhere. The AudioManager here draws the line right out of what
* gets read from mozSettings.
*
* In other words, the conversion is not important to clients of
* this class, who should treat the volume as a float with no
* conversion. The only weirdness here is that unit tests must be
* aware of the slight rounding differences when converting from a
* float to the system level.
*/
////////////////////////////////////////////////////////////////
// VolumeManager
function isValidVolume(volume) {
return (typeof volume === 'number' &&
volume <= 1.0 &&
volume >= 0.0);
}
var VOLUME_SETTING = 'audio.volume.alarm';
var SYSTEM_VOLUME_MAX = 15;
function systemVolumeToFloat(volume) {
return (volume / SYSTEM_VOLUME_MAX);
}
function floatToSystemVolume(volume) {
return Math.round(volume * SYSTEM_VOLUME_MAX);
}
function requestAlarmSystemVolume() {
// Asynchronously load the alarm volume from mozSettings.
return new Promise(function(resolve, reject) {
var lock = navigator.mozSettings.createLock();
var req = lock.get(VOLUME_SETTING);
req.onsuccess = function() {
var volume = systemVolumeToFloat(req.result[VOLUME_SETTING]);
if (isValidVolume(volume)) {
globalVolumeManager._volume = volume;
resolve(volume);
}
};
req.onerror = function() {
var DEFAULT_VOLUME = 1.0;
resolve(DEFAULT_VOLUME);
};
});
}
function VolumeManager() {
this.VOLUME_KEY = 'defaultAlarmVolume';
this.DEFAULT_VOLUME = 1.0;
this._volume = this.DEFAULT_VOLUME;
if (navigator.mozSettings) {
navigator.mozSettings.addObserver(
VOLUME_SETTING,
this.onSystemAlarmVolumeChange.bind(this));
}
}
VolumeManager.prototype = {
onSystemAlarmVolumeChange: function(e) {
// don't use the setter here
this._volume = systemVolumeToFloat(e.settingValue);
},
get volume() {
return this._volume;
},
set volume(volume) {
this.setVolume(volume);
},
/** Set the volume with an optional completion callback. */
setVolume: function(volume, cb) {
if (isValidVolume(volume)) {
this._volume = volume;
if (navigator.mozSettings) {
var lock = navigator.mozSettings.createLock();
var opts = {};
opts[VOLUME_SETTING] = floatToSystemVolume(volume);
var req = lock.set(opts);
if (cb) {
req.onsuccess = cb;
}
}
}
}
};
////////////////////////////////////////////////////////////////
// AudioPlayer
var globalVolumeManager = new VolumeManager();
/**
* The AudioPlayer class manages the playback of alarm ringtones. It
* is lazy-loading, so that you can instantiate it immediately;
* Audio objects are not actually created or loaded until you need
* to play a sound.
*/
function AudioPlayer() {
this._audio = null;
}
AudioPlayer.prototype = {
/**
* Play a ringtone from the shared/resources/media/alarms
* directory, using the current global volume settings by default.
* You can override the volume through opts.volume.
*
* @param {string} ringtoneName
* @param {number} opts.volume Value between 0 and 1
*/
playRingtone: function(ringtoneName, opts) {
var volume = globalVolumeManager.volume;
if (opts && 'volume' in opts) {
volume = opts.volume;
}
this._prepare(); // Load up the audio element.
this._audio.pause();
this._audio.src = 'shared/resources/media/alarms/' + ringtoneName;
this._audio.load(); // Required per MDN's HTMLMediaElement spec.
this._audio.volume = volume;
this._audio.play();
},
/**
* Pause the currently-playing audio, if possible.
*/
pause: function() {
if (this._audio) {
this._audio.pause();
}
},
// Private methods:
/**
* Instantiate the Audio element and prepare it for playback.
* For internal use only.
* @private
*/
_prepare: function() {
if (!this._audio) {
this._audio = new Audio();
this._audio.mozAudioChannelType = 'alarm';
this._audio.loop = true;
}
}
};
return {
getAlarmVolume: function() {
return globalVolumeManager.volume;
},
requestAlarmVolume: function() {
return requestAlarmSystemVolume();
},
setAlarmVolume: function(volume, cb) {
globalVolumeManager.setVolume(volume, cb);
},
createAudioPlayer: function() {
return new AudioPlayer();
},
// Exposed for tests:
systemVolumeToFloat: systemVolumeToFloat,
floatToSystemVolume: floatToSystemVolume
};
});
define('panels/alarm/post_message_proxy',['require'],function(require) {
var objectMap = {}; // Map of object handlers.
var responseMap = {}; // Map of callbacks awaiting function results.
var sequenceNumber = 0; // ID for matching requests to responses.
var PostMessageProxy = {
/**
* Create a PostMessageProxy. This returns an object that you can
* call like any other JavaScript object, by proxying methods
* through to another window.
*
* When you call a method, rather than returning its return value,
* it reutrns a Promise that you can resolve to get the return
* value from the remote function.
*
* @param whichWindow The window (usually window.parent or a child)
* @param objectId A string to identify the object you're proxying
*/
create: function(whichWindow, objectId) {
return new Proxy({ window: whichWindow }, {
get: function(target, name) {
if (name in target) {
return target[name];
}
return function() {
var args = Array.slice(arguments);
return new Promise((resolve, reject) => {
var responseId = ++sequenceNumber;
responseMap[responseId] = {
resolve: resolve,
reject: reject
};
target.window.postMessage({
postMessageProxy: objectId,
responseId: responseId,
fn: name,
args: args
}, '*');
});
};
}
});
},
/**
* On the other window, call PostMessateProxy.receive() to hook up
* an object that processes messages from a proxy in another window.
*/
receive: function(objectId, obj) {
objectMap[objectId] = obj;
},
/**
* Handle 'message' events from postMessage, both when receiving a
* message to invoke a function call, and receiving a message with
* the return value of that function call. Both callbacks are
* handled here so that we only bind one listener for each
* relevant window.
*/
_messageHandler: function(evt) {
if (!evt.source || evt.origin !== window.location.origin || !evt.data) return;
var data = evt.data;
if (data.postMessageProxy) {
// Remote side (calling a function):
var obj = objectMap[data.postMessageProxy];
var { fn, args, responseId } = data;
try {
evt.source.postMessage({
postMessageProxyResult: responseId,
result: obj[fn].apply(obj, args)
}, window.location.origin);
} catch(e) {
evt.source.postMessage({
postMessageProxyResult: responseId,
exception: e.toString(),
stack: e.stack
}, evt.origin);
}
} else if (data.postMessageProxyResult) {
// Local side (return value):
if (responseMap[data.postMessageProxyResult]) {
var { resolve, reject } = responseMap[data.postMessageProxyResult];
delete responseMap[data.postMessageProxyResult];
if (data.exception) {
var e = new Error();
e.name = data.exception;
e.stack = data.stack;
reject(e);
} else {
resolve(data.result);
}
}
}
}
};
window.addEventListener('message', PostMessageProxy._messageHandler);
return PostMessageProxy;
});
define('panels/alarm/child_window_manager',['require','utils'],function(require) {
'use strict';
var Utils = require('utils');
const READY_EVENT_TYPE = 'childWindowReady';
/**
* ChildWindowManager maintains the lifecycle of a child attention
* window. When you instantiate a ChildWindowManager, no window
* exists yet; use .whenReady() or postMessage() to call up the
* child window and perform actions on it. Provided the child window
* calls ChildWindowManager.fireReady(), this class takes care of
* the bookkeeping to make sure that the child window is ready to
* receive events before sending over any messages. Similarly, this
* class acquires a CPU wake lock when opening the window to ensure
* that your messages are delivered reliably without the phone going
* to sleep while waiting for the child window to receive a message.
*/
function ChildWindowManager(url) {
this.url = url;
this.childWindow = null;
this.childWindowReady = false;
this.childOnReadyCallbacks = [];
this.releaseCpuLock = null;
window.addEventListener('message', this);
}
ChildWindowManager.prototype = {
/**
* Post a message to the child window. If the window is not yet
* open, acquire a CPU wake lock and open the window, then deliver
* the message. Subsequent calls to postMessage will similarly
* wait until the window is ready before delivery.
*/
postMessage: function(message) {
this.whenReady(() => {
this.childWindow.postMessage(message, window.location.origin);
});
},
/**
* Closes the window. You may reinstantiate a window again by
* sending another postMessage.
*/
close: function() {
if (this.childWindow && !this.childWindow.closed) {
this.childWindow.close();
}
this.childWindow = null;
},
/**
* Call a function when the window is ready and opened. This is
* used internally by postMessage.
*/
whenReady: function(callback) {
if (!this.childWindow || this.childWindow.closed) {
Utils.safeWakeLock({ timeoutMs: 30000 }, (releaseCpu) => {
this.childWindow = window.VulpesCompat ? VulpesCompat.openAttention(this.url) : window.open(this.url, '_blank', 'attention');
this.childWindowReady = false;
this.releaseCpuLock = releaseCpu;
});
}
if (this.childWindowReady) {
callback();
} else {
this.childOnReadyCallbacks.push(callback);
}
},
/** Private. Handle DOM events. */
handleEvent: function(evt) {
if (evt.data.type === READY_EVENT_TYPE) {
this.childWindowReady = true;
while (this.childOnReadyCallbacks.length) {
var callback = this.childOnReadyCallbacks.shift();
callback();
}
if (this.releaseCpuLock) {
this.releaseCpuLock();
}
}
}
};
/**
* Call this method from a child window when the child has loaded.
* This fires a message to the parent window, instructing it to pass
* along any queued events.
*/
ChildWindowManager.fireReady = function() {
if (!window.opener) {
throw new Error('fireReady must be called from the child window.');
}
window.opener.postMessage({
type: READY_EVENT_TYPE
}, window.location.origin);
};
return ChildWindowManager;
});
define('ring_view',['require','utils','audio_manager','./panels/alarm/post_message_proxy','./panels/alarm/child_window_manager'],function(require) {
'use strict';
var Utils = require('utils');
var AudioManager = require('audio_manager');
var PostMessageProxy = require('./panels/alarm/post_message_proxy');
var ChildWindowManager = require('./panels/alarm/child_window_manager');
/**
* RingView displays the alert screen when a timer or alarm fires.
* This screen may receive multiple different alarm/timer events;
* ideally it would display all events that fire, but currently it
* only shows the most recent event.
*/
function whenDocumentReady() {
return new Promise(function(resolve, reject) {
if (document.readyState === 'complete') {
resolve();
} else {
const listener = function() {
if (document.readyState === 'complete') {
resolve();
document.removeEventListener('readystatechange', listener);
}
};
document.addEventListener('readystatechange', listener);
}
});
}
function RingView() {
this.alerts = [];
this.ringtonePlayer = AudioManager.createAudioPlayer();
this.snoozeButton.addEventListener('click', this.onClickSnooze.bind(this));
this.closeButton.addEventListener('click', this.onClickClose.bind(this));
window.addEventListener('beforeunload', this.onBeforeUnload.bind(this));
window.addEventListener('timeformatchange', this.refreshDisplay.bind(this));
this.activeAlarm = PostMessageProxy.create(window.opener, 'activeAlarm');
PostMessageProxy.receive('ringView', this);
if (window.opener) {
// We have to wait for the document.readyState to be complete before
// we fireReady. Otherwise, when the screen is off, it doesn't get
// turned back on at all
whenDocumentReady().then(() => {
ChildWindowManager.fireReady();
});
}
}
// The time, in milliseconds, to keep the screen awake while showing
// an alarm. After this time, the screen shuts off and the alarm
// silences itself.
const WAKE_DURATION = 10 * 60 * 1000;
RingView.prototype = {
/**
* Fire the notification for an alarm or timer.
*
* Presently, we only display one notification at a time, and the
* _most recent_ one at that. Each notification gets its own wake
* lock, ensuring that the screen will remain on for WAKE_DURATION.
*
* @param {string} alert.type 'alarm' or 'timer'
* @param {string} alert.label Label to display (optional).
* @param {string} alert.sound Filename of a sound to play (optional).
* @param {boolean} alert.vibrate True if the alert should vibrate.
*/
addAlert: function(alert) {
// If we previously had an alert visible, this one is
// going to override it, as though the previous alert was
// dismissed.
if (this.alerts.length) {
var oldAlert = this.alerts.shift();
oldAlert.releaseScreenWakeLock();
}
alert.releaseScreenWakeLock = function() { };
if (this._silenceTimeout) {
clearTimeout(this._silenceTimeout);
this._silenceTimeout = null;
}
this._silenceTimeout = setTimeout(() => {
this.silence();
alert.releaseScreenWakeLock();
}, WAKE_DURATION);
// Insert this alert at the front of the stack, so that it
// overrides any previous alert that was being displayed.
this.alerts.unshift(alert);
this.refreshDisplay();
// Acquire a CPU wake lock so that we don't fall asleep waiting
// for the document to become visible. We'll only try to hold a
// lock for a few seconds as we wait for the document to become
// visible, out of an abundance of caution.
Utils.safeWakeLock({ type: 'cpu', timeoutMs: 5000 }, (releaseCpu) => {
// When the document is visible, acquire a screen wake lock so
// that we can safely display the alert.
this.whenVisible(() => {
Utils.safeWakeLock({ type: 'screen', timeoutMs: WAKE_DURATION },
(releaseScreenWakeLock) => {
// Once we have acquired the screen wake lock, we can
// release the CPU lock.
releaseCpu();
// Save off the screen wake lock for when we dismiss the
// alert; all alarms each have their own screen wake lock.
alert.releaseScreenWakeLock = releaseScreenWakeLock;
});
});
});
},
/**
* Update the display to show the currently active alert. If there
* are a stack of alerts pending, only the most recent alert is
* shown, as added to this.alerts by this.addAlert().
*/
refreshDisplay: function() {
// First, silence any existing sound or vibration. If a previous
// alarm was going off, this alarm may have different settings.
// The new alarm will replace any prior settings.
this.silence();
var alert = this.alerts[0];
// Set the label (blank or falsey becomes a default string).
if (alert.label) {
this.ringLabel.removeAttribute('data-l10n-id');
this.ringLabel.textContent = alert.label;
} else {
this.ringLabel.setAttribute('data-l10n-id',
alert.type === 'alarm' ? 'alarm' : 'timer');
}
// Display the proper screen widgets.
this.ringDisplay.dataset.ringType = alert.type;
var displayedTime;
if (alert.type === 'alarm') {
// Display the time as specified with "hour" and "minute". The timestamp
// stored by mozAlarms does not move when the timezone changes, so we
// cannot just use the value stored in '.time'. See
// https://bugzilla.mozilla.org/show_bug.cgi?id=1178208#c4
displayedTime = new Date();
displayedTime.setHours(alert.hour);
displayedTime.setMinutes(alert.minute);
} else {
// XXX: Timers may be affected by timezone changes just like alarms, but
// the fix here is not as straightforward, since we cannot take the
// same shortcut as above. Since timers are rarely used across timezone
// changes (unlike alarms), I'm punting on this for now.
displayedTime = alert.time;
}
// Set the time to display.
this.time.innerHTML = Utils.getLocalizedTimeText(displayedTime);
if (alert.sound) {
this.ringtonePlayer.playRingtone(alert.sound);
}
// Vibrate if we want to shakey shakey.
if (alert.vibrate && ('vibrate' in navigator)) {
clearInterval(this.vibrateInterval);
var vibrateOnce = function() {
navigator.vibrate([1000]);
};
this.vibrateInterval = setInterval(vibrateOnce, 2000);
vibrateOnce();
}
document.documentElement.classList.add('ready');
// If the window has been hidden, show the window.
if (document.hidden) {
window.focus();
}
},
/**
* Stop all sounds and vibration immediately.
*/
silence: function() {
// Stop the alert sound, if one was playin'.
this.ringtonePlayer.pause();
// Stop vibrating, if we were shakin'.
clearInterval(this.vibrateInterval);
this.vibrateInterval = null;
},
/**
* Clean up any state when we close this alert window. This includes
* silencing the alarm and releasing any locks we have acquired.
*/
onBeforeUnload: function(evt) {
// Clean up any wake locks we still have.
while (this.alerts.length) {
var alert = this.alerts.shift();
alert.releaseScreenWakeLock();
}
this.silence();
},
/**
* Snooze the current alarm. (The snooze button is only visible for
* alarms, not timers. Alarms have an ID; timers do not.)
*/
onClickSnooze: function(evt) {
var alert = this.alerts[0];
this.activeAlarm.snoozeAlarm(alert.id).then(() => window.close()).catch(console.error);
},
/**
* Close this window, notifying ActiveAlarm, which will pop the user
* back to the appropriate location if they are still using the
* Clock app.
*/
onClickClose: function(evt) {
var alert = this.alerts[0];
this.activeAlarm.close(alert.type, alert.id).then(() => window.close()).catch(console.error);
},
/**
* Call the callback when `document.hidden` is false. Due to a bug
* in the B2G Browser API <https://bugzil.la/810431>, the window may
* not be immediately visible, particularly if the screen is off.
* The recommended workaround for that bug was to use setTimeout. If
* the page is still hidden after that, we listen for
* `visibilitychange`. When that bug has some action, we should
* revisit how much of this method is needed.
*/
whenVisible: function(cb) {
if (!document.hidden) {
cb();
} else {
setTimeout(() => {
if (!document.hidden) {
cb();
} else {
var listener = function(e) {
if (!document.hidden) {
document.removeEventListener('visibilitychange', listener);
cb();
}
};
document.addEventListener('visibilitychange', listener);
}
});
}
}
};
Utils.extendWithDomGetters(RingView.prototype, {
time: '#ring-clock-time',
ringLabel: '#ring-label',
snoozeButton: '#ring-button-snooze',
closeButton: '#ring-button-stop',
ringDisplay: '.ring-display'
});
return RingView;
});
requirejs(['require_config'], function() {
requirejs(['ring_view'], function(RingView) {
window.ringView = new RingView();
});
});
define("onring", function(){});