/* 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 Components, XPCOMUtils, Services, PluralForm, Logger, Rect, Utils, States, Relations, Roles, dump, Events, PivotContext, PrefCache */ /* exported Utils, Logger, PivotContext, PrefCache, SettingCache */ 'use strict'; const {classes: Cc, utils: Cu, interfaces: Ci} = Components; Cu.import('resource://gre/modules/XPCOMUtils.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'Services', // jshint ignore:line 'resource://gre/modules/Services.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'Rect', // jshint ignore:line 'resource://gre/modules/Geometry.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line 'resource://gre/modules/accessibility/Constants.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'Events', // jshint ignore:line 'resource://gre/modules/accessibility/Constants.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'Relations', // jshint ignore:line 'resource://gre/modules/accessibility/Constants.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line 'resource://gre/modules/accessibility/Constants.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm', // jshint ignore:line 'resource://gre/modules/PluralForm.jsm'); this.EXPORTED_SYMBOLS = ['Utils', 'Logger', 'PivotContext', 'PrefCache', // jshint ignore:line 'SettingCache']; this.Utils = { // jshint ignore:line _buildAppMap: { '{3c2e2abc-06d4-11e1-ac3b-374f68613e61}': 'b2g', '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'browser', '{aa3c5121-dab2-40e2-81ca-7ea25febc110}': 'mobile/android', '{a23983c0-fd0e-11dc-95ff-0800200c9a66}': 'mobile/xul' }, init: function Utils_init(aWindow) { if (this._win) { // XXX: only supports attaching to one window now. throw new Error('Only one top-level window could used with AccessFu'); } this._win = Cu.getWeakReference(aWindow); }, uninit: function Utils_uninit() { if (!this._win) { return; } delete this._win; }, get win() { if (!this._win) { return null; } return this._win.get(); }, get winUtils() { let win = this.win; if (!win) { return null; } return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface( Ci.nsIDOMWindowUtils); }, get AccRetrieval() { if (!this._AccRetrieval) { this._AccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1']. getService(Ci.nsIAccessibleRetrieval); } return this._AccRetrieval; }, set MozBuildApp(value) { this._buildApp = value; }, get MozBuildApp() { if (!this._buildApp) { this._buildApp = this._buildAppMap[Services.appinfo.ID]; } return this._buildApp; }, get OS() { if (!this._OS) { this._OS = Services.appinfo.OS; } return this._OS; }, get widgetToolkit() { if (!this._widgetToolkit) { this._widgetToolkit = Services.appinfo.widgetToolkit; } return this._widgetToolkit; }, get ScriptName() { if (!this._ScriptName) { this._ScriptName = (Services.appinfo.processType == 2) ? 'AccessFuContent' : 'AccessFu'; } return this._ScriptName; }, get AndroidSdkVersion() { if (!this._AndroidSdkVersion) { if (Services.appinfo.OS == 'Android') { this._AndroidSdkVersion = Services.sysinfo.getPropertyAsInt32( 'version'); } else { // Most useful in desktop debugging. this._AndroidSdkVersion = 16; } } return this._AndroidSdkVersion; }, set AndroidSdkVersion(value) { // When we want to mimic another version. this._AndroidSdkVersion = value; }, get BrowserApp() { if (!this.win) { return null; } switch (this.MozBuildApp) { case 'mobile/android': return this.win.BrowserApp; case 'browser': return this.win.gBrowser; case 'b2g': return this.win.shell; default: return null; } }, get CurrentBrowser() { if (!this.BrowserApp) { return null; } if (this.MozBuildApp == 'b2g') { return this.BrowserApp.contentBrowser; } return this.BrowserApp.selectedBrowser; }, get CurrentContentDoc() { let browser = this.CurrentBrowser; return browser ? browser.contentDocument : null; }, get AllMessageManagers() { let messageManagers = []; function collectLeafMessageManagers(mm) { for (let i = 0; i < mm.childCount; i++) { let childMM = mm.getChildAt(i); if ('sendAsyncMessage' in childMM) { messageManagers.push(childMM); } else { collectLeafMessageManagers(childMM); } } } collectLeafMessageManagers(this.win.messageManager); let document = this.CurrentContentDoc; if (document) { let remoteframes = document.querySelectorAll('iframe'); for (let i = 0; i < remoteframes.length; ++i) { let mm = this.getMessageManager(remoteframes[i]); if (mm) { messageManagers.push(mm); } } } return messageManagers; }, get isContentProcess() { delete this.isContentProcess; this.isContentProcess = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; return this.isContentProcess; }, localize: function localize(aOutput) { let outputArray = Array.isArray(aOutput) ? aOutput : [aOutput]; let localized = [this.stringBundle.get(details) for (details of outputArray)]; // jshint ignore:line // Clean up the white space. let trimmed; return [trimmed for (word of localized) if (word && // jshint ignore:line (trimmed = word.trim()))]; // jshint ignore:line }, get stringBundle() { delete this.stringBundle; let bundle = Services.strings.createBundle( 'chrome://global/locale/AccessFu.properties'); this.stringBundle = { get: function stringBundle_get(aDetails = {}) { if (!aDetails || typeof aDetails === 'string') { return aDetails; } let str = ''; let string = aDetails.string; if (!string) { return str; } try { let args = aDetails.args; let count = aDetails.count; if (args) { str = bundle.formatStringFromName(string, args, args.length); } else { str = bundle.GetStringFromName(string); } if (count) { str = PluralForm.get(count, str); str = str.replace('#1', count); } } catch (e) { Logger.debug('Failed to get a string from a bundle for', string); } finally { return str; } } }; return this.stringBundle; }, getMessageManager: function getMessageManager(aBrowser) { try { return aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner). frameLoader.messageManager; } catch (x) { return null; } }, getViewport: function getViewport(aWindow) { switch (this.MozBuildApp) { case 'mobile/android': return aWindow.BrowserApp.selectedTab.getViewport(); default: return null; } }, getState: function getState(aAccessibleOrEvent) { if (aAccessibleOrEvent instanceof Ci.nsIAccessibleStateChangeEvent) { return new State( aAccessibleOrEvent.isExtraState ? 0 : aAccessibleOrEvent.state, aAccessibleOrEvent.isExtraState ? aAccessibleOrEvent.state : 0); } else { let state = {}; let extState = {}; aAccessibleOrEvent.getState(state, extState); return new State(state.value, extState.value); } }, getAttributes: function getAttributes(aAccessible) { let attributes = {}; if (aAccessible && aAccessible.attributes) { let attributesEnum = aAccessible.attributes.enumerate(); // Populate |attributes| object with |aAccessible|'s attribute key-value // pairs. while (attributesEnum.hasMoreElements()) { let attribute = attributesEnum.getNext().QueryInterface( Ci.nsIPropertyElement); attributes[attribute.key] = attribute.value; } } return attributes; }, getVirtualCursor: function getVirtualCursor(aDocument) { let doc = (aDocument instanceof Ci.nsIAccessible) ? aDocument : this.AccRetrieval.getAccessibleFor(aDocument); return doc.QueryInterface(Ci.nsIAccessibleDocument).virtualCursor; }, getBounds: function getBounds(aAccessible) { let objX = {}, objY = {}, objW = {}, objH = {}; aAccessible.getBounds(objX, objY, objW, objH); return new Rect(objX.value, objY.value, objW.value, objH.value); }, getTextBounds: function getTextBounds(aAccessible, aStart, aEnd) { let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText); let objX = {}, objY = {}, objW = {}, objH = {}; accText.getRangeExtents(aStart, aEnd, objX, objY, objW, objH, Ci.nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE); return new Rect(objX.value, objY.value, objW.value, objH.value); }, /** * Get current display DPI. */ get dpi() { delete this.dpi; this.dpi = this.winUtils.displayDPI; return this.dpi; }, isInSubtree: function isInSubtree(aAccessible, aSubTreeRoot) { let acc = aAccessible; while (acc) { if (acc == aSubTreeRoot) { return true; } try { acc = acc.parent; } catch (x) { Logger.debug('Failed to get parent:', x); acc = null; } } return false; }, inHiddenSubtree: function inHiddenSubtree(aAccessible) { for (let acc=aAccessible; acc; acc=acc.parent) { let hidden = Utils.getAttributes(acc).hidden; if (hidden && JSON.parse(hidden)) { return true; } } return false; }, isAliveAndVisible: function isAliveAndVisible(aAccessible, aIsOnScreen) { if (!aAccessible) { return false; } try { let state = this.getState(aAccessible); if (state.contains(States.DEFUNCT) || state.contains(States.INVISIBLE) || (aIsOnScreen && state.contains(States.OFFSCREEN)) || Utils.inHiddenSubtree(aAccessible)) { return false; } } catch (x) { return false; } return true; }, matchAttributeValue: function matchAttributeValue(aAttributeValue, values) { let attrSet = new Set(aAttributeValue.split(' ')); for (let value of values) { if (attrSet.has(value)) { return value; } } }, getLandmarkName: function getLandmarkName(aAccessible) { const landmarks = [ 'banner', 'complementary', 'contentinfo', 'main', 'navigation', 'search' ]; let roles = this.getAttributes(aAccessible)['xml-roles']; if (!roles) { return; } // Looking up a role that would match a landmark. return this.matchAttributeValue(roles, landmarks); }, getEmbeddedControl: function getEmbeddedControl(aLabel) { if (aLabel) { let relation = aLabel.getRelationByType(Relations.LABEL_FOR); for (let i = 0; i < relation.targetsCount; i++) { let target = relation.getTarget(i); if (target.parent === aLabel) { return target; } } } return null; }, isListItemDecorator: function isListItemDecorator(aStaticText, aExcludeOrdered) { let parent = aStaticText.parent; if (aExcludeOrdered && parent.parent.DOMNode.nodeName === 'OL') { return false; } return parent.role === Roles.LISTITEM && parent.childCount > 1 && aStaticText.indexInParent === 0; } }; /** * State object used internally to process accessible's states. * @param {Number} aBase Base state. * @param {Number} aExtended Extended state. */ function State(aBase, aExtended) { this.base = aBase; this.extended = aExtended; } State.prototype = { contains: function State_contains(other) { return !!(this.base & other.base || this.extended & other.extended); }, toString: function State_toString() { let stateStrings = Utils.AccRetrieval. getStringStates(this.base, this.extended); let statesArray = new Array(stateStrings.length); for (let i = 0; i < statesArray.length; i++) { statesArray[i] = stateStrings.item(i); } return '[' + statesArray.join(', ') + ']'; } }; this.Logger = { // jshint ignore:line DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3, _LEVEL_NAMES: ['DEBUG', 'INFO', 'WARNING', 'ERROR'], logLevel: 1, // INFO; test: false, log: function log(aLogLevel) { if (aLogLevel < this.logLevel) { return; } let args = Array.prototype.slice.call(arguments, 1); let message = (typeof(args[0]) === 'function' ? args[0]() : args).join(' '); message = '[' + Utils.ScriptName + '] ' + this._LEVEL_NAMES[aLogLevel] + ' ' + message + '\n'; dump(message); // Note: used for testing purposes. If |this.test| is true, also log to // the console service. if (this.test) { try { Services.console.logStringMessage(message); } catch (ex) { // There was an exception logging to the console service. } } }, info: function info() { this.log.apply( this, [this.INFO].concat(Array.prototype.slice.call(arguments))); }, debug: function debug() { this.log.apply( this, [this.DEBUG].concat(Array.prototype.slice.call(arguments))); }, warning: function warning() { this.log.apply( this, [this.WARNING].concat(Array.prototype.slice.call(arguments))); }, error: function error() { this.log.apply( this, [this.ERROR].concat(Array.prototype.slice.call(arguments))); }, logException: function logException( aException, aErrorMessage = 'An exception has occured') { try { let stackMessage = ''; if (aException.stack) { stackMessage = ' ' + aException.stack.replace(/\n/g, '\n '); } else if (aException.location) { let frame = aException.location; let stackLines = []; while (frame && frame.lineNumber) { stackLines.push( ' ' + frame.name + '@' + frame.filename + ':' + frame.lineNumber); frame = frame.caller; } stackMessage = stackLines.join('\n'); } else { stackMessage = '(' + aException.fileName + ':' + aException.lineNumber + ')'; } this.error(aErrorMessage + ':\n ' + aException.message + '\n' + stackMessage); } catch (x) { this.error(x); } }, accessibleToString: function accessibleToString(aAccessible) { let str = '[ defunct ]'; try { str = '[ ' + Utils.AccRetrieval.getStringRole(aAccessible.role) + ' | ' + aAccessible.name + ' ]'; } catch (x) { } return str; }, eventToString: function eventToString(aEvent) { let str = Utils.AccRetrieval.getStringEventType(aEvent.eventType); if (aEvent.eventType == Events.STATE_CHANGE) { let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent); let stateStrings = event.isExtraState ? Utils.AccRetrieval.getStringStates(0, event.state) : Utils.AccRetrieval.getStringStates(event.state, 0); str += ' (' + stateStrings.item(0) + ')'; } if (aEvent.eventType == Events.VIRTUALCURSOR_CHANGED) { let event = aEvent.QueryInterface( Ci.nsIAccessibleVirtualCursorChangeEvent); let pivot = aEvent.accessible.QueryInterface( Ci.nsIAccessibleDocument).virtualCursor; str += ' (' + this.accessibleToString(event.oldAccessible) + ' -> ' + this.accessibleToString(pivot.position) + ')'; } return str; }, statesToString: function statesToString(aAccessible) { return Utils.getState(aAccessible).toString(); }, dumpTree: function dumpTree(aLogLevel, aRootAccessible) { if (aLogLevel < this.logLevel) { return; } this._dumpTreeInternal(aLogLevel, aRootAccessible, 0); }, _dumpTreeInternal: function _dumpTreeInternal(aLogLevel, aAccessible, aIndent) { let indentStr = ''; for (let i = 0; i < aIndent; i++) { indentStr += ' '; } this.log(aLogLevel, indentStr, this.accessibleToString(aAccessible), '(' + this.statesToString(aAccessible) + ')'); for (let i = 0; i < aAccessible.childCount; i++) { this._dumpTreeInternal(aLogLevel, aAccessible.getChildAt(i), aIndent + 1); } } }; /** * PivotContext: An object that generates and caches context information * for a given accessible and its relationship with another accessible. * * If the given accessible is a label for a nested control, then this * context will represent the nested control instead of the label. * With the exception of bounds calculation, which will use the containing * label. In this case the |accessible| field would be the embedded control, * and the |accessibleForBounds| field would be the label. */ this.PivotContext = function PivotContext(aAccessible, aOldAccessible, // jshint ignore:line aStartOffset, aEndOffset, aIgnoreAncestry = false, aIncludeInvisible = false) { this._accessible = aAccessible; this._nestedControl = Utils.getEmbeddedControl(aAccessible); this._oldAccessible = this._isDefunct(aOldAccessible) ? null : aOldAccessible; this.startOffset = aStartOffset; this.endOffset = aEndOffset; this._ignoreAncestry = aIgnoreAncestry; this._includeInvisible = aIncludeInvisible; }; PivotContext.prototype = { get accessible() { // If the current pivot accessible has a nested control, // make this context use it publicly. return this._nestedControl || this._accessible; }, get oldAccessible() { return this._oldAccessible; }, get isNestedControl() { return !!this._nestedControl; }, get accessibleForBounds() { return this._accessible; }, get textAndAdjustedOffsets() { if (this.startOffset === -1 && this.endOffset === -1) { return null; } if (!this._textAndAdjustedOffsets) { let result = {startOffset: this.startOffset, endOffset: this.endOffset, text: this._accessible.QueryInterface(Ci.nsIAccessibleText). getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT)}; let hypertextAcc = this._accessible.QueryInterface( Ci.nsIAccessibleHyperText); // Iterate through the links in backwards order so text replacements don't // affect the offsets of links yet to be processed. for (let i = hypertextAcc.linkCount - 1; i >= 0; i--) { let link = hypertextAcc.getLinkAt(i); let linkText = ''; if (link instanceof Ci.nsIAccessibleText) { linkText = link.QueryInterface(Ci.nsIAccessibleText). getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT); } let start = link.startIndex; let end = link.endIndex; for (let offset of ['startOffset', 'endOffset']) { if (this[offset] >= end) { result[offset] += linkText.length - (end - start); } } result.text = result.text.substring(0, start) + linkText + result.text.substring(end); } this._textAndAdjustedOffsets = result; } return this._textAndAdjustedOffsets; }, /** * Get a list of |aAccessible|'s ancestry up to the root. * @param {nsIAccessible} aAccessible. * @return {Array} Ancestry list. */ _getAncestry: function _getAncestry(aAccessible) { let ancestry = []; let parent = aAccessible; try { while (parent && (parent = parent.parent)) { ancestry.push(parent); } } catch (x) { // A defunct accessible will raise an exception geting parent. Logger.debug('Failed to get parent:', x); } return ancestry.reverse(); }, /** * A list of the old accessible's ancestry. */ get oldAncestry() { if (!this._oldAncestry) { if (!this._oldAccessible || this._ignoreAncestry) { this._oldAncestry = []; } else { this._oldAncestry = this._getAncestry(this._oldAccessible); this._oldAncestry.push(this._oldAccessible); } } return this._oldAncestry; }, /** * A list of the current accessible's ancestry. */ get currentAncestry() { if (!this._currentAncestry) { this._currentAncestry = this._ignoreAncestry ? [] : this._getAncestry(this.accessible); } return this._currentAncestry; }, /* * This is a list of the accessible's ancestry up to the common ancestor * of the accessible and the old accessible. It is useful for giving the * user context as to where they are in the heirarchy. */ get newAncestry() { if (!this._newAncestry) { this._newAncestry = this._ignoreAncestry ? [] : [currentAncestor for ( // jshint ignore:line [index, currentAncestor] of Iterator(this.currentAncestry)) if ( // jshint ignore:line currentAncestor !== this.oldAncestry[index])]; // jshint ignore:line } return this._newAncestry; }, /* * Traverse the accessible's subtree in pre or post order. * It only includes the accessible's visible chidren. * Note: needSubtree is a function argument that can be used to determine * whether aAccessible's subtree is required. */ _traverse: function* _traverse(aAccessible, aPreorder, aStop) { if (aStop && aStop(aAccessible)) { return; } let child = aAccessible.firstChild; while (child) { let include; if (this._includeInvisible) { include = true; } else { include = !(Utils.getState(child).contains(States.INVISIBLE)); } if (include) { if (aPreorder) { yield child; [yield node for (node of this._traverse(child, aPreorder, aStop))]; // jshint ignore:line } else { [yield node for (node of this._traverse(child, aPreorder, aStop))]; // jshint ignore:line yield child; } } child = child.nextSibling; } }, /* * A subtree generator function, used to generate a flattened * list of the accessible's subtree in pre or post order. * It only includes the accessible's visible chidren. * @param {boolean} aPreorder A flag for traversal order. If true, traverse * in preorder; if false, traverse in postorder. * @param {function} aStop An optional function, indicating whether subtree * traversal should stop. */ subtreeGenerator: function subtreeGenerator(aPreorder, aStop) { return this._traverse(this.accessible, aPreorder, aStop); }, getCellInfo: function getCellInfo(aAccessible) { if (!this._cells) { this._cells = new WeakMap(); } let domNode = aAccessible.DOMNode; if (this._cells.has(domNode)) { return this._cells.get(domNode); } let cellInfo = {}; let getAccessibleCell = function getAccessibleCell(aAccessible) { if (!aAccessible) { return null; } if ([Roles.CELL, Roles.COLUMNHEADER, Roles.ROWHEADER].indexOf( aAccessible.role) < 0) { return null; } try { return aAccessible.QueryInterface(Ci.nsIAccessibleTableCell); } catch (x) { Logger.logException(x); return null; } }; let getHeaders = function* getHeaders(aHeaderCells) { let enumerator = aHeaderCells.enumerate(); while (enumerator.hasMoreElements()) { yield enumerator.getNext().QueryInterface(Ci.nsIAccessible).name; } }; cellInfo.current = getAccessibleCell(aAccessible); if (!cellInfo.current) { Logger.warning(aAccessible, 'does not support nsIAccessibleTableCell interface.'); this._cells.set(domNode, null); return null; } let table = cellInfo.current.table; if (table.isProbablyForLayout()) { this._cells.set(domNode, null); return null; } cellInfo.previous = null; let oldAncestry = this.oldAncestry.reverse(); let ancestor = oldAncestry.shift(); while (!cellInfo.previous && ancestor) { let cell = getAccessibleCell(ancestor); if (cell && cell.table === table) { cellInfo.previous = cell; } ancestor = oldAncestry.shift(); } if (cellInfo.previous) { cellInfo.rowChanged = cellInfo.current.rowIndex !== cellInfo.previous.rowIndex; cellInfo.columnChanged = cellInfo.current.columnIndex !== cellInfo.previous.columnIndex; } else { cellInfo.rowChanged = true; cellInfo.columnChanged = true; } cellInfo.rowExtent = cellInfo.current.rowExtent; cellInfo.columnExtent = cellInfo.current.columnExtent; cellInfo.columnIndex = cellInfo.current.columnIndex; cellInfo.rowIndex = cellInfo.current.rowIndex; cellInfo.columnHeaders = []; if (cellInfo.columnChanged && cellInfo.current.role !== Roles.COLUMNHEADER) { cellInfo.columnHeaders = [headers for (headers of getHeaders( // jshint ignore:line cellInfo.current.columnHeaderCells))]; } cellInfo.rowHeaders = []; if (cellInfo.rowChanged && cellInfo.current.role === Roles.CELL) { cellInfo.rowHeaders = [headers for (headers of getHeaders( // jshint ignore:line cellInfo.current.rowHeaderCells))]; } this._cells.set(domNode, cellInfo); return cellInfo; }, get bounds() { if (!this._bounds) { this._bounds = Utils.getBounds(this.accessibleForBounds); } return this._bounds.clone(); }, _isDefunct: function _isDefunct(aAccessible) { try { return Utils.getState(aAccessible).contains(States.DEFUNCT); } catch (x) { return true; } } }; this.PrefCache = function PrefCache(aName, aCallback, aRunCallbackNow) { // jshint ignore:line this.name = aName; this.callback = aCallback; let branch = Services.prefs; this.value = this._getValue(branch); if (this.callback && aRunCallbackNow) { try { this.callback(this.name, this.value); } catch (x) { Logger.logException(x); } } branch.addObserver(aName, this, true); }; PrefCache.prototype = { _getValue: function _getValue(aBranch) { try { if (!this.type) { this.type = aBranch.getPrefType(this.name); } switch (this.type) { case Ci.nsIPrefBranch.PREF_STRING: return aBranch.getCharPref(this.name); case Ci.nsIPrefBranch.PREF_INT: return aBranch.getIntPref(this.name); case Ci.nsIPrefBranch.PREF_BOOL: return aBranch.getBoolPref(this.name); default: return null; } } catch (x) { // Pref does not exist. return null; } }, observe: function observe(aSubject) { this.value = this._getValue(aSubject.QueryInterface(Ci.nsIPrefBranch)); if (this.callback) { try { this.callback(this.name, this.value); } catch (x) { Logger.logException(x); } } }, QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]) }; this.SettingCache = function SettingCache(aName, aCallback, aOptions = {}) { // jshint ignore:line this.value = aOptions.defaultValue; let runCallback = () => { if (aCallback) { aCallback(aName, this.value); if (aOptions.callbackOnce) { runCallback = () => {}; } } }; let settings = Utils.win.navigator.mozSettings; if (!settings) { if (aOptions.callbackNow) { runCallback(); } return; } let lock = settings.createLock(); let req = lock.get(aName); req.addEventListener('success', () => { this.value = req.result[aName] === undefined ? aOptions.defaultValue : req.result[aName]; if (aOptions.callbackNow) { runCallback(); } }); settings.addObserver(aName, (evt) => { this.value = evt.settingValue; runCallback(); }); };