'use strict';
/* global Service, SettingsListener, BrowserKeyEventManager, LazyLoader */
(function(exports) {
// Tell JSHint the following script are allow to use these variables.
// The actual functions will be defined too.
var HardwareButtonsBaseState;
var HardwareButtonsHomeState;
var HardwareButtonsCameraState;
var HardwareButtonsSleepState;
var HardwareButtonsVolumeState;
var HardwareButtonsWakeState;
var HardwareButtonsScreenshotState;
var HardwareButtonsSystemLogState;
/**
* After bug 989198 landing, we will be able to listen to KeyboardEvent
* (e.g. keydown and keyup). Also there will be new events called
* BeforeAfterKeybaordEvent for mozbrowser-embedder iframe, say system app,
* to control or override the bahavior of keydown/keyup event in
* mozbrowser-embedded iframes (other apps or homescreen app).
* These events are:
*
* * mozbrowserbeforekeydown
* * mozbrowserafterkeydown
* * mozbrowserbeforekeyup
* * mozbrowserafterkeyup
*
* When a key is pressed down, the event sequence would be:
* 1. `mozbrowserbeforekeydown` is dispatched to mozbrowser-embedder iframe
* 2. `keydown` is dispatched to mozbrowser-embedded iframe
* 3. `mozbrowserkeydown` is dispatched to mozbrowser-embedder iframe
*
* For detail, please see
* https://wiki.mozilla.org/WebAPI/BrowserAPI/KeyboardEvent
*
* This module listens for KeyboardEvent and BeforeAfterKeybaordEvent,
* processes them (with the help of BrowserKeyEventManager submodule)
* and generates higher-level events to handle autorepeat on
* the volume keys, long presses on Home and Sleep, and the Volume Down+Sleep
* key combination.
*
* Other system app modules should listen for the high-level button events
* generated by this module.
*
* The high-level events generated by this module are simple Event objects
* that are not cancelable and do not bubble. They are dispatched at the
* window object. The type property is set to one of these:
*
* | Event Type | Meaning |
* |--------------|----------------------------------------------------------|
* | home | short press and release of home button |
* | holdhome | long press and hold of home button |
* | sleep | short press and release of sleep button |
* | wake | sleep or home pressed while sleeping |
* | holdsleep | long press and hold of sleep button |
* | volumeup | volume up pressed and released or autorepeated |
* | volumedown | volume down pressed and released or autorepeated |
* | volumedown | volume down and sleep pressed at same time (used for |
* | + sleep | screenshots) |
* | volumedown | volume up and sleep pressed at same time (used for |
* | + volumeup | systemlog capture) |
* | camera | short press and release of camera button |
* | holdcamera | long press and hold of camera button |
*
* Because these events are fired at the window object, they cannot be
* captured. Many modules listen for the home event. Those that want
* to respond to it and prevent others from responding should call
* `stopImmediatePropagation()`. Overlays that want to prevent the window
* manager from showing the homescreen on the home event should call that
* method. Note, however, that this only works for scripts that run and
* register their event handlers before `AppWindowManager` does.
*
* As of the implementation itself, we process events with a
* finite state machine.
* Each state object has a `process()` method for handling events.
* And optionally has `enter()` and `exit()` methods called when the FSM
* enters and exits that state.
*
* @example
* var hardwareButtons = new HardwareButtons();
* hardwareButtons.start(); // Attach the event listeners.
* hardwareButtons.stop(); // Deattach the event listeners.
*
* @class HardwareButtons
* @requires Service
* @requires BrowserKeyEventManager
**/
var HardwareButtons = function HardwareButtons() {
this._started = false;
this._softwareHome = false;
};
/**
* A mapping between state keyboard and the constructor.
* @type {Object}
*/
HardwareButtons.STATES = {};
/**
* How long for press and hold Home or Sleep.
* @memberof HardwareButtons.prototype
* @type {Number}
*/
HardwareButtons.prototype.HOLD_INTERVAL = 750;
/**
* How long before volume autorepeat begins.
* @memberof HardwareButtons.prototype
* @type {Number}
*/
HardwareButtons.prototype.REPEAT_DELAY = 700;
/**
* How fast the autorepeat is.
* @memberof HardwareButtons.prototype
* @type {Number}
*/
HardwareButtons.prototype.REPEAT_INTERVAL = 100;
/**
* Start listening to events from Gecko and FSM.
* @memberof HardwareButtons.prototype
*/
HardwareButtons.prototype.start = function hb_start() {
if (this._started) {
throw 'Instance should not be start()\'ed twice.';
}
this._started = true;
// Kick off the FSM in the base state
this.state = new HardwareButtonsBaseState(this);
window.addEventListener('softwareButtonEvent', this);
// These event handler listens for hardware button events and passes the
// event type to the process() method of the current state for processing
window.addEventListener('mozbrowserbeforekeydown', this);
window.addEventListener('mozbrowserbeforekeyup', this);
window.addEventListener('mozbrowserafterkeydown', this);
window.addEventListener('mozbrowserafterkeyup', this);
window.addEventListener('keydown', this);
window.addEventListener('keyup', this);
SettingsListener.observe('software-button.enabled', false, function(value) {
this._softwareHome = value;
}.bind(this));
return LazyLoader.load(['js/browser_key_event_manager.js']).then(() => {
// initiate BrowserKeyEventManager submodule
this.browserKeyEventManager = new BrowserKeyEventManager();
});
};
/**
* Stop listening to events. Must call before throwing away the instance
* to avoid memory leaks.
* @memberof HardwareButtons.prototype
*/
HardwareButtons.prototype.stop = function hb_stop() {
if (!this._started) {
throw 'Instance was never start()\'ed but stop() is called.';
}
this._started = false;
// Exit the current state()
if (this.state && this.state.exit) {
this.state.exit();
}
this.state = null;
window.removeEventListener('softwareButtonEvent', this);
window.removeEventListener('mozbrowserbeforekeydown', this);
window.removeEventListener('mozbrowserbeforekeyup', this);
window.removeEventListener('mozbrowserafterkeydown', this);
window.removeEventListener('mozbrowserafterkeyup', this);
window.removeEventListener('keydown', this);
window.removeEventListener('keyup', this);
};
/**
* Dispatch a high-level event of the specified type.
* @memberof HardwareButtons.prototype
* @param {String} type name of the event.
*/
HardwareButtons.prototype.publish = function hb_publish(type) {
window.dispatchEvent(new CustomEvent(type, { bubbles: type === 'home' }));
};
/**
* Transit the FSM to a new state.
* @memberof HardwareButtons.prototype
* @param {String} s Keyword of the new state to switch to.
* @param {String} type Name of the event to handle
*/
HardwareButtons.prototype.setState = function hb_setState(s, type) {
// Exit the current state()
if (this.state && this.state.exit) {
this.state.exit(type);
}
this.state = new HardwareButtons.STATES[s](this);
// Enter the new this.state
if (this.state && this.state.enter) {
this.state.enter(type);
}
};
/**
* Handle events from Gecko.
* @memberof HardwareButtons.prototype
* @param {Object} evt Event.
*/
HardwareButtons.prototype.handleEvent = function hb_handleEvent(evt) {
// When the software home button is displayed we ignore the hardware
// home button if there is one
var hardwareHomeEvent =
(this.browserKeyEventManager.isHardwareKeyEvent(evt.type)) &&
this.browserKeyEventManager.isHomeKey(evt);
if (this._softwareHome && hardwareHomeEvent) {
// Given that software home button is enabled, when we receive
// mozbrowserbeforekeydown/mozbrowserbeforekeyup event of home button in
// system app, Gecko will dispatch keydown/keyup events to embedded frame
// (i.e. Homescreen app or web pages in browser). Even though these apps
// or web pages do not listen to 'Home' key events, there are still
// default action. The default action usually is to scroll to top of the
// page. That's why we need to call preventDefault() here.
// See https://bugzil.la/1105285#c12 for detail.
evt.preventDefault();
return;
}
if (evt.type === 'softwareButtonEvent') {
var type = evt.detail.type;
switch (type) {
case 'home-button-press':
case 'home-button-release':
case 'sleep-button-press':
case 'sleep-button-release':
case 'volume-up-button-press':
case 'volume-up-button-release':
case 'volume-down-button-press':
case 'volume-down-button-release':
this.state.process(type);
break;
}
} else {
var buttonEventType = this.browserKeyEventManager.getButtonEventType(evt);
// having a falsey buttonEventType value means the event should be
// ignored by System
if (!buttonEventType) {
return;
}
this.state.process(buttonEventType);
}
};
/**
* The base state is the default, when no hardware buttons are pressed.
*
* @class HardwareButtonsBaseState
*/
HardwareButtonsBaseState =
HardwareButtons.STATES.base = function HardwareButtonsBaseState(hb) {
this.hardwareButtons = hb;
};
/**
* Process the event, maybe transition the state.
* @memberof HardwareButtonsBaseState.prototype
* @param {String} type Name of the event to process.
*/
HardwareButtonsBaseState.prototype.process = function(type) {
switch (type) {
case 'home-button-press':
/**
* If the phone is sleeping, then pressing Home wakes it
* (on press, not release)
* @event HardwareButtonsBaseState#wake
*/
// XXX: Unresolved dependency to screenManager
if (!Service.query('screenEnabled')) {
this.hardwareButtons.publish('wake');
this.hardwareButtons.setState('wake', type);
} else {
this.hardwareButtons.setState('home', type);
}
return;
case 'sleep-button-press':
/**
* If the phone is sleeping, then pressing Home wakes it
* (on press, not release)
* @event HardwareButtonsBaseState#wake
*/
// XXX: Unresolved dependency to screenManager
if (!Service.query('screenEnabled')) {
this.hardwareButtons.publish('wake');
this.hardwareButtons.setState('wake', type);
} else {
this.hardwareButtons.setState('sleep', type);
}
return;
case 'volume-up-button-press':
case 'volume-down-button-press':
this.hardwareButtons.setState('volume', type);
return;
case 'camera-button-press':
this.hardwareButtons.setState('camera', type);
return;
case 'home-button-release':
case 'sleep-button-release':
case 'volume-up-button-release':
case 'volume-down-button-release':
case 'camera-button-release':
// Ignore button releases that occur in this state.
// These can happen after volumedown+sleep and home+volume.
return;
}
console.error('Unexpected hardware key: ', type);
};
/**
* We enter the home state when the user presses the Home button
* We can fire home or holdhome events from this state
*
* @class HardwareButtonsHomeState
*/
HardwareButtonsHomeState =
HardwareButtons.STATES.home = function HardwareButtonsHomeState(hb) {
this.hardwareButtons = hb;
this.timer = undefined;
};
/**
* Entering the state.
* @memberof HardwareButtonsHomeState.prototype
*/
HardwareButtonsHomeState.prototype.enter = function() {
this.timer = setTimeout(function() {
/**
* When the user holds Home button more than HOLD_INTERVAL.
* @event HardwareButtonsHomeState#holdhome
*/
this.hardwareButtons.publish('holdhome');
navigator.vibrate(50);
this.hardwareButtons.setState('base');
}.bind(this), this.hardwareButtons.HOLD_INTERVAL);
};
/**
* Leaving the state.
* @memberof HardwareButtonsHomeState.prototype
*/
HardwareButtonsHomeState.prototype.exit = function() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
};
/**
* Process the event, maybe transition the state.
* @memberof HardwareButtonsHomeState.prototype
* @param {String} type Name of the event to process.
*/
HardwareButtonsHomeState.prototype.process = function(type) {
switch (type) {
case 'home-button-release':
/**
* When the user releases Home button before HOLD_INTERVAL.
* @event HardwareButtonsHomeState#home
*/
this.hardwareButtons.publish('home');
navigator.vibrate(50);
this.hardwareButtons.setState('base', type);
return;
case 'volume-up-button-press':
case 'volume-down-button-press':
case 'sleep-button-press':
this.hardwareButtons.setState('base', type);
return;
}
console.error('Unexpected hardware key: ', type);
this.hardwareButtons.setState('base', type);
};
/**
* We enter the sleep state when the user presses the Sleep button
* We can fire sleep, or holdsleep events from this state.
*
* @class HardwareButtonsSleepState
*/
HardwareButtonsSleepState =
HardwareButtons.STATES.sleep = function HardwareButtonsSleepState(hb) {
this.hardwareButtons = hb;
this.timer = undefined;
};
/**
* Entering the state.
* @memberof HardwareButtonsSleepState.prototype
*/
HardwareButtonsSleepState.prototype.enter = function() {
this.timer = setTimeout(function() {
/**
* When the user holds Sleep button more than HOLD_INTERVAL.
* @event HardwareButtonsSleepState#holdsleep
*/
this.hardwareButtons.publish('holdsleep');
this.hardwareButtons.setState('base');
}.bind(this), this.hardwareButtons.HOLD_INTERVAL);
};
/**
* Leaving the state.
* @memberof HardwareButtonsSleepState.prototype
*/
HardwareButtonsSleepState.prototype.exit = function() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
};
/**
* Process the event, maybe transition the state.
* @memberof HardwareButtonsSleepState.prototype
* @param {String} type Name of the event to process.
*/
HardwareButtonsSleepState.prototype.process = function(type) {
switch (type) {
case 'sleep-button-release':
/**
* When the user releases Sleep button before HOLD_INTERVAL.
* @event HardwareButtonsSleepState#sleep
*/
this.hardwareButtons.publish('sleep');
this.hardwareButtons.setState('base', type);
return;
case 'volume-down-button-press':
/**
* When the user presses Volume Down button, before HOLD_INTERVAL,
* while holding the Sleep button.
*/
this.hardwareButtons.setState('screenshot', type);
return;
case 'home-button-press':
this.hardwareButtons.setState('base', type);
return;
}
console.error('Unexpected hardware key: ', type);
this.hardwareButtons.setState('base', type);
};
/**
* We enter the volume state when the user presses the volume up or
* volume down buttons.
* We can fire volumeup and volumedown events from this state
*
* @class HardwareButtonsVolumeState
*
*/
HardwareButtonsVolumeState =
HardwareButtons.STATES.volume = function HardwareButtonsVolumeState(hb) {
this.hardwareButtons = hb;
this.timer = undefined;
this.repeating = false;
};
/**
* Trigger repeat actions for volume buttons.
* @memberof HardwareButtonsVolumeState.prototype
*/
HardwareButtonsVolumeState.prototype.repeat = function() {
this.repeating = true;
if (this.direction === 'volume-up-button-press') {
/**
* Volume up pressed and released or autorepeated.
* @event HardwareButtonsVolumeState#volumeup
*/
this.hardwareButtons.publish('volumeup');
} else {
/**
* Volume down pressed and released or autorepeated.
* @event HardwareButtonsVolumeState#volumedown
*/
this.hardwareButtons.publish('volumedown');
}
this.timer = setTimeout(this.repeat.bind(this),
this.hardwareButtons.REPEAT_INTERVAL);
};
/**
* Entering the state.
* @memberof HardwareButtonsVolumeState.prototype
* @param {String} type Name of the event to process.
*/
HardwareButtonsVolumeState.prototype.enter = function(type) {
this.direction = type; // Is volume going up or down?
this.repeating = false;
this.timer =
setTimeout(this.repeat.bind(this), this.hardwareButtons.REPEAT_DELAY);
};
/**
* Leaving the state.
* @memberof HardwareButtonsVolumeState.prototype
*/
HardwareButtonsVolumeState.prototype.exit = function() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
};
/**
* Process the event, maybe transition the state.
* @memberof HardwareButtonsVolumeState.prototype
* @param {String} type Name of the event to process.
*/
HardwareButtonsVolumeState.prototype.process = function(type) {
switch (type) {
case 'home-button-press':
this.hardwareButtons.setState('base', type);
return;
case 'sleep-button-press':
if (this.direction === 'volume-down-button-press') {
/**
* When the user presses Sleep button, before HOLD_INTERVAL,
* while holding the Volume Down button.
*/
this.hardwareButtons.setState('screenshot', type);
return;
}
this.hardwareButtons.setState('sleep', type);
return;
case 'volume-down-button-press':
case 'volume-up-button-press':
this.hardwareButtons.setState('systemlog', type);
return;
case 'volume-up-button-release':
if (this.direction === 'volume-up-button-press') {
if (!this.repeating) {
this.hardwareButtons.publish('volumeup');
}
this.hardwareButtons.setState('base', type);
return;
}
break;
case 'volume-down-button-release':
if (this.direction === 'volume-down-button-press') {
if (!this.repeating) {
this.hardwareButtons.publish('volumedown');
}
this.hardwareButtons.setState('base', type);
return;
}
break;
default:
// Ignore anything else (such as sleep button release)
return;
}
console.error('Unexpected hardware key: ', type);
this.hardwareButtons.setState('base', type);
};
/**
* We enter this state when the user presses Home or Sleep on a sleeping
* phone. We give immediate feedback by waking the phone up on the press
* rather than waiting for the release, but this means we need a special
* state so that we don't actually send a home or sleep event on the
* key release. Note, however, that this state does set a timer so that
* it can send holdhome or holdsleep events. (This means that pressing and
* holding sleep will bring up the power menu, even on a sleeping phone.)
*
* @class HardwareButtonsWakeState
*/
HardwareButtonsWakeState =
HardwareButtons.STATES.wake = function HardwareButtonsWakeState(hb) {
this.hardwareButtons = hb;
this.timer = undefined;
this.delegateState = null;
};
/**
* Entering the state.
* @memberof HardwareButtonsWakeState.prototype
*/
HardwareButtonsWakeState.prototype.enter = function(type) {
if (type === 'home-button-press') {
this.delegateState = new HardwareButtonsHomeState(this.hardwareButtons);
} else {
this.delegateState = new HardwareButtonsSleepState(this.hardwareButtons);
}
this.timer = setTimeout(function() {
if (type === 'home-button-press') {
/**
* When the user holds Home button more than HOLD_INTERVAL.
* @event HardwareButtonsWakeState#holdhome
*/
this.hardwareButtons.publish('holdhome');
} else if (type === 'sleep-button-press') {
/**
* When the user holds Sleep button more than HOLD_INTERVAL.
* @event HardwareButtonsWakeState#holdsleep
*/
this.hardwareButtons.publish('holdsleep');
}
this.hardwareButtons.setState('base', type);
}.bind(this), this.hardwareButtons.HOLD_INTERVAL);
};
/**
* Leaving the state.
* @memberof HardwareButtonsWakeState.prototype
*/
HardwareButtonsWakeState.prototype.exit = function() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
};
/**
* Process the event, maybe transition the state.
* @memberof HardwareButtonsWakeState.prototype
* @param {String} type Name of the event to process.
*/
HardwareButtonsWakeState.prototype.process = function(type) {
switch (type) {
case 'home-button-release':
case 'sleep-button-release':
this.hardwareButtons.setState('base', type);
return;
default:
this.delegateState.process(type);
return;
}
};
/**
* We enter the screenshot home state when the user presses the Power button
* and Volume Down button within less than HOLD_INTERVAL of each other
* We can fire home or holdhome events from this state
*
* @class HardwareButtonsScreenshotState
*/
HardwareButtonsScreenshotState =
HardwareButtons.STATES.screenshot =
function HardwareButtonsScreenshotState(hb) {
this.hardwareButtons = hb;
this.timer = undefined;
};
/**
* Entering the state.
* @memberof HardwareButtonsScreenshotState.prototype
*/
HardwareButtonsScreenshotState.prototype.enter = function() {
this.timer = setTimeout(function() {
/**
* When the user holds Volume Down and Power button
* more than HOLD_INTERVAL.
* @event HardwareButtonsHomeState#volumedown+sleep
*/
this.hardwareButtons.publish('volumedown+sleep');
this.hardwareButtons.setState('base');
}.bind(this), this.hardwareButtons.HOLD_INTERVAL);
};
/**
* Leaving the state.
* @memberof HardwareButtonsScreenshotState.prototype
*/
HardwareButtonsScreenshotState.prototype.exit = function() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
};
/**
* Pressing any other hardware button will cancel this state.
* @memberof HardwareButtonsScreenshotState.prototype
* @param {String} type Name of the event to process.
*/
HardwareButtonsScreenshotState.prototype.process = function(type) {
this.hardwareButtons.setState('base', type);
};
/**
* We enter the systemlog home state when the user presses the Power button
* and Volume Down button within less than HOLD_INTERVAL of each other
* We can fire home or holdhome events from this state
*
* @class HardwareButtonsSystemLogState
*/
HardwareButtonsSystemLogState =
HardwareButtons.STATES.systemlog =
function HardwareButtonsSystemLogState(hb) {
this.hardwareButtons = hb;
this.timer = undefined;
};
/**
* Entering the state.
* @memberof HardwareButtonsSystemLogState.prototype
*/
HardwareButtonsSystemLogState.prototype.enter = function() {
this.timer = setTimeout(function() {
/**
* When the user holds Volume Up and Volume Down button
* more than HOLD_INTERVAL.
* @event HardwareButtonsHomeState#volumeup+volumedown
*/
this.hardwareButtons.publish('volumeup+volumedown');
this.hardwareButtons.setState('base');
}.bind(this), this.hardwareButtons.HOLD_INTERVAL);
};
/**
* Leaving the state.
* @memberof HardwareButtonsSystemLogState.prototype
*/
HardwareButtonsSystemLogState.prototype.exit = function() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
};
/**
* Pressing any other hardware button will cancel this state.
* @memberof HardwareButtonsSystemLogState.prototype
* @param {String} type Name of the event to process.
*/
HardwareButtonsSystemLogState.prototype.process = function(type) {
this.hardwareButtons.setState('base', type);
};
/**
* We enter the camera state when the user presses the Camera button
* We can fire camera or holdcamera events from this state
*
* @class HardwareButtonsCameraState
*/
HardwareButtonsCameraState =
HardwareButtons.STATES.camera = function HardwareButtonsCameraState(hb) {
this.hardwareButtons = hb;
this.timer = undefined;
};
/**
* Entering the state.
* @memberof HardwareButtonsCameraState.prototype
*/
HardwareButtonsCameraState.prototype.enter = function() {
this.timer = setTimeout(function() {
/**
* When the user holds Camera button more than HOLD_INTERVAL.
* @event HardwareButtonsCameraState#holdcamera
*/
this.hardwareButtons.publish('holdcamera');
navigator.vibrate(50);
this.hardwareButtons.setState('base');
}.bind(this), this.hardwareButtons.HOLD_INTERVAL);
};
/**
* Leaving the state.
* @memberof HardwareButtonsCameraState.prototype
*/
HardwareButtonsCameraState.prototype.exit = function() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
};
/**
* Process the event, maybe transition the state.
* @memberof HardwareButtonsCameraState.prototype
* @param {String} type Name of the event to process.
*/
HardwareButtonsCameraState.prototype.process = function(type) {
switch (type) {
case 'camera-button-release':
/**
* When the user releases Camera button before HOLD_INTERVAL.
* @event HardwareButtonsCameraState#camera
*/
this.hardwareButtons.setState('base', type);
return;
}
this.hardwareButtons.setState('base', type);
};
exports.HardwareButtons = HardwareButtons;
}(window));