'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 { // Figure out if we should process this key event here in the // system app. The browserKeyEventManager returns an event name // (like 'volume-down-button-press') if we should process it and // returns undefined otherwise. Note that the value returned // may depend on whether or not there is already a button pressed. var isButtonPressed = !(this.state instanceof HardwareButtonsBaseState); var buttonEventType = this.browserKeyEventManager.getButtonEventType(evt, isButtonPressed); if (buttonEventType) { 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));